Browse Source

Merge pull request #10634 from growilabs/support/156162-176215-app-some-client-components-biome-4

support: Configure biome for some client components in app 4
Yuki Takei 3 months ago
parent
commit
7874ad9a08
39 changed files with 1644 additions and 1024 deletions
  1. 14 0
      apps/app/.eslintrc.js
  2. 4 4
      apps/app/src/client/components/DescendantsPageListModal/DescendantsPageListModal.spec.tsx
  3. 72 36
      apps/app/src/client/components/DescendantsPageListModal/DescendantsPageListModal.tsx
  4. 4 1
      apps/app/src/client/components/DescendantsPageListModal/dynamic.tsx
  5. 0 2
      apps/app/src/client/components/ItemsTree/ItemsTreeContentSkeleton.tsx
  6. 21 9
      apps/app/src/client/components/LoginForm/ExternalAuthButton.tsx
  7. 34 20
      apps/app/src/client/components/LoginForm/LoginForm.spec.tsx
  8. 299 183
      apps/app/src/client/components/LoginForm/LoginForm.tsx
  9. 16 11
      apps/app/src/client/components/Page/DisplaySwitcher.tsx
  10. 5 4
      apps/app/src/client/components/Page/EditablePageEffects.tsx
  11. 7 4
      apps/app/src/client/components/Page/PageContentsUtilities.tsx
  12. 18 21
      apps/app/src/client/components/Page/RevisionLoader.tsx
  13. 2 5
      apps/app/src/client/components/Page/SlideRenderer.tsx
  14. 6 1
      apps/app/src/client/components/Page/markdown-drawio-util-for-view.ts
  15. 17 5
      apps/app/src/client/components/Page/markdown-table-util-for-view.ts
  16. 31 30
      apps/app/src/client/components/PageAttachment/DeleteAttachmentModal.tsx
  17. 7 10
      apps/app/src/client/components/PageAttachment/PageAttachmentList.tsx
  18. 4 1
      apps/app/src/client/components/PageAttachment/dynamic.tsx
  19. 231 138
      apps/app/src/client/components/PageDeleteModal/PageDeleteModal.tsx
  20. 4 1
      apps/app/src/client/components/PageDeleteModal/dynamic.tsx
  21. 144 84
      apps/app/src/client/components/PageDuplicateModal/PageDuplicateModal.tsx
  22. 4 1
      apps/app/src/client/components/PageDuplicateModal/dynamic.tsx
  23. 15 14
      apps/app/src/client/components/PageList/PageList.tsx
  24. 212 134
      apps/app/src/client/components/PageList/PageListItemL.tsx
  25. 23 19
      apps/app/src/client/components/PageList/PageListItemS.tsx
  26. 49 29
      apps/app/src/client/components/PageManagement/ApiErrorMessage.jsx
  27. 10 5
      apps/app/src/client/components/PageManagement/ApiErrorMessageList.jsx
  28. 24 11
      apps/app/src/client/components/PagePathNavSticky/CollapsedParentsDropdown.tsx
  29. 56 27
      apps/app/src/client/components/PagePathNavSticky/PagePathNavSticky.tsx
  30. 26 22
      apps/app/src/client/components/PagePresentationModal/PagePresentationModal.tsx
  31. 4 1
      apps/app/src/client/components/PagePresentationModal/dynamic.tsx
  32. 181 96
      apps/app/src/client/components/PageRenameModal/PageRenameModal.tsx
  33. 4 1
      apps/app/src/client/components/PageRenameModal/dynamic.tsx
  34. 19 17
      apps/app/src/client/components/PageSelectModal/PageSelectModal.tsx
  35. 8 11
      apps/app/src/client/components/PageSelectModal/TreeItemForModal.tsx
  36. 4 1
      apps/app/src/client/components/PageSelectModal/dynamic.tsx
  37. 14 18
      apps/app/src/client/components/PageSideContents/PageAccessoriesControl.tsx
  38. 50 32
      apps/app/src/client/components/PageSideContents/PageSideContents.tsx
  39. 1 15
      biome.json

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

@@ -37,6 +37,20 @@ module.exports = {
     'src/interfaces/**',
     'src/utils/**',
     'src/components/**',
+    'src/client/components/DescendantsPageListModal/**',
+    'src/client/components/ItemsTree/**',
+    'src/client/components/LoginForm/**',
+    'src/client/components/Page/**',
+    'src/client/components/PageAttachment/**',
+    'src/client/components/PageDeleteModal/**',
+    'src/client/components/PageDuplicateModal/**',
+    'src/client/components/PageList/**',
+    'src/client/components/PageManagement/**',
+    'src/client/components/PagePathNavSticky/**',
+    'src/client/components/PagePresentationModal/**',
+    'src/client/components/PageRenameModal/**',
+    'src/client/components/PageSelectModal/**',
+    'src/client/components/PageSideContents/**',
     'src/client/components/*.tsx',
     'src/client/components/*.jsx',
     'src/client/components/*.ts',

+ 4 - 4
apps/app/src/client/components/DescendantsPageListModal/DescendantsPageListModal.spec.tsx

@@ -1,4 +1,4 @@
-import { render, screen, fireEvent } from '@testing-library/react';
+import { fireEvent, render, screen } from '@testing-library/react';
 
 import { DescendantsPageListModal } from './DescendantsPageListModal';
 
@@ -33,7 +33,9 @@ vi.mock('~/states/context', () => ({
 }));
 
 vi.mock('../DescendantsPageList', () => ({
-  DescendantsPageList: () => <div data-testid="descendants-page-list">DescendantsPageList</div>,
+  DescendantsPageList: () => (
+    <div data-testid="descendants-page-list">DescendantsPageList</div>
+  ),
 }));
 
 vi.mock('../PageTimeline', () => ({
@@ -41,7 +43,6 @@ vi.mock('../PageTimeline', () => ({
 }));
 
 describe('DescendantsPageListModal.tsx', () => {
-
   it('should render the modal when isOpened is true', () => {
     render(<DescendantsPageListModal />);
     expect(screen.getByTestId('descendants-page-list-modal')).not.toBeNull();
@@ -55,7 +56,6 @@ describe('DescendantsPageListModal.tsx', () => {
   });
 
   describe('when device is larger than lg', () => {
-
     it('should render CustomNavTab', () => {
       render(<DescendantsPageListModal />);
       expect(screen.getByTestId('custom-nav-tab')).not.toBeNull();

+ 72 - 36
apps/app/src/client/components/DescendantsPageListModal/DescendantsPageListModal.tsx

@@ -1,18 +1,16 @@
-
-import React, {
-  useState, useMemo, useEffect, useCallback,
-} from 'react';
-
-import { useTranslation } from 'next-i18next';
+import type React from 'react';
+import { useCallback, useEffect, useMemo, useState } from 'react';
 import dynamic from 'next/dynamic';
 import { useRouter } from 'next/router';
-import {
-  Modal, ModalHeader, ModalBody,
-} from 'reactstrap';
+import { useTranslation } from 'next-i18next';
+import { Modal, ModalBody, ModalHeader } from 'reactstrap';
 
 import { useIsSharedUser } from '~/states/context';
 import { useDeviceLargerThanLg } from '~/states/ui/device';
-import { useDescendantsPageListModalActions, useDescendantsPageListModalStatus } from '~/states/ui/modal/descendants-page-list';
+import {
+  useDescendantsPageListModalActions,
+  useDescendantsPageListModalStatus,
+} from '~/states/ui/modal/descendants-page-list';
 
 import { CustomNavDropdown, CustomNavTab } from '../CustomNavigation/CustomNav';
 import CustomTabContent from '../CustomNavigation/CustomTabContent';
@@ -21,9 +19,38 @@ import ExpandOrContractButton from '../ExpandOrContractButton';
 
 import styles from './DescendantsPageListModal.module.scss';
 
-const DescendantsPageList = dynamic<DescendantsPageListProps>(() => import('../DescendantsPageList').then(mod => mod.DescendantsPageList), { ssr: false });
+const DescendantsPageList = dynamic<DescendantsPageListProps>(
+  () => import('../DescendantsPageList').then((mod) => mod.DescendantsPageList),
+  { ssr: false },
+);
+
+const PageTimeline = dynamic(
+  () => import('../PageTimeline').then((mod) => mod.PageTimeline),
+  { ssr: false },
+);
+
+const PageListTabIcon = (): React.JSX.Element => (
+  <span className="material-symbols-outlined">subject</span>
+);
+
+const PageListTabContent = (): React.JSX.Element => {
+  const status = useDescendantsPageListModalStatus();
+  const path = status?.path;
+
+  if (path == null) {
+    return <></>;
+  }
+
+  return <DescendantsPageList path={path} />;
+};
+
+const TimelineTabIcon = (): React.JSX.Element => (
+  <span data-testid="timeline-tab-button" className="material-symbols-outlined">
+    timeline
+  </span>
+);
 
-const PageTimeline = dynamic(() => import('../PageTimeline').then(mod => mod.PageTimeline), { ssr: false });
+const TimelineTabContent = (): React.JSX.Element => <PageTimeline />;
 
 /**
  * DescendantsPageListModalSubstance - Presentation component (all logic here)
@@ -58,26 +85,19 @@ const DescendantsPageListModalSubstance = ({
   const navTabMapping = useMemo(() => {
     return {
       pagelist: {
-        Icon: () => <span className="material-symbols-outlined">subject</span>,
-        Content: () => {
-          if (path == null) {
-            return <></>;
-          }
-          return <DescendantsPageList path={path} />;
-        },
+        Icon: PageListTabIcon,
+        Content: PageListTabContent,
         i18n: t('page_list'),
         isLinkEnabled: () => !isSharedUser,
       },
       timeline: {
-        Icon: () => <span data-testid="timeline-tab-button" className="material-symbols-outlined">timeline</span>,
-        Content: () => {
-          return <PageTimeline />;
-        },
+        Icon: TimelineTabIcon,
+        Content: TimelineTabContent,
         i18n: t('Timeline View'),
         isLinkEnabled: () => !isSharedUser,
       },
     };
-  }, [isSharedUser, path, t]);
+  }, [isSharedUser, t]);
 
   // Memoize event handlers
   const expandWindow = useCallback(() => {
@@ -90,20 +110,32 @@ const DescendantsPageListModalSubstance = ({
   }, [onExpandedChange]);
   const onNavSelected = useCallback((v: string) => setActiveTab(v), []);
 
-  const buttons = useMemo(() => (
-    <span className="me-3">
-      <ExpandOrContractButton
-        isWindowExpanded={isWindowExpanded}
-        expandWindow={expandWindow}
-        contractWindow={contractWindow}
-      />
-      <button type="button" className="btn btn-close ms-2" onClick={closeModal} aria-label="Close"></button>
-    </span>
-  ), [closeModal, isWindowExpanded, expandWindow, contractWindow]);
+  const buttons = useMemo(
+    () => (
+      <span className="me-3">
+        <ExpandOrContractButton
+          isWindowExpanded={isWindowExpanded}
+          expandWindow={expandWindow}
+          contractWindow={contractWindow}
+        />
+        <button
+          type="button"
+          className="btn btn-close ms-2"
+          onClick={closeModal}
+          aria-label="Close"
+        ></button>
+      </span>
+    ),
+    [closeModal, isWindowExpanded, expandWindow, contractWindow],
+  );
 
   return (
     <div>
-      <ModalHeader className={isDeviceLargerThanLg ? 'p-0' : ''} toggle={closeModal} close={buttons}>
+      <ModalHeader
+        className={isDeviceLargerThanLg ? 'p-0' : ''}
+        toggle={closeModal}
+        close={buttons}
+      >
         {isDeviceLargerThanLg && (
           <CustomNavTab
             activeTab={activeTab}
@@ -125,7 +157,11 @@ const DescendantsPageListModalSubstance = ({
         <CustomTabContent
           activeTab={activeTab}
           navTabMapping={navTabMapping}
-          additionalClassNames={!isDeviceLargerThanLg ? ['grw-tab-content-style-md-down'] : undefined}
+          additionalClassNames={
+            !isDeviceLargerThanLg
+              ? ['grw-tab-content-style-md-down']
+              : undefined
+          }
         />
       </ModalBody>
     </div>

+ 4 - 1
apps/app/src/client/components/DescendantsPageListModal/dynamic.tsx

@@ -10,7 +10,10 @@ export const DescendantsPageListModalLazyLoaded = (): JSX.Element => {
 
   const DescendantsPageListModal = useLazyLoader<DescendantsPageListModalProps>(
     'descendants-page-list-modal',
-    () => import('./DescendantsPageListModal').then(mod => ({ default: mod.DescendantsPageListModal })),
+    () =>
+      import('./DescendantsPageListModal').then((mod) => ({
+        default: mod.DescendantsPageListModal,
+      })),
     status?.isOpened ?? false,
   );
 

+ 0 - 2
apps/app/src/client/components/ItemsTree/ItemsTreeContentSkeleton.tsx

@@ -4,9 +4,7 @@ import { Skeleton } from '~/client/components/Skeleton';
 
 import styles from './ItemsTreeContentSkeleton.module.scss';
 
-
 const ItemsTreeContentSkeleton = (): JSX.Element => {
-
   return (
     <ul className="list-group py-3">
       <Skeleton additionalClass={`${styles['text-skeleton-level1']} pe-3`} />

+ 21 - 9
apps/app/src/client/components/LoginForm/ExternalAuthButton.tsx

@@ -1,14 +1,21 @@
-import { useCallback, type JSX } from 'react';
-
+import { type JSX, useCallback } from 'react';
 import { useTranslation } from 'next-i18next';
 
 import { IExternalAuthProviderType } from '~/interfaces/external-auth-provider';
 
 const authIcon = {
-  [IExternalAuthProviderType.google]: <span className="growi-custom-icons align-bottom">google</span>,
-  [IExternalAuthProviderType.github]: <span className="growi-custom-icons align-bottom">github</span>,
-  [IExternalAuthProviderType.oidc]: <span className="growi-custom-icons align-bottom">openid</span>,
-  [IExternalAuthProviderType.saml]: <span className="material-symbols-outlined align-bottom">key</span>,
+  [IExternalAuthProviderType.google]: (
+    <span className="growi-custom-icons align-bottom">google</span>
+  ),
+  [IExternalAuthProviderType.github]: (
+    <span className="growi-custom-icons align-bottom">github</span>
+  ),
+  [IExternalAuthProviderType.oidc]: (
+    <span className="growi-custom-icons align-bottom">openid</span>
+  ),
+  [IExternalAuthProviderType.saml]: (
+    <span className="material-symbols-outlined align-bottom">key</span>
+  ),
 };
 
 const authLabel = {
@@ -18,8 +25,11 @@ const authLabel = {
   [IExternalAuthProviderType.saml]: 'SAML',
 };
 
-
-export const ExternalAuthButton = ({ authType }: {authType: IExternalAuthProviderType}): JSX.Element => {
+export const ExternalAuthButton = ({
+  authType,
+}: {
+  authType: IExternalAuthProviderType;
+}): JSX.Element => {
   const { t } = useTranslation();
 
   const key = `btn-auth-${authType.toString()}`;
@@ -37,7 +47,9 @@ export const ExternalAuthButton = ({ authType }: {authType: IExternalAuthProvide
       onClick={handleLoginWithExternalAuth}
     >
       <span>{authIcon[authType]}</span>
-      <span className="flex-grow-1">{t('Sign in with External auth', { signin: authLabel[authType] })}</span>
+      <span className="flex-grow-1">
+        {t('Sign in with External auth', { signin: authLabel[authType] })}
+      </span>
     </button>
   );
 };

+ 34 - 20
apps/app/src/client/components/LoginForm/LoginForm.spec.tsx

@@ -1,11 +1,6 @@
 import React from 'react';
-
-import {
-  render, screen, fireEvent, waitFor,
-} from '@testing-library/react';
-import {
-  describe, it, expect, vi, beforeEach,
-} from 'vitest';
+import { fireEvent, render, screen, waitFor } from '@testing-library/react';
+import { beforeEach, describe, expect, it, vi } from 'vitest';
 
 import { apiv3Post } from '~/client/util/apiv3-client';
 import type { IExternalAuthProviderType } from '~/interfaces/external-auth-provider';
@@ -89,7 +84,9 @@ describe('LoginForm - Error Display', () => {
 
       render(<LoginForm {...props} />);
 
-      expect(screen.getByText('jwks must be a JSON Web Key Set formatted object')).toBeInTheDocument();
+      expect(
+        screen.getByText('jwks must be a JSON Web Key Set formatted object'),
+      ).toBeInTheDocument();
     });
   });
 
@@ -110,7 +107,9 @@ describe('LoginForm - Error Display', () => {
 
       render(<LoginForm {...props} />);
 
-      expect(screen.getByText('jwks must be a JSON Web Key Set formatted object')).toBeInTheDocument();
+      expect(
+        screen.getByText('jwks must be a JSON Web Key Set formatted object'),
+      ).toBeInTheDocument();
     });
 
     it('should not render local/LDAP form but should still show errors', () => {
@@ -129,9 +128,15 @@ describe('LoginForm - Error Display', () => {
 
       render(<LoginForm {...props} />);
 
-      expect(screen.queryByTestId('tiUsernameForLogin')).not.toBeInTheDocument();
-      expect(screen.queryByTestId('tiPasswordForLogin')).not.toBeInTheDocument();
-      expect(screen.getByText('OIDC authentication failed')).toBeInTheDocument();
+      expect(
+        screen.queryByTestId('tiUsernameForLogin'),
+      ).not.toBeInTheDocument();
+      expect(
+        screen.queryByTestId('tiPasswordForLogin'),
+      ).not.toBeInTheDocument();
+      expect(
+        screen.getByText('OIDC authentication failed'),
+      ).toBeInTheDocument();
     });
   });
 
@@ -153,7 +158,7 @@ describe('LoginForm - Error Display', () => {
       expect(screen.getByText('External error message')).toBeInTheDocument();
     });
 
-    it('should prioritize login errors over external account login errors after failed login', async() => {
+    it('should prioritize login errors over external account login errors after failed login', async () => {
       const externalAccountLoginError = {
         message: 'External error message',
         name: 'ExternalAccountLoginError',
@@ -190,18 +195,23 @@ describe('LoginForm - Error Display', () => {
 
       // Wait for login error to appear and external error to be replaced
       await waitFor(() => {
-        expect(screen.getByText('Invalid username or password')).toBeInTheDocument();
+        expect(
+          screen.getByText('Invalid username or password'),
+        ).toBeInTheDocument();
       });
 
       // External error should no longer be visible when login error exists
-      expect(screen.queryByText('External error message')).not.toBeInTheDocument();
+      expect(
+        screen.queryByText('External error message'),
+      ).not.toBeInTheDocument();
     });
 
-    it('should display dangerouslySetInnerHTML errors for PROVIDER_DUPLICATED_USERNAME_EXCEPTION', async() => {
+    it('should display dangerouslySetInnerHTML errors for PROVIDER_DUPLICATED_USERNAME_EXCEPTION', async () => {
       // Mock API call to return PROVIDER_DUPLICATED_USERNAME_EXCEPTION error
       mockApiv3Post.mockRejectedValueOnce([
         {
-          message: 'This username is already taken by <a href="/login">another provider</a>',
+          message:
+            'This username is already taken by <a href="/login">another provider</a>',
           code: 'provider-duplicated-username-exception',
           args: {},
         },
@@ -226,11 +236,13 @@ describe('LoginForm - Error Display', () => {
       // Wait for the dangerouslySetInnerHTML error to appear
       await waitFor(() => {
         // Check that the error with HTML content is rendered
-        expect(screen.getByText(/This username is already taken by/)).toBeInTheDocument();
+        expect(
+          screen.getByText(/This username is already taken by/),
+        ).toBeInTheDocument();
       });
     });
 
-    it('should handle multiple login errors correctly', async() => {
+    it('should handle multiple login errors correctly', async () => {
       // Mock API call to return multiple errors
       mockApiv3Post.mockRejectedValueOnce([
         {
@@ -281,7 +293,9 @@ describe('LoginForm - Error Display', () => {
 
       render(<LoginForm {...props} />);
 
-      expect(screen.getByText('Authentication service unavailable')).toBeInTheDocument();
+      expect(
+        screen.getByText('Authentication service unavailable'),
+      ).toBeInTheDocument();
     });
   });
 });

+ 299 - 183
apps/app/src/client/components/LoginForm/LoginForm.tsx

@@ -1,10 +1,7 @@
-import React, {
-  useState, useEffect, useCallback, type JSX,
-} from 'react';
-
+import React, { type JSX, useCallback, useEffect, useState } from 'react';
+import { useRouter } from 'next/router';
 import { LoadingSpinner } from '@growi/ui/dist/components';
 import { useTranslation } from 'next-i18next';
-import { useRouter } from 'next/router';
 import ReactCardFlip from 'react-card-flip';
 
 import { apiv3Post } from '~/client/util/apiv3-client';
@@ -17,43 +14,50 @@ import { RegistrationMode } from '~/interfaces/registration-mode';
 import { toArrayIfNot } from '~/utils/array-utils';
 
 import { CompleteUserRegistration } from '../CompleteUserRegistration';
-
 import { ExternalAuthButton } from './ExternalAuthButton';
 
 import styles from './LoginForm.module.scss';
 
-
 const moduleClass = styles['login-form'];
 
-
 type LoginFormProps = {
-  username?: string,
-  name?: string,
-  email?: string,
-  isEmailAuthenticationEnabled: boolean,
-  registrationMode: RegistrationMode,
-  registrationWhitelist: string[],
-  isPasswordResetEnabled: boolean,
-  isLocalStrategySetup: boolean,
-  isLdapStrategySetup: boolean,
-  isLdapSetupFailed: boolean,
-  enabledExternalAuthType?: IExternalAuthProviderType[],
-  isMailerSetup?: boolean,
-  externalAccountLoginError?: IExternalAccountLoginError,
-  minPasswordLength: number,
-}
+  username?: string;
+  name?: string;
+  email?: string;
+  isEmailAuthenticationEnabled: boolean;
+  registrationMode: RegistrationMode;
+  registrationWhitelist: string[];
+  isPasswordResetEnabled: boolean;
+  isLocalStrategySetup: boolean;
+  isLdapStrategySetup: boolean;
+  isLdapSetupFailed: boolean;
+  enabledExternalAuthType?: IExternalAuthProviderType[];
+  isMailerSetup?: boolean;
+  externalAccountLoginError?: IExternalAccountLoginError;
+  minPasswordLength: number;
+};
 export const LoginForm = (props: LoginFormProps): JSX.Element => {
   const { t } = useTranslation();
 
   const router = useRouter();
 
   const {
-    isLocalStrategySetup, isLdapStrategySetup, isLdapSetupFailed, isPasswordResetEnabled,
-    isEmailAuthenticationEnabled, registrationMode, registrationWhitelist, isMailerSetup, enabledExternalAuthType, minPasswordLength,
+    isLocalStrategySetup,
+    isLdapStrategySetup,
+    isLdapSetupFailed,
+    isPasswordResetEnabled,
+    isEmailAuthenticationEnabled,
+    registrationMode,
+    registrationWhitelist,
+    isMailerSetup,
+    enabledExternalAuthType,
+    minPasswordLength,
   } = props;
 
-  const isLocalOrLdapStrategiesEnabled = isLocalStrategySetup || isLdapStrategySetup;
-  const isSomeExternalAuthEnabled = enabledExternalAuthType != null && enabledExternalAuthType.length > 0;
+  const isLocalOrLdapStrategiesEnabled =
+    isLocalStrategySetup || isLdapStrategySetup;
+  const isSomeExternalAuthEnabled =
+    enabledExternalAuthType != null && enabledExternalAuthType.length > 0;
 
   // states
   const [isRegistering, setIsRegistering] = useState(false);
@@ -69,11 +73,13 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
   const [passwordForRegister, setPasswordForRegister] = useState('');
   const [registerErrors, setRegisterErrors] = useState<IErrorV3[]>([]);
   // For UserActivation
-  const [emailForRegistrationOrder, setEmailForRegistrationOrder] = useState('');
+  const [emailForRegistrationOrder, setEmailForRegistrationOrder] =
+    useState('');
 
   const [isSuccessToRagistration, setIsSuccessToRagistration] = useState(false);
 
-  const isRegistrationEnabled = isLocalStrategySetup && registrationMode !== RegistrationMode.CLOSED;
+  const isRegistrationEnabled =
+    isLocalStrategySetup && registrationMode !== RegistrationMode.CLOSED;
 
   const tWithOpt = useTWithOpt();
 
@@ -89,34 +95,35 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
     setLoginErrors([]);
   }, [loginErrors.length]);
 
-  const handleLoginWithLocalSubmit = useCallback(async(e) => {
-    e.preventDefault();
-    resetLoginErrors();
-    setIsLoading(true);
-
-    const loginForm = {
-      username: usernameForLogin,
-      password: passwordForLogin,
-    };
+  const handleLoginWithLocalSubmit = useCallback(
+    async (e) => {
+      e.preventDefault();
+      resetLoginErrors();
+      setIsLoading(true);
 
-    try {
-      const res = await apiv3Post('/login', { loginForm });
-      const { redirectTo } = res.data;
+      const loginForm = {
+        username: usernameForLogin,
+        password: passwordForLogin,
+      };
 
-      if (redirectTo != null) {
-        return router.push(redirectTo);
-      }
+      try {
+        const res = await apiv3Post('/login', { loginForm });
+        const { redirectTo } = res.data;
 
-      return router.push('/');
-    }
-    catch (err) {
-      const errs = toArrayIfNot(err);
-      setLoginErrors(errs);
-      setIsLoading(false);
-    }
-    return;
+        if (redirectTo != null) {
+          return router.push(redirectTo);
+        }
 
-  }, [passwordForLogin, resetLoginErrors, router, usernameForLogin]);
+        return router.push('/');
+      } catch (err) {
+        const errs = toArrayIfNot(err);
+        setLoginErrors(errs);
+        setIsLoading(false);
+      }
+      return;
+    },
+    [passwordForLogin, resetLoginErrors, router, usernameForLogin],
+  );
 
   // separate errors based on error code
   const separateErrorsBasedOnErrorCode = useCallback((errors: IErrorV3[]) => {
@@ -126,8 +133,7 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
     errors.forEach((err) => {
       if (err.code === LoginErrorCode.PROVIDER_DUPLICATED_USERNAME_EXCEPTION) {
         loginErrorListForDangerouslySetInnerHTML.push(err);
-      }
-      else {
+      } else {
         loginErrorList.push(err);
       }
     });
@@ -136,31 +142,48 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
   }, []);
 
   // wrap error elements which use dangerouslySetInnerHtml
-  const generateDangerouslySetErrors = useCallback((errors: IErrorV3[]): JSX.Element => {
-    if (errors == null || errors.length === 0) return <></>;
-    return (
-      <div className="alert alert-danger">
-        {errors.map((err) => {
-          // eslint-disable-next-line react/no-danger
-          return <small dangerouslySetInnerHTML={{ __html: tWithOpt(err.message, err.args) }}></small>;
-        })}
-      </div>
-    );
-  }, [tWithOpt]);
+  const generateDangerouslySetErrors = useCallback(
+    (errors: IErrorV3[]): JSX.Element => {
+      if (errors == null || errors.length === 0) return <></>;
+      return (
+        <div className="alert alert-danger">
+          {errors.map((err, index) => {
+            // eslint-disable-next-line react/no-danger
+            return (
+              <small
+                key={`${err.code}-${index}`}
+                // biome-ignore lint/security/noDangerouslySetInnerHtml: rendered HTML from translations
+                dangerouslySetInnerHTML={{
+                  __html: tWithOpt(err.message, err.args),
+                }}
+              ></small>
+            );
+          })}
+        </div>
+      );
+    },
+    [tWithOpt],
+  );
 
   // wrap error elements which do not use dangerouslySetInnerHtml
-  const generateSafelySetErrors = useCallback((errors: (IErrorV3 | IExternalAccountLoginError)[]): JSX.Element => {
-    if (errors == null || errors.length === 0) return <></>;
-    return (
-      <ul className="alert alert-danger">
-        {errors.map((err, index) => (
-          <small className={index > 0 ? 'mt-1' : ''}>
-            {tWithOpt(err.message, err.args)}
-          </small>
-        ))}
-      </ul>
-    );
-  }, [tWithOpt]);
+  const generateSafelySetErrors = useCallback(
+    (errors: (IErrorV3 | IExternalAccountLoginError)[]): JSX.Element => {
+      if (errors == null || errors.length === 0) return <></>;
+      return (
+        <ul className="alert alert-danger">
+          {errors.map((err, index) => (
+            <small
+              key={`${err.message}-${index}`}
+              className={index > 0 ? 'mt-1' : ''}
+            >
+              {tWithOpt(err.message, err.args)}
+            </small>
+          ))}
+        </ul>
+      );
+    },
+    [tWithOpt],
+  );
 
   const renderLocalOrLdapLoginForm = useCallback(() => {
     const { isLdapStrategySetup } = props;
@@ -175,16 +198,30 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
         {/* !! - END OF HIDDEN ELEMENT - !! */}
         {isLdapSetupFailed && (
           <div className="alert alert-warning small">
-            <strong><span className="material-symbols-outlined">info</span>{t('login.enabled_ldap_has_configuration_problem')}</strong><br />
+            <strong>
+              <span className="material-symbols-outlined">info</span>
+              {t('login.enabled_ldap_has_configuration_problem')}
+            </strong>
+            <br />
             {/* eslint-disable-next-line react/no-danger */}
-            <span dangerouslySetInnerHTML={{ __html: t('login.set_env_var_for_logs') }}></span>
+            <span
+              // biome-ignore lint/security/noDangerouslySetInnerHtml: rendered HTML from translations
+              dangerouslySetInnerHTML={{
+                __html: t('login.set_env_var_for_logs'),
+              }}
+            ></span>
           </div>
         )}
 
-        <form role="form" onSubmit={handleLoginWithLocalSubmit} id="login-form">
+        <form onSubmit={handleLoginWithLocalSubmit} id="login-form">
           <div className="input-group">
-            <label className="text-white opacity-75 d-flex align-items-center" htmlFor="tiUsernameForLogin">
-              <span className="material-symbols-outlined" aria-label="Username or E-mail">person</span>
+            <label
+              className="text-white opacity-75 d-flex align-items-center"
+              htmlFor="tiUsernameForLogin"
+            >
+              <span className="material-symbols-outlined" aria-hidden="true">
+                person
+              </span>
             </label>
             <input
               id="tiUsernameForLogin"
@@ -192,7 +229,9 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
               className={`form-control rounded ms-2 ${isLdapStrategySetup ? 'ldap-space' : ''}`}
               data-testid="tiUsernameForLogin"
               placeholder="Username or E-mail"
-              onChange={(e) => { setUsernameForLogin(e.target.value) }}
+              onChange={(e) => {
+                setUsernameForLogin(e.target.value);
+              }}
               name="usernameForLogin"
             />
             {isLdapStrategySetup && (
@@ -201,12 +240,16 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
                 <span className="">LDAP</span>
               </small>
             )}
-
           </div>
 
           <div className="input-group">
-            <label className="text-white opacity-75 d-flex align-items-center" htmlFor="tiPasswordForLogin">
-              <span className="material-symbols-outlined" aria-label="Password">lock</span>
+            <label
+              className="text-white opacity-75 d-flex align-items-center"
+              htmlFor="tiPasswordForLogin"
+            >
+              <span className="material-symbols-outlined" aria-hidden="true">
+                lock
+              </span>
             </label>
             <input
               id="tiPasswordForLogin"
@@ -214,7 +257,9 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
               className="form-control rounded ms-2"
               data-testid="tiPasswordForLogin"
               placeholder="Password"
-              onChange={(e) => { setPasswordForLogin(e.target.value) }}
+              onChange={(e) => {
+                setPasswordForLogin(e.target.value);
+              }}
               name="passwordForLogin"
             />
           </div>
@@ -230,7 +275,12 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
                 {isLoading ? (
                   <LoadingSpinner />
                 ) : (
-                  <span className="material-symbols-outlined" aria-label="Login">login</span>
+                  <span
+                    className="material-symbols-outlined"
+                    aria-hidden="true"
+                  >
+                    login
+                  </span>
                 )}
               </span>
               <span className="flex-grow-1">{t('Sign in')}</span>
@@ -239,10 +289,7 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
         </form>
       </>
     );
-  }, [
-    props, isLdapSetupFailed, t, handleLoginWithLocalSubmit, isLoading,
-  ]);
-
+  }, [props, isLdapSetupFailed, t, handleLoginWithLocalSubmit, isLoading]);
 
   const renderExternalAuthLoginForm = useCallback(() => {
     const { enabledExternalAuthType } = props;
@@ -254,7 +301,9 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
     return (
       <>
         <div className="mt-2">
-          {enabledExternalAuthType.map(authType => <ExternalAuthButton authType={authType} />)}
+          {enabledExternalAuthType.map((authType) => (
+            <ExternalAuthButton key={authType} authType={authType} />
+          ))}
         </div>
       </>
     );
@@ -265,45 +314,55 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
     setRegisterErrors([]);
   }, [registerErrors.length]);
 
-  const handleRegisterFormSubmit = useCallback(async(e, requestPath) => {
-    e.preventDefault();
-    setEmailForRegistrationOrder('');
-    setIsSuccessToRagistration(false);
-    setIsLoading(true);
-
-    const registerForm = {
-      username: usernameForRegister,
-      name: nameForRegister,
-      email: emailForRegister,
-      password: passwordForRegister,
-    };
-    try {
-      const res = await apiv3Post(requestPath, { registerForm });
-
-      setIsSuccessToRagistration(true);
-      resetRegisterErrors();
-      setIsLoading(false);
-
-      const { redirectTo } = res.data;
-
-      if (redirectTo != null) {
-        router.push(redirectTo);
-      }
+  const handleRegisterFormSubmit = useCallback(
+    async (e, requestPath) => {
+      e.preventDefault();
+      setEmailForRegistrationOrder('');
+      setIsSuccessToRagistration(false);
+      setIsLoading(true);
+
+      const registerForm = {
+        username: usernameForRegister,
+        name: nameForRegister,
+        email: emailForRegister,
+        password: passwordForRegister,
+      };
+      try {
+        const res = await apiv3Post(requestPath, { registerForm });
+
+        setIsSuccessToRagistration(true);
+        resetRegisterErrors();
+        setIsLoading(false);
+
+        const { redirectTo } = res.data;
+
+        if (redirectTo != null) {
+          router.push(redirectTo);
+        }
 
-      if (isEmailAuthenticationEnabled) {
-        setEmailForRegistrationOrder(emailForRegister);
-        return;
-      }
-    }
-    catch (err) {
-      // Execute if error exists
-      if (err != null || err.length > 0) {
-        setRegisterErrors(err);
+        if (isEmailAuthenticationEnabled) {
+          setEmailForRegistrationOrder(emailForRegister);
+          return;
+        }
+      } catch (err) {
+        // Execute if error exists
+        if (err != null || err.length > 0) {
+          setRegisterErrors(err);
+        }
+        setIsLoading(false);
       }
-      setIsLoading(false);
-    }
-    return;
-  }, [usernameForRegister, nameForRegister, emailForRegister, passwordForRegister, resetRegisterErrors, router, isEmailAuthenticationEnabled]);
+      return;
+    },
+    [
+      usernameForRegister,
+      nameForRegister,
+      emailForRegister,
+      passwordForRegister,
+      resetRegisterErrors,
+      router,
+      isEmailAuthenticationEnabled,
+    ],
+  );
 
   const switchForm = useCallback(() => {
     setIsRegistering(!isRegistering);
@@ -329,34 +388,37 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
             {t('page_register.notice.restricted_defail')}
           </p>
         )}
-        {(!isMailerSetup && isEmailAuthenticationEnabled) && (
+        {!isMailerSetup && isEmailAuthenticationEnabled && (
           <p className="alert alert-danger">
             <span>{t('commons:alert.please_enable_mailer')}</span>
           </p>
         )}
 
-        {
-          registerErrors != null && registerErrors.length > 0 && (
-            <p className="alert alert-danger">
-              {registerErrors.map(err => (
-                <span>
-                  {tWithOpt(err.message, err.args)}<br />
-                </span>
-              ))}
-            </p>
-          )
-        }
-
-        {
-          (isEmailAuthenticationEnabled && isSuccessToRagistration) && (
-            <p className="alert alert-success">
-              <span>{t('message.successfully_send_email_auth', { email: emailForRegistrationOrder })}</span>
-            </p>
-          )
-        }
+        {registerErrors != null && registerErrors.length > 0 && (
+          <p className="alert alert-danger">
+            {registerErrors.map((err, index) => (
+              <span key={`${err.message}-${index}`}>
+                {tWithOpt(err.message, err.args)}
+                <br />
+              </span>
+            ))}
+          </p>
+        )}
 
-        <form role="form" onSubmit={e => handleRegisterFormSubmit(e, registerAction)} id="register-form">
+        {isEmailAuthenticationEnabled && isSuccessToRagistration && (
+          <p className="alert alert-success">
+            <span>
+              {t('message.successfully_send_email_auth', {
+                email: emailForRegistrationOrder,
+              })}
+            </span>
+          </p>
+        )}
 
+        <form
+          onSubmit={(e) => handleRegisterFormSubmit(e, registerAction)}
+          id="register-form"
+        >
           {!isEmailAuthenticationEnabled && (
             <div>
               <div className="input-group" id="input-group-username">
@@ -367,7 +429,9 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
                 <input
                   type="text"
                   className="form-control rounded ms-2"
-                  onChange={(e) => { setUsernameForRegister(e.target.value) }}
+                  onChange={(e) => {
+                    setUsernameForRegister(e.target.value);
+                  }}
                   placeholder={t('User ID')}
                   name="username"
                   defaultValue={props.username}
@@ -385,7 +449,9 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
                 <input
                   type="text"
                   className="form-control rounded ms-2"
-                  onChange={(e) => { setNameForRegister(e.target.value) }}
+                  onChange={(e) => {
+                    setNameForRegister(e.target.value);
+                  }}
                   placeholder={t('Name')}
                   name="name"
                   defaultValue={props.name}
@@ -404,7 +470,9 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
               type="email"
               disabled={!isMailerSetup && isEmailAuthenticationEnabled}
               className="form-control rounded ms-2"
-              onChange={(e) => { setEmailForRegister(e.target.value) }}
+              onChange={(e) => {
+                setEmailForRegister(e.target.value);
+              }}
               placeholder={t('Email')}
               name="email"
               defaultValue={props.email}
@@ -437,7 +505,9 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
                 <input
                   type="password"
                   className="form-control rounded ms-2"
-                  onChange={(e) => { setPasswordForRegister(e.target.value) }}
+                  onChange={(e) => {
+                    setPasswordForRegister(e.target.value);
+                  }}
                   placeholder={t('Password')}
                   name="password"
                   required
@@ -452,7 +522,9 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
             <button
               type="submit"
               className="btn btn-secondary btn-register d-flex col-7"
-              disabled={(!isMailerSetup && isEmailAuthenticationEnabled) || isLoading}
+              disabled={
+                (!isMailerSetup && isEmailAuthenticationEnabled) || isLoading
+              }
             >
               <span>
                 {isLoading ? (
@@ -468,45 +540,82 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
 
         <div className="row">
           <div className="text-end col-12 mb-5">
-            <a
-              href="#login"
+            <button
+              type="button"
               className="btn btn-sm btn-secondary btn-function col-10 col-sm-9 mx-auto py-1 d-flex"
               style={{ pointerEvents: isLoading ? 'none' : undefined }}
               onClick={switchForm}
             >
               <span className="material-symbols-outlined fs-5">login</span>
               <span className="flex-grow-1">{t('Sign in is here')}</span>
-            </a>
+            </button>
           </div>
         </div>
       </React.Fragment>
     );
   }, [
-    t, isEmailAuthenticationEnabled, registrationMode, isMailerSetup, registerErrors, isSuccessToRagistration, emailForRegistrationOrder,
-    props.username, props.name, props.email, registrationWhitelist, minPasswordLength, isLoading, switchForm, tWithOpt, handleRegisterFormSubmit,
+    t,
+    isEmailAuthenticationEnabled,
+    registrationMode,
+    isMailerSetup,
+    registerErrors,
+    isSuccessToRagistration,
+    emailForRegistrationOrder,
+    props.username,
+    props.name,
+    props.email,
+    registrationWhitelist,
+    minPasswordLength,
+    isLoading,
+    switchForm,
+    tWithOpt,
+    handleRegisterFormSubmit,
   ]);
 
-  if (registrationMode === RegistrationMode.RESTRICTED && isSuccessToRagistration && !isEmailAuthenticationEnabled) {
+  if (
+    registrationMode === RegistrationMode.RESTRICTED &&
+    isSuccessToRagistration &&
+    !isEmailAuthenticationEnabled
+  ) {
     return <CompleteUserRegistration />;
   }
 
   return (
     <div className={moduleClass}>
-      <div className="nologin-dialog mx-auto rounded-4 rounded-top-0" id="nologin-dialog" data-testid="login-form">
+      <div
+        className="nologin-dialog mx-auto rounded-4 rounded-top-0"
+        id="nologin-dialog"
+        data-testid="login-form"
+      >
         <div className="row mx-0">
           <div className="col-12 px-md-4 pb-5">
-            <ReactCardFlip isFlipped={isRegistering} flipDirection="horizontal" cardZIndex="3">
+            <ReactCardFlip
+              isFlipped={isRegistering}
+              flipDirection="horizontal"
+              cardZIndex="3"
+            >
               <div className="front">
                 {/* Error display section - always shown regardless of login method configuration */}
                 {(() => {
                   // separate login errors into two arrays based on error code
-                  const [loginErrorListForDangerouslySetInnerHTML, loginErrorList] = separateErrorsBasedOnErrorCode(loginErrors);
+                  const [
+                    loginErrorListForDangerouslySetInnerHTML,
+                    loginErrorList,
+                  ] = separateErrorsBasedOnErrorCode(loginErrors);
                   // Generate login error elements using dangerouslySetInnerHTML
-                  const loginErrorElementWithDangerouslySetInnerHTML = generateDangerouslySetErrors(loginErrorListForDangerouslySetInnerHTML);
+                  const loginErrorElementWithDangerouslySetInnerHTML =
+                    generateDangerouslySetErrors(
+                      loginErrorListForDangerouslySetInnerHTML,
+                    );
                   // Generate login error elements - prioritize loginErrorList, fallback to externalAccountLoginError
-                  const loginErrorElement = (loginErrorList ?? []).length > 0
-                    ? generateSafelySetErrors(loginErrorList)
-                    : generateSafelySetErrors(props.externalAccountLoginError != null ? [props.externalAccountLoginError] : []);
+                  const loginErrorElement =
+                    (loginErrorList ?? []).length > 0
+                      ? generateSafelySetErrors(loginErrorList)
+                      : generateSafelySetErrors(
+                          props.externalAccountLoginError != null
+                            ? [props.externalAccountLoginError]
+                            : [],
+                        );
 
                   return (
                     <>
@@ -517,11 +626,12 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
                 })()}
 
                 {isLocalOrLdapStrategiesEnabled && renderLocalOrLdapLoginForm()}
-                {isLocalOrLdapStrategiesEnabled && isSomeExternalAuthEnabled && (
-                  <div className="text-center text-line d-flex align-items-center mb-3">
-                    <p className="text-white mb-0">{t('or')}</p>
-                  </div>
-                )}
+                {isLocalOrLdapStrategiesEnabled &&
+                  isSomeExternalAuthEnabled && (
+                    <div className="text-center text-line d-flex align-items-center mb-3">
+                      <p className="text-white mb-0">{t('or')}</p>
+                    </div>
+                  )}
                 {isSomeExternalAuthEnabled && renderExternalAuthLoginForm()}
                 {isLocalOrLdapStrategiesEnabled && isPasswordResetEnabled && (
                   <div className="mt-4">
@@ -531,22 +641,28 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
                       style={{ pointerEvents: isLoading ? 'none' : 'auto' }}
                     >
                       <span className="material-symbols-outlined">vpn_key</span>
-                      <span className="flex-grow-1">{t('forgot_password.forgot_password')}</span>
+                      <span className="flex-grow-1">
+                        {t('forgot_password.forgot_password')}
+                      </span>
                     </a>
                   </div>
                 )}
                 {/* Sign up link */}
                 {isRegistrationEnabled && (
                   <div className="mt-2">
-                    <a
-                      href="#register"
+                    <button
+                      type="button"
                       className="btn btn-sm btn-secondary btn-function col-10 col-sm-9 mx-auto py-1 d-flex"
                       style={{ pointerEvents: isLoading ? 'none' : 'auto' }}
                       onClick={switchForm}
                     >
-                      <span className="material-symbols-outlined">person_add</span>
-                      <span className="flex-grow-1">{t('Sign up is here')}</span>
-                    </a>
+                      <span className="material-symbols-outlined">
+                        person_add
+                      </span>
+                      <span className="flex-grow-1">
+                        {t('Sign up is here')}
+                      </span>
+                    </button>
                   </div>
                 )}
               </div>
@@ -558,10 +674,10 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
           </div>
         </div>
         <a href="https://growi.org" className="link-growi-org ps-3">
-          <span className="growi">GROWI</span><span className="org">.org</span>
+          <span className="growi">GROWI</span>
+          <span className="org">.org</span>
         </a>
       </div>
     </div>
   );
-
 };

+ 16 - 11
apps/app/src/client/components/Page/DisplaySwitcher.tsx

@@ -1,20 +1,26 @@
 import type { JSX } from 'react';
-
 import dynamic from 'next/dynamic';
 
 import { useHashChangedEffect } from '~/client/services/side-effects/hash-changed';
 import { useIsEditable, useRevisionIdFromUrl } from '~/states/page';
-import { EditorMode, useEditorMode, useReservedNextCaretLine } from '~/states/ui/editor';
+import {
+  EditorMode,
+  useEditorMode,
+  useReservedNextCaretLine,
+} from '~/states/ui/editor';
 
 import { LazyRenderer } from '../Common/LazyRenderer';
 
-
 const PageEditor = dynamic(() => import('../PageEditor'), { ssr: false });
-const PageEditorReadOnly = dynamic(() => import('../PageEditor/PageEditorReadOnly').then(mod => mod.PageEditorReadOnly), { ssr: false });
-
+const PageEditorReadOnly = dynamic(
+  () =>
+    import('../PageEditor/PageEditorReadOnly').then(
+      (mod) => mod.PageEditorReadOnly,
+    ),
+  { ssr: false },
+);
 
 export const DisplaySwitcher = (): JSX.Element => {
-
   const { editorMode } = useEditorMode();
   const isEditable = useIsEditable();
   const revisionIdFromUrl = useRevisionIdFromUrl();
@@ -23,12 +29,11 @@ export const DisplaySwitcher = (): JSX.Element => {
   useReservedNextCaretLine();
 
   return (
-    <LazyRenderer shouldRender={isEditable === true && editorMode === EditorMode.Editor}>
+    <LazyRenderer
+      shouldRender={isEditable === true && editorMode === EditorMode.Editor}
+    >
       {/* Display <PageEditorReadOnly /> when the user is intentionally viewing a specific (past) revision. */}
-      { revisionIdFromUrl == null
-        ? <PageEditor />
-        : <PageEditorReadOnly />
-      }
+      {revisionIdFromUrl == null ? <PageEditor /> : <PageEditorReadOnly />}
     </LazyRenderer>
   );
 };

+ 5 - 4
apps/app/src/client/components/Page/EditablePageEffects.tsx

@@ -1,11 +1,13 @@
 import type { JSX } from 'react';
 
 import { usePageUpdatedEffect } from '~/client/services/side-effects/page-updated';
-import { useAwarenessSyncingEffect, useNewlyYjsDataSyncingEffect, useCurrentPageYjsDataAutoLoadEffect } from '~/features/collaborative-editor/side-effects';
-
+import {
+  useAwarenessSyncingEffect,
+  useCurrentPageYjsDataAutoLoadEffect,
+  useNewlyYjsDataSyncingEffect,
+} from '~/features/collaborative-editor/side-effects';
 
 export const EditablePageEffects = (): JSX.Element => {
-
   usePageUpdatedEffect();
 
   useCurrentPageYjsDataAutoLoadEffect();
@@ -13,5 +15,4 @@ export const EditablePageEffects = (): JSX.Element => {
   useAwarenessSyncingEffect();
 
   return <></>;
-
 };

+ 7 - 4
apps/app/src/client/components/Page/PageContentsUtilities.tsx

@@ -3,11 +3,10 @@ import { useTranslation } from 'next-i18next';
 import { useUpdateStateAfterSave } from '~/client/services/page-operation';
 import { useDrawioModalLauncherForView } from '~/client/services/side-effects/drawio-modal-launcher-for-view';
 import { useHandsontableModalLauncherForView } from '~/client/services/side-effects/handsontable-modal-launcher-for-view';
-import { toastSuccess, toastError, toastWarning } from '~/client/util/toastr';
+import { toastError, toastSuccess, toastWarning } from '~/client/util/toastr';
 import { PageUpdateErrorCode } from '~/interfaces/apiv3';
 import { useCurrentPageId } from '~/states/page';
 
-
 export const PageContentsUtilities = (): null => {
   const { t } = useTranslation();
 
@@ -23,7 +22,9 @@ export const PageContentsUtilities = (): null => {
     onSaveError: (errors) => {
       for (const error of errors) {
         if (error.code === PageUpdateErrorCode.CONFLICT) {
-          toastWarning(t('modal_resolve_conflict.conflicts_with_new_body_on_server_side'));
+          toastWarning(
+            t('modal_resolve_conflict.conflicts_with_new_body_on_server_side'),
+          );
           return;
         }
       }
@@ -41,7 +42,9 @@ export const PageContentsUtilities = (): null => {
     onSaveError: (errors) => {
       for (const error of errors) {
         if (error.code === PageUpdateErrorCode.CONFLICT) {
-          toastWarning(t('modal_resolve_conflict.conflicts_with_new_body_on_server_side'));
+          toastWarning(
+            t('modal_resolve_conflict.conflicts_with_new_body_on_server_side'),
+          );
           return;
         }
       }

+ 18 - 21
apps/app/src/client/components/Page/RevisionLoader.tsx

@@ -1,6 +1,5 @@
-import React, { useState, useEffect, type JSX } from 'react';
-
-import type { Ref, IRevision, IRevisionHasId } from '@growi/core';
+import React, { type JSX, useEffect, useState } from 'react';
+import type { IRevision, IRevisionHasId, Ref } from '@growi/core';
 import { LoadingSpinner } from '@growi/ui/dist/components';
 import { useTranslation } from 'next-i18next';
 
@@ -8,17 +7,16 @@ import type { RendererOptions } from '~/interfaces/renderer-options';
 import { useSWRxPageRevision } from '~/stores/page';
 import loggerFactory from '~/utils/logger';
 
-
 import RevisionRenderer from '../../../components/PageView/RevisionRenderer';
 
 export const ROOT_ELEM_ID = 'revision-loader' as const;
 
 export type RevisionLoaderProps = {
-  rendererOptions: RendererOptions,
-  pageId: string,
-  revisionId: Ref<IRevision>,
-  onRevisionLoaded?: (revision: IRevisionHasId) => void,
-}
+  rendererOptions: RendererOptions;
+  pageId: string;
+  revisionId: Ref<IRevision>;
+  onRevisionLoaded?: (revision: IRevisionHasId) => void;
+};
 
 const logger = loggerFactory('growi:Page:RevisionLoader');
 
@@ -28,11 +26,13 @@ const logger = loggerFactory('growi:Page:RevisionLoader');
 export const RevisionLoader = (props: RevisionLoaderProps): JSX.Element => {
   const { t } = useTranslation();
 
-  const {
-    rendererOptions, pageId, revisionId, onRevisionLoaded,
-  } = props;
+  const { rendererOptions, pageId, revisionId, onRevisionLoaded } = props;
 
-  const { data: pageRevision, isLoading, error } = useSWRxPageRevision(pageId, revisionId);
+  const {
+    data: pageRevision,
+    isLoading,
+    error,
+  } = useSWRxPageRevision(pageId, revisionId);
 
   const [markdown, setMarkdown] = useState<string>('');
 
@@ -44,16 +44,16 @@ export const RevisionLoader = (props: RevisionLoaderProps): JSX.Element => {
         onRevisionLoaded(pageRevision);
       }
     }
-
   }, [onRevisionLoaded, pageRevision]);
 
   useEffect(() => {
     if (error != null) {
       const isForbidden = error != null && error[0].code === 'forbidden-page';
       if (isForbidden) {
-        setMarkdown(`<span className="material-symbols-outlined p-1">cancel</span>${t('not_allowed_to_see_this_page')}`);
-      }
-      else {
+        setMarkdown(
+          `<span className="material-symbols-outlined p-1">cancel</span>${t('not_allowed_to_see_this_page')}`,
+        );
+      } else {
         const errorMessages = error.map((error) => {
           return `<span className="material-symbols-outlined p-1">cancel</span><span class="text-muted"><em>${error.message}</em></span>`;
         });
@@ -73,9 +73,6 @@ export const RevisionLoader = (props: RevisionLoaderProps): JSX.Element => {
   }
 
   return (
-    <RevisionRenderer
-      rendererOptions={rendererOptions}
-      markdown={markdown}
-    />
+    <RevisionRenderer rendererOptions={rendererOptions} markdown={markdown} />
   );
 };

+ 2 - 5
apps/app/src/client/components/Page/SlideRenderer.tsx

@@ -1,19 +1,16 @@
 import type { JSX } from 'react';
-
 import type { Options as ReactMarkdownOptions } from 'react-markdown';
 
 import { usePresentationViewOptions } from '~/stores/renderer';
 
 import { Slides } from '../Presentation/Slides';
 
-
 type SlideRendererProps = {
-  markdown: string,
-  marp?: boolean,
+  markdown: string;
+  marp?: boolean;
 };
 
 export const SlideRenderer = (props: SlideRendererProps): JSX.Element => {
-
   const { markdown, marp = false } = props;
 
   const { data: rendererOptions } = usePresentationViewOptions();

+ 6 - 1
apps/app/src/client/components/Page/markdown-drawio-util-for-view.ts

@@ -1,7 +1,12 @@
 /**
  * return markdown where the drawioData specified by line number params is replaced to the drawioData specified by drawioData param
  */
-export const replaceDrawioInMarkdown = (drawioData: string, markdown: string, beginLineNumber: number, endLineNumber: number): string => {
+export const replaceDrawioInMarkdown = (
+  drawioData: string,
+  markdown: string,
+  beginLineNumber: number,
+  endLineNumber: number,
+): string => {
   const splitMarkdown = markdown.split(/\r\n|\r|\n/);
   const markdownBeforeDrawio = splitMarkdown.slice(0, beginLineNumber - 1);
   const markdownAfterDrawio = splitMarkdown.slice(endLineNumber);

+ 17 - 5
apps/app/src/client/components/Page/markdown-table-util-for-view.ts

@@ -1,14 +1,26 @@
 import { MarkdownTable } from '@growi/editor';
 
-export const getMarkdownTableFromLine = (markdown: string, bol: number, eol: number): MarkdownTable => {
-  const tableLines = markdown.split(/\r\n|\r|\n/).slice(bol - 1, eol).join('\n');
+export const getMarkdownTableFromLine = (
+  markdown: string,
+  bol: number,
+  eol: number,
+): MarkdownTable => {
+  const tableLines = markdown
+    .split(/\r\n|\r|\n/)
+    .slice(bol - 1, eol)
+    .join('\n');
   return MarkdownTable.fromMarkdownString(tableLines);
 };
 
 /**
-   * return markdown where the markdown table specified by line number params is replaced to the markdown table specified by table param
-   */
-export const replaceMarkdownTableInMarkdown = (table: MarkdownTable, markdown: string, beginLineNumber: number, endLineNumber: number): string => {
+ * return markdown where the markdown table specified by line number params is replaced to the markdown table specified by table param
+ */
+export const replaceMarkdownTableInMarkdown = (
+  table: MarkdownTable,
+  markdown: string,
+  beginLineNumber: number,
+  endLineNumber: number,
+): string => {
   const splitMarkdown = markdown.split(/\r\n|\r|\n/);
   const markdownBeforeTable = splitMarkdown.slice(0, beginLineNumber - 1);
   const markdownAfterTable = splitMarkdown.slice(endLineNumber);

+ 31 - 30
apps/app/src/client/components/PageAttachment/DeleteAttachmentModal.tsx

@@ -1,16 +1,15 @@
-import React, {
-  useCallback, useMemo, useState,
-} from 'react';
-
+import type React from 'react';
+import { useCallback, useMemo, useState } from 'react';
 import type { IAttachmentHasId } from '@growi/core';
-import { UserPicture, LoadingSpinner } from '@growi/ui/dist/components';
+import { LoadingSpinner, UserPicture } from '@growi/ui/dist/components';
 import { useTranslation } from 'next-i18next';
-import {
-  Button, Modal, ModalHeader, ModalBody, ModalFooter,
-} from 'reactstrap';
+import { Button, Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap';
 
-import { toastSuccess, toastError } from '~/client/util/toastr';
-import { useDeleteAttachmentModalStatus, useDeleteAttachmentModalActions } from '~/states/ui/modal/delete-attachment';
+import { toastError, toastSuccess } from '~/client/util/toastr';
+import {
+  useDeleteAttachmentModalActions,
+  useDeleteAttachmentModalStatus,
+} from '~/states/ui/modal/delete-attachment';
 import loggerFactory from '~/utils/logger';
 
 import { Username } from '../../../components/User/Username';
@@ -48,7 +47,7 @@ const DeleteAttachmentModalSubstance = ({
     setDeleteError('');
   }, [closeModal]);
 
-  const onClickDeleteButtonHandler = useCallback(async() => {
+  const onClickDeleteButtonHandler = useCallback(async () => {
     if (remove == null || attachment == null) {
       return;
     }
@@ -60,8 +59,7 @@ const DeleteAttachmentModalSubstance = ({
       setDeleting(false);
       closeModal();
       toastSuccess(`Delete ${attachment.originalName}`);
-    }
-    catch (err) {
+    } catch (err) {
       setDeleting(false);
       setDeleteError('Attachment could not be deleted.');
       toastError(err);
@@ -74,18 +72,25 @@ const DeleteAttachmentModalSubstance = ({
       return;
     }
 
-    const content = (attachment.fileFormat.match(/image\/.+/i))
+    const content = attachment.fileFormat.match(/image\/.+/i) ? (
       // eslint-disable-next-line @next/next/no-img-element
-      ? <img src={attachment.filePathProxied} alt="deleting image" />
-      : '';
+      <img src={attachment.filePathProxied} alt="deleting attachment" />
+    ) : (
+      ''
+    );
 
     return (
       <div className="attachment-delete-image">
         <p>
-          <span className="material-symbols-outlined">{iconByFormat(attachment.fileFormat)}</span> {attachment.originalName}
+          <span className="material-symbols-outlined">
+            {iconByFormat(attachment.fileFormat)}
+          </span>{' '}
+          {attachment.originalName}
         </p>
         <p>
-          uploaded by <UserPicture user={attachment.creator} size="sm"></UserPicture> <Username user={attachment.creator}></Username>
+          uploaded by{' '}
+          <UserPicture user={attachment.creator} size="sm"></UserPicture>{' '}
+          <Username user={attachment.creator}></Username>
         </p>
         {content}
       </div>
@@ -105,26 +110,22 @@ const DeleteAttachmentModalSubstance = ({
   return (
     <div>
       <ModalHeader tag="h4" toggle={toggleHandler} className="text-danger">
-        <span id="contained-modal-title-lg">{t('delete_attachment_modal.confirm_delete_attachment')}</span>
+        <span id="contained-modal-title-lg">
+          {t('delete_attachment_modal.confirm_delete_attachment')}
+        </span>
       </ModalHeader>
-      <ModalBody>
-        {attachmentFileFormat}
-      </ModalBody>
+      <ModalBody>{attachmentFileFormat}</ModalBody>
       <ModalFooter>
-        <div className="me-3 d-inline-block">
-          {deletingIndicator}
-        </div>
-        <Button
-          color="outline-neutral-secondary"
-          onClick={toggleHandler}
-        >
+        <div className="me-3 d-inline-block">{deletingIndicator}</div>
+        <Button color="outline-neutral-secondary" onClick={toggleHandler}>
           {t('commons:Cancel')}
         </Button>
         <Button
           color="danger"
           onClick={onClickDeleteButtonHandler}
           disabled={deleting}
-        >{t('commons:Delete')}
+        >
+          {t('commons:Delete')}
         </Button>
       </ModalFooter>
     </div>

+ 7 - 10
apps/app/src/client/components/PageAttachment/PageAttachmentList.tsx

@@ -1,22 +1,20 @@
 import React, { type JSX } from 'react';
-
 import type { IAttachmentHasId } from '@growi/core';
 import { Attachment } from '@growi/ui/dist/components';
 import { useTranslation } from 'next-i18next';
 
 type Props = {
-  attachments: (IAttachmentHasId)[],
-  inUse: { [id: string]: boolean },
-  onAttachmentDeleteClicked: (attachment: IAttachmentHasId) => void,
-  isUserLoggedIn?: boolean,
-}
+  attachments: IAttachmentHasId[];
+  inUse: { [id: string]: boolean };
+  onAttachmentDeleteClicked: (attachment: IAttachmentHasId) => void;
+  isUserLoggedIn?: boolean;
+};
 
 export const PageAttachmentList = (props: Props): JSX.Element => {
   const { t } = useTranslation();
 
-  const {
-    attachments, inUse, onAttachmentDeleteClicked, isUserLoggedIn,
-  } = props;
+  const { attachments, inUse, onAttachmentDeleteClicked, isUserLoggedIn } =
+    props;
 
   if (attachments.length === 0) {
     return <>{t('No_attachments_yet')}</>;
@@ -39,5 +37,4 @@ export const PageAttachmentList = (props: Props): JSX.Element => {
       </ul>
     </div>
   );
-
 };

+ 4 - 1
apps/app/src/client/components/PageAttachment/dynamic.tsx

@@ -10,7 +10,10 @@ export const DeleteAttachmentModalLazyLoaded = (): JSX.Element => {
 
   const DeleteAttachmentModal = useLazyLoader<DeleteAttachmentModalProps>(
     'delete-attachment-modal',
-    () => import('./DeleteAttachmentModal').then(mod => ({ default: mod.DeleteAttachmentModal })),
+    () =>
+      import('./DeleteAttachmentModal').then((mod) => ({
+        default: mod.DeleteAttachmentModal,
+      })),
     status?.isOpened ?? false,
   );
 

+ 231 - 138
apps/app/src/client/components/PageDeleteModal/PageDeleteModal.tsx

@@ -1,32 +1,30 @@
 import type { FC } from 'react';
-import React, {
-  useState, useMemo, useEffect, useCallback,
-} from 'react';
-
+import React, { useCallback, useEffect, useMemo, useState } from 'react';
 import type { IPageInfoForEntity, IPageToDeleteWithMeta } from '@growi/core';
 import { isIPageInfoForEntity } from '@growi/core';
 import { pagePathUtils } from '@growi/core/dist/utils';
 import { useTranslation } from 'next-i18next';
-import {
-  Modal, ModalHeader, ModalBody, ModalFooter,
-} from 'reactstrap';
+import { Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap';
 
 import { apiPost } from '~/client/util/apiv1-client';
 import { apiv3Post } from '~/client/util/apiv3-client';
-import type { IDeleteSinglePageApiv1Result, IDeleteManyPageApiv3Result } from '~/interfaces/page';
-import { usePageDeleteModalStatus, usePageDeleteModalActions } from '~/states/ui/modal/page-delete';
+import type {
+  IDeleteManyPageApiv3Result,
+  IDeleteSinglePageApiv1Result,
+} from '~/interfaces/page';
+import {
+  usePageDeleteModalActions,
+  usePageDeleteModalStatus,
+} from '~/states/ui/modal/page-delete';
 import { useSWRxPageInfoForList } from '~/stores/page-listing';
 import loggerFactory from '~/utils/logger';
 
-
 import ApiErrorMessageList from '../PageManagement/ApiErrorMessageList';
 
 const { isTrashPage } = pagePathUtils;
 
-
 const logger = loggerFactory('growi:cli:PageDeleteModal');
 
-
 const deleteIconAndKey = {
   completely: {
     color: 'danger',
@@ -41,8 +39,14 @@ const deleteIconAndKey = {
 };
 
 // eslint-disable-next-line @typescript-eslint/no-explicit-any
-const isIPageInfoForEntityForDeleteModal = (pageInfo: any | undefined): pageInfo is IPageInfoForEntity => {
-  return pageInfo != null && 'isDeletable' in pageInfo && 'isAbleToDeleteCompletely' in pageInfo;
+const isIPageInfoForEntityForDeleteModal = (
+  pageInfo: any | undefined,
+): pageInfo is IPageInfoForEntity => {
+  return (
+    pageInfo != null &&
+    'isDeletable' in pageInfo &&
+    'isAbleToDeleteCompletely' in pageInfo
+  );
 };
 
 export const PageDeleteModal: FC = () => {
@@ -51,56 +55,78 @@ export const PageDeleteModal: FC = () => {
   const { close: closeDeleteModal } = usePageDeleteModalActions();
 
   // Optimize deps: use page IDs and length instead of pages array reference
-  const pageIds = useMemo(() => pages?.map(p => p.data._id) ?? [], [pages]);
+  const pageIds = useMemo(() => pages?.map((p) => p.data._id) ?? [], [pages]);
   const pagesLength = pages?.length ?? 0;
 
-  const notOperatablePages: IPageToDeleteWithMeta[] = useMemo(() => (pages ?? []).filter(p => !isIPageInfoForEntityForDeleteModal(p.meta)),
+  // biome-ignore lint/correctness/useExhaustiveDependencies: keep optimized deps
+  const notOperatablePages: IPageToDeleteWithMeta[] = useMemo(
+    () =>
+      (pages ?? []).filter((p) => !isIPageInfoForEntityForDeleteModal(p.meta)),
     // Optimization: Use pageIds and pagesLength instead of pages array reference to avoid unnecessary re-computation
     // eslint-disable-next-line react-hooks/exhaustive-deps
-    [pageIds, pagesLength]);
+    [pageIds, pagesLength],
+  );
 
-  const notOperatablePageIds = useMemo(() => notOperatablePages.map(p => p.data._id), [notOperatablePages]);
+  const notOperatablePageIds = useMemo(
+    () => notOperatablePages.map((p) => p.data._id),
+    [notOperatablePages],
+  );
 
   const { injectTo } = useSWRxPageInfoForList(notOperatablePageIds);
 
   // inject IPageInfo to operate
-  const injectedPages = useMemo(() => {
-    if (pages != null) {
-      return injectTo(pages);
-    }
-    return null;
-  },
-  // Optimization: Use pageIds and pagesLength instead of pages array reference to avoid unnecessary re-computation
-  // eslint-disable-next-line react-hooks/exhaustive-deps
-  [pageIds, pagesLength, injectTo]);
+  // biome-ignore lint/correctness/useExhaustiveDependencies: keep optimized deps
+  const injectedPages = useMemo(
+    () => {
+      if (pages != null) {
+        return injectTo(pages);
+      }
+      return null;
+    },
+    // Optimization: Use pageIds and pagesLength instead of pages array reference to avoid unnecessary re-computation
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+    [pageIds, pagesLength, injectTo],
+  );
 
   // calculate conditions to delete
   const [isDeletable, isAbleToDeleteCompletely] = useMemo(() => {
     if (injectedPages != null && injectedPages.length > 0) {
-      const isDeletable = injectedPages.every(pageWithMeta => pageWithMeta.meta?.isDeletable);
-      const isAbleToDeleteCompletely = injectedPages.every(pageWithMeta => pageWithMeta.meta?.isAbleToDeleteCompletely);
+      const isDeletable = injectedPages.every(
+        (pageWithMeta) => pageWithMeta.meta?.isDeletable,
+      );
+      const isAbleToDeleteCompletely = injectedPages.every(
+        (pageWithMeta) => pageWithMeta.meta?.isAbleToDeleteCompletely,
+      );
       return [isDeletable, isAbleToDeleteCompletely];
     }
     return [true, true];
   }, [injectedPages]);
 
   // Optimize deps: use page paths for trash detection
-  const pagePaths = useMemo(() => pages?.map(p => p.data?.path ?? '') ?? [],
+  // biome-ignore lint/correctness/useExhaustiveDependencies: keep optimized deps
+  const pagePaths = useMemo(
+    () => pages?.map((p) => p.data?.path ?? '') ?? [],
     // Optimization: Use pageIds and pagesLength instead of pages array reference to avoid unnecessary re-computation
     // eslint-disable-next-line react-hooks/exhaustive-deps
-    [pageIds, pagesLength]);
+    [pageIds, pagesLength],
+  );
 
   // calculate condition to determine modal status
   const forceDeleteCompletelyMode = useMemo(() => {
     if (pagesLength > 0) {
-      return pagePaths.every(path => isTrashPage(path));
+      return pagePaths.every((path) => isTrashPage(path));
     }
     return false;
   }, [pagePaths, pagesLength]);
 
   const [isDeleteRecursively, setIsDeleteRecursively] = useState(true);
-  const [isDeleteCompletely, setIsDeleteCompletely] = useState(forceDeleteCompletelyMode);
-  const deleteMode = forceDeleteCompletelyMode || isDeleteCompletely ? 'completely' : 'temporary';
+  const [isDeleteCompletely, setIsDeleteCompletely] = useState(
+    forceDeleteCompletelyMode,
+  );
+  const deleteMode =
+    forceDeleteCompletelyMode || isDeleteCompletely
+      ? 'completely'
+      : 'temporary';
 
   // eslint-disable-next-line @typescript-eslint/no-unused-vars
   const [errs, setErrs] = useState<Error[] | null>(null);
@@ -128,77 +154,95 @@ export const PageDeleteModal: FC = () => {
     setIsDeleteCompletely(!isDeleteCompletely);
   }, [forceDeleteCompletelyMode, isDeleteCompletely]);
 
-  const deletePage = useCallback(async() => {
-    if (pages == null) {
-      return;
-    }
+  // biome-ignore lint/correctness/useExhaustiveDependencies: keep optimized deps
+  const deletePage = useCallback(
+    async () => {
+      if (pages == null) {
+        return;
+      }
 
-    if (!isDeletable) {
-      logger.error('At least one page is not deletable.');
-      return;
-    }
+      if (!isDeletable) {
+        logger.error('At least one page is not deletable.');
+        return;
+      }
 
-    /*
-     * When multiple pages
-     */
-    if (pages.length > 1) {
-      try {
-        const isRecursively = isDeleteRecursively === true ? true : undefined;
-        const isCompletely = isDeleteCompletely === true ? true : undefined;
-
-        const pageIdToRevisionIdMap = {};
-        pages.forEach((p) => { pageIdToRevisionIdMap[p.data._id] = p.data.revision as string });
-
-        const { data } = await apiv3Post<IDeleteManyPageApiv3Result>('/pages/delete', {
-          pageIdToRevisionIdMap,
-          isRecursively,
-          isCompletely,
-        });
-
-        const onDeleted = opts?.onDeleted;
-        if (onDeleted != null) {
-          onDeleted(data.paths, data.isRecursively, data.isCompletely);
+      /*
+       * When multiple pages
+       */
+      if (pages.length > 1) {
+        try {
+          const isRecursively = isDeleteRecursively === true ? true : undefined;
+          const isCompletely = isDeleteCompletely === true ? true : undefined;
+
+          const pageIdToRevisionIdMap = {};
+          pages.forEach((p) => {
+            pageIdToRevisionIdMap[p.data._id] = p.data.revision as string;
+          });
+
+          const { data } = await apiv3Post<IDeleteManyPageApiv3Result>(
+            '/pages/delete',
+            {
+              pageIdToRevisionIdMap,
+              isRecursively,
+              isCompletely,
+            },
+          );
+
+          const onDeleted = opts?.onDeleted;
+          if (onDeleted != null) {
+            onDeleted(data.paths, data.isRecursively, data.isCompletely);
+          }
+          closeDeleteModal();
+        } catch (err) {
+          setErrs([err]);
         }
-        closeDeleteModal();
-      }
-      catch (err) {
-        setErrs([err]);
-      }
-    }
-    /*
-     * When single page
-     */
-    else {
-      try {
-        const recursively = isDeleteRecursively === true ? true : undefined;
-        const completely = forceDeleteCompletelyMode || isDeleteCompletely ? true : undefined;
-
-        const page = pages[0].data;
-
-        const { path, isRecursively, isCompletely } = await apiPost('/pages.remove', {
-          page_id: page._id,
-          revision_id: page.revision,
-          recursively,
-          completely,
-        }) as IDeleteSinglePageApiv1Result;
-
-        const onDeleted = opts?.onDeleted;
-        if (onDeleted != null) {
-          onDeleted(path, isRecursively, isCompletely);
+      } else {
+        /*
+         * When single page
+         */
+        try {
+          const recursively = isDeleteRecursively === true ? true : undefined;
+          const completely =
+            forceDeleteCompletelyMode || isDeleteCompletely ? true : undefined;
+
+          const page = pages[0].data;
+
+          const { path, isRecursively, isCompletely } = (await apiPost(
+            '/pages.remove',
+            {
+              page_id: page._id,
+              revision_id: page.revision,
+              recursively,
+              completely,
+            },
+          )) as IDeleteSinglePageApiv1Result;
+
+          const onDeleted = opts?.onDeleted;
+          if (onDeleted != null) {
+            onDeleted(path, isRecursively, isCompletely);
+          }
+
+          closeDeleteModal();
+        } catch (err) {
+          setErrs([err]);
         }
-
-        closeDeleteModal();
       }
-      catch (err) {
-        setErrs([err]);
-      }
-    }
-  },
-  // Optimization: Use pageIds and pagesLength instead of pages array reference to avoid unnecessary re-computation
-  // eslint-disable-next-line react-hooks/exhaustive-deps
-  [pageIds, pagesLength, isDeletable, isDeleteRecursively, isDeleteCompletely, forceDeleteCompletelyMode, opts?.onDeleted, closeDeleteModal]);
+    },
+    // Optimization: Use pageIds and pagesLength instead of pages array reference to avoid unnecessary re-computation
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+    [
+      pageIds,
+      pagesLength,
+      isDeletable,
+      isDeleteRecursively,
+      isDeleteCompletely,
+      forceDeleteCompletelyMode,
+      opts?.onDeleted,
+      closeDeleteModal,
+    ],
+  );
 
-  const deleteButtonHandler = useCallback(async() => {
+  const deleteButtonHandler = useCallback(async () => {
     await deletePage();
   }, [deletePage]);
 
@@ -213,9 +257,15 @@ export const PageDeleteModal: FC = () => {
           onChange={changeIsDeleteRecursivelyHandler}
           // disabled // Todo: enable this at https://redmine.weseek.co.jp/issues/82222
         />
-        <label className="form-label form-check-label" htmlFor="deleteRecursively">
-          { t('modal_delete.delete_recursively') }
-          <p className="form-text text-muted mt-0"> { t('modal_delete.recursively') }</p>
+        <label
+          className="form-label form-check-label"
+          htmlFor="deleteRecursively"
+        >
+          {t('modal_delete.delete_recursively')}
+          <p className="form-text text-muted mt-0">
+            {' '}
+            {t('modal_delete.recursively')}
+          </p>
         </label>
       </div>
     );
@@ -233,19 +283,30 @@ export const PageDeleteModal: FC = () => {
           checked={isDeleteCompletely}
           onChange={changeIsDeleteCompletelyHandler}
         />
-        <label className="form-label form-check-label" htmlFor="deleteCompletely">
-          { t('modal_delete.delete_completely')}
-          <p className="form-text text-muted mt-0"> { t('modal_delete.completely') }</p>
+        <label
+          className="form-label form-check-label"
+          htmlFor="deleteCompletely"
+        >
+          {t('modal_delete.delete_completely')}
+          <p className="form-text text-muted mt-0">
+            {' '}
+            {t('modal_delete.completely')}
+          </p>
         </label>
-        {!isAbleToDeleteCompletely
-        && (
+        {!isAbleToDeleteCompletely && (
           <p className="alert alert-warning p-2 my-0">
-            <span className="material-symbols-outlined">block</span>{ t('modal_delete.delete_completely_restriction') }
+            <span className="material-symbols-outlined">block</span>
+            {t('modal_delete.delete_completely_restriction')}
           </p>
         )}
       </div>
     );
-  }, [isAbleToDeleteCompletely, isDeleteCompletely, changeIsDeleteCompletelyHandler, t]);
+  }, [
+    isAbleToDeleteCompletely,
+    isDeleteCompletely,
+    changeIsDeleteCompletelyHandler,
+    t,
+  ]);
 
   const headerContent = useMemo(() => {
     if (!isOpened) {
@@ -253,43 +314,73 @@ export const PageDeleteModal: FC = () => {
     }
 
     return (
-      <span className={`text-${deleteIconAndKey[deleteMode].color} d-flex align-items-center`}>
-        <span className="material-symbols-outlined me-1">{deleteIconAndKey[deleteMode].icon}</span>
-        <b>{ t(`modal_delete.delete_${deleteIconAndKey[deleteMode].translationKey}`) }</b>
+      <span
+        className={`text-${deleteIconAndKey[deleteMode].color} d-flex align-items-center`}
+      >
+        <span className="material-symbols-outlined me-1">
+          {deleteIconAndKey[deleteMode].icon}
+        </span>
+        <b>
+          {t(
+            `modal_delete.delete_${deleteIconAndKey[deleteMode].translationKey}`,
+          )}
+        </b>
       </span>
     );
   }, [isOpened, deleteMode, t]);
 
+  // biome-ignore lint/correctness/useExhaustiveDependencies: keep optimized deps
   const bodyContent = useMemo(() => {
     if (!isOpened) {
       return <></>;
     }
 
     // Render page paths to delete inline for better performance
-    const renderingPages = injectedPages != null && injectedPages.length > 0 ? injectedPages : pages;
-    const pagePathsElements = renderingPages != null ? renderingPages.map(page => (
-      <p key={page.data._id} className="mb-1">
-        <code>{ page.data.path }</code>
-        { isIPageInfoForEntity(page.meta)
-          && !page.meta.isDeletable
-          && <span className="ms-3 text-danger"><strong>(CAN NOT TO DELETE)</strong></span> }
-      </p>
-    )) : <></>;
+    const renderingPages =
+      injectedPages != null && injectedPages.length > 0 ? injectedPages : pages;
+    const pagePathsElements =
+      renderingPages != null ? (
+        renderingPages.map((page) => (
+          <p key={page.data._id} className="mb-1">
+            <code>{page.data.path}</code>
+            {isIPageInfoForEntity(page.meta) && !page.meta.isDeletable && (
+              <span className="ms-3 text-danger">
+                <strong>(CAN NOT TO DELETE)</strong>
+              </span>
+            )}
+          </p>
+        ))
+      ) : (
+        <></>
+      );
 
     return (
       <>
         <div className="grw-scrollable-modal-body pb-1">
-          <label className="form-label">{ t('modal_delete.deleting_page') }:</label><br />
+          <span className="form-label">{t('modal_delete.deleting_page')}:</span>
+          <br />
           {/* Todo: change the way to show path on modal when too many pages are selected */}
           {pagePathsElements}
         </div>
-        { isDeletable && renderDeleteRecursivelyForm()}
-        { isDeletable && !forceDeleteCompletelyMode && renderDeleteCompletelyForm() }
+        {isDeletable && renderDeleteRecursivelyForm()}
+        {isDeletable &&
+          !forceDeleteCompletelyMode &&
+          renderDeleteCompletelyForm()}
       </>
     );
     // Optimization: Use direct dependencies instead of JSX.Element reference for better performance
     // eslint-disable-next-line react-hooks/exhaustive-deps
-  }, [isOpened, t, pageIds, pagesLength, injectedPages, isDeletable, renderDeleteRecursivelyForm, forceDeleteCompletelyMode, renderDeleteCompletelyForm]);
+  }, [
+    isOpened,
+    t,
+    pageIds,
+    pagesLength,
+    injectedPages,
+    isDeletable,
+    renderDeleteRecursivelyForm,
+    forceDeleteCompletelyMode,
+    renderDeleteCompletelyForm,
+  ]);
 
   const footerContent = useMemo(() => {
     if (!isOpened) {
@@ -306,25 +397,27 @@ export const PageDeleteModal: FC = () => {
           onClick={deleteButtonHandler}
           data-testid="delete-page-button"
         >
-          <span className="material-symbols-outlined me-1" aria-hidden="true">{deleteIconAndKey[deleteMode].icon}</span>
-          { t(`modal_delete.delete_${deleteIconAndKey[deleteMode].translationKey}`) }
+          <span className="material-symbols-outlined me-1" aria-hidden="true">
+            {deleteIconAndKey[deleteMode].icon}
+          </span>
+          {t(
+            `modal_delete.delete_${deleteIconAndKey[deleteMode].translationKey}`,
+          )}
         </button>
       </>
     );
   }, [isOpened, errs, deleteMode, isDeletable, deleteButtonHandler, t]);
 
   return (
-    <Modal size="lg" isOpen={isOpened} toggle={closeDeleteModal} data-testid="page-delete-modal">
-      <ModalHeader toggle={closeDeleteModal}>
-        {headerContent}
-      </ModalHeader>
-      <ModalBody>
-        {bodyContent}
-      </ModalBody>
-      <ModalFooter>
-        {footerContent}
-      </ModalFooter>
+    <Modal
+      size="lg"
+      isOpen={isOpened}
+      toggle={closeDeleteModal}
+      data-testid="page-delete-modal"
+    >
+      <ModalHeader toggle={closeDeleteModal}>{headerContent}</ModalHeader>
+      <ModalBody>{bodyContent}</ModalBody>
+      <ModalFooter>{footerContent}</ModalFooter>
     </Modal>
-
   );
 };

+ 4 - 1
apps/app/src/client/components/PageDeleteModal/dynamic.tsx

@@ -10,7 +10,10 @@ export const PageDeleteModalLazyLoaded = (): JSX.Element => {
 
   const PageDeleteModal = useLazyLoader<PageDeleteModalProps>(
     'page-delete-modal',
-    () => import('./PageDeleteModal').then(mod => ({ default: mod.PageDeleteModal })),
+    () =>
+      import('./PageDeleteModal').then((mod) => ({
+        default: mod.PageDeleteModal,
+      })),
     status?.isOpened ?? false,
   );
 

+ 144 - 84
apps/app/src/client/components/PageDuplicateModal/PageDuplicateModal.tsx

@@ -1,19 +1,18 @@
-import React, {
-  useState, useEffect, useCallback, useMemo,
-} from 'react';
-
+import type React from 'react';
+import { useCallback, useEffect, useMemo, useState } from 'react';
 import { useAtomValue } from 'jotai';
 import { useTranslation } from 'next-i18next';
-import {
-  Modal, ModalHeader, ModalBody, ModalFooter,
-} from 'reactstrap';
+import { Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap';
 import { debounce } from 'throttle-debounce';
 
 import { apiv3Get, apiv3Post } from '~/client/util/apiv3-client';
 import { toastError } from '~/client/util/toastr';
 import { useSiteUrl } from '~/states/global';
 import { isSearchServiceReachableAtom } from '~/states/server-configurations';
-import { usePageDuplicateModalStatus, usePageDuplicateModalActions } from '~/states/ui/modal/page-duplicate';
+import {
+  usePageDuplicateModalActions,
+  usePageDuplicateModalStatus,
+} from '~/states/ui/modal/page-duplicate';
 
 import DuplicatePathsTable from '../DuplicatedPathsTable';
 import ApiErrorMessageList from '../PageManagement/ApiErrorMessageList';
@@ -38,16 +37,32 @@ const PageDuplicateModalSubstance: React.FC = () => {
   const [subordinatedPages, setSubordinatedPages] = useState([]);
   const [existingPaths, setExistingPaths] = useState<string[]>([]);
   const [isDuplicateRecursively, setIsDuplicateRecursively] = useState(true);
-  const [isDuplicateRecursivelyWithoutExistPath, setIsDuplicateRecursivelyWithoutExistPath] = useState(true);
-  const [onlyDuplicateUserRelatedResources, setOnlyDuplicateUserRelatedResources] = useState(false);
+  const [
+    isDuplicateRecursivelyWithoutExistPath,
+    setIsDuplicateRecursivelyWithoutExistPath,
+  ] = useState(true);
+  const [
+    onlyDuplicateUserRelatedResources,
+    setOnlyDuplicateUserRelatedResources,
+  ] = useState(false);
 
   // Memoize computed values
-  const isTargetPageDuplicate = useMemo(() => existingPaths.includes(pageNameInput), [existingPaths, pageNameInput]);
-  const submitButtonEnabled = useMemo(() => (
-    existingPaths.length === 0 || (isDuplicateRecursively && isDuplicateRecursivelyWithoutExistPath)
-  ), [existingPaths.length, isDuplicateRecursively, isDuplicateRecursivelyWithoutExistPath]);
+  const isTargetPageDuplicate = useMemo(
+    () => existingPaths.includes(pageNameInput),
+    [existingPaths, pageNameInput],
+  );
+  const submitButtonEnabled = useMemo(
+    () =>
+      existingPaths.length === 0 ||
+      (isDuplicateRecursively && isDuplicateRecursivelyWithoutExistPath),
+    [
+      existingPaths.length,
+      isDuplicateRecursively,
+      isDuplicateRecursivelyWithoutExistPath,
+    ],
+  );
 
-  const updateSubordinatedList = useCallback(async() => {
+  const updateSubordinatedList = useCallback(async () => {
     if (page == null) {
       return;
     }
@@ -56,28 +71,32 @@ const PageDuplicateModalSubstance: React.FC = () => {
     try {
       const res = await apiv3Get('/pages/subordinated-list', { path });
       setSubordinatedPages(res.data.subordinatedPages);
-    }
-    catch (err) {
+    } catch (err) {
       setErrs(err);
       toastError(t('modal_duplicate.label.Failed to get subordinated pages'));
     }
   }, [page, t]);
 
-  const checkExistPaths = useCallback(async(fromPath, toPath) => {
-    if (page == null) {
-      return;
-    }
+  const checkExistPaths = useCallback(
+    async (fromPath, toPath) => {
+      if (page == null) {
+        return;
+      }
 
-    try {
-      const res = await apiv3Get<{ existPaths: string[] }>('/page/exist-paths', { fromPath, toPath });
-      const { existPaths } = res.data;
-      setExistingPaths(existPaths);
-    }
-    catch (err) {
-      setErrs(err);
-      toastError(t('modal_rename.label.Failed to get exist path'));
-    }
-  }, [page, t]);
+      try {
+        const res = await apiv3Get<{ existPaths: string[] }>(
+          '/page/exist-paths',
+          { fromPath, toPath },
+        );
+        const { existPaths } = res.data;
+        setExistingPaths(existPaths);
+      } catch (err) {
+        setErrs(err);
+        toastError(t('modal_rename.label.Failed to get exist path'));
+      }
+    },
+    [page, t],
+  );
 
   const checkExistPathsDebounce = useMemo(() => {
     return debounce(1000, checkExistPaths);
@@ -87,7 +106,7 @@ const PageDuplicateModalSubstance: React.FC = () => {
     if (isOpened && page != null && pageNameInput !== page.path) {
       checkExistPathsDebounce(page.path, pageNameInput);
     }
-  }, [isOpened, pageNameInput, subordinatedPages, checkExistPathsDebounce, page]);
+  }, [isOpened, pageNameInput, checkExistPathsDebounce, page]);
 
   const ppacInputChangeHandler = useCallback((value: string) => {
     setErrs(null);
@@ -114,7 +133,7 @@ const PageDuplicateModalSubstance: React.FC = () => {
     }
   }, [isOpened, page, updateSubordinatedList]);
 
-  const duplicate = useCallback(async() => {
+  const duplicate = useCallback(async () => {
     if (page == null) {
       return;
     }
@@ -124,7 +143,10 @@ const PageDuplicateModalSubstance: React.FC = () => {
     const { pageId, path } = page;
     try {
       const { data } = await apiv3Post('/pages/duplicate', {
-        pageId, pageNameInput, isRecursively: isDuplicateRecursively, onlyDuplicateUserRelatedResources,
+        pageId,
+        pageNameInput,
+        isRecursively: isDuplicateRecursively,
+        onlyDuplicateUserRelatedResources,
       });
       const onDuplicated = opts?.onDuplicated;
       const fromPath = path;
@@ -134,11 +156,17 @@ const PageDuplicateModalSubstance: React.FC = () => {
         onDuplicated(fromPath, toPath);
       }
       closeDuplicateModal();
-    }
-    catch (err) {
+    } catch (err) {
       setErrs(err);
     }
-  }, [closeDuplicateModal, opts?.onDuplicated, isDuplicateRecursively, page, pageNameInput, onlyDuplicateUserRelatedResources]);
+  }, [
+    closeDuplicateModal,
+    opts?.onDuplicated,
+    isDuplicateRecursively,
+    page,
+    pageNameInput,
+    onlyDuplicateUserRelatedResources,
+  ]);
 
   useEffect(() => {
     if (isOpened) {
@@ -154,10 +182,8 @@ const PageDuplicateModalSubstance: React.FC = () => {
       setIsDuplicateRecursively(true);
       setIsDuplicateRecursivelyWithoutExistPath(false);
     }, 1000);
-
   }, [isOpened]);
 
-
   const renderBodyContent = () => {
     if (!isOpened || page == null) {
       return <></>;
@@ -167,41 +193,46 @@ const PageDuplicateModalSubstance: React.FC = () => {
 
     return (
       <>
-        <div className="mt-3"><label className="form-label">{t('modal_duplicate.label.Current page name')}</label><br />
+        <div className="mt-3">
+          <span className="form-label">
+            {t('modal_duplicate.label.Current page name')}
+          </span>
+          <br />
           <code>{path}</code>
         </div>
         <div className="mt-3">
-          <label className="form-label" htmlFor="duplicatePageName">{ t('modal_duplicate.label.New page name') }</label><br />
+          <label className="form-label" htmlFor="duplicatePageName">
+            {t('modal_duplicate.label.New page name')}
+          </label>
+          <br />
           <div className="input-group">
             <div>
               <span className="input-group-text">{siteUrl}</span>
             </div>
             <div className="flex-fill">
-              {isReachable
-                ? (
-                  <PagePathAutoComplete
-                    initializedPath={path}
-                    onSubmit={duplicate}
-                    onInputChange={ppacInputChangeHandler}
-                    autoFocus
-                  />
-                )
-                : (
-                  <input
-                    type="text"
-                    value={pageNameInput}
-                    className="form-control"
-                    onChange={e => inputChangeHandler(e.target.value)}
-                    required
-                  />
-                )}
+              {isReachable ? (
+                <PagePathAutoComplete
+                  initializedPath={path}
+                  onSubmit={duplicate}
+                  onInputChange={ppacInputChangeHandler}
+                  autoFocus
+                />
+              ) : (
+                <input
+                  type="text"
+                  value={pageNameInput}
+                  className="form-control"
+                  onChange={(e) => inputChangeHandler(e.target.value)}
+                  required
+                />
+              )}
             </div>
           </div>
         </div>
 
-        { isTargetPageDuplicate && (
+        {isTargetPageDuplicate && (
           <p className="text-danger">Error: Target path is duplicated.</p>
-        ) }
+        )}
 
         <div className="form-check form-check-warning mt-3">
           <input
@@ -212,9 +243,14 @@ const PageDuplicateModalSubstance: React.FC = () => {
             checked={isDuplicateRecursively}
             onChange={changeIsDuplicateRecursivelyHandler}
           />
-          <label className="form-label form-check-label" htmlFor="cbDuplicateRecursively">
-            { t('modal_duplicate.label.Recursively') }
-            <p className="form-text text-muted my-0">{ t('modal_duplicate.help.recursive') }</p>
+          <label
+            className="form-label form-check-label"
+            htmlFor="cbDuplicateRecursively"
+          >
+            {t('modal_duplicate.label.Recursively')}
+            <p className="form-text text-muted my-0">
+              {t('modal_duplicate.help.recursive')}
+            </p>
           </label>
 
           <div className="mt-3">
@@ -226,11 +262,20 @@ const PageDuplicateModalSubstance: React.FC = () => {
                   id="cbDuplicatewithoutExistRecursively"
                   type="checkbox"
                   checked={isDuplicateRecursivelyWithoutExistPath}
-                  onChange={() => setIsDuplicateRecursivelyWithoutExistPath(!isDuplicateRecursivelyWithoutExistPath)}
+                  onChange={() =>
+                    setIsDuplicateRecursivelyWithoutExistPath(
+                      !isDuplicateRecursivelyWithoutExistPath,
+                    )
+                  }
                 />
-                <label className="form-label form-check-label" htmlFor="cbDuplicatewithoutExistRecursively">
-                  { t('modal_duplicate.label.Duplicate without exist path') }
-                  <p className="form-text text-muted my-0">{ t('modal_duplicate.help.recursive') }</p>
+                <label
+                  className="form-label form-check-label"
+                  htmlFor="cbDuplicatewithoutExistRecursively"
+                >
+                  {t('modal_duplicate.label.Duplicate without exist path')}
+                  <p className="form-text text-muted my-0">
+                    {t('modal_duplicate.help.recursive')}
+                  </p>
                 </label>
               </div>
             )}
@@ -243,17 +288,30 @@ const PageDuplicateModalSubstance: React.FC = () => {
             id="cbOnlyDuplicateUserRelatedResources"
             type="checkbox"
             checked={onlyDuplicateUserRelatedResources}
-            onChange={() => setOnlyDuplicateUserRelatedResources(!onlyDuplicateUserRelatedResources)}
+            onChange={() =>
+              setOnlyDuplicateUserRelatedResources(
+                !onlyDuplicateUserRelatedResources,
+              )
+            }
           />
-          <label className="form-label form-check-label" htmlFor="cbOnlyDuplicateUserRelatedResources">
-            { t('modal_duplicate.label.Only duplicate user related pages') }
-            <p className="form-text text-muted my-0">{ t('modal_duplicate.help.only_inherit_user_related_groups') }</p>
+          <label
+            className="form-label form-check-label"
+            htmlFor="cbOnlyDuplicateUserRelatedResources"
+          >
+            {t('modal_duplicate.label.Only duplicate user related pages')}
+            <p className="form-text text-muted my-0">
+              {t('modal_duplicate.help.only_inherit_user_related_groups')}
+            </p>
           </label>
         </div>
         <div className="mt-3">
           {isDuplicateRecursively && existingPaths.length !== 0 && (
-            <DuplicatePathsTable existingPaths={existingPaths} fromPath={path} toPath={pageNameInput} />
-          ) }
+            <DuplicatePathsTable
+              existingPaths={existingPaths}
+              fromPath={path}
+              toPath={pageNameInput}
+            />
+          )}
         </div>
       </>
     );
@@ -274,24 +332,19 @@ const PageDuplicateModalSubstance: React.FC = () => {
           onClick={duplicate}
           disabled={!submitButtonEnabled}
         >
-          { t('modal_duplicate.label.Duplicate page') }
+          {t('modal_duplicate.label.Duplicate page')}
         </button>
       </>
     );
   };
 
-
   return (
     <>
       <ModalHeader tag="h4" toggle={closeDuplicateModal}>
-        { t('modal_duplicate.label.Duplicate page') }
+        {t('modal_duplicate.label.Duplicate page')}
       </ModalHeader>
-      <ModalBody>
-        {renderBodyContent()}
-      </ModalBody>
-      <ModalFooter>
-        {renderFooterContent()}
-      </ModalFooter>
+      <ModalBody>{renderBodyContent()}</ModalBody>
+      <ModalFooter>{renderFooterContent()}</ModalFooter>
     </>
   );
 };
@@ -304,7 +357,14 @@ export const PageDuplicateModal = (): React.JSX.Element => {
   const { close: closeDuplicateModal } = usePageDuplicateModalActions();
 
   return (
-    <Modal size="lg" isOpen={isOpened} toggle={closeDuplicateModal} data-testid="page-duplicate-modal" className="grw-duplicate-page" autoFocus={false}>
+    <Modal
+      size="lg"
+      isOpen={isOpened}
+      toggle={closeDuplicateModal}
+      data-testid="page-duplicate-modal"
+      className="grw-duplicate-page"
+      autoFocus={false}
+    >
       {isOpened && <PageDuplicateModalSubstance />}
     </Modal>
   );

+ 4 - 1
apps/app/src/client/components/PageDuplicateModal/dynamic.tsx

@@ -10,7 +10,10 @@ export const PageDuplicateModalLazyLoaded = (): JSX.Element => {
 
   const PageDuplicateModal = useLazyLoader<PageDuplicateModalProps>(
     'page-duplicate-modal',
-    () => import('./PageDuplicateModal').then(mod => ({ default: mod.PageDuplicateModal })),
+    () =>
+      import('./PageDuplicateModal').then((mod) => ({
+        default: mod.PageDuplicateModal,
+      })),
     status?.isOpened ?? false,
   );
 

+ 15 - 14
apps/app/src/client/components/PageList/PageList.tsx

@@ -1,5 +1,4 @@
 import React, { type JSX } from 'react';
-
 import type { IPageInfoForEntity, IPageWithMeta } from '@growi/core';
 import { LoadingSpinner } from '@growi/ui/dist/components';
 import { useTranslation } from 'next-i18next';
@@ -7,24 +6,28 @@ import { useTranslation } from 'next-i18next';
 import type { OnDeletedFunction, OnPutBackedFunction } from '~/interfaces/ui';
 
 import type { ForceHideMenuItems } from '../Common/Dropdown/PageItemControl';
-
 import { PageListItemL } from './PageListItemL';
 
 import styles from './PageList.module.scss';
 
 type Props<M extends IPageInfoForEntity> = {
-  pages: IPageWithMeta<M>[],
-  isEnableActions?: boolean,
-  isReadOnlyUser: boolean,
-  forceHideMenuItems?: ForceHideMenuItems,
-  onPagesDeleted?: OnDeletedFunction,
-  onPagePutBacked?: OnPutBackedFunction,
-}
+  pages: IPageWithMeta<M>[];
+  isEnableActions?: boolean;
+  isReadOnlyUser: boolean;
+  forceHideMenuItems?: ForceHideMenuItems;
+  onPagesDeleted?: OnDeletedFunction;
+  onPagePutBacked?: OnPutBackedFunction;
+};
 
 const PageList = (props: Props<IPageInfoForEntity>): JSX.Element => {
   const { t } = useTranslation();
   const {
-    pages, isEnableActions, isReadOnlyUser, forceHideMenuItems, onPagesDeleted, onPagePutBacked,
+    pages,
+    isEnableActions,
+    isReadOnlyUser,
+    forceHideMenuItems,
+    onPagesDeleted,
+    onPagePutBacked,
   } = props;
 
   if (pages == null) {
@@ -37,7 +40,7 @@ const PageList = (props: Props<IPageInfoForEntity>): JSX.Element => {
     );
   }
 
-  const pageList = pages.map(page => (
+  const pageList = pages.map((page) => (
     <PageListItemL
       key={page.data._id}
       page={page}
@@ -59,9 +62,7 @@ const PageList = (props: Props<IPageInfoForEntity>): JSX.Element => {
 
   return (
     <div className={`page-list ${styles['page-list']}`}>
-      <ul className="page-list-ul list-group list-group-flush">
-        {pageList}
-      </ul>
+      <ul className="page-list-ul list-group list-group-flush">{pageList}</ul>
     </div>
   );
 };

+ 212 - 134
apps/app/src/client/components/PageList/PageListItemL.tsx

@@ -1,28 +1,38 @@
 import type { ForwardRefRenderFunction, JSX } from 'react';
 import React, {
-  forwardRef, useState, memo, useCallback, useImperativeHandle, useRef, useEffect,
+  forwardRef,
+  memo,
+  useCallback,
+  useEffect,
+  useImperativeHandle,
+  useRef,
+  useState,
 } from 'react';
-
+import Link from 'next/link';
 import type {
-  IPageInfoExt, IPageWithMeta, IPageInfoForListing,
+  IPageInfoExt,
+  IPageInfoForListing,
+  IPageWithMeta,
 } from '@growi/core';
-import { isIPageInfoForListing, isIPageInfoForEntity } from '@growi/core';
+import { isIPageInfoForEntity, isIPageInfoForListing } from '@growi/core';
 import { DevidedPagePath } from '@growi/core/dist/models';
 import { pathUtils } from '@growi/core/dist/utils';
-import { UserPicture, PageListMeta } from '@growi/ui/dist/components';
+import { PageListMeta, UserPicture } from '@growi/ui/dist/components';
 import { format } from 'date-fns/format';
 import { useTranslation } from 'next-i18next';
-import Link from 'next/link';
 import Clamp from 'react-multiline-clamp';
 import { Input } from 'reactstrap';
 
 import type { ISelectable } from '~/client/interfaces/selectable-all';
-import { unlink, bookmark, unbookmark } from '~/client/services/page-operation';
+import { bookmark, unbookmark, unlink } from '~/client/services/page-operation';
 import { toastError } from '~/client/util/toastr';
 import type { IPageSearchMeta, IPageWithSearchMeta } from '~/interfaces/search';
 import { isIPageSearchMeta } from '~/interfaces/search';
 import type {
-  OnDuplicatedFunction, OnRenamedFunction, OnDeletedFunction, OnPutBackedFunction,
+  OnDeletedFunction,
+  OnDuplicatedFunction,
+  OnPutBackedFunction,
+  OnRenamedFunction,
 } from '~/interfaces/ui';
 import LinkedPagePath from '~/models/linked-page-path';
 import { useDeviceLargerThanLg } from '~/states/ui/device';
@@ -38,32 +48,47 @@ import type { ForceHideMenuItems } from '../Common/Dropdown/PageItemControl';
 import { PageItemControl } from '../Common/Dropdown/PageItemControl';
 
 type Props = {
-  page: IPageWithSearchMeta | IPageWithMeta<IPageInfoForListing & IPageSearchMeta>,
-  isSelected?: boolean, // is item selected(focused)
-  isEnableActions?: boolean,
-  isReadOnlyUser: boolean,
-  forceHideMenuItems?: ForceHideMenuItems,
-  showPageUpdatedTime?: boolean, // whether to show page's updated time at the top-right corner of item
-  onCheckboxChanged?: (isChecked: boolean, pageId: string) => void,
-  onClickItem?: (pageId: string) => void,
-  onPageDuplicated?: OnDuplicatedFunction,
-  onPageRenamed?: OnRenamedFunction,
-  onPageDeleted?: OnDeletedFunction,
-  onPagePutBacked?: OnPutBackedFunction,
-}
-
-const PageListItemLSubstance: ForwardRefRenderFunction<ISelectable, Props> = (props: Props, ref): JSX.Element => {
+  page:
+    | IPageWithSearchMeta
+    | IPageWithMeta<IPageInfoForListing & IPageSearchMeta>;
+  isSelected?: boolean; // is item selected(focused)
+  isEnableActions?: boolean;
+  isReadOnlyUser: boolean;
+  forceHideMenuItems?: ForceHideMenuItems;
+  showPageUpdatedTime?: boolean; // whether to show page's updated time at the top-right corner of item
+  onCheckboxChanged?: (isChecked: boolean, pageId: string) => void;
+  onClickItem?: (pageId: string) => void;
+  onPageDuplicated?: OnDuplicatedFunction;
+  onPageRenamed?: OnRenamedFunction;
+  onPageDeleted?: OnDeletedFunction;
+  onPagePutBacked?: OnPutBackedFunction;
+};
+
+const PageListItemLSubstance: ForwardRefRenderFunction<ISelectable, Props> = (
+  props: Props,
+  ref,
+): JSX.Element => {
   const {
-    page: { data: pageData, meta: pageMeta }, isSelected, isEnableActions, isReadOnlyUser,
+    page: { data: pageData, meta: pageMeta },
+    isSelected,
+    isEnableActions,
+    isReadOnlyUser,
     forceHideMenuItems,
     showPageUpdatedTime,
-    onClickItem, onCheckboxChanged, onPageDuplicated, onPageRenamed, onPageDeleted, onPagePutBacked,
+    onClickItem,
+    onCheckboxChanged,
+    onPageDuplicated,
+    onPageRenamed,
+    onPageDeleted,
+    onPagePutBacked,
   } = props;
 
   const { returnPathForURL } = pathUtils;
 
   const [likerCount, setLikerCount] = useState(pageData.liker.length);
-  const [bookmarkCount, setBookmarkCount] = useState(pageMeta && pageMeta.bookmarkCount ? pageMeta.bookmarkCount : 0);
+  const [bookmarkCount, setBookmarkCount] = useState(
+    pageMeta && pageMeta.bookmarkCount ? pageMeta.bookmarkCount : 0,
+  );
 
   const { t } = useTranslation();
 
@@ -92,20 +117,37 @@ const PageListItemLSubstance: ForwardRefRenderFunction<ISelectable, Props> = (pr
   const { open: openPutBackPageModal } = usePutBackPageModalActions();
 
   const shouldFetch = isSelected && (pageData != null || pageMeta != null);
-  const { data: pageInfo } = useSWRxPageInfo(shouldFetch ? pageData?._id : null);
+  const { data: pageInfo } = useSWRxPageInfo(
+    shouldFetch ? pageData?._id : null,
+  );
   const { trigger: mutatePageInfo } = useSWRMUTxPageInfo(pageData?._id ?? null);
-  const { trigger: mutateCurrentUserBookmarks } = useSWRMUTxCurrentUserBookmarks();
-  const elasticSearchResult = isIPageSearchMeta(pageMeta) ? pageMeta.elasticSearchResult : null;
-  const revisionShortBody = isIPageInfoForListing(pageMeta) ? pageMeta.revisionShortBody : null;
+  const { trigger: mutateCurrentUserBookmarks } =
+    useSWRMUTxCurrentUserBookmarks();
+  const elasticSearchResult = isIPageSearchMeta(pageMeta)
+    ? pageMeta.elasticSearchResult
+    : null;
+  const revisionShortBody = isIPageInfoForListing(pageMeta)
+    ? pageMeta.revisionShortBody
+    : null;
 
   const dPagePath: DevidedPagePath = new DevidedPagePath(pageData.path, false);
   const linkedPagePathFormer = new LinkedPagePath(dPagePath.former);
 
-  const dPagePathHighlighted: DevidedPagePath = new DevidedPagePath(elasticSearchResult?.highlightedPath || pageData.path, true);
-  const linkedPagePathHighlightedFormer = new LinkedPagePath(dPagePathHighlighted.former);
-  const linkedPagePathHighlightedLatter = new LinkedPagePath(dPagePathHighlighted.latter);
+  const dPagePathHighlighted: DevidedPagePath = new DevidedPagePath(
+    elasticSearchResult?.highlightedPath || pageData.path,
+    true,
+  );
+  const linkedPagePathHighlightedFormer = new LinkedPagePath(
+    dPagePathHighlighted.former,
+  );
+  const linkedPagePathHighlightedLatter = new LinkedPagePath(
+    dPagePathHighlighted.latter,
+  );
 
-  const lastUpdateDate = format(new Date(pageData.updatedAt), 'yyyy/MM/dd HH:mm:ss');
+  const lastUpdateDate = format(
+    new Date(pageData.updatedAt),
+    'yyyy/MM/dd HH:mm:ss',
+  );
 
   useEffect(() => {
     if (isIPageInfoForEntity(pageInfo)) {
@@ -128,7 +170,10 @@ const PageListItemLSubstance: ForwardRefRenderFunction<ISelectable, Props> = (pr
     }
   }, [isDeviceLargerThanLg, onClickItem, pageData._id]);
 
-  const bookmarkMenuItemClickHandler = async (_pageId: string, _newValue: boolean): Promise<void> => {
+  const bookmarkMenuItemClickHandler = async (
+    _pageId: string,
+    _newValue: boolean,
+  ): Promise<void> => {
     const bookmarkOperation = _newValue ? bookmark : unbookmark;
     await bookmarkOperation(_pageId);
     mutateCurrentUserBookmarks();
@@ -143,18 +188,23 @@ const PageListItemLSubstance: ForwardRefRenderFunction<ISelectable, Props> = (pr
     openDuplicateModal(page, { onDuplicated: onPageDuplicated });
   }, [onPageDuplicated, openDuplicateModal, pageData._id, pageData.path]);
 
-  const renameMenuItemClickHandler = useCallback((_id: string, pageInfo: IPageInfoExt | undefined) => {
-    const page = { data: pageData, meta: pageInfo };
-    openRenameModal(page, { onRenamed: onPageRenamed });
-  }, [pageData, onPageRenamed, openRenameModal]);
-
+  const renameMenuItemClickHandler = useCallback(
+    (_id: string, pageInfo: IPageInfoExt | undefined) => {
+      const page = { data: pageData, meta: pageInfo };
+      openRenameModal(page, { onRenamed: onPageRenamed });
+    },
+    [pageData, onPageRenamed, openRenameModal],
+  );
 
-  const deleteMenuItemClickHandler = useCallback((_id: string, pageInfo: IPageInfoExt | undefined) => {
-    const pageToDelete = { data: pageData, meta: pageInfo };
+  const deleteMenuItemClickHandler = useCallback(
+    (_id: string, pageInfo: IPageInfoExt | undefined) => {
+      const pageToDelete = { data: pageData, meta: pageInfo };
 
-    // open modal
-    openDeleteModal([pageToDelete], { onDeleted: onPageDeleted });
-  }, [pageData, openDeleteModal, onPageDeleted]);
+      // open modal
+      openDeleteModal([pageToDelete], { onDeleted: onPageDeleted });
+    },
+    [pageData, openDeleteModal, onPageDeleted],
+  );
 
   const revertMenuItemClickHandler = useCallback(async () => {
     const { _id: pageId, path } = pageData;
@@ -163,8 +213,7 @@ const PageListItemLSubstance: ForwardRefRenderFunction<ISelectable, Props> = (pr
       try {
         // pageData path should be `/trash/fuga` (`/trash` should be included to the prefix)
         await unlink(pageData.path);
-      }
-      catch (err) {
+      } catch (err) {
         toastError(err);
       }
 
@@ -176,94 +225,115 @@ const PageListItemLSubstance: ForwardRefRenderFunction<ISelectable, Props> = (pr
     openPutBackPageModal({ pageId, path }, { onPutBacked: putBackedHandler });
   }, [onPagePutBacked, openPutBackPageModal, pageData]);
 
-  const styleListGroupItem = (isDeviceLargerThanLg && onClickItem != null) ? 'list-group-item-action' : '';
+  const styleListGroupItem =
+    isDeviceLargerThanLg && onClickItem != null ? 'list-group-item-action' : '';
   // background color of list item changes when class "active" exists under 'list-group-item'
   const styleActive = isDeviceLargerThanLg && isSelected ? 'active' : '';
 
-  const shouldDangerouslySetInnerHTMLForPaths = elasticSearchResult != null && elasticSearchResult.highlightedPath != null;
+  const shouldDangerouslySetInnerHTMLForPaths =
+    elasticSearchResult != null && elasticSearchResult.highlightedPath != null;
 
-  const canRenderESSnippet = elasticSearchResult != null && elasticSearchResult.snippet != null;
+  const canRenderESSnippet =
+    elasticSearchResult != null && elasticSearchResult.snippet != null;
   const canRenderRevisionSnippet = revisionShortBody != null;
 
   const hasBrowsingRights = canRenderESSnippet || canRenderRevisionSnippet;
 
   return (
-    <li
-      key={pageData._id}
-      className={`list-group-item d-flex align-items-center px-3 px-md-1 ${styleListGroupItem} ${styleActive}`}
-      data-testid="page-list-item-L"
-      onClick={clickHandler}
-    >
-      <div className="text-break w-100">
-        <div className="d-flex">
-          {/* checkbox */}
-          {onCheckboxChanged != null && (
-            <div className="d-flex align-items-center justify-content-center">
-              <Input
-                type="checkbox"
-                id={`cbSelect-${pageData._id}`}
-                data-testid="cb-select"
-                innerRef={inputRef}
-                onChange={(e) => { onCheckboxChanged(e.target.checked, pageData._id) }}
-              />
-            </div>
-          )}
-
-          <div className="flex-grow-1 px-2 px-md-4">
-            <div className="d-flex justify-content-between">
-              {/* page path */}
-              <PagePathHierarchicalLink
-                linkedPagePath={linkedPagePathFormer}
-                linkedPagePathByHtml={linkedPagePathHighlightedFormer}
-              />
-              {showPageUpdatedTime && (
-                <span className="page-list-updated-at text-muted">Last update: {lastUpdateDate}</span>
-              )}
-            </div>
-            <div className="d-flex align-items-center mb-1">
-              {/* Picture */}
-              <span className="me-2 d-none d-md-block">
-                <UserPicture user={pageData.lastUpdateUser} size="md" />
-              </span>
-              {/* page title */}
-              <Clamp lines={1}>
-                <span className="h5 mb-0">
-                  {/* Use permanent links to care for pages with the same name (Cannot use page path url) */}
-                  <span className="text-break">
-                    <Link
-                      legacyBehavior
-                      href={returnPathForURL(pageData.path, pageData._id)}
-                      prefetch={false}
-                    >
-                      {shouldDangerouslySetInnerHTMLForPaths
-                        ? (
+    <li key={pageData._id}>
+      <button
+        type="button"
+        className={`list-group-item d-flex align-items-center px-3 px-md-1 text-start w-100 ${styleListGroupItem} ${styleActive}`}
+        data-testid="page-list-item-L"
+        onClick={clickHandler}
+      >
+        <div className="text-break w-100">
+          <div className="d-flex">
+            {/* checkbox */}
+            {onCheckboxChanged != null && (
+              <div className="d-flex align-items-center justify-content-center">
+                <Input
+                  type="checkbox"
+                  id={`cbSelect-${pageData._id}`}
+                  data-testid="cb-select"
+                  innerRef={inputRef}
+                  onChange={(e) => {
+                    onCheckboxChanged(e.target.checked, pageData._id);
+                  }}
+                />
+              </div>
+            )}
+
+            <div className="flex-grow-1 px-2 px-md-4">
+              <div className="d-flex justify-content-between">
+                {/* page path */}
+                <PagePathHierarchicalLink
+                  linkedPagePath={linkedPagePathFormer}
+                  linkedPagePathByHtml={linkedPagePathHighlightedFormer}
+                />
+                {showPageUpdatedTime && (
+                  <span className="page-list-updated-at text-muted">
+                    Last update: {lastUpdateDate}
+                  </span>
+                )}
+              </div>
+              <div className="d-flex align-items-center mb-1">
+                {/* Picture */}
+                <span className="me-2 d-none d-md-block">
+                  <UserPicture user={pageData.lastUpdateUser} size="md" />
+                </span>
+                {/* page title */}
+                <Clamp lines={1}>
+                  <span className="h5 mb-0">
+                    {/* Use permanent links to care for pages with the same name (Cannot use page path url) */}
+                    <span className="text-break">
+                      <Link
+                        legacyBehavior
+                        href={returnPathForURL(pageData.path, pageData._id)}
+                        prefetch={false}
+                      >
+                        {shouldDangerouslySetInnerHTMLForPaths ? (
                           <a
                             className="page-segment"
+                            href={returnPathForURL(pageData.path, pageData._id)}
                             // eslint-disable-next-line react/no-danger
-                            dangerouslySetInnerHTML={{ __html: linkedPagePathHighlightedLatter.pathName }}
+                            // biome-ignore lint/security/noDangerouslySetInnerHtml: highlight markup is sanitized
+                            dangerouslySetInnerHTML={{
+                              __html: linkedPagePathHighlightedLatter.pathName,
+                            }}
+                          ></a>
+                        ) : (
+                          <a
+                            className="page-segment"
+                            href={returnPathForURL(pageData.path, pageData._id)}
                           >
+                            {linkedPagePathHighlightedLatter.pathName}
                           </a>
-                        )
-                        : <a className="page-segment">{linkedPagePathHighlightedLatter.pathName}</a>
-                      }
-                    </Link>
+                        )}
+                      </Link>
+                    </span>
                   </span>
-                </span>
-              </Clamp>
-
-              {/* page meta */}
-              <div className="d-none d-md-flex py-0 px-1 ms-2 text-nowrap">
-                <PageListMeta page={pageData} likerCount={likerCount} bookmarkCount={bookmarkCount} shouldSpaceOutIcon />
-              </div>
-
-              {/* doropdown icon includes page control buttons */}
-              {hasBrowsingRights
-                && (
+                </Clamp>
+
+                {/* page meta */}
+                <div className="d-none d-md-flex py-0 px-1 ms-2 text-nowrap">
+                  <PageListMeta
+                    page={pageData}
+                    likerCount={likerCount}
+                    bookmarkCount={bookmarkCount}
+                    shouldSpaceOutIcon
+                  />
+                </div>
+
+                {/* doropdown icon includes page control buttons */}
+                {hasBrowsingRights && (
                   <div className="ms-auto">
                     <PageItemControl
                       alignEnd
                       pageId={pageData._id}
-                      pageInfo={isIPageInfoForListing(pageMeta) ? pageMeta : undefined}
+                      pageInfo={
+                        isIPageInfoForListing(pageMeta) ? pageMeta : undefined
+                      }
                       isEnableActions={isEnableActions}
                       isReadOnlyUser={isReadOnlyUser}
                       forceHideMenuItems={forceHideMenuItems}
@@ -274,32 +344,40 @@ const PageListItemLSubstance: ForwardRefRenderFunction<ISelectable, Props> = (pr
                       onClickRevertMenuItem={revertMenuItemClickHandler}
                     />
                   </div>
-                )
-              }
-            </div>
-            <div className="page-list-snippet py-1">
-              <Clamp lines={2}>
-                {elasticSearchResult != null && elasticSearchResult.snippet != null && (
-                  // eslint-disable-next-line react/no-danger
-                  (<div dangerouslySetInnerHTML={{ __html: elasticSearchResult.snippet }}></div>)
-                )}
-                {revisionShortBody != null && (
-                  <div data-testid="revision-short-body-in-page-list-item-L">{revisionShortBody}</div>
                 )}
-                {
-                  !hasBrowsingRights && (
+              </div>
+              <div className="page-list-snippet py-1">
+                <Clamp lines={2}>
+                  {elasticSearchResult != null &&
+                    elasticSearchResult.snippet != null && (
+                      // eslint-disable-next-line react/no-danger
+                      <div
+                        // biome-ignore lint/security/noDangerouslySetInnerHtml: snippet markup is sanitized
+                        dangerouslySetInnerHTML={{
+                          __html: elasticSearchResult.snippet,
+                        }}
+                      ></div>
+                    )}
+                  {revisionShortBody != null && (
+                    <div data-testid="revision-short-body-in-page-list-item-L">
+                      {revisionShortBody}
+                    </div>
+                  )}
+                  {!hasBrowsingRights && (
                     <>
-                      <span className="material-symbols-outlined p-1">error</span>
+                      <span className="material-symbols-outlined p-1">
+                        error
+                      </span>
                       {t('not_allowed_to_see_this_page')}
                     </>
-                  )
-                }
-              </Clamp>
+                  )}
+                </Clamp>
+              </div>
             </div>
           </div>
         </div>
         {/* TODO: adjust snippet position */}
-      </div>
+      </button>
     </li>
   );
 };

+ 23 - 19
apps/app/src/client/components/PageList/PageListItemS.tsx

@@ -1,33 +1,36 @@
 import React, { type JSX } from 'react';
-
-import type { IPageHasId } from '@growi/core';
-import { UserPicture, PageListMeta, PagePathLabel } from '@growi/ui/dist/components';
 import Link from 'next/link';
+import type { IPageHasId } from '@growi/core';
+import {
+  PageListMeta,
+  PagePathLabel,
+  UserPicture,
+} from '@growi/ui/dist/components';
 import Clamp from 'react-multiline-clamp';
 
 import styles from './PageListItemS.module.scss';
 
 type PageListItemSProps = {
-  page: IPageHasId,
-  noLink?: boolean,
-  pageTitle?: string
-  isNarrowView?: boolean,
-}
+  page: IPageHasId;
+  noLink?: boolean;
+  pageTitle?: string;
+  isNarrowView?: boolean;
+};
 
 export const PageListItemS = (props: PageListItemSProps): JSX.Element => {
-
-  const {
-    page,
-    noLink = false,
-    pageTitle,
-    isNarrowView = false,
-  } = props;
+  const { page, noLink = false, pageTitle, isNarrowView = false } = props;
 
   const path = pageTitle != null ? pageTitle : page.path;
 
-  let pagePathElement = <PagePathLabel path={path} additionalClassNames={['mx-1']} />;
+  let pagePathElement = (
+    <PagePathLabel path={path} additionalClassNames={['mx-1']} />
+  );
   if (!noLink) {
-    pagePathElement = <Link href={`/${page._id}`} className="text-break" prefetch={false}>{pagePathElement}</Link>;
+    pagePathElement = (
+      <Link href={`/${page._id}`} className="text-break" prefetch={false}>
+        {pagePathElement}
+      </Link>
+    );
   }
 
   return (
@@ -35,7 +38,9 @@ export const PageListItemS = (props: PageListItemSProps): JSX.Element => {
       <UserPicture user={page.lastUpdateUser} noLink={noLink} />
       {isNarrowView ? (
         <Clamp lines={2}>
-          <div className={`mx-1 ${styles['page-title']} ${noLink ? 'text-break' : ''}`}>
+          <div
+            className={`mx-1 ${styles['page-title']} ${noLink ? 'text-break' : ''}`}
+          >
             {pagePathElement}
           </div>
         </Clamp>
@@ -47,5 +52,4 @@ export const PageListItemS = (props: PageListItemSProps): JSX.Element => {
       </span>
     </>
   );
-
 };

+ 49 - 29
apps/app/src/client/components/PageManagement/ApiErrorMessage.jsx

@@ -1,13 +1,10 @@
 import React from 'react';
-
 import { useTranslation } from 'next-i18next';
 import PropTypes from 'prop-types';
 
 const ApiErrorMessage = (props) => {
   const { t } = useTranslation();
-  const {
-    errorCode, errorMessage, targetPath,
-  } = props;
+  const { errorCode, errorMessage, targetPath } = props;
 
   function reload() {
     window.location.reload();
@@ -18,71 +15,94 @@ const ApiErrorMessage = (props) => {
       case 'already_exists':
         return (
           <>
-            <strong><span className="material-symbols-outlined me-1">cancel</span>{ t('page_api_error.already_exists') }</strong>
-            <small><a href={targetPath}>{targetPath} <span className="material-symbols-outlined me-1">login</span></a></small>
+            <strong>
+              <span className="material-symbols-outlined me-1">cancel</span>
+              {t('page_api_error.already_exists')}
+            </strong>
+            <small>
+              <a href={targetPath}>
+                {targetPath}{' '}
+                <span className="material-symbols-outlined me-1">login</span>
+              </a>
+            </small>
           </>
         );
       case 'notfound_or_forbidden':
         return (
-          <strong><span className="material-symbols-outlined me-1">cancel</span>{ t('page_api_error.notfound_or_forbidden') }</strong>
+          <strong>
+            <span className="material-symbols-outlined me-1">cancel</span>
+            {t('page_api_error.notfound_or_forbidden')}
+          </strong>
         );
       case 'user_not_admin':
         return (
-          <strong><span className="material-symbols-outlined me-1">cancel</span>{ t('page_api_error.user_not_admin') }</strong>
+          <strong>
+            <span className="material-symbols-outlined me-1">cancel</span>
+            {t('page_api_error.user_not_admin')}
+          </strong>
         );
       case 'complete_deletion_not_allowed_for_user':
         return (
-          <strong><span className="material-symbols-outlined me-1">cancel</span>{ t('page_api_error.complete_deletion_not_allowed_for_user') }</strong>
+          <strong>
+            <span className="material-symbols-outlined me-1">cancel</span>
+            {t('page_api_error.complete_deletion_not_allowed_for_user')}
+          </strong>
         );
       case 'outdated':
         return (
           <>
-            <strong><span className="material-symbols-outlined me-1">lightbulb</span> { t('page_api_error.outdated') }</strong>
-            <a className="btn-link" onClick={reload}>
-              <span className="material-symbols-outlined">keyboard_double_arrow_right</span> { t('Load latest') }
-            </a>
+            <strong>
+              <span className="material-symbols-outlined me-1">lightbulb</span>{' '}
+              {t('page_api_error.outdated')}
+            </strong>
+            <button type="button" className="btn-link" onClick={reload}>
+              <span className="material-symbols-outlined">
+                keyboard_double_arrow_right
+              </span>{' '}
+              {t('Load latest')}
+            </button>
           </>
         );
       case 'invalid_path':
         return (
-          <strong><span className="material-symbols-outlined me-1">cancel</span> Invalid path</strong>
+          <strong>
+            <span className="material-symbols-outlined me-1">cancel</span>{' '}
+            Invalid path
+          </strong>
         );
       case 'single_deletion_empty_pages':
         return (
-          <strong><span className="material-symbols-outlined me-1">cancel</span>{ t('page_api_error.single_deletion_empty_pages') }</strong>
+          <strong>
+            <span className="material-symbols-outlined me-1">cancel</span>
+            {t('page_api_error.single_deletion_empty_pages')}
+          </strong>
         );
       default:
         return (
-          <strong><span className="material-symbols-outlined me-1">cancel</span> Unknown error occured</strong>
+          <strong>
+            <span className="material-symbols-outlined me-1">cancel</span>{' '}
+            Unknown error occured
+          </strong>
         );
     }
   }
 
   if (errorCode != null) {
-    return (
-      <span className="text-danger">
-        {renderMessageByErrorCode()}
-      </span>
-    );
+    return <span className="text-danger">{renderMessageByErrorCode()}</span>;
   }
 
   if (errorMessage != null) {
-    return (
-      <span className="text-danger">
-        {errorMessage}
-      </span>
-    );
+    return <span className="text-danger">{errorMessage}</span>;
   }
 
   // render null if no error has occurred
   return null;
-
 };
 
 ApiErrorMessage.propTypes = {
-  errorCode:    PropTypes.string,
+  errorCode: PropTypes.string,
   errorMessage: PropTypes.string,
-  targetPath:   PropTypes.string,
+  targetPath: PropTypes.string,
 };
 
 export default ApiErrorMessage;

+ 10 - 5
apps/app/src/client/components/PageManagement/ApiErrorMessageList.jsx

@@ -1,5 +1,4 @@
 import React from 'react';
-
 import PropTypes from 'prop-types';
 
 import { toArrayIfNot } from '~/utils/array-utils';
@@ -11,15 +10,21 @@ function ApiErrorMessageList(props) {
 
   return (
     <>
-      {errs.map(err => <ApiErrorMessage key={err.code} errorCode={err.code} errorMessage={err.message} targetPath={props.targetPath} />)}
+      {errs.map((err) => (
+        <ApiErrorMessage
+          key={err.code}
+          errorCode={err.code}
+          errorMessage={err.message}
+          targetPath={props.targetPath}
+        />
+      ))}
     </>
   );
-
 }
 
 ApiErrorMessageList.propTypes = {
-  errs:         PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
-  targetPath:   PropTypes.string,
+  errs: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
+  targetPath: PropTypes.string,
 };
 
 export default ApiErrorMessageList;

+ 24 - 11
apps/app/src/client/components/PagePathNavSticky/CollapsedParentsDropdown.tsx

@@ -1,8 +1,10 @@
-import { useMemo, type JSX } from 'react';
-
+import { type JSX, useMemo } from 'react';
 import Link from 'next/link';
 import {
-  DropdownItem, DropdownMenu, DropdownToggle, UncontrolledDropdown,
+  DropdownItem,
+  DropdownMenu,
+  DropdownToggle,
+  UncontrolledDropdown,
 } from 'reactstrap';
 
 import type LinkedPagePath from '~/models/linked-page-path';
@@ -10,11 +12,14 @@ import type LinkedPagePath from '~/models/linked-page-path';
 import styles from './CollapsedParentsDropdown.module.scss';
 
 const getAncestorPathAndPathNames = (linkedPagePath: LinkedPagePath) => {
-  const pathAndPathName: Array<{ path: string, pathName: string }> = [];
+  const pathAndPathName: Array<{ path: string; pathName: string }> = [];
   let currentLinkedPagePath = linkedPagePath;
 
   while (currentLinkedPagePath.parent != null) {
-    pathAndPathName.unshift({ path: currentLinkedPagePath.path, pathName: currentLinkedPagePath.pathName });
+    pathAndPathName.unshift({
+      path: currentLinkedPagePath.path,
+      pathName: currentLinkedPagePath.pathName,
+    });
     currentLinkedPagePath = currentLinkedPagePath.parent;
   }
 
@@ -22,22 +27,30 @@ const getAncestorPathAndPathNames = (linkedPagePath: LinkedPagePath) => {
 };
 
 type Props = {
-  linkedPagePath: LinkedPagePath,
-}
+  linkedPagePath: LinkedPagePath;
+};
 
 export const CollapsedParentsDropdown = (props: Props): JSX.Element => {
   const { linkedPagePath } = props;
 
-  const ancestorPathAndPathNames = useMemo(() => getAncestorPathAndPathNames(linkedPagePath), [linkedPagePath]);
+  const ancestorPathAndPathNames = useMemo(
+    () => getAncestorPathAndPathNames(linkedPagePath),
+    [linkedPagePath],
+  );
 
   return (
     <UncontrolledDropdown className="d-inline-block">
       <DropdownToggle color="transparent">...</DropdownToggle>
-      <DropdownMenu className={`dropdown-menu ${styles['collapsed-parents-dropdown-menu']}`} container="body">
-        {ancestorPathAndPathNames.map(data => (
+      <DropdownMenu
+        className={`dropdown-menu ${styles['collapsed-parents-dropdown-menu']}`}
+        container="body"
+      >
+        {ancestorPathAndPathNames.map((data) => (
           <DropdownItem key={data.path}>
             <Link href={data.path} legacyBehavior>
-              <a role="menuitem">{data.pathName}</a>
+              <a role="menuitem" href={data.path}>
+                {data.pathName}
+              </a>
             </Link>
           </DropdownItem>
         ))}

+ 56 - 27
apps/app/src/client/components/PagePathNavSticky/PagePathNavSticky.tsx

@@ -1,7 +1,4 @@
-import {
-  useEffect, useMemo, useRef, useState, type JSX,
-} from 'react';
-
+import { type JSX, useEffect, useMemo, useRef, useState } from 'react';
 import { DevidedPagePath } from '@growi/core/dist/models';
 import { pagePathUtils } from '@growi/core/dist/utils';
 import Sticky from 'react-stickynode';
@@ -9,12 +6,15 @@ import Sticky from 'react-stickynode';
 import { usePrintMode } from '~/client/services/use-print-mode';
 import LinkedPagePath from '~/models/linked-page-path';
 import { usePageControlsX } from '~/states/ui/page';
-import { useSidebarMode, useCurrentProductNavWidth } from '~/states/ui/sidebar';
+import { useCurrentProductNavWidth, useSidebarMode } from '~/states/ui/sidebar';
 
 import { PagePathHierarchicalLink } from '../../../components/Common/PagePathHierarchicalLink';
 import type { PagePathNavLayoutProps } from '../../../components/Common/PagePathNav';
-import { PagePathNav, PagePathNavLayout, Separator } from '../../../components/Common/PagePathNav';
-
+import {
+  PagePathNav,
+  PagePathNavLayout,
+  Separator,
+} from '../../../components/Common/PagePathNav';
 import { CollapsedParentsDropdown } from './CollapsedParentsDropdown';
 
 import styles from './PagePathNavSticky.module.scss';
@@ -23,8 +23,9 @@ const moduleClass = styles['grw-page-path-nav-sticky'];
 
 const { isTrashPage } = pagePathUtils;
 
-
-export const PagePathNavSticky = (props: PagePathNavLayoutProps): JSX.Element => {
+export const PagePathNavSticky = (
+  props: PagePathNavLayoutProps,
+): JSX.Element => {
   const { pagePath, latterLinkClassName, ...rest } = props;
 
   const isPrinting = usePrintMode();
@@ -37,24 +38,36 @@ export const PagePathNavSticky = (props: PagePathNavLayoutProps): JSX.Element =>
   const [navMaxWidth, setNavMaxWidth] = useState<number | undefined>();
 
   useEffect(() => {
-    if (pageControlsX == null || pagePathNavRef.current == null || sidebarWidth == null) {
+    if (
+      pageControlsX == null ||
+      pagePathNavRef.current == null ||
+      sidebarWidth == null
+    ) {
       return;
     }
-    setNavMaxWidth(pageControlsX - pagePathNavRef.current.getBoundingClientRect().x - 10);
-  }, [pageControlsX, pagePathNavRef, sidebarWidth]);
+    setNavMaxWidth(
+      pageControlsX - pagePathNavRef.current.getBoundingClientRect().x - 10,
+    );
+  }, [pageControlsX, sidebarWidth]);
 
   useEffect(() => {
     // wait for the end of the animation of the opening and closing of the sidebar
     const timeout = setTimeout(() => {
-      if (pageControlsX == null || pagePathNavRef.current == null || sidebarMode == null) {
+      if (
+        pageControlsX == null ||
+        pagePathNavRef.current == null ||
+        sidebarMode == null
+      ) {
         return;
       }
-      setNavMaxWidth(pageControlsX - pagePathNavRef.current.getBoundingClientRect().x - 10);
+      setNavMaxWidth(
+        pageControlsX - pagePathNavRef.current.getBoundingClientRect().x - 10,
+      );
     }, 200);
     return () => {
       clearTimeout(timeout);
     };
-  }, [pageControlsX, pagePathNavRef, sidebarMode]);
+  }, [pageControlsX, sidebarMode]);
 
   const latterLink = useMemo(() => {
     const dPagePath = new DevidedPagePath(pagePath, false, true);
@@ -67,7 +80,12 @@ export const PagePathNavSticky = (props: PagePathNavLayoutProps): JSX.Element =>
     // not collapsed
     if (dPagePath.isRoot || dPagePath.isFormerRoot) {
       const linkedPagePath = new LinkedPagePath(pagePath);
-      return <PagePathHierarchicalLink linkedPagePath={linkedPagePath} isInTrash={isInTrash} />;
+      return (
+        <PagePathHierarchicalLink
+          linkedPagePath={linkedPagePath}
+          isInTrash={isInTrash}
+        />
+      );
     }
 
     // collapsed
@@ -75,7 +93,11 @@ export const PagePathNavSticky = (props: PagePathNavLayoutProps): JSX.Element =>
       <>
         <CollapsedParentsDropdown linkedPagePath={linkedPagePathFormer} />
         <Separator />
-        <PagePathHierarchicalLink linkedPagePath={linkedPagePathLatter} basePath={dPagePath.former} isInTrash={isInTrash} />
+        <PagePathHierarchicalLink
+          linkedPagePath={linkedPagePathLatter}
+          basePath={dPagePath.former}
+          isInTrash={isInTrash}
+        />
       </>
     );
   }, [pagePath]);
@@ -84,18 +106,23 @@ export const PagePathNavSticky = (props: PagePathNavLayoutProps): JSX.Element =>
     // Controlling pointer-events
     //  1. disable pointer-events with 'pe-none'
     <div ref={pagePathNavRef}>
-      <Sticky className={moduleClass} enabled={!isPrinting} innerClass="z-2 pe-none" innerActiveClass="active z-3 mt-1">
+      <Sticky
+        className={moduleClass}
+        enabled={!isPrinting}
+        innerClass="z-2 pe-none"
+        innerActiveClass="active z-3 mt-1"
+      >
         {({ status }) => {
           const isStatusFixed = status === Sticky.STATUS_FIXED;
 
           return (
             <>
               {/*
-                * Controlling pointer-events
-                * 2. enable pointer-events with 'pe-auto' only against the children
-                *      which width is minimized by 'd-inline-block'
-                */}
-              { isStatusFixed && (
+               * Controlling pointer-events
+               * 2. enable pointer-events with 'pe-auto' only against the children
+               *      which width is minimized by 'd-inline-block'
+               */}
+              {isStatusFixed && (
                 <div className="d-inline-block pe-auto position-absolute">
                   <PagePathNavLayout
                     pagePath={pagePath}
@@ -108,10 +135,12 @@ export const PagePathNavSticky = (props: PagePathNavLayoutProps): JSX.Element =>
               )}
 
               {/*
-                * Use 'd-block' to make the children take the full width
-                * This is to improve UX when opening/closing CopyDropdown
-                */}
-              <div className={`d-block pe-auto ${isStatusFixed ? 'invisible' : ''}`}>
+               * Use 'd-block' to make the children take the full width
+               * This is to improve UX when opening/closing CopyDropdown
+               */}
+              <div
+                className={`d-block pe-auto ${isStatusFixed ? 'invisible' : ''}`}
+              >
                 <PagePathNav
                   pagePath={pagePath}
                   latterLinkClassName={latterLinkClassName}

+ 26 - 22
apps/app/src/client/components/PagePresentationModal/PagePresentationModal.tsx

@@ -1,20 +1,21 @@
-import React, { useCallback } from 'react';
-
+import type React from 'react';
+import { useCallback } from 'react';
+import dynamic from 'next/dynamic';
 import type { PresentationProps } from '@growi/presentation/dist/client';
 import { useSlidesByFrontmatter } from '@growi/presentation/dist/services';
 import { LoadingSpinner } from '@growi/ui/dist/components';
 import { useFullScreen } from '@growi/ui/dist/utils';
-import dynamic from 'next/dynamic';
 import type { Options as ReactMarkdownOptions } from 'react-markdown';
-import {
-  Modal, ModalBody,
-} from 'reactstrap';
+import { Modal, ModalBody } from 'reactstrap';
 
 import { useCurrentPageData } from '~/states/page';
 import { useRendererConfig } from '~/states/server-configurations';
-import { usePresentationModalActions, usePresentationModalStatus } from '~/states/ui/modal/page-presentation';
-import { useNextThemes } from '~/stores-universal/use-next-themes';
+import {
+  usePresentationModalActions,
+  usePresentationModalStatus,
+} from '~/states/ui/modal/page-presentation';
 import { usePresentationViewOptions } from '~/stores/renderer';
+import { useNextThemes } from '~/stores-universal/use-next-themes';
 
 import { RendererErrorMessage } from '../Common/RendererErrorMessage';
 
@@ -22,19 +23,18 @@ import styles from './PagePresentationModal.module.scss';
 
 const moduleClass = styles['grw-presentation-modal'] ?? '';
 
-
-const Presentation = dynamic<PresentationProps>(() => import('../Presentation/Presentation').then(mod => mod.Presentation), {
-  ssr: false,
-  loading: () => (
-    <LoadingSpinner className="text-muted fs-1" />
-  ),
-});
+const Presentation = dynamic<PresentationProps>(
+  () => import('../Presentation/Presentation').then((mod) => mod.Presentation),
+  {
+    ssr: false,
+    loading: () => <LoadingSpinner className="text-muted fs-1" />,
+  },
+);
 
 /**
  * PagePresentationModalSubstance - Heavy processing component (rendered only when modal is open)
  */
 const PagePresentationModalSubstance: React.FC = () => {
-
   const { close: closePresentationModal } = usePresentationModalActions();
 
   const { isDarkMode } = useNextThemes();
@@ -52,8 +52,7 @@ const PagePresentationModalSubstance: React.FC = () => {
   const toggleFullscreenHandler = useCallback(() => {
     if (fullscreen.active) {
       fullscreen.exit();
-    }
-    else {
+    } else {
       fullscreen.enter();
     }
   }, [fullscreen]);
@@ -76,11 +75,16 @@ const PagePresentationModalSubstance: React.FC = () => {
         >
           {fullscreen.active ? 'close_fullscreen' : 'open_in_full'}
         </button>
-        <button className="btn-close" type="button" aria-label="Close" onClick={closeHandler}></button>
+        <button
+          className="btn-close"
+          type="button"
+          aria-label="Close"
+          onClick={closeHandler}
+        ></button>
       </div>
       <ModalBody className="modal-body d-flex justify-content-center align-items-center">
-        { !isLoading && rendererOptions == null && <RendererErrorMessage />}
-        { rendererOptions != null && isEnabledMarp != null && (
+        {!isLoading && rendererOptions == null && <RendererErrorMessage />}
+        {rendererOptions != null && isEnabledMarp != null && (
           <Presentation
             options={{
               rendererOptions: rendererOptions as ReactMarkdownOptions,
@@ -94,7 +98,7 @@ const PagePresentationModalSubstance: React.FC = () => {
           >
             {markdown}
           </Presentation>
-        ) }
+        )}
       </ModalBody>
     </>
   );

+ 4 - 1
apps/app/src/client/components/PagePresentationModal/dynamic.tsx

@@ -10,7 +10,10 @@ export const PagePresentationModalLazyLoaded = (): JSX.Element => {
 
   const PagePresentationModal = useLazyLoader<PagePresentationModalProps>(
     'page-presentation-modal',
-    () => import('./PagePresentationModal').then(mod => ({ default: mod.PagePresentationModal })),
+    () =>
+      import('./PagePresentationModal').then((mod) => ({
+        default: mod.PagePresentationModal,
+      })),
     status?.isOpened ?? false,
   );
 

+ 181 - 96
apps/app/src/client/components/PageRenameModal/PageRenameModal.tsx

@@ -1,13 +1,15 @@
-import React, {
-  useState, useEffect, useCallback, useMemo,
-} from 'react';
-
+import type React from 'react';
+import { useCallback, useEffect, useMemo, useState } from 'react';
 import { isIPageInfoForEntity } from '@growi/core';
 import { pagePathUtils } from '@growi/core/dist/utils';
 import { useAtomValue } from 'jotai';
 import { useTranslation } from 'next-i18next';
 import {
-  Collapse, Modal, ModalHeader, ModalBody, ModalFooter,
+  Collapse,
+  Modal,
+  ModalBody,
+  ModalFooter,
+  ModalHeader,
 } from 'reactstrap';
 import { debounce } from 'throttle-debounce';
 
@@ -15,7 +17,10 @@ import { apiv3Get, apiv3Put } from '~/client/util/apiv3-client';
 import { toastError } from '~/client/util/toastr';
 import { useSiteUrl } from '~/states/global';
 import { isSearchServiceReachableAtom } from '~/states/server-configurations';
-import { usePageRenameModalStatus, usePageRenameModalActions } from '~/states/ui/modal/page-rename';
+import {
+  usePageRenameModalActions,
+  usePageRenameModalStatus,
+} from '~/states/ui/modal/page-rename';
 import { useSWRxPageInfo } from '~/stores/page';
 
 import DuplicatedPathsTable from '../DuplicatedPathsTable';
@@ -38,8 +43,11 @@ const PageRenameModalSubstance: React.FC = () => {
   const { close: closeRenameModal } = usePageRenameModalActions();
   const isReachable = useAtomValue(isSearchServiceReachableAtom);
 
-  const shouldFetch = isOpened && page != null && !isIPageInfoForEntity(page.meta);
-  const { data: pageInfo } = useSWRxPageInfo(shouldFetch ? page?.data._id : null);
+  const shouldFetch =
+    isOpened && page != null && !isIPageInfoForEntity(page.meta);
+  const { data: pageInfo } = useSWRxPageInfo(
+    shouldFetch ? page?.data._id : null,
+  );
 
   if (page != null && pageInfo != null) {
     page.meta = pageInfo;
@@ -56,9 +64,10 @@ const PageRenameModalSubstance: React.FC = () => {
   const [isRemainMetadata, setIsRemainMetadata] = useState(false);
   const [expandOtherOptions, setExpandOtherOptions] = useState(false);
   const [subordinatedError] = useState(null);
-  const [isMatchedWithUserHomepagePath, setIsMatchedWithUserHomepagePath] = useState(false);
+  const [isMatchedWithUserHomepagePath, setIsMatchedWithUserHomepagePath] =
+    useState(false);
 
-  const updateSubordinatedList = useCallback(async() => {
+  const updateSubordinatedList = useCallback(async () => {
     if (page == null) {
       return;
     }
@@ -67,8 +76,7 @@ const PageRenameModalSubstance: React.FC = () => {
     try {
       const res = await apiv3Get('/pages/subordinated-list', { path });
       setSubordinatedPages(res.data.subordinatedPages);
-    }
-    catch (err) {
+    } catch (err) {
       setErrs(err);
       toastError(t('modal_rename.label.Failed to get subordinated pages'));
     }
@@ -82,20 +90,37 @@ const PageRenameModalSubstance: React.FC = () => {
   }, [isOpened, page, updateSubordinatedList]);
 
   // Memoize computed values
-  const isTargetPageDuplicate = useMemo(() => existingPaths.includes(pageNameInput), [existingPaths, pageNameInput]);
-  const isV5CompatiblePage = useMemo(() => (page != null ? isV5Compatible(page.meta) : true), [page]);
+  const isTargetPageDuplicate = useMemo(
+    () => existingPaths.includes(pageNameInput),
+    [existingPaths, pageNameInput],
+  );
+  const isV5CompatiblePage = useMemo(
+    () => (page != null ? isV5Compatible(page.meta) : true),
+    [page],
+  );
 
   const canRename = useMemo(() => {
-    if (page == null || isMatchedWithUserHomepagePath || page.data.path === pageNameInput) {
+    if (
+      page == null ||
+      isMatchedWithUserHomepagePath ||
+      page.data.path === pageNameInput
+    ) {
       return false;
     }
     if (isV5CompatiblePage) {
       return existingPaths.length === 0; // v5 data
     }
     return isRenameRecursively; // v4 data
-  }, [existingPaths.length, isMatchedWithUserHomepagePath, isRenameRecursively, page, pageNameInput, isV5CompatiblePage]);
-
-  const rename = useCallback(async() => {
+  }, [
+    existingPaths.length,
+    isMatchedWithUserHomepagePath,
+    isRenameRecursively,
+    page,
+    pageNameInput,
+    isV5CompatiblePage,
+  ]);
+
+  const rename = useCallback(async () => {
     if (page == null || !canRename) {
       return;
     }
@@ -127,31 +152,44 @@ const PageRenameModalSubstance: React.FC = () => {
         onRenamed(path);
       }
       closeRenameModal();
-    }
-    catch (err) {
+    } catch (err) {
       setErrs(err);
     }
-  }, [closeRenameModal, canRename, isRemainMetadata, isRenameRecursively, isRenameRedirect, page, pageNameInput, opts?.onRenamed]);
-
-  const checkExistPaths = useCallback(async(fromPath, toPath) => {
-    if (page == null) {
-      return;
-    }
-
-    try {
-      const res = await apiv3Get<{ existPaths: string[]}>('/page/exist-paths', { fromPath, toPath });
-      const { existPaths } = res.data;
-      setExistingPaths(existPaths);
-    }
-    catch (err) {
-      // Do not toast in case of this error because debounce process may be executed after the renaming process is completed.
-      if (err.length === 1 && err[0].code === 'from-page-is-not-exist') {
+  }, [
+    closeRenameModal,
+    canRename,
+    isRemainMetadata,
+    isRenameRecursively,
+    isRenameRedirect,
+    page,
+    pageNameInput,
+    opts?.onRenamed,
+  ]);
+
+  const checkExistPaths = useCallback(
+    async (fromPath, toPath) => {
+      if (page == null) {
         return;
       }
-      setErrs(err);
-      toastError(t('modal_rename.label.Failed to get exist path'));
-    }
-  }, [page, t]);
+
+      try {
+        const res = await apiv3Get<{ existPaths: string[] }>(
+          '/page/exist-paths',
+          { fromPath, toPath },
+        );
+        const { existPaths } = res.data;
+        setExistingPaths(existPaths);
+      } catch (err) {
+        // Do not toast in case of this error because debounce process may be executed after the renaming process is completed.
+        if (err.length === 1 && err[0].code === 'from-page-is-not-exist') {
+          return;
+        }
+        setErrs(err);
+        toastError(t('modal_rename.label.Failed to get exist path'));
+      }
+    },
+    [page, t],
+  );
 
   const checkExistPathsDebounce = useMemo(() => {
     return debounce(1000, checkExistPaths);
@@ -170,7 +208,13 @@ const PageRenameModalSubstance: React.FC = () => {
       checkExistPathsDebounce(page.data.path, pageNameInput);
       checkIsUsersHomepageDebounce(pageNameInput);
     }
-  }, [isOpened, pageNameInput, subordinatedPages, checkExistPathsDebounce, page, checkIsUsersHomepageDebounce]);
+  }, [
+    isOpened,
+    pageNameInput,
+    checkExistPathsDebounce,
+    page,
+    checkIsUsersHomepageDebounce,
+  ]);
 
   const ppacInputChangeHandler = useCallback((value: string) => {
     setErrs(null);
@@ -202,7 +246,6 @@ const PageRenameModalSubstance: React.FC = () => {
       setIsRemainMetadata(false);
       setExpandOtherOptions(false);
     }, 1000);
-
   }, [isOpened, page]);
 
   const bodyContent = () => {
@@ -215,46 +258,54 @@ const PageRenameModalSubstance: React.FC = () => {
     return (
       <>
         <div className="mb-3">
-          <label className="form-label w-100">{ t('modal_rename.label.Current page name') }</label>
-          <code className="fs-6">{ path }</code>
+          <span className="form-label w-100">
+            {t('modal_rename.label.Current page name')}
+          </span>
+          <code className="fs-6">{path}</code>
         </div>
         <div className="mb-3">
-          <label htmlFor="newPageName" className="form-label w-100">{ t('modal_rename.label.New page name') }</label>
+          <label htmlFor="newPageName" className="form-label w-100">
+            {t('modal_rename.label.New page name')}
+          </label>
           <div className="input-group">
             <div>
               <span className="input-group-text">{siteUrl}</span>
             </div>
-            <form className="flex-fill" onSubmit={(e) => { e.preventDefault(); rename() }}>
-              {isReachable
-                ? (
-                  <PagePathAutoComplete
-                    initializedPath={path}
-                    onSubmit={rename}
-                    onInputChange={ppacInputChangeHandler}
-                    autoFocus
-                  />
-                )
-                : (
-                  <input
-                    type="text"
-                    value={pageNameInput}
-                    className="form-control"
-                    onChange={e => inputChangeHandler(e.target.value)}
-                    required
-                    autoFocus
-                  />
-                )}
+            <form
+              className="flex-fill"
+              onSubmit={(e) => {
+                e.preventDefault();
+                rename();
+              }}
+            >
+              {isReachable ? (
+                <PagePathAutoComplete
+                  initializedPath={path}
+                  onSubmit={rename}
+                  onInputChange={ppacInputChangeHandler}
+                />
+              ) : (
+                <input
+                  type="text"
+                  value={pageNameInput}
+                  className="form-control"
+                  onChange={(e) => inputChangeHandler(e.target.value)}
+                  required
+                />
+              )}
             </form>
           </div>
-          { isTargetPageDuplicate && (
+          {isTargetPageDuplicate && (
             <p className="text-danger">Error: Target path is duplicated.</p>
-          ) }
-          { isMatchedWithUserHomepagePath && (
-            <p className="text-danger">Error: Cannot move to directory under /user page.</p>
-          ) }
+          )}
+          {isMatchedWithUserHomepagePath && (
+            <p className="text-danger">
+              Error: Cannot move to directory under /user page.
+            </p>
+          )}
         </div>
 
-        { !isV5Compatible(page.meta) && (
+        {!isV5Compatible(page.meta) && (
           <>
             <div className="form-check form-check-warning">
               <input
@@ -265,8 +316,11 @@ const PageRenameModalSubstance: React.FC = () => {
                 checked={!isRenameRecursively}
                 onChange={() => setIsRenameRecursively(!isRenameRecursively)}
               />
-              <label className="form-label form-check-label" htmlFor="cbRenameThisPageOnly">
-                { t('modal_rename.label.Rename this page only') }
+              <label
+                className="form-label form-check-label"
+                htmlFor="cbRenameThisPageOnly"
+              >
+                {t('modal_rename.label.Rename this page only')}
               </label>
             </div>
             <div className="form-check form-check-warning mt-1">
@@ -278,21 +332,39 @@ const PageRenameModalSubstance: React.FC = () => {
                 checked={isRenameRecursively}
                 onChange={() => setIsRenameRecursively(!isRenameRecursively)}
               />
-              <label className="form-label form-check-label" htmlFor="cbForceRenameRecursively">
-                { t('modal_rename.label.Force rename all child pages') }
-                <p className="form-text text-muted mt-0">{ t('modal_rename.help.recursive') }</p>
+              <label
+                className="form-label form-check-label"
+                htmlFor="cbForceRenameRecursively"
+              >
+                {t('modal_rename.label.Force rename all child pages')}
+                <p className="form-text text-muted mt-0">
+                  {t('modal_rename.help.recursive')}
+                </p>
               </label>
               {isRenameRecursively && existingPaths.length !== 0 && (
-                <DuplicatedPathsTable existingPaths={existingPaths} fromPath={path} toPath={pageNameInput} />
-              ) }
+                <DuplicatedPathsTable
+                  existingPaths={existingPaths}
+                  fromPath={path}
+                  toPath={pageNameInput}
+                />
+              )}
             </div>
           </>
-        ) }
+        )}
 
         <p className="mt-2">
-          <button type="button" className="btn btn-link mt-2 p-0" aria-expanded="false" onClick={() => setExpandOtherOptions(!expandOtherOptions)}>
-            <span className={`material-symbols-outlined me-1 ${expandOtherOptions ? 'rotate-90' : ''}`}>navigate_next</span>
-            { t('modal_rename.label.Other options') }
+          <button
+            type="button"
+            className="btn btn-link mt-2 p-0"
+            aria-expanded="false"
+            onClick={() => setExpandOtherOptions(!expandOtherOptions)}
+          >
+            <span
+              className={`material-symbols-outlined me-1 ${expandOtherOptions ? 'rotate-90' : ''}`}
+            >
+              navigate_next
+            </span>
+            {t('modal_rename.label.Other options')}
           </button>
         </p>
         <Collapse isOpen={expandOtherOptions}>
@@ -305,9 +377,14 @@ const PageRenameModalSubstance: React.FC = () => {
               checked={isRenameRedirect}
               onChange={() => setIsRenameRedirect(!isRenameRedirect)}
             />
-            <label className="form-label form-check-label" htmlFor="cbRenameRedirect">
-              { t('modal_rename.label.Redirect') }
-              <p className="form-text text-muted mt-0">{ t('modal_rename.help.redirect') }</p>
+            <label
+              className="form-label form-check-label"
+              htmlFor="cbRenameRedirect"
+            >
+              {t('modal_rename.label.Redirect')}
+              <p className="form-text text-muted mt-0">
+                {t('modal_rename.help.redirect')}
+              </p>
             </label>
           </div>
 
@@ -320,9 +397,14 @@ const PageRenameModalSubstance: React.FC = () => {
               checked={isRemainMetadata}
               onChange={() => setIsRemainMetadata(!isRemainMetadata)}
             />
-            <label className="form-label form-check-label" htmlFor="cbRemainMetadata">
-              { t('modal_rename.label.Do not update metadata') }
-              <p className="form-text text-muted mt-0">{ t('modal_rename.help.metadata') }</p>
+            <label
+              className="form-label form-check-label"
+              htmlFor="cbRemainMetadata"
+            >
+              {t('modal_rename.label.Do not update metadata')}
+              <p className="form-text text-muted mt-0">
+                {t('modal_rename.help.metadata')}
+              </p>
             </label>
           </div>
           <div> {subordinatedError} </div>
@@ -347,7 +429,8 @@ const PageRenameModalSubstance: React.FC = () => {
           className="btn btn-primary"
           onClick={rename}
           disabled={submitButtonDisabled}
-        >Rename
+        >
+          Rename
         </button>
       </>
     );
@@ -356,14 +439,10 @@ const PageRenameModalSubstance: React.FC = () => {
   return (
     <>
       <ModalHeader tag="h4" toggle={closeRenameModal}>
-        { t('modal_rename.label.Move/Rename page') }
+        {t('modal_rename.label.Move/Rename page')}
       </ModalHeader>
-      <ModalBody>
-        {bodyContent()}
-      </ModalBody>
-      <ModalFooter>
-        {footerContent()}
-      </ModalFooter>
+      <ModalBody>{bodyContent()}</ModalBody>
+      <ModalFooter>{footerContent()}</ModalFooter>
     </>
   );
 };
@@ -376,7 +455,13 @@ export const PageRenameModal = (): React.JSX.Element => {
   const { close: closeRenameModal } = usePageRenameModalActions();
 
   return (
-    <Modal size="lg" isOpen={isOpened} toggle={closeRenameModal} data-testid="page-rename-modal" autoFocus={false}>
+    <Modal
+      size="lg"
+      isOpen={isOpened}
+      toggle={closeRenameModal}
+      data-testid="page-rename-modal"
+      autoFocus={false}
+    >
       {isOpened && <PageRenameModalSubstance />}
     </Modal>
   );

+ 4 - 1
apps/app/src/client/components/PageRenameModal/dynamic.tsx

@@ -10,7 +10,10 @@ export const PageRenameModalLazyLoaded = (): JSX.Element => {
 
   const PageRenameModal = useLazyLoader<PageRenameModalProps>(
     'page-rename-modal',
-    () => import('./PageRenameModal').then(mod => ({ default: mod.PageRenameModal })),
+    () =>
+      import('./PageRenameModal').then((mod) => ({
+        default: mod.PageRenameModal,
+      })),
     status?.isOpened ?? false,
   );
 

+ 19 - 17
apps/app/src/client/components/PageSelectModal/PageSelectModal.tsx

@@ -1,25 +1,19 @@
 import type { FC, JSX } from 'react';
-import {
-  Suspense, useState, useCallback, useMemo,
-} from 'react';
-
+import { Suspense, useCallback, useMemo, useState } from 'react';
 import { useTranslation } from 'next-i18next';
 import { dirname } from 'pathe';
-import {
-  Modal, ModalHeader, ModalBody, ModalFooter, Button,
-} from 'reactstrap';
+import { Button, Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap';
 
 import { ItemsTree } from '~/features/page-tree/components';
 import { useIsGuestUser, useIsReadOnlyUser } from '~/states/context';
 import { useCurrentPageData } from '~/states/page';
 import {
-  usePageSelectModalStatus,
   usePageSelectModalActions,
+  usePageSelectModalStatus,
   useSelectedPageInModal,
 } from '~/states/ui/modal/page-select';
 
 import ItemsTreeContentSkeleton from '../ItemsTree/ItemsTreeContentSkeleton';
-
 import { TreeItemForModal, treeItemForModalSize } from './TreeItemForModal';
 
 const PageSelectModalSubstance: FC = () => {
@@ -43,7 +37,8 @@ const PageSelectModalSubstance: FC = () => {
   // Get selected page from atom
   const selectedPage = useSelectedPageInModal();
 
-  const isHierarchicalSelectionMode = opts?.isHierarchicalSelectionMode ?? false;
+  const isHierarchicalSelectionMode =
+    opts?.isHierarchicalSelectionMode ?? false;
 
   const onClickCancel = useCallback(() => {
     closeModal();
@@ -66,9 +61,10 @@ const PageSelectModalSubstance: FC = () => {
   }, [currentPage?.path]);
 
   // Memoize target path calculation
-  const targetPath = useMemo(() => (
-    selectedPage?.path || parentPagePath
-  ), [selectedPage?.path, parentPagePath]);
+  const targetPath = useMemo(
+    () => selectedPage?.path || parentPagePath,
+    [selectedPage?.path, parentPagePath],
+  );
 
   // Memoize checkbox handler
   const handleIncludeSubPageChange = useCallback(() => {
@@ -81,7 +77,9 @@ const PageSelectModalSubstance: FC = () => {
 
   return (
     <>
-      <ModalHeader toggle={closeModal}>{t('page_select_modal.select_page_location')}</ModalHeader>
+      <ModalHeader toggle={closeModal}>
+        {t('page_select_modal.select_page_location')}
+      </ModalHeader>
       <ModalBody className="p-0">
         <Suspense fallback={<ItemsTreeContentSkeleton />}>
           {/* 133px = 63px(ModalHeader) + 70px(ModalFooter) */}
@@ -105,7 +103,7 @@ const PageSelectModalSubstance: FC = () => {
         </Suspense>
       </ModalBody>
       <ModalFooter className="border-top d-flex flex-column">
-        { isHierarchicalSelectionMode && (
+        {isHierarchicalSelectionMode && (
           <div className="form-check form-check-info align-self-start ms-4">
             <input
               type="checkbox"
@@ -124,8 +122,12 @@ const PageSelectModalSubstance: FC = () => {
           </div>
         )}
         <div className="d-flex gap-2 align-self-end">
-          <Button color="secondary" onClick={onClickCancel}>{t('Cancel')}</Button>
-          <Button color="primary" onClick={onClickDone}>{t('Done')}</Button>
+          <Button color="secondary" onClick={onClickCancel}>
+            {t('Cancel')}
+          </Button>
+          <Button color="primary" onClick={onClickDone}>
+            {t('Done')}
+          </Button>
         </div>
       </ModalFooter>
     </>

+ 8 - 11
apps/app/src/client/components/PageSelectModal/TreeItemForModal.tsx

@@ -17,11 +17,7 @@ type TreeItemForModalProps = TreeItemProps & {
 };
 
 export const TreeItemForModal: FC<TreeItemForModalProps> = (props) => {
-  const {
-    item,
-    targetPathOrId,
-    onToggle,
-  } = props;
+  const { item, targetPathOrId, onToggle } = props;
 
   const page = item.getItemData();
   const selectPage = useSelectPageInModal();
@@ -32,13 +28,14 @@ export const TreeItemForModal: FC<TreeItemForModalProps> = (props) => {
   }, [page._id, page.path, targetPathOrId]);
 
   // Handle click to select this page
-  const handleClick = useCallback((selectedPage: IPageForItem) => {
-    selectPage(selectedPage);
-  }, [selectPage]);
+  const handleClick = useCallback(
+    (selectedPage: IPageForItem) => {
+      selectPage(selectedPage);
+    },
+    [selectPage],
+  );
 
-  const itemClassNames = [
-    isSelected ? 'active' : '',
-  ];
+  const itemClassNames = [isSelected ? 'active' : ''];
 
   return (
     <TreeItemLayout

+ 4 - 1
apps/app/src/client/components/PageSelectModal/dynamic.tsx

@@ -10,7 +10,10 @@ export const PageSelectModalLazyLoaded = (): JSX.Element => {
 
   const PageSelectModal = useLazyLoader<PageSelectModalProps>(
     'page-select-modal',
-    () => import('./PageSelectModal').then(mod => ({ default: mod.PageSelectModal })),
+    () =>
+      import('./PageSelectModal').then((mod) => ({
+        default: mod.PageSelectModal,
+      })),
     status?.isOpened ?? false,
   );
 

+ 14 - 18
apps/app/src/client/components/PageSideContents/PageAccessoriesControl.tsx

@@ -1,28 +1,22 @@
-import { type ReactNode, memo, type JSX } from 'react';
+import { type JSX, memo, type ReactNode } from 'react';
 
 import CountBadge from '../Common/CountBadge';
 
-
 import styles from './PageAccessoriesControl.module.scss';
 
 const moduleClass = styles['btn-page-accessories'];
 
-
 type Props = {
-  className?: string,
-  icon: ReactNode,
-  label: ReactNode,
-  count?: number,
-  offset?: number,
-  onClick?: () => void,
-}
+  className?: string;
+  icon: ReactNode;
+  label: ReactNode;
+  count?: number;
+  offset?: number;
+  onClick?: () => void;
+};
 
 export const PageAccessoriesControl = memo((props: Props): JSX.Element => {
-  const {
-    icon, label, count, offset,
-    className,
-    onClick,
-  } = props;
+  const { icon, label, count, offset, className, onClick } = props;
 
   return (
     <button
@@ -34,9 +28,11 @@ export const PageAccessoriesControl = memo((props: Props): JSX.Element => {
       <span className="grw-labels d-none d-lg-flex">
         {label}
         {/* Do not display CountBadge if '/trash/*': https://github.com/growilabs/growi/pull/7600 */}
-        {count != null
-          ? <CountBadge count={count} offset={offset} />
-          : <div className="px-2"></div>}
+        {count != null ? (
+          <CountBadge count={count} offset={offset} />
+        ) : (
+          <div className="px-2"></div>
+        )}
       </span>
     </button>
   );

+ 50 - 32
apps/app/src/client/components/PageSideContents/PageSideContents.tsx

@@ -1,16 +1,10 @@
-import React, {
-  Suspense,
-  useCallback,
-  useRef,
-  type JSX,
-} from 'react';
-
+import React, { type JSX, Suspense, useCallback, useRef } from 'react';
+import dynamic from 'next/dynamic';
 import type { IPagePopulatedToShowRevision } from '@growi/core';
 import { isIPageInfoForOperation } from '@growi/core/dist/interfaces';
 import { pagePathUtils } from '@growi/core/dist/utils';
 import { useAtomValue } from 'jotai';
 import { useTranslation } from 'next-i18next';
-import dynamic from 'next/dynamic';
 import { scroller } from 'react-scroll';
 
 import { useIsGuestUser, useIsReadOnlyUser } from '~/states/context';
@@ -23,24 +17,27 @@ import { useSWRxPageInfo, useSWRxTagsInfo } from '~/stores/page';
 import { ContentLinkButtons } from '../ContentLinkButtons';
 import { PageTagsSkeleton } from '../PageTags';
 import TableOfContents from '../TableOfContents';
-
 import { PageAccessoriesControl } from './PageAccessoriesControl';
 
-
 const { isTopPage, isUsersHomepage, isTrashPage } = pagePathUtils;
 
+const PageTags = dynamic(
+  () => import('../PageTags').then((mod) => mod.PageTags),
+  {
+    ssr: false,
+    loading: PageTagsSkeleton,
+  },
+);
 
-const PageTags = dynamic(() => import('../PageTags').then(mod => mod.PageTags), {
-  ssr: false,
-  loading: PageTagsSkeleton,
-});
-
-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 },
+);
 
 type TagsProps = {
-  pageId: string,
-  revisionId: string,
-}
+  pageId: string;
+  revisionId: string;
+};
 
 const Tags = (props: TagsProps): JSX.Element => {
   const { pageId, revisionId } = props;
@@ -76,16 +73,16 @@ const Tags = (props: TagsProps): JSX.Element => {
   );
 };
 
-
 type PageSideContentsProps = {
-  page: IPagePopulatedToShowRevision,
-  isSharedUser?: boolean,
-}
+  page: IPagePopulatedToShowRevision;
+  isSharedUser?: boolean;
+};
 
 export const PageSideContents = (props: PageSideContentsProps): JSX.Element => {
   const { t } = useTranslation();
 
-  const { open: openDescendantPageListModal } = useDescendantsPageListModalActions();
+  const { open: openDescendantPageListModal } =
+    useDescendantsPageListModalActions();
 
   const { page, isSharedUser } = props;
 
@@ -94,9 +91,7 @@ export const PageSideContents = (props: PageSideContentsProps): JSX.Element => {
   const { data: pageInfo } = useSWRxPageInfo(page._id);
   const showPageSideAuthors = useAtomValue(showPageSideAuthorsAtom);
 
-  const {
-    creator, lastUpdateUser, createdAt, updatedAt,
-  } = page;
+  const { creator, lastUpdateUser, createdAt, updatedAt } = page;
 
   const pagePath = page.path;
   const isTopPagePath = isTopPage(pagePath);
@@ -108,8 +103,18 @@ export const PageSideContents = (props: PageSideContentsProps): JSX.Element => {
       {/* AuthorInfo */}
       {showPageSideAuthors && (
         <div className="d-none d-md-block page-meta border-bottom pb-2 ms-lg-3 mb-3">
-          <AuthorInfo user={creator} date={createdAt} mode="create" locate="pageSide" />
-          <AuthorInfo user={lastUpdateUser} date={updatedAt} mode="update" locate="pageSide" />
+          <AuthorInfo
+            user={creator}
+            date={createdAt}
+            mode="create"
+            locate="pageSide"
+          />
+          <AuthorInfo
+            user={lastUpdateUser}
+            date={updatedAt}
+            mode="update"
+            locate="pageSide"
+          />
         </div>
       )}
 
@@ -130,7 +135,11 @@ export const PageSideContents = (props: PageSideContentsProps): JSX.Element => {
               icon={<span className="material-symbols-outlined">subject</span>}
               label={t('page_list')}
               // Do not display CountBadge if '/trash/*': https://github.com/growilabs/growi/pull/7600
-              count={!isTrash && isIPageInfoForOperation(pageInfo) ? pageInfo.descendantCount : undefined}
+              count={
+                !isTrash && isIPageInfoForOperation(pageInfo)
+                  ? pageInfo.descendantCount
+                  : undefined
+              }
               offset={1}
               onClick={() => openDescendantPageListModal(pagePath)}
             />
@@ -143,8 +152,17 @@ export const PageSideContents = (props: PageSideContentsProps): JSX.Element => {
             <PageAccessoriesControl
               icon={<span className="material-symbols-outlined">chat</span>}
               label={t('comments')}
-              count={isIPageInfoForOperation(pageInfo) ? pageInfo.commentCount : undefined}
-              onClick={() => scroller.scrollTo('comments-container', { smooth: false, offset: -120 })}
+              count={
+                isIPageInfoForOperation(pageInfo)
+                  ? pageInfo.commentCount
+                  : undefined
+              }
+              onClick={() =>
+                scroller.scrollTo('comments-container', {
+                  smooth: false,
+                  offset: -120,
+                })
+              }
             />
           </div>
         )}

+ 1 - 15
biome.json

@@ -28,21 +28,7 @@
       "!apps/slackbot-proxy/src/public/bootstrap",
       "!packages/pdf-converter-client/src/index.ts",
       "!packages/pdf-converter-client/specs",
-      "!apps/app/src/client/components/Admin",
-      "!apps/app/src/client/components/DescendantsPageListModal",
-      "!apps/app/src/client/components/ItemsTree",
-      "!apps/app/src/client/components/LoginForm",
-      "!apps/app/src/client/components/Page",
-      "!apps/app/src/client/components/PageAttachment",
-      "!apps/app/src/client/components/PageDeleteModal",
-      "!apps/app/src/client/components/PageDuplicateModal",
-      "!apps/app/src/client/components/PageList",
-      "!apps/app/src/client/components/PageManagement",
-      "!apps/app/src/client/components/PagePathNavSticky",
-      "!apps/app/src/client/components/PagePresentationModal",
-      "!apps/app/src/client/components/PageRenameModal",
-      "!apps/app/src/client/components/PageSelectModal",
-      "!apps/app/src/client/components/PageSideContents"
+      "!apps/app/src/client/components/Admin"
     ]
   },
   "formatter": {