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

Merge pull request #10635 from growilabs/support/156162-176216-app-some-client-components-biome-5

support: Configure biome for some client components in app 5
Yuki Takei 3 месяцев назад
Родитель
Сommit
3d3d3d8ff3
33 измененных файлов с 1293 добавлено и 808 удалено
  1. 17 1
      apps/app/.eslintrc.js
  2. 45 36
      apps/app/src/client/components/AuthorInfo/AuthorInfo.tsx
  3. 46 32
      apps/app/src/client/components/CreateTemplateModal/CreateTemplateModal.tsx
  4. 12 3
      apps/app/src/client/components/CreateTemplateModal/dynamic.tsx
  5. 105 69
      apps/app/src/client/components/CustomNavigation/CustomNav.tsx
  6. 27 15
      apps/app/src/client/components/CustomNavigation/CustomNavAndContents.tsx
  7. 11 13
      apps/app/src/client/components/CustomNavigation/CustomTabContent.tsx
  8. 24 14
      apps/app/src/client/components/DeleteBookmarkFolderModal/DeleteBookmarkFolderModal.tsx
  9. 9 5
      apps/app/src/client/components/DeleteBookmarkFolderModal/dynamic.tsx
  10. 25 16
      apps/app/src/client/components/EmptyTrashModal/EmptyTrashModal.tsx
  11. 4 1
      apps/app/src/client/components/EmptyTrashModal/dynamic.tsx
  12. 48 22
      apps/app/src/client/components/GrantedGroupsInheritanceSelectModal/GrantedGroupsInheritanceSelectModal.tsx
  13. 18 9
      apps/app/src/client/components/GrantedGroupsInheritanceSelectModal/dynamic.tsx
  14. 41 29
      apps/app/src/client/components/Maintenance/Maintenance.tsx
  15. 77 52
      apps/app/src/client/components/PageHistory/PageRevisionTable.tsx
  16. 45 26
      apps/app/src/client/components/PageHistory/Revision.tsx
  17. 42 23
      apps/app/src/client/components/PageHistory/RevisionDiff.tsx
  18. 4 2
      apps/app/src/client/components/Presentation/Presentation.tsx
  19. 4 2
      apps/app/src/client/components/Presentation/Slides.tsx
  20. 85 54
      apps/app/src/client/components/PutbackPageModal/PutbackPageModal.tsx
  21. 4 1
      apps/app/src/client/components/PutbackPageModal/dynamic.tsx
  22. 25 28
      apps/app/src/client/components/RecentActivity/ActivityListItem.tsx
  23. 32 23
      apps/app/src/client/components/RecentActivity/RecentActivity.tsx
  24. 29 31
      apps/app/src/client/components/RecentCreated/RecentCreated.tsx
  25. 51 34
      apps/app/src/client/components/RevisionComparer/RevisionComparer.tsx
  26. 173 58
      apps/app/src/client/components/ShortcutsModal/ShortcutsModal.tsx
  27. 4 1
      apps/app/src/client/components/ShortcutsModal/dynamic.tsx
  28. 39 31
      apps/app/src/client/components/StaffCredit/StaffCredit.tsx
  29. 237 144
      apps/app/src/client/components/TemplateModal/TemplateModal.tsx
  30. 2 2
      apps/app/src/client/components/TemplateModal/dynamic.tsx
  31. 3 7
      apps/app/src/client/components/TemplateModal/use-formatter.spec.tsx
  32. 4 7
      apps/app/src/client/components/TemplateModal/use-formatter.tsx
  33. 1 17
      biome.json

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

@@ -41,11 +41,27 @@ module.exports = {
     'src/client/components/*.jsx',
     'src/client/components/*.ts',
     'src/client/components/*.js',
+    'src/client/components/AuthorInfo/**',
     'src/client/components/Common/**',
+    'src/client/components/CreateTemplateModal/**',
+    'src/client/components/CustomNavigation/**',
+    'src/client/components/DeleteBookmarkFolderModal/**',
+    'src/client/components/EmptyTrashModal/**',
+    'src/client/components/GrantedGroupsInheritanceSelectModal/**',
+    'src/client/components/Icons/**',
+    'src/client/components/Maintenance/**',
     'src/client/components/PageControls/**',
     'src/client/components/PageComment/**',
     'src/client/components/PageAccessoriesModal/**',
-    'src/client/components/Icons/**',
+    'src/client/components/PageHistory/**',
+    'src/client/components/Presentation/**',
+    'src/client/components/PutbackPageModal/**',
+    'src/client/components/RecentActivity/**',
+    'src/client/components/RecentCreated/**',
+    'src/client/components/RevisionComparer/**',
+    'src/client/components/ShortcutsModal/**',
+    'src/client/components/StaffCredit/**',
+    'src/client/components/TemplateModal/**',
     'src/client/components/PageEditor/**',
     'src/client/components/Hotkeys/**',
     'src/client/components/Navbar/**',

+ 45 - 36
apps/app/src/client/components/AuthorInfo/AuthorInfo.tsx

@@ -1,17 +1,19 @@
 import React, { type JSX } from 'react';
-
+import Link from 'next/link';
 import type { IUserHasId } from '@growi/core';
-import { isPopulated, type IUser, type Ref } from '@growi/core';
+import { type IUser, isPopulated, type Ref } from '@growi/core';
 import { pagePathUtils } from '@growi/core/dist/utils';
 import { UserPicture } from '@growi/ui/dist/components';
 import { format } from 'date-fns/format';
 import { useTranslation } from 'next-i18next';
-import Link from 'next/link';
-
 
 import styles from './AuthorInfo.module.scss';
 
-const UserLabel = ({ user }: { user: IUserHasId | Ref<IUser> }): JSX.Element => {
+const UserLabel = ({
+  user,
+}: {
+  user: IUserHasId | Ref<IUser>;
+}): JSX.Element => {
   if (isPopulated(user)) {
     return (
       <Link href={pagePathUtils.userHomepagePath(user)} prefetch={false}>
@@ -23,44 +25,47 @@ const UserLabel = ({ user }: { user: IUserHasId | Ref<IUser> }): JSX.Element =>
   return <i>(anyone)</i>;
 };
 
-
 type AuthorInfoProps = {
-  date: Date,
-  user?: IUserHasId | Ref<IUser>,
-  mode: 'create' | 'update',
-  locate: 'pageSide' | 'footer',
-}
+  date: Date;
+  user?: IUserHasId | Ref<IUser>;
+  mode: 'create' | 'update';
+  locate: 'pageSide' | 'footer';
+};
 
 export const AuthorInfo = (props: AuthorInfoProps): JSX.Element => {
   const { t } = useTranslation();
-  const {
-    date, user, mode = 'create', locate = 'pageSide',
-  } = props;
+  const { date, user, mode = 'create', locate = 'pageSide' } = props;
 
   const formatType = 'yyyy/MM/dd HH:mm';
 
-  const infoLabelForPageSide = mode === 'create'
-    ? t('author_info.created_by')
-    : t('author_info.updated_by');
-  const nullinfoLabelForFooter = mode === 'create'
-    ? 'Created by'
-    : 'Updated by';
-  const infoLabelForFooter = mode === 'create'
-    ? t('author_info.created_at')
-    : t('author_info.last_revision_posted_at');
-  const userLabel = user != null
-    ? (
-      <UserLabel user={user} />
-    )
-    : <i>Unknown</i>;
+  const infoLabelForPageSide =
+    mode === 'create'
+      ? t('author_info.created_by')
+      : t('author_info.updated_by');
+  const nullinfoLabelForFooter =
+    mode === 'create' ? 'Created by' : 'Updated by';
+  const infoLabelForFooter =
+    mode === 'create'
+      ? t('author_info.created_at')
+      : t('author_info.last_revision_posted_at');
+  const userLabel = user != null ? <UserLabel user={user} /> : <i>Unknown</i>;
 
   if (locate === 'footer') {
     try {
-      return <p>{infoLabelForFooter} {format(new Date(date), formatType)} by <UserPicture user={user} size="sm" /> {userLabel}</p>;
-    }
-    catch (err) {
+      return (
+        <p>
+          {infoLabelForFooter} {format(new Date(date), formatType)} by{' '}
+          <UserPicture user={user} size="sm" /> {userLabel}
+        </p>
+      );
+    } catch (err) {
       if (err instanceof RangeError) {
-        return <p>{nullinfoLabelForFooter} <UserPicture user={user} size="sm" /> {userLabel}</p>;
+        return (
+          <p>
+            {nullinfoLabelForFooter} <UserPicture user={user} size="sm" />{' '}
+            {userLabel}
+          </p>
+        );
       }
       return <></>;
     }
@@ -69,19 +74,23 @@ export const AuthorInfo = (props: AuthorInfoProps): JSX.Element => {
   const renderParsedDate = () => {
     try {
       return format(new Date(date), formatType);
-    }
-    catch (err) {
+    } catch (err) {
       return '';
     }
   };
 
   return (
-    <div className={`grw-author-info ${styles['grw-author-info']} d-flex align-items-center mb-2`}>
+    <div
+      className={`grw-author-info ${styles['grw-author-info']} d-flex align-items-center mb-2`}
+    >
       <div className="me-2 d-none d-lg-block">
         <UserPicture user={user} size="sm" />
       </div>
       <div>
-        <div className="text-secondary mb-1">{infoLabelForPageSide} <br className="d-lg-none" />{userLabel}</div>
+        <div className="text-secondary mb-1">
+          {infoLabelForPageSide} <br className="d-lg-none" />
+          {userLabel}
+        </div>
         <div className="text-secondary text-date" data-vrt-blackout-datetime>
           {renderParsedDate()}
         </div>

+ 46 - 32
apps/app/src/client/components/CreateTemplateModal/CreateTemplateModal.tsx

@@ -1,13 +1,12 @@
-import React, { useCallback, useMemo } from 'react';
-
+import type React from 'react';
+import { useCallback, useMemo } from 'react';
 import { pathUtils } from '@growi/core/dist/utils';
 import { useTranslation } from 'next-i18next';
-import { Modal, ModalHeader, ModalBody } from 'reactstrap';
+import { Modal, ModalBody, ModalHeader } from 'reactstrap';
 
 import { useCreateTemplatePage } from '~/client/services/create-page';
 import { toastError } from '~/client/util/toastr';
-import type { TargetType, LabelType } from '~/interfaces/template';
-
+import type { LabelType, TargetType } from '~/interfaces/template';
 
 type TemplateCardProps = {
   target: TargetType;
@@ -17,7 +16,10 @@ type TemplateCardProps = {
 };
 
 const TemplateCard: React.FC<TemplateCardProps> = ({
-  target, label, isPageCreating, onClickHandler,
+  target,
+  label,
+  isPageCreating,
+  onClickHandler,
 }) => {
   const { t } = useTranslation();
 
@@ -25,8 +27,12 @@ const TemplateCard: React.FC<TemplateCardProps> = ({
     <div className="card card-select-template">
       <div className="card-header">{t(`template.${target}.label`)}</div>
       <div className="card-body">
-        <p className="text-center"><code>{label}</code></p>
-        <p className="form-text text-muted text-center"><small>{t(`template.${target}.desc`)}</small></p>
+        <p className="text-center">
+          <code>{label}</code>
+        </p>
+        <p className="form-text text-muted text-center">
+          <small>{t(`template.${target}.desc`)}</small>
+        </p>
       </div>
       <div className="card-footer text-center">
         <button
@@ -51,50 +57,58 @@ type CreateTemplateModalProps = {
 };
 
 export const CreateTemplateModal: React.FC<CreateTemplateModalProps> = ({
-  path, isOpen, onClose,
+  path,
+  isOpen,
+  onClose,
 }) => {
   const { t } = useTranslation(['translation', 'commons']);
 
   const { createTemplate, isCreating, isCreatable } = useCreateTemplatePage();
 
-  const onClickTemplateButtonHandler = useCallback(async(label: LabelType) => {
-    try {
-      await createTemplate?.(label);
-      onClose();
-    }
-    catch (err) {
-      toastError(t('toaster.create_failed', { target: path }));
-    }
-  }, [createTemplate, onClose, path, t]);
+  const onClickTemplateButtonHandler = useCallback(
+    async (label: LabelType) => {
+      try {
+        await createTemplate?.(label);
+        onClose();
+      } catch (err) {
+        toastError(t('toaster.create_failed', { target: path }));
+      }
+    },
+    [createTemplate, onClose, path, t],
+  );
 
   // Memoize computed path
   const parentPath = useMemo(() => pathUtils.addTrailingSlash(path), [path]);
 
   // Memoize template card rendering function
-  const renderTemplateCard = useCallback((target: TargetType, label: LabelType) => (
-    <div className="col">
-      <TemplateCard
-        target={target}
-        label={label}
-        isPageCreating={isCreating}
-        onClickHandler={() => onClickTemplateButtonHandler(label)}
-      />
-    </div>
-  ), [isCreating, onClickTemplateButtonHandler]);
+  const renderTemplateCard = useCallback(
+    (target: TargetType, label: LabelType) => (
+      <div className="col">
+        <TemplateCard
+          target={target}
+          label={label}
+          isPageCreating={isCreating}
+          onClickHandler={() => onClickTemplateButtonHandler(label)}
+        />
+      </div>
+    ),
+    [isCreating, onClickTemplateButtonHandler],
+  );
 
   return (
     <Modal isOpen={isOpen} toggle={onClose} data-testid="page-template-modal">
-      {(isCreatable && isOpen) && (
+      {isCreatable && isOpen && (
         <>
           <ModalHeader tag="h4" toggle={onClose}>
             {t('template.modal_label.Create/Edit Template Page')}
           </ModalHeader>
           <ModalBody>
             <div>
-              <label className="form-label mb-4">
-                <code>{parentPath}</code><br />
+              <div className="form-label mb-4">
+                <code>{parentPath}</code>
+                <br />
                 {t('template.modal_label.Create template under')}
-              </label>
+              </div>
               <div className="row row-cols-2">
                 {renderTemplateCard('children', '_template')}
                 {renderTemplateCard('descendants', '__template')}

+ 12 - 3
apps/app/src/client/components/CreateTemplateModal/dynamic.tsx

@@ -8,12 +8,21 @@ type CreateTemplateModalProps = {
   onClose: () => void;
 };
 
-export const CreateTemplateModalLazyLoaded = (props: CreateTemplateModalProps): JSX.Element => {
+export const CreateTemplateModalLazyLoaded = (
+  props: CreateTemplateModalProps,
+): JSX.Element => {
   const CreateTemplateModal = useLazyLoader<CreateTemplateModalProps>(
     'create-template-modal',
-    () => import('./CreateTemplateModal').then(mod => ({ default: mod.CreateTemplateModal })),
+    () =>
+      import('./CreateTemplateModal').then((mod) => ({
+        default: mod.CreateTemplateModal,
+      })),
     props.isOpen,
   );
 
-  return CreateTemplateModal != null ? <CreateTemplateModal {...props} /> : <></>;
+  return CreateTemplateModal != null ? (
+    <CreateTemplateModal {...props} />
+  ) : (
+    <></>
+  );
 };

+ 105 - 69
apps/app/src/client/components/CustomNavigation/CustomNav.tsx

@@ -1,18 +1,21 @@
 import React, {
-  useEffect, useState, useRef, useMemo, useCallback, type JSX,
+  type JSX,
+  useCallback,
+  useEffect,
+  useMemo,
+  useRef,
+  useState,
 } from 'react';
-
 import type { Breakpoint } from '@growi/ui/dist/interfaces';
-import {
-  Nav, NavItem, NavLink,
-} from 'reactstrap';
+import { Nav, NavItem, NavLink } from 'reactstrap';
 
 import type { ICustomNavTabMappings } from '~/interfaces/ui';
 
 import styles from './CustomNav.module.scss';
 
-
-function getBreakpointOneLevelLarger(breakpoint: Breakpoint): Omit<Breakpoint, 'xs' | 'sm'> {
+function getBreakpointOneLevelLarger(
+  breakpoint: Breakpoint,
+): Omit<Breakpoint, 'xs' | 'sm'> {
   switch (breakpoint) {
     case 'xs':
       return 'sm';
@@ -28,17 +31,16 @@ function getBreakpointOneLevelLarger(breakpoint: Breakpoint): Omit<Breakpoint, '
   }
 }
 
-
 type CustomNavDropdownProps = {
-  navTabMapping: ICustomNavTabMappings,
-  activeTab: string,
-  onNavSelected?: (selectedTabKey: string) => void,
+  navTabMapping: ICustomNavTabMappings;
+  activeTab: string;
+  onNavSelected?: (selectedTabKey: string) => void;
 };
 
-export const CustomNavDropdown = (props: CustomNavDropdownProps): JSX.Element => {
-  const {
-    activeTab, navTabMapping, onNavSelected,
-  } = props;
+export const CustomNavDropdown = (
+  props: CustomNavDropdownProps,
+): JSX.Element => {
+  const { activeTab, navTabMapping, onNavSelected } = props;
 
   const { Icon, i18n } = navTabMapping[activeTab];
 
@@ -47,19 +49,22 @@ export const CustomNavDropdown = (props: CustomNavDropdownProps): JSX.Element =>
   const dropdownButtonRef = useRef<HTMLButtonElement>(null);
 
   const toggleDropdown = () => {
-    setIsDropdownOpen(prev => !prev);
+    setIsDropdownOpen((prev) => !prev);
   };
 
-  const menuItemClickHandler = useCallback((key) => {
-    if (onNavSelected != null) {
-      onNavSelected(key);
-    }
-    // Manually close the dropdown
-    setIsDropdownOpen(false);
-    if (dropdownButtonRef.current) {
-      dropdownButtonRef.current.classList.remove('show');
-    }
-  }, [onNavSelected]);
+  const menuItemClickHandler = useCallback(
+    (key) => {
+      if (onNavSelected != null) {
+        onNavSelected(key);
+      }
+      // Manually close the dropdown
+      setIsDropdownOpen(false);
+      if (dropdownButtonRef.current) {
+        dropdownButtonRef.current.classList.remove('show');
+      }
+    },
+    [onNavSelected],
+  );
 
   return (
     <div className="btn-group">
@@ -74,15 +79,19 @@ export const CustomNavDropdown = (props: CustomNavDropdownProps): JSX.Element =>
         data-testid="custom-nav-dropdown"
       >
         <span className="float-start">
-          { Icon != null && <Icon /> } {i18n}
+          {Icon != null && <Icon />} {i18n}
         </span>
       </button>
-      <div className={`dropdown-menu dropdown-menu-right w-100 ${isDropdownOpen ? 'show' : ''} ${styles['dropdown-menu']}`}>
+      <div
+        className={`dropdown-menu dropdown-menu-right w-100 ${isDropdownOpen ? 'show' : ''} ${styles['dropdown-menu']}`}
+      >
         {Object.entries(navTabMapping).map(([key, value]) => {
-
           const isActive = activeTab === key;
           const _isLinkEnabled = value.isLinkEnabled ?? true;
-          const isLinkEnabled = typeof _isLinkEnabled === 'boolean' ? _isLinkEnabled : _isLinkEnabled(value);
+          const isLinkEnabled =
+            typeof _isLinkEnabled === 'boolean'
+              ? _isLinkEnabled
+              : _isLinkEnabled(value);
           const { Icon, i18n } = value;
 
           return (
@@ -93,7 +102,7 @@ export const CustomNavDropdown = (props: CustomNavDropdownProps): JSX.Element =>
               disabled={!isLinkEnabled}
               onClick={() => menuItemClickHandler(key)}
             >
-              { Icon != null && <Icon /> } {i18n}
+              {Icon != null && <Icon />} {i18n}
             </button>
           );
         })}
@@ -102,14 +111,13 @@ export const CustomNavDropdown = (props: CustomNavDropdownProps): JSX.Element =>
   );
 };
 
-
 type CustomNavTabProps = {
-  activeTab: string,
-  navTabMapping: ICustomNavTabMappings,
-  onNavSelected?: (selectedTabKey: string) => void,
-  hideBorderBottom?: boolean,
-  breakpointToHideInactiveTabsDown?: Breakpoint,
-  navRightElement?: JSX.Element,
+  activeTab: string;
+  navTabMapping: ICustomNavTabMappings;
+  onNavSelected?: (selectedTabKey: string) => void;
+  hideBorderBottom?: boolean;
+  breakpointToHideInactiveTabsDown?: Breakpoint;
+  navRightElement?: JSX.Element;
 };
 
 export const CustomNavTab = (props: CustomNavTabProps): JSX.Element => {
@@ -117,9 +125,12 @@ export const CustomNavTab = (props: CustomNavTabProps): JSX.Element => {
   const [sliderMarginLeft, setSliderMarginLeft] = useState(0);
 
   const {
-    activeTab, navTabMapping, onNavSelected,
+    activeTab,
+    navTabMapping,
+    onNavSelected,
     hideBorderBottom,
-    breakpointToHideInactiveTabsDown, navRightElement,
+    breakpointToHideInactiveTabsDown,
+    navRightElement,
   } = props;
 
   const navContainerRef = useRef<HTMLDivElement>(null);
@@ -132,11 +143,14 @@ export const CustomNavTab = (props: CustomNavTabProps): JSX.Element => {
     return obj;
   }, [navTabMapping]);
 
-  const navLinkClickHandler = useCallback((key) => {
-    if (onNavSelected != null) {
-      onNavSelected(key);
-    }
-  }, [onNavSelected]);
+  const navLinkClickHandler = useCallback(
+    (key) => {
+      if (onNavSelected != null) {
+        onNavSelected(key);
+      }
+    },
+    [onNavSelected],
+  );
 
   function registerNavLink(key: string, anchorElem: HTMLAnchorElement | null) {
     if (anchorElem != null) {
@@ -145,9 +159,9 @@ export const CustomNavTab = (props: CustomNavTabProps): JSX.Element => {
   }
 
   // Might make this dynamic for px, %, pt, em
-  function getPercentage(min, max) {
-    return min / max * 100;
-  }
+  const getPercentage = useCallback((min: number, max: number) => {
+    return (min / max) * 100;
+  }, []);
 
   useEffect(() => {
     if (activeTab == null || activeTab === '') {
@@ -162,7 +176,10 @@ export const CustomNavTab = (props: CustomNavTabProps): JSX.Element => {
 
     let marginLeft = 0;
     for (const [key, anchorElem] of Object.entries(navTabRefs)) {
-      const width = getPercentage(anchorElem.offsetWidth, navContainer.offsetWidth);
+      const width = getPercentage(
+        anchorElem.offsetWidth,
+        navContainer.offsetWidth,
+      );
 
       if (key === activeTab) {
         setSliderWidth(width);
@@ -172,25 +189,32 @@ export const CustomNavTab = (props: CustomNavTabProps): JSX.Element => {
 
       marginLeft += width;
     }
-  }, [activeTab, navTabRefs, navTabMapping]);
+  }, [activeTab, getPercentage, navTabRefs]);
 
   // determine inactive classes to hide NavItem
   const inactiveClassnames: string[] = [];
   if (breakpointToHideInactiveTabsDown != null) {
-    const breakpointOneLevelLarger = getBreakpointOneLevelLarger(breakpointToHideInactiveTabsDown);
+    const breakpointOneLevelLarger = getBreakpointOneLevelLarger(
+      breakpointToHideInactiveTabsDown,
+    );
     inactiveClassnames.push('d-none');
     inactiveClassnames.push(`d-${breakpointOneLevelLarger}-block`);
   }
 
   return (
-    <div data-testid="custom-nav-tab" className={`grw-custom-nav-tab ${styles['grw-custom-nav-tab']}`}>
+    <div
+      data-testid="custom-nav-tab"
+      className={`grw-custom-nav-tab ${styles['grw-custom-nav-tab']}`}
+    >
       <div ref={navContainerRef} className="d-flex justify-content-between">
         <Nav className="nav-title">
           {Object.entries(navTabMapping).map(([key, value]) => {
-
             const isActive = activeTab === key;
             const _isLinkEnabled = value.isLinkEnabled ?? true;
-            const isLinkEnabled = typeof _isLinkEnabled === 'boolean' ? _isLinkEnabled : _isLinkEnabled(value);
+            const isLinkEnabled =
+              typeof _isLinkEnabled === 'boolean'
+                ? _isLinkEnabled
+                : _isLinkEnabled(value);
             const { Icon, i18n } = value;
 
             return (
@@ -198,8 +222,19 @@ export const CustomNavTab = (props: CustomNavTabProps): JSX.Element => {
                 key={key}
                 className={`p-0 ${isActive ? 'active' : inactiveClassnames.join(' ')}`}
               >
-                <NavLink type="button" key={key} innerRef={elm => registerNavLink(key, elm)} disabled={!isLinkEnabled} onClick={() => navLinkClickHandler(key)}>
-                  { Icon != null && <span className="me-1"><Icon /></span> } {i18n}
+                <NavLink
+                  type="button"
+                  key={key}
+                  innerRef={(elm) => registerNavLink(key, elm)}
+                  disabled={!isLinkEnabled}
+                  onClick={() => navLinkClickHandler(key)}
+                >
+                  {Icon != null && (
+                    <span className="me-1">
+                      <Icon />
+                    </span>
+                  )}{' '}
+                  {i18n}
                 </NavLink>
               </NavItem>
             );
@@ -207,30 +242,32 @@ export const CustomNavTab = (props: CustomNavTabProps): JSX.Element => {
         </Nav>
         {navRightElement}
       </div>
-      <hr className="my-0 grw-nav-slide-hr border-none" style={{ width: `${sliderWidth}%`, marginLeft: `${sliderMarginLeft}%` }} />
-      { !hideBorderBottom && <hr className="my-0 border-top-0 border-bottom" /> }
+      <hr
+        className="my-0 grw-nav-slide-hr border-none"
+        style={{ width: `${sliderWidth}%`, marginLeft: `${sliderMarginLeft}%` }}
+      />
+      {!hideBorderBottom && <hr className="my-0 border-top-0 border-bottom" />}
     </div>
   );
-
 };
 
-
 type CustomNavProps = {
-  activeTab: string,
-  navTabMapping: ICustomNavTabMappings,
-  onNavSelected?: (selectedTabKey: string) => void,
-  hideBorderBottom?: boolean,
-  breakpointToHideInactiveTabsDown?: Breakpoint,
-  breakpointToSwitchDropdownDown?: Breakpoint,
+  activeTab: string;
+  navTabMapping: ICustomNavTabMappings;
+  onNavSelected?: (selectedTabKey: string) => void;
+  hideBorderBottom?: boolean;
+  breakpointToHideInactiveTabsDown?: Breakpoint;
+  breakpointToSwitchDropdownDown?: Breakpoint;
 };
 
 const CustomNav = (props: CustomNavProps): JSX.Element => {
-
   const tabClassnames = ['d-none'];
   const dropdownClassnames = ['d-block'];
 
   // determine classes to show/hide
-  const breakpointOneLevelLarger = getBreakpointOneLevelLarger(props.breakpointToSwitchDropdownDown ?? 'sm');
+  const breakpointOneLevelLarger = getBreakpointOneLevelLarger(
+    props.breakpointToSwitchDropdownDown ?? 'sm',
+  );
   tabClassnames.push(`d-${breakpointOneLevelLarger}-block`);
   dropdownClassnames.push(`d-${breakpointOneLevelLarger}-none`);
 
@@ -244,7 +281,6 @@ const CustomNav = (props: CustomNavProps): JSX.Element => {
       </div>
     </div>
   );
-
 };
 
 export default CustomNav;

+ 27 - 15
apps/app/src/client/components/CustomNavigation/CustomNavAndContents.tsx

@@ -1,26 +1,34 @@
 import type { ReactNode } from 'react';
-import React, { useState, type JSX } from 'react';
+import React, { type JSX, useState } from 'react';
 
-import CustomNav, { CustomNavTab, CustomNavDropdown } from './CustomNav';
+import CustomNav, { CustomNavDropdown, CustomNavTab } from './CustomNav';
 import CustomTabContent from './CustomTabContent';
 
 type CustomNavAndContentsProps = {
-  navTabMapping: any,
-  defaultTabIndex?: number,
-  navigationMode?: 'both' | 'tab' | 'dropdown',
-  tabContentClasses?: string[],
-  breakpointToHideInactiveTabsDown?: 'xs' | 'sm' | 'md' | 'lg' | 'xl',
-  navRightElement?: ReactNode
-}
-
+  navTabMapping: any;
+  defaultTabIndex?: number;
+  navigationMode?: 'both' | 'tab' | 'dropdown';
+  tabContentClasses?: string[];
+  breakpointToHideInactiveTabsDown?: 'xs' | 'sm' | 'md' | 'lg' | 'xl';
+  navRightElement?: ReactNode;
+};
 
-const CustomNavAndContents = (props: CustomNavAndContentsProps): JSX.Element => {
+const CustomNavAndContents = (
+  props: CustomNavAndContentsProps,
+): JSX.Element => {
   const {
-    navTabMapping, defaultTabIndex, navigationMode = 'tab', tabContentClasses = ['p-4'], breakpointToHideInactiveTabsDown, navRightElement,
+    navTabMapping,
+    defaultTabIndex,
+    navigationMode = 'tab',
+    tabContentClasses = ['p-4'],
+    breakpointToHideInactiveTabsDown,
+    navRightElement,
   } = props;
-  const [activeTab, setActiveTab] = useState(Object.keys(props.navTabMapping)[defaultTabIndex || 0]);
+  const [activeTab, setActiveTab] = useState(
+    Object.keys(props.navTabMapping)[defaultTabIndex || 0],
+  );
 
-  let SelectedNav;
+  let SelectedNav: (props) => JSX.Element;
   switch (navigationMode) {
     case 'tab':
       SelectedNav = CustomNavTab;
@@ -42,7 +50,11 @@ const CustomNavAndContents = (props: CustomNavAndContentsProps): JSX.Element =>
         breakpointToHideInactiveTabsDown={breakpointToHideInactiveTabsDown}
         navRightElement={navRightElement}
       />
-      <CustomTabContent activeTab={activeTab} navTabMapping={navTabMapping} additionalClassNames={tabContentClasses} />
+      <CustomTabContent
+        activeTab={activeTab}
+        navTabMapping={navTabMapping}
+        additionalClassNames={tabContentClasses}
+      />
     </>
   );
 };

+ 11 - 13
apps/app/src/client/components/CustomNavigation/CustomTabContent.tsx

@@ -1,28 +1,27 @@
 import React, { type JSX } from 'react';
-
-import {
-  TabContent, TabPane,
-} from 'reactstrap';
+import { TabContent, TabPane } from 'reactstrap';
 
 import type { ICustomNavTabMappings } from '~/interfaces/ui';
 
 import { LazyRenderer } from '../Common/LazyRenderer';
 
-
 type Props = {
-  navTabMapping: ICustomNavTabMappings,
-  activeTab?: string,
-  additionalClassNames?: string[],
-}
+  navTabMapping: ICustomNavTabMappings;
+  activeTab?: string;
+  additionalClassNames?: string[];
+};
 
 const CustomTabContent = (props: Props): JSX.Element => {
-
   const { activeTab, navTabMapping, additionalClassNames } = props;
 
   return (
-    <TabContent activeTab={activeTab} className={additionalClassNames != null ? additionalClassNames.join(' ') : ''}>
+    <TabContent
+      activeTab={activeTab}
+      className={
+        additionalClassNames != null ? additionalClassNames.join(' ') : ''
+      }
+    >
       {Object.entries(navTabMapping).map(([key, value]) => {
-
         const { Content } = value;
         const content = Content != null ? <Content /> : <></>;
 
@@ -36,7 +35,6 @@ const CustomTabContent = (props: Props): JSX.Element => {
       })}
     </TabContent>
   );
-
 };
 
 export default CustomTabContent;

+ 24 - 14
apps/app/src/client/components/DeleteBookmarkFolderModal/DeleteBookmarkFolderModal.tsx

@@ -1,17 +1,16 @@
-
 import type { FC } from 'react';
 import { useCallback } from 'react';
-
 import { useTranslation } from 'next-i18next';
-import {
-  Modal, ModalBody, ModalFooter, ModalHeader,
-} from 'reactstrap';
+import { Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap';
 
 import { FolderIcon } from '~/client/components/Icons/FolderIcon';
 import { deleteBookmarkFolder } from '~/client/util/bookmark-utils';
 import { toastError } from '~/client/util/toastr';
 import type { BookmarkFolderItems } from '~/interfaces/bookmark-info';
-import { useDeleteBookmarkFolderModalStatus, useDeleteBookmarkFolderModalActions } from '~/states/ui/modal/delete-bookmark-folder';
+import {
+  useDeleteBookmarkFolderModalActions,
+  useDeleteBookmarkFolderModalStatus,
+} from '~/states/ui/modal/delete-bookmark-folder';
 
 /**
  * DeleteBookmarkFolderModalSubstance - Presentation component (all logic here)
@@ -29,20 +28,19 @@ const DeleteBookmarkFolderModalSubstance = ({
 }: DeleteBookmarkFolderModalSubstanceProps): React.JSX.Element => {
   const { t } = useTranslation();
 
-  const deleteBookmark = useCallback(async() => {
+  const deleteBookmark = useCallback(async () => {
     try {
       await deleteBookmarkFolder(bookmarkFolder._id);
       if (onDeleted != null) {
         onDeleted(bookmarkFolder._id);
       }
       closeModal();
-    }
-    catch (err) {
+    } catch (err) {
       toastError(err);
     }
   }, [bookmarkFolder, onDeleted, closeModal]);
 
-  const onClickDeleteButton = useCallback(async() => {
+  const onClickDeleteButton = useCallback(async () => {
     await deleteBookmark();
   }, [deleteBookmark]);
 
@@ -54,7 +52,10 @@ const DeleteBookmarkFolderModalSubstance = ({
       </ModalHeader>
       <ModalBody>
         <div className="pb-1 text-break">
-          <label className="form-label">{ t('bookmark_folder.delete_modal.modal_body_description') }:</label><br />
+          <span className="form-label">
+            {t('bookmark_folder.delete_modal.modal_body_description')}:
+          </span>
+          <br />
           <FolderIcon isOpen={false} /> {bookmarkFolder?.name}
         </div>
         {t('bookmark_folder.delete_modal.modal_body_alert')}
@@ -65,7 +66,9 @@ const DeleteBookmarkFolderModalSubstance = ({
           className="btn btn-danger"
           onClick={onClickDeleteButton}
         >
-          <span className="material-symbols-outlined" aria-hidden="true">delete</span>
+          <span className="material-symbols-outlined" aria-hidden="true">
+            delete
+          </span>
           {t('bookmark_folder.delete_modal.modal_footer_button')}
         </button>
       </ModalFooter>
@@ -77,11 +80,18 @@ const DeleteBookmarkFolderModalSubstance = ({
  * DeleteBookmarkFolderModal - Container component (lightweight, always rendered)
  */
 const DeleteBookmarkFolderModal: FC = () => {
-  const { isOpened, bookmarkFolder, opts } = useDeleteBookmarkFolderModalStatus();
+  const { isOpened, bookmarkFolder, opts } =
+    useDeleteBookmarkFolderModalStatus();
   const { close: closeModal } = useDeleteBookmarkFolderModalActions();
 
   return (
-    <Modal size="md" isOpen={isOpened} toggle={closeModal} data-testid="page-delete-modal" className="grw-create-page">
+    <Modal
+      size="md"
+      isOpen={isOpened}
+      toggle={closeModal}
+      data-testid="page-delete-modal"
+      className="grw-create-page"
+    >
       {isOpened && bookmarkFolder != null && (
         <DeleteBookmarkFolderModalSubstance
           bookmarkFolder={bookmarkFolder}

+ 9 - 5
apps/app/src/client/components/DeleteBookmarkFolderModal/dynamic.tsx

@@ -8,11 +8,15 @@ type DeleteBookmarkFolderModalProps = Record<string, unknown>;
 export const DeleteBookmarkFolderModalLazyLoaded = (): JSX.Element => {
   const status = useDeleteBookmarkFolderModalStatus();
 
-  const DeleteBookmarkFolderModal = useLazyLoader<DeleteBookmarkFolderModalProps>(
-    'delete-bookmark-folder-modal',
-    () => import('./DeleteBookmarkFolderModal').then(mod => ({ default: mod.DeleteBookmarkFolderModal })),
-    status?.isOpened ?? false,
-  );
+  const DeleteBookmarkFolderModal =
+    useLazyLoader<DeleteBookmarkFolderModalProps>(
+      'delete-bookmark-folder-modal',
+      () =>
+        import('./DeleteBookmarkFolderModal').then((mod) => ({
+          default: mod.DeleteBookmarkFolderModal,
+        })),
+      status?.isOpened ?? false,
+    );
 
   return DeleteBookmarkFolderModal ? <DeleteBookmarkFolderModal /> : <></>;
 };

+ 25 - 16
apps/app/src/client/components/EmptyTrashModal/EmptyTrashModal.tsx

@@ -1,14 +1,15 @@
+import type React from 'react';
 import type { FC } from 'react';
-import React, { useState, useCallback, useMemo } from 'react';
-
+import { useCallback, useMemo, useState } from 'react';
 import type { IPageToDeleteWithMeta } from '@growi/core';
 import { useTranslation } from 'next-i18next';
-import {
-  Modal, ModalHeader, ModalBody, ModalFooter,
-} from 'reactstrap';
+import { Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap';
 
 import { apiv3Delete } from '~/client/util/apiv3-client';
-import { useEmptyTrashModalStatus, useEmptyTrashModalActions } from '~/states/ui/modal/empty-trash';
+import {
+  useEmptyTrashModalActions,
+  useEmptyTrashModalStatus,
+} from '~/states/ui/modal/empty-trash';
 
 import ApiErrorMessageList from '../PageManagement/ApiErrorMessageList';
 
@@ -32,7 +33,7 @@ const EmptyTrashModalSubstance = ({
 
   const [errs, setErrs] = useState<Error[] | null>(null);
 
-  const emptyTrash = useCallback(async() => {
+  const emptyTrash = useCallback(async () => {
     if (pages == null) {
       return;
     }
@@ -43,22 +44,21 @@ const EmptyTrashModalSubstance = ({
         onEmptiedTrash();
       }
       closeModal();
-    }
-    catch (err) {
+    } catch (err) {
       setErrs([err]);
     }
   }, [pages, onEmptiedTrash, closeModal]);
 
-  const emptyTrashButtonHandler = useCallback(async() => {
+  const emptyTrashButtonHandler = useCallback(async () => {
     await emptyTrash();
   }, [emptyTrash]);
 
   // Memoize page paths rendering
   const renderPagePaths = useMemo(() => {
     if (pages != null) {
-      return pages.map(page => (
+      return pages.map((page) => (
         <p key={page.data._id} className="mb-1">
-          <code>{ page.data.path }</code>
+          <code>{page.data.path}</code>
         </p>
       ));
     }
@@ -73,11 +73,13 @@ const EmptyTrashModalSubstance = ({
       </ModalHeader>
       <ModalBody>
         <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 */}
           {renderPagePaths}
         </div>
-        {!canDeleteAllPages && t('modal_empty.not_deletable_notice')}<br />
+        {!canDeleteAllPages && t('modal_empty.not_deletable_notice')}
+        <br />
         {t('modal_empty.notice')}
       </ModalBody>
       <ModalFooter>
@@ -87,7 +89,9 @@ const EmptyTrashModalSubstance = ({
           className="btn btn-danger"
           onClick={emptyTrashButtonHandler}
         >
-          <span className="material-symbols-outlined" aria-hidden="true">delete_forever</span>
+          <span className="material-symbols-outlined" aria-hidden="true">
+            delete_forever
+          </span>
           {t('modal_empty.empty_the_trash_button')}
         </button>
       </ModalFooter>
@@ -103,7 +107,12 @@ export const EmptyTrashModal: FC = () => {
   const { close: closeModal } = useEmptyTrashModalActions();
 
   return (
-    <Modal size="lg" isOpen={isOpened} toggle={closeModal} data-testid="page-delete-modal">
+    <Modal
+      size="lg"
+      isOpen={isOpened}
+      toggle={closeModal}
+      data-testid="page-delete-modal"
+    >
       {isOpened && (
         <EmptyTrashModalSubstance
           pages={pages}

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

@@ -11,7 +11,10 @@ export const EmptyTrashModalLazyLoaded = (): JSX.Element => {
 
   const EmptyTrashModal = useLazyLoader<EmptyTrashModalProps>(
     'empty-trash-modal',
-    () => import('./EmptyTrashModal').then(mod => ({ default: mod.EmptyTrashModal })),
+    () =>
+      import('./EmptyTrashModal').then((mod) => ({
+        default: mod.EmptyTrashModal,
+      })),
     status?.isOpened ?? false,
   );
 

+ 48 - 22
apps/app/src/client/components/GrantedGroupsInheritanceSelectModal/GrantedGroupsInheritanceSelectModal.tsx

@@ -1,35 +1,46 @@
-import { useState, useCallback } from 'react';
-
+import { useCallback, useState } from 'react';
 import { useTranslation } from 'react-i18next';
-import {
-  Modal, ModalHeader, ModalBody, ModalFooter,
-} from 'reactstrap';
+import { Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap';
 
 import {
-  useGrantedGroupsInheritanceSelectModalActions, useGrantedGroupsInheritanceSelectModalStatus,
+  useGrantedGroupsInheritanceSelectModalActions,
+  useGrantedGroupsInheritanceSelectModalStatus,
 } from '~/states/ui/modal/granted-groups-inheritance-select';
 
 /**
  * GrantedGroupsInheritanceSelectModalSubstance - Presentation component (heavy logic, rendered only when isOpen)
  */
 type GrantedGroupsInheritanceSelectModalSubstanceProps = {
-  onCreateBtnClick: ((onlyInheritUserRelatedGrantedGroups: boolean) => Promise<void>) | undefined;
+  onCreateBtnClick:
+    | ((onlyInheritUserRelatedGrantedGroups: boolean) => Promise<void>)
+    | undefined;
   closeModal: () => void;
 };
 
-const GrantedGroupsInheritanceSelectModalSubstance = (props: GrantedGroupsInheritanceSelectModalSubstanceProps): React.JSX.Element => {
+const GrantedGroupsInheritanceSelectModalSubstance = (
+  props: GrantedGroupsInheritanceSelectModalSubstanceProps,
+): React.JSX.Element => {
   const { onCreateBtnClick: _onCreateBtnClick, closeModal } = props;
   const { t } = useTranslation();
 
-  const [onlyInheritUserRelatedGrantedGroups, setOnlyInheritUserRelatedGrantedGroups] = useState(false);
+  const [
+    onlyInheritUserRelatedGrantedGroups,
+    setOnlyInheritUserRelatedGrantedGroups,
+  ] = useState(false);
 
-  const onCreateBtnClick = useCallback(async() => {
+  const onCreateBtnClick = useCallback(async () => {
     await _onCreateBtnClick?.(onlyInheritUserRelatedGrantedGroups);
     setOnlyInheritUserRelatedGrantedGroups(false); // reset to false after create request
   }, [_onCreateBtnClick, onlyInheritUserRelatedGrantedGroups]);
 
-  const setInheritAll = useCallback(() => setOnlyInheritUserRelatedGrantedGroups(false), []);
-  const setInheritRelatedOnly = useCallback(() => setOnlyInheritUserRelatedGrantedGroups(true), []);
+  const setInheritAll = useCallback(
+    () => setOnlyInheritUserRelatedGrantedGroups(false),
+    [],
+  );
+  const setInheritRelatedOnly = useCallback(
+    () => setOnlyInheritUserRelatedGrantedGroups(true),
+    [],
+  );
 
   return (
     <>
@@ -48,7 +59,9 @@ const GrantedGroupsInheritanceSelectModalSubstance = (props: GrantedGroupsInheri
               onChange={setInheritAll}
             />
             <label className="form-check-label" htmlFor="inheritAllGroupsRadio">
-              {t('modal_granted_groups_inheritance_select.inherit_all_granted_groups_from_parent')}
+              {t(
+                'modal_granted_groups_inheritance_select.inherit_all_granted_groups_from_parent',
+              )}
             </label>
           </div>
           <div className="form-check radio-primary">
@@ -60,15 +73,30 @@ const GrantedGroupsInheritanceSelectModalSubstance = (props: GrantedGroupsInheri
               checked={onlyInheritUserRelatedGrantedGroups}
               onChange={setInheritRelatedOnly}
             />
-            <label className="form-check-label" htmlFor="onlyInheritRelatedGroupsRadio">
-              {t('modal_granted_groups_inheritance_select.only_inherit_related_groups')}
+            <label
+              className="form-check-label"
+              htmlFor="onlyInheritRelatedGroupsRadio"
+            >
+              {t(
+                'modal_granted_groups_inheritance_select.only_inherit_related_groups',
+              )}
             </label>
           </div>
         </div>
       </ModalBody>
       <ModalFooter className="grw-modal-footer">
-        <button type="button" className="me-2 btn btn-secondary" onClick={() => closeModal()}>{t('Cancel')}</button>
-        <button className="btn btn-primary" type="button" onClick={onCreateBtnClick}>
+        <button
+          type="button"
+          className="me-2 btn btn-secondary"
+          onClick={() => closeModal()}
+        >
+          {t('Cancel')}
+        </button>
+        <button
+          className="btn btn-primary"
+          type="button"
+          onClick={onCreateBtnClick}
+        >
           {t('modal_granted_groups_inheritance_select.create_page')}
         </button>
       </ModalFooter>
@@ -80,14 +108,12 @@ const GrantedGroupsInheritanceSelectModalSubstance = (props: GrantedGroupsInheri
  * GrantedGroupsInheritanceSelectModal - Container component (lightweight, always rendered)
  */
 export const GrantedGroupsInheritanceSelectModal = (): React.JSX.Element => {
-  const { isOpened, onCreateBtnClick } = useGrantedGroupsInheritanceSelectModalStatus();
+  const { isOpened, onCreateBtnClick } =
+    useGrantedGroupsInheritanceSelectModalStatus();
   const { close: closeModal } = useGrantedGroupsInheritanceSelectModalActions();
 
   return (
-    <Modal
-      isOpen={isOpened}
-      toggle={() => closeModal()}
-    >
+    <Modal isOpen={isOpened} toggle={() => closeModal()}>
       {isOpened && (
         <GrantedGroupsInheritanceSelectModalSubstance
           onCreateBtnClick={onCreateBtnClick}

+ 18 - 9
apps/app/src/client/components/GrantedGroupsInheritanceSelectModal/dynamic.tsx

@@ -5,14 +5,23 @@ import { useGrantedGroupsInheritanceSelectModalStatus } from '~/states/ui/modal/
 
 type GrantedGroupsInheritanceSelectModalProps = Record<string, unknown>;
 
-export const GrantedGroupsInheritanceSelectModalLazyLoaded = (): JSX.Element => {
-  const status = useGrantedGroupsInheritanceSelectModalStatus();
+export const GrantedGroupsInheritanceSelectModalLazyLoaded =
+  (): JSX.Element => {
+    const status = useGrantedGroupsInheritanceSelectModalStatus();
 
-  const GrantedGroupsInheritanceSelectModal = useLazyLoader<GrantedGroupsInheritanceSelectModalProps>(
-    'granted-groups-inheritance-select-modal',
-    () => import('./GrantedGroupsInheritanceSelectModal').then(mod => ({ default: mod.GrantedGroupsInheritanceSelectModal })),
-    status?.isOpened ?? false,
-  );
+    const GrantedGroupsInheritanceSelectModal =
+      useLazyLoader<GrantedGroupsInheritanceSelectModalProps>(
+        'granted-groups-inheritance-select-modal',
+        () =>
+          import('./GrantedGroupsInheritanceSelectModal').then((mod) => ({
+            default: mod.GrantedGroupsInheritanceSelectModal,
+          })),
+        status?.isOpened ?? false,
+      );
 
-  return GrantedGroupsInheritanceSelectModal ? <GrantedGroupsInheritanceSelectModal /> : <></>;
-};
+    return GrantedGroupsInheritanceSelectModal ? (
+      <GrantedGroupsInheritanceSelectModal />
+    ) : (
+      <></>
+    );
+  };

+ 41 - 29
apps/app/src/client/components/Maintenance/Maintenance.tsx

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

+ 77 - 52
apps/app/src/client/components/PageHistory/PageRevisionTable.tsx

@@ -1,71 +1,85 @@
-import React, {
-  useEffect, useRef, useState, type JSX,
-} from 'react';
-
+import React, { type JSX, useEffect, useRef, useState } from 'react';
 import type { IRevisionHasId } from '@growi/core';
 import { useTranslation } from 'next-i18next';
 
 import { useSWRxInfinitePageRevisions } from '~/stores/page';
 
 import { RevisionComparer } from '../RevisionComparer/RevisionComparer';
-
 import { Revision } from './Revision';
 
 import styles from './PageRevisionTable.module.scss';
 
 type PageRevisionTableProps = {
-  sourceRevisionId?: string
-  targetRevisionId?: string
-  onClose: () => void,
-  currentPageId: string
-  currentPagePath: string
-}
-
-export const PageRevisionTable = (props: PageRevisionTableProps): JSX.Element => {
+  sourceRevisionId?: string;
+  targetRevisionId?: string;
+  onClose: () => void;
+  currentPageId: string;
+  currentPagePath: string;
+};
+
+export const PageRevisionTable = (
+  props: PageRevisionTableProps,
+): JSX.Element => {
   const { t } = useTranslation();
 
   const REVISIONS_PER_PAGE = 10;
 
   const {
-    sourceRevisionId, targetRevisionId, onClose, currentPageId, currentPagePath,
+    sourceRevisionId,
+    targetRevisionId,
+    onClose,
+    currentPageId,
+    currentPagePath,
   } = props;
 
   // Load all data if source revision id and target revision id not null
-  const revisionPerPage = (sourceRevisionId != null && targetRevisionId != null) ? 0 : REVISIONS_PER_PAGE;
-  const swrInifiniteResponse = useSWRxInfinitePageRevisions(currentPageId, revisionPerPage);
-
+  const revisionPerPage =
+    sourceRevisionId != null && targetRevisionId != null
+      ? 0
+      : REVISIONS_PER_PAGE;
+  const swrInifiniteResponse = useSWRxInfinitePageRevisions(
+    currentPageId,
+    revisionPerPage,
+  );
 
-  const {
-    data, size, error, setSize, isValidating,
-  } = swrInifiniteResponse;
+  const { data, size, error, setSize, isValidating } = swrInifiniteResponse;
 
   const revisions = data && data[0].revisions;
   const oldestRevision = revisions && revisions[revisions.length - 1];
 
   // First load
   const isLoadingInitialData = !data && !error;
-  const isLoadingMore = isLoadingInitialData
-    || (isValidating && data != null && typeof data[size - 1] === 'undefined');
-  const isReachingEnd = (revisionPerPage === 0) || !!(data != null && data[data.length - 1]?.revisions.length < REVISIONS_PER_PAGE);
+  const isLoadingMore =
+    isLoadingInitialData ||
+    (isValidating && data != null && typeof data[size - 1] === 'undefined');
+  const isReachingEnd =
+    revisionPerPage === 0 ||
+    !!(
+      data != null &&
+      data[data.length - 1]?.revisions.length < REVISIONS_PER_PAGE
+    );
 
   const [sourceRevision, setSourceRevision] = useState<IRevisionHasId>();
   const [targetRevision, setTargetRevision] = useState<IRevisionHasId>();
 
   const tbodyRef = useRef<HTMLTableSectionElement>(null);
 
-
   useEffect(() => {
     if (revisions != null) {
       // when both source and target are specified
       if (sourceRevisionId != null && targetRevisionId != null) {
-        const sourceRevision = revisions.filter(revision => revision._id === sourceRevisionId)[0];
-        const targetRevision = revisions.filter(revision => revision._id === targetRevisionId)[0];
+        const sourceRevision = revisions.filter(
+          (revision) => revision._id === sourceRevisionId,
+        )[0];
+        const targetRevision = revisions.filter(
+          (revision) => revision._id === targetRevisionId,
+        )[0];
         setSourceRevision(sourceRevision);
         setTargetRevision(targetRevision);
-      }
-      else {
+      } else {
         const latestRevision = revisions != null ? revisions[0] : undefined;
-        const previousRevision = revisions.length >= 2 ? revisions[1] : latestRevision;
+        const previousRevision =
+          revisions.length >= 2 ? revisions[1] : latestRevision;
         setTargetRevision(latestRevision);
         setSourceRevision(previousRevision);
       }
@@ -79,7 +93,8 @@ export const PageRevisionTable = (props: PageRevisionTableProps): JSX.Element =>
       const offset = 30; // Threshold before scroll actually reaching the end
       if (tbody) {
         // Scroll end
-        const isEnd = tbody.scrollTop + tbody.clientHeight + offset >= tbody.scrollHeight;
+        const isEnd =
+          tbody.scrollTop + tbody.clientHeight + offset >= tbody.scrollHeight;
         if (isEnd && !isLoadingMore && !isReachingEnd) {
           setSize(size + 1);
         }
@@ -95,10 +110,13 @@ export const PageRevisionTable = (props: PageRevisionTableProps): JSX.Element =>
     };
   }, [isLoadingMore, isReachingEnd, setSize, size]);
 
-
-  const renderRow = (revision: IRevisionHasId, previousRevision: IRevisionHasId, latestRevision: IRevisionHasId,
-      isOldestRevision: boolean, hasDiff: boolean) => {
-
+  const renderRow = (
+    revision: IRevisionHasId,
+    previousRevision: IRevisionHasId,
+    latestRevision: IRevisionHasId,
+    isOldestRevision: boolean,
+    hasDiff: boolean,
+  ) => {
     const revisionId = revision._id;
 
     const handleCompareLatestRevisionButton = () => {
@@ -159,7 +177,6 @@ export const PageRevisionTable = (props: PageRevisionTableProps): JSX.Element =>
                 checked={revisionId === sourceRevision?._id}
                 onChange={() => setSourceRevision(revision)}
               />
-              <label className="form-label form-check-label" htmlFor={`compareSource-${revisionId}`} />
             </div>
           )}
         </td>
@@ -175,7 +192,6 @@ export const PageRevisionTable = (props: PageRevisionTableProps): JSX.Element =>
                 checked={revisionId === targetRevision?._id}
                 onChange={() => setTargetRevision(revision)}
               />
-              <label className="form-label form-check-label" htmlFor={`compareTarget-${revisionId}`} />
             </div>
           )}
         </td>
@@ -185,7 +201,9 @@ export const PageRevisionTable = (props: PageRevisionTableProps): JSX.Element =>
 
   return (
     <>
-      <table className={`${styles['revision-history-table']} table revision-history-table`}>
+      <table
+        className={`${styles['revision-history-table']} table revision-history-table`}
+      >
         <thead>
           <tr className="d-flex">
             <th className="col">{t('page_history.revision')}</th>
@@ -194,18 +212,27 @@ export const PageRevisionTable = (props: PageRevisionTableProps): JSX.Element =>
           </tr>
         </thead>
         <tbody className="overflow-auto d-block" ref={tbodyRef}>
-          {revisions != null && data != null && data.map(apiResult => apiResult.revisions).flat()
-            .map((revision, idx) => {
-              const previousRevision = (idx + 1 < revisions?.length) ? revisions[idx + 1] : revision;
-
-              const isOldestRevision = revision === oldestRevision;
-              const latestRevision = revisions[0];
-
-              // set 'true' if undefined for backward compatibility
-              const hasDiff = revision.hasDiffToPrev !== false;
-              return renderRow(revision, previousRevision, latestRevision, isOldestRevision, hasDiff);
-            })
-          }
+          {revisions != null &&
+            data != null &&
+            data
+              .flatMap((apiResult) => apiResult.revisions)
+              .map((revision, idx) => {
+                const previousRevision =
+                  idx + 1 < revisions?.length ? revisions[idx + 1] : revision;
+
+                const isOldestRevision = revision === oldestRevision;
+                const latestRevision = revisions[0];
+
+                // set 'true' if undefined for backward compatibility
+                const hasDiff = revision.hasDiffToPrev !== false;
+                return renderRow(
+                  revision,
+                  previousRevision,
+                  latestRevision,
+                  isOldestRevision,
+                  hasDiff,
+                );
+              })}
         </tbody>
       </table>
 
@@ -219,9 +246,7 @@ export const PageRevisionTable = (props: PageRevisionTableProps): JSX.Element =>
             onClose={onClose}
           />
         </div>
-      )
-      }
+      )}
     </>
   );
-
 };

+ 45 - 26
apps/app/src/client/components/PageHistory/Revision.tsx

@@ -1,10 +1,9 @@
 import React, { type JSX } from 'react';
-
+import Link from 'next/link';
 import type { IRevisionHasId } from '@growi/core';
 import { returnPathForURL } from '@growi/core/dist/utils/path-utils';
 import { UserPicture } from '@growi/ui/dist/components';
 import { useTranslation } from 'next-i18next';
-import Link from 'next/link';
 import urljoin from 'url-join';
 
 import UserDate from '../../../components/User/UserDate';
@@ -13,34 +12,42 @@ import { Username } from '../../../components/User/Username';
 import styles from './Revision.module.scss';
 
 type RevisionProps = {
-  revision: IRevisionHasId,
-  isLatestRevision: boolean,
-  hasDiff: boolean,
-  currentPageId: string
-  currentPagePath: string
-  onClose: () => void,
-}
+  revision: IRevisionHasId;
+  isLatestRevision: boolean;
+  hasDiff: boolean;
+  currentPageId: string;
+  currentPagePath: string;
+  onClose: () => void;
+};
 
 export const Revision = (props: RevisionProps): JSX.Element => {
   const { t } = useTranslation();
 
   const {
-    revision, isLatestRevision, hasDiff, onClose, currentPageId, currentPagePath,
+    revision,
+    isLatestRevision,
+    hasDiff,
+    onClose,
+    currentPageId,
+    currentPagePath,
   } = props;
 
   const renderSimplifiedNodiff = (revision: IRevisionHasId) => {
-
     const author = revision.author;
 
-    const pic = (typeof author === 'object') ? <UserPicture user={author} size="sm" /> : <></>;
+    const pic =
+      typeof author === 'object' ? (
+        <UserPicture user={author} size="sm" />
+      ) : (
+        <></>
+      );
 
     return (
-      <div className={`${styles['revision-history-main']} ${styles['revision-history-main-nodiff']}
+      <div
+        className={`${styles['revision-history-main']} ${styles['revision-history-main-nodiff']}
         revision-history-main revision-history-main-nodiff my-1 d-flex align-items-center`}
       >
-        <div className="picture-container">
-          { pic }
-        </div>
+        <div className="picture-container">{pic}</div>
         <div className="ms-3">
           <span className="text-muted small">
             <UserDate dateTime={revision.createdAt} /> {t('No diff')}
@@ -51,31 +58,43 @@ export const Revision = (props: RevisionProps): JSX.Element => {
   };
 
   const renderFull = (revision: IRevisionHasId) => {
-
     const author = revision.author;
 
-    const pic = (typeof author === 'object') ? <UserPicture user={author} size="lg" /> : <></>;
+    const pic =
+      typeof author === 'object' ? (
+        <UserPicture user={author} size="lg" />
+      ) : (
+        <></>
+      );
 
     return (
-      <div className={`${styles['revision-history-main']} revision-history-main d-flex`}>
-        <div className="picture-container">
-          { pic }
-        </div>
+      <div
+        className={`${styles['revision-history-main']} revision-history-main d-flex`}
+      >
+        <div className="picture-container">{pic}</div>
         <div className="ms-2">
           <div className="revision-history-author mb-1">
-            <strong><Username user={author}></Username></strong>
-            { isLatestRevision && <span className="badge bg-info ms-2">{t('Latest')}</span> }
+            <strong>
+              <Username user={author}></Username>
+            </strong>
+            {isLatestRevision && (
+              <span className="badge bg-info ms-2">{t('Latest')}</span>
+            )}
           </div>
           <div className="mb-1">
             <UserDate dateTime={revision.createdAt} />
             <br className="d-xl-none d-block" />
             <Link
-              href={urljoin(returnPathForURL(currentPagePath, currentPageId), `?revisionId=${revision._id}`)}
+              href={urljoin(
+                returnPathForURL(currentPagePath, currentPageId),
+                `?revisionId=${revision._id}`,
+              )}
               className="ms-xl-3"
               onClick={onClose}
               prefetch={false}
             >
-              <span className="material-symbols-outlined">login</span> {t('Go to this version')}
+              <span className="material-symbols-outlined">login</span>{' '}
+              {t('Go to this version')}
             </Link>
           </div>
         </div>

+ 42 - 23
apps/app/src/client/components/PageHistory/RevisionDiff.tsx

@@ -1,5 +1,5 @@
-import { useMemo, type JSX } from 'react';
-
+import { type JSX, useMemo } from 'react';
+import Link from 'next/link';
 import type { IRevisionHasId } from '@growi/core';
 import { GrowiThemeSchemeType } from '@growi/core';
 import { returnPathForURL } from '@growi/core/dist/utils/path-utils';
@@ -9,16 +9,13 @@ import type { Diff2HtmlConfig } from 'diff2html';
 import { html } from 'diff2html';
 import { ColorSchemeType } from 'diff2html/lib/types';
 import { useTranslation } from 'next-i18next';
-import Link from 'next/link';
 import urljoin from 'url-join';
 
-
 import { Themes, useNextThemes } from '~/stores-universal/use-next-themes';
 
 import UserDate from '../../../components/User/UserDate';
 import { useSWRxGrowiThemeSetting } from '../../../stores/admin/customize';
 
-
 import styles from './RevisionDiff.module.scss';
 
 import 'diff2html/bundles/css/diff2html.min.css';
@@ -26,19 +23,24 @@ import 'diff2html/bundles/css/diff2html.min.css';
 const moduleClass = styles['revision-diff-container'];
 
 type RevisioinDiffProps = {
-  currentRevision: IRevisionHasId,
-  previousRevision: IRevisionHasId,
-  revisionDiffOpened: boolean,
-  currentPageId: string,
-  currentPagePath: string,
-  onClose: () => void,
-}
+  currentRevision: IRevisionHasId;
+  previousRevision: IRevisionHasId;
+  revisionDiffOpened: boolean;
+  currentPageId: string;
+  currentPagePath: string;
+  onClose: () => void;
+};
 
 export const RevisionDiff = (props: RevisioinDiffProps): JSX.Element => {
   const { t } = useTranslation();
 
   const {
-    currentRevision, previousRevision, revisionDiffOpened, currentPageId, currentPagePath, onClose,
+    currentRevision,
+    previousRevision,
+    revisionDiffOpened,
+    currentPageId,
+    currentPagePath,
+    onClose,
   } = props;
 
   const { theme: userTheme } = useNextThemes();
@@ -49,8 +51,11 @@ export const RevisionDiff = (props: RevisioinDiffProps): JSX.Element => {
       return ColorSchemeType.AUTO;
     }
 
-    const growiThemeSchemeType = growiTheme.pluginThemesMetadatas[0]?.schemeType
-        ?? PresetThemesMetadatas.find(theme => theme.name === growiTheme.currentTheme)?.schemeType;
+    const growiThemeSchemeType =
+      growiTheme.pluginThemesMetadatas[0]?.schemeType ??
+      PresetThemesMetadatas.find(
+        (theme) => theme.name === growiTheme.currentTheme,
+      )?.schemeType;
 
     switch (growiThemeSchemeType) {
       case GrowiThemeSchemeType.DARK:
@@ -58,7 +63,7 @@ export const RevisionDiff = (props: RevisioinDiffProps): JSX.Element => {
       case GrowiThemeSchemeType.LIGHT:
         return ColorSchemeType.LIGHT;
       default:
-        // growiThemeSchemeType === GrowiThemeSchemeType.BOTH
+      // growiThemeSchemeType === GrowiThemeSchemeType.BOTH
     }
     switch (userTheme) {
       case Themes.DARK:
@@ -70,7 +75,8 @@ export const RevisionDiff = (props: RevisioinDiffProps): JSX.Element => {
     }
   }, [growiTheme, userTheme]);
 
-  const previousText = (currentRevision._id === previousRevision._id) ? '' : previousRevision.body;
+  const previousText =
+    currentRevision._id === previousRevision._id ? '' : previousRevision.body;
 
   const patch = createPatch(
     currentRevision.pageId, // currentRevision.path is DEPRECATED
@@ -93,9 +99,14 @@ export const RevisionDiff = (props: RevisioinDiffProps): JSX.Element => {
       <div className="container">
         <div className="row mt-2">
           <div className="col px-0 py-2">
-            <span className="fw-bold">{t('page_history.comparing_source')}</span>
+            <span className="fw-bold">
+              {t('page_history.comparing_source')}
+            </span>
             <Link
-              href={urljoin(returnPathForURL(currentPagePath, currentPageId), `?revisionId=${previousRevision._id}`)}
+              href={urljoin(
+                returnPathForURL(currentPagePath, currentPageId),
+                `?revisionId=${previousRevision._id}`,
+              )}
               className="small ms-2
                 link-created-at
                 link-secondary link-opacity-75 link-opacity-100-hover
@@ -107,9 +118,14 @@ export const RevisionDiff = (props: RevisioinDiffProps): JSX.Element => {
             </Link>
           </div>
           <div className="col px-0 py-2">
-            <span className="fw-bold">{t('page_history.comparing_target')}</span>
+            <span className="fw-bold">
+              {t('page_history.comparing_target')}
+            </span>
             <Link
-              href={urljoin(returnPathForURL(currentPagePath, currentPageId), `?revisionId=${currentRevision._id}`)}
+              href={urljoin(
+                returnPathForURL(currentPagePath, currentPageId),
+                `?revisionId=${currentRevision._id}`,
+              )}
               className="small ms-2
                 link-created-at
                 link-secondary link-opacity-75 link-opacity-100-hover
@@ -123,8 +139,11 @@ export const RevisionDiff = (props: RevisioinDiffProps): JSX.Element => {
         </div>
       </div>
       {/* eslint-disable-next-line react/no-danger */}
-      <div className="revision-history-diff pb-1" dangerouslySetInnerHTML={diffView} />
+      <div
+        className="revision-history-diff pb-1"
+        // biome-ignore lint/security/noDangerouslySetInnerHtml: diff view is pre-sanitized HTML
+        dangerouslySetInnerHTML={diffView}
+      />
     </div>
   );
-
 };

+ 4 - 2
apps/app/src/client/components/Presentation/Presentation.tsx

@@ -1,6 +1,8 @@
 import type { JSX } from 'react';
-
-import { Presentation as PresentationSubstance, type PresentationProps } from '@growi/presentation/dist/client';
+import {
+  type PresentationProps,
+  Presentation as PresentationSubstance,
+} from '@growi/presentation/dist/client';
 
 import '@growi/presentation/dist/style.css';
 

+ 4 - 2
apps/app/src/client/components/Presentation/Slides.tsx

@@ -1,6 +1,8 @@
 import type { JSX } from 'react';
-
-import { Slides as SlidesSubstance, type SlidesProps } from '@growi/presentation/dist/client';
+import {
+  type SlidesProps,
+  Slides as SlidesSubstance,
+} from '@growi/presentation/dist/client';
 
 import '@growi/presentation/dist/style.css';
 

+ 85 - 54
apps/app/src/client/components/PutbackPageModal/PutbackPageModal.tsx

@@ -1,14 +1,14 @@
 import type { FC } from 'react';
-import { useState, useCallback, useMemo } from 'react';
-
+import { useCallback, useMemo, useState } from 'react';
 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 type { PutBackPageModalStatus } from '~/states/ui/modal/put-back-page';
-import { usePutBackPageModalActions, usePutBackPageModalStatus } from '~/states/ui/modal/put-back-page';
+import {
+  usePutBackPageModalActions,
+  usePutBackPageModalStatus,
+} from '~/states/ui/modal/put-back-page';
 import { mutateAllPageInfo } from '~/stores/page';
 
 import ApiErrorMessageList from '../PageManagement/ApiErrorMessageList';
@@ -24,11 +24,16 @@ type ApiResponse = {
 };
 
 type PutBackPageModalSubstanceProps = {
-  pageDataToRevert: PutBackPageModalStatus & { page: NonNullable<PutBackPageModalStatus['page']> };
+  pageDataToRevert: PutBackPageModalStatus & {
+    page: NonNullable<PutBackPageModalStatus['page']>;
+  };
   closePutBackPageModal: () => void;
 };
 
-const PutBackPageModalSubstance: FC<PutBackPageModalSubstanceProps> = ({ pageDataToRevert, closePutBackPageModal }) => {
+const PutBackPageModalSubstance: FC<PutBackPageModalSubstanceProps> = ({
+  pageDataToRevert,
+  closePutBackPageModal,
+}) => {
   const { t } = useTranslation();
 
   const { page } = pageDataToRevert;
@@ -44,7 +49,7 @@ const PutBackPageModalSubstance: FC<PutBackPageModalSubstanceProps> = ({ pageDat
     setIsPutbackRecursively(!isPutbackRecursively);
   }, [isPutbackRecursively]);
 
-  const putbackPageButtonHandler = useCallback(async() => {
+  const putbackPageButtonHandler = useCallback(async () => {
     setErrs(null);
 
     try {
@@ -62,8 +67,7 @@ const PutBackPageModalSubstance: FC<PutBackPageModalSubstanceProps> = ({ pageDat
         onPutBacked(response.page.path);
       }
       closePutBackPageModal();
-    }
-    catch (err) {
+    } catch (err) {
       setTargetPath((err as ApiError).data ?? null);
       setErrs([err as ApiError]);
     }
@@ -74,56 +78,79 @@ const PutBackPageModalSubstance: FC<PutBackPageModalSubstanceProps> = ({ pageDat
     setErrs(null);
   }, [closePutBackPageModal]);
 
-  const headerContent = useMemo(() => (
-    <>
-      <span className="material-symbols-outlined" aria-hidden="true">undo</span> { t('modal_putback.label.Put Back Page') }
-    </>
-  ), [t]);
+  const headerContent = useMemo(
+    () => (
+      <>
+        <span className="material-symbols-outlined" aria-hidden="true">
+          undo
+        </span>{' '}
+        {t('modal_putback.label.Put Back Page')}
+      </>
+    ),
+    [t],
+  );
 
-  const bodyContent = useMemo(() => (
-    <>
-      <div>
-        <label className="form-label">{t('modal_putback.label.Put Back Page')}:</label><br />
-        <code>{path}</code>
-      </div>
-      <div className="form-check form-check-warning">
-        <input
-          className="form-check-input"
-          id="cbPutBackRecursively"
-          type="checkbox"
-          checked={isPutbackRecursively}
-          onChange={changeIsPutbackRecursivelyHandler}
-        />
-        <label htmlFor="cbPutBackRecursively" className="form-label form-check-label">
-          { t('modal_putback.label.recursively') }
-        </label>
-        <p className="form-text text-muted mt-0">
-          <code>{ path }</code>{ t('modal_putback.help.recursively') }
-        </p>
-      </div>
-    </>
-  ), [t, path, isPutbackRecursively, changeIsPutbackRecursivelyHandler]);
+  const bodyContent = useMemo(
+    () => (
+      <>
+        <div>
+          <span className="form-label">
+            {t('modal_putback.label.Put Back Page')}:
+          </span>
+          <br />
+          <code>{path}</code>
+        </div>
+        <div className="form-check form-check-warning">
+          <input
+            className="form-check-input"
+            id="cbPutBackRecursively"
+            type="checkbox"
+            checked={isPutbackRecursively}
+            onChange={changeIsPutbackRecursivelyHandler}
+          />
+          <label
+            htmlFor="cbPutBackRecursively"
+            className="form-label form-check-label"
+          >
+            {t('modal_putback.label.recursively')}
+          </label>
+          <p className="form-text text-muted mt-0">
+            <code>{path}</code>
+            {t('modal_putback.help.recursively')}
+          </p>
+        </div>
+      </>
+    ),
+    [t, path, isPutbackRecursively, changeIsPutbackRecursivelyHandler],
+  );
 
-  const footerContent = useMemo(() => (
-    <>
-      <ApiErrorMessageList errs={errs} targetPath={targetPath} />
-      <button type="button" className="btn btn-info" onClick={putbackPageButtonHandler} data-testid="put-back-execution-button">
-        <span className="material-symbols-outlined" aria-hidden="true">undo</span> { t('Put Back') }
-      </button>
-    </>
-  ), [errs, targetPath, putbackPageButtonHandler, t]);
+  const footerContent = useMemo(
+    () => (
+      <>
+        <ApiErrorMessageList errs={errs} targetPath={targetPath} />
+        <button
+          type="button"
+          className="btn btn-info"
+          onClick={putbackPageButtonHandler}
+          data-testid="put-back-execution-button"
+        >
+          <span className="material-symbols-outlined" aria-hidden="true">
+            undo
+          </span>{' '}
+          {t('Put Back')}
+        </button>
+      </>
+    ),
+    [errs, targetPath, putbackPageButtonHandler, t],
+  );
 
   return (
     <>
       <ModalHeader tag="h4" toggle={closeModalHandler} className="text-info">
         {headerContent}
       </ModalHeader>
-      <ModalBody>
-        {bodyContent}
-      </ModalBody>
-      <ModalFooter>
-        {footerContent}
-      </ModalFooter>
+      <ModalBody>{bodyContent}</ModalBody>
+      <ModalFooter>{footerContent}</ModalFooter>
     </>
   );
 };
@@ -142,7 +169,11 @@ const PutBackPageModal: FC = () => {
   }
 
   return (
-    <Modal isOpen={isOpened} toggle={closeModalHandler} data-testid="put-back-page-modal">
+    <Modal
+      isOpen={isOpened}
+      toggle={closeModalHandler}
+      data-testid="put-back-page-modal"
+    >
       {isOpened && (
         <PutBackPageModalSubstance
           pageDataToRevert={{ ...pageDataToRevert, page }}

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

@@ -10,7 +10,10 @@ export const PutBackPageModalLazyLoaded = (): JSX.Element => {
 
   const PutBackPageModal = useLazyLoader<PutBackPageModalProps>(
     'put-back-page-modal',
-    () => import('./PutbackPageModal').then(mod => ({ default: mod.PutBackPageModal })),
+    () =>
+      import('./PutbackPageModal').then((mod) => ({
+        default: mod.PutBackPageModal,
+      })),
     status?.isOpened ?? false,
   );
 

+ 25 - 28
apps/app/src/client/components/RecentActivity/ActivityListItem.tsx

@@ -1,12 +1,14 @@
 import { formatDistanceToNow } from 'date-fns';
-import { type Locale } from 'date-fns/locale';
+import type { Locale } from 'date-fns/locale';
 import { useTranslation } from 'next-i18next';
 
-import type { SupportedActivityActionType, ActivityHasTargetPage } from '~/interfaces/activity';
+import type {
+  ActivityHasTargetPage,
+  SupportedActivityActionType,
+} from '~/interfaces/activity';
 import { ActivityLogActions } from '~/interfaces/activity';
 import { getLocale } from '~/server/util/locale-utils';
 
-
 export const ActivityActionTranslationMap: Record<
   SupportedActivityActionType,
   string
@@ -38,16 +40,16 @@ export const IconActivityTranslationMap: Record<
 };
 
 type ActivityListItemProps = {
-  activity: ActivityHasTargetPage,
-}
+  activity: ActivityHasTargetPage;
+};
 
 type AllowPageDisplayPayload = {
-  grant: number | undefined,
-  status: string,
-  wip: boolean,
-  deletedAt?: Date,
-  path: string,
-}
+  grant: number | undefined;
+  status: string;
+  wip: boolean;
+  deletedAt?: Date;
+  path: string;
+};
 
 const translateAction = (action: SupportedActivityActionType): string => {
   return ActivityActionTranslationMap[action] || 'unknown_action';
@@ -66,10 +68,10 @@ const calculateTimePassed = (date: Date, locale: Locale): string => {
   return timePassed;
 };
 
-const pageAllowedForDisplay = (allowDisplayPayload: AllowPageDisplayPayload): boolean => {
-  const {
-    grant, status, wip, deletedAt,
-  } = allowDisplayPayload;
+const pageAllowedForDisplay = (
+  allowDisplayPayload: AllowPageDisplayPayload,
+): boolean => {
+  const { grant, status, wip, deletedAt } = allowDisplayPayload;
   if (grant !== 1) return false;
 
   if (status !== 'published') return false;
@@ -87,18 +89,18 @@ const setPath = (path: string, allowed: boolean): string => {
   return '';
 };
 
-
-export const ActivityListItem = ({ props }: { props: ActivityListItemProps }): JSX.Element => {
+export const ActivityListItem = ({
+  props,
+}: {
+  props: ActivityListItemProps;
+}): JSX.Element => {
   const { t, i18n } = useTranslation();
   const currentLangCode = i18n.language;
   const dateFnsLocale = getLocale(currentLangCode);
 
   const { activity } = props;
 
-  const {
-    path, grant, status, wip, deletedAt,
-  } = activity.target;
-
+  const { path, grant, status, wip, deletedAt } = activity.target;
 
   const allowDisplayPayload: AllowPageDisplayPayload = {
     grant,
@@ -127,21 +129,16 @@ export const ActivityListItem = ({ props }: { props: ActivityListItemProps }): J
               href={setPath(path, isPageAllowed)}
               className="activity-target-link fw-bold text-wrap d-block"
             >
-              <span>
-                {setPath(path, isPageAllowed)}
-              </span>
+              <span>{setPath(path, isPageAllowed)}</span>
             </a>
           </div>
 
           <div className="activity-details-line d-flex">
-            <span>
-              {t(fullKeyPath)}
-            </span>
+            <span>{t(fullKeyPath)}</span>
 
             <span className="text-secondary small ms-3 align-self-center">
               {calculateTimePassed(activity.createdAt, dateFnsLocale)}
             </span>
-
           </div>
         </div>
       </div>

+ 32 - 23
apps/app/src/client/components/RecentActivity/RecentActivity.tsx

@@ -1,28 +1,31 @@
-import React, {
-  useState, useCallback, useEffect, type JSX,
-} from 'react';
+import React, { type JSX, useCallback, useEffect, useState } from 'react';
 
 import { toastError } from '~/client/util/toastr';
-import type { IActivityHasId, ActivityHasTargetPage } from '~/interfaces/activity';
+import type {
+  ActivityHasTargetPage,
+  IActivityHasId,
+} from '~/interfaces/activity';
 import { useSWRxRecentActivity } from '~/stores/recent-activity';
 import loggerFactory from '~/utils/logger';
 
 import PaginationWrapper from '../PaginationWrapper';
-
 import { ActivityListItem } from './ActivityListItem';
 
-
 const logger = loggerFactory('growi:RecentActivity');
 
 type RecentActivityProps = {
-  userId: string,
-}
-
-const hasTargetPage = (activity: IActivityHasId): activity is ActivityHasTargetPage => {
-  return activity.user != null
-         && typeof activity.user === 'object'
-         && activity.target != null
-         && typeof activity.target === 'object';
+  userId: string;
+};
+
+const hasTargetPage = (
+  activity: IActivityHasId,
+): activity is ActivityHasTargetPage => {
+  return (
+    activity.user != null &&
+    typeof activity.user === 'object' &&
+    activity.target != null &&
+    typeof activity.target === 'object'
+  );
 };
 
 export const RecentActivity = (props: RecentActivityProps): JSX.Element => {
@@ -33,14 +36,21 @@ export const RecentActivity = (props: RecentActivityProps): JSX.Element => {
   const [limit] = useState(10);
   const [offset, setOffset] = useState(0);
 
-  const { data: paginatedData, error } = useSWRxRecentActivity(limit, offset, userId);
+  const { data: paginatedData, error } = useSWRxRecentActivity(
+    limit,
+    offset,
+    userId,
+  );
 
-  const handlePage = useCallback(async(selectedPage: number) => {
-    const newOffset = (selectedPage - 1) * limit;
+  const handlePage = useCallback(
+    async (selectedPage: number) => {
+      const newOffset = (selectedPage - 1) * limit;
 
-    setOffset(newOffset);
-    setActivePage(selectedPage);
-  }, [limit]);
+      setOffset(newOffset);
+      setActivePage(selectedPage);
+    },
+    [limit],
+  );
 
   useEffect(() => {
     if (error) {
@@ -50,8 +60,7 @@ export const RecentActivity = (props: RecentActivityProps): JSX.Element => {
     }
 
     if (paginatedData) {
-      const activitiesWithPages = paginatedData.docs
-        .filter(hasTargetPage);
+      const activitiesWithPages = paginatedData.docs.filter(hasTargetPage);
 
       setActivities(activitiesWithPages);
     }
@@ -63,7 +72,7 @@ export const RecentActivity = (props: RecentActivityProps): JSX.Element => {
   return (
     <div className="page-list-container-activity">
       <ul className="page-list-ul page-list-ul-flat mb-3">
-        {activities.map(activity => (
+        {activities.map((activity) => (
           <li key={`recent-activity-view:${activity._id}`} className="mt-4">
             <ActivityListItem props={{ activity }} />
           </li>

+ 29 - 31
apps/app/src/client/components/RecentCreated/RecentCreated.tsx

@@ -1,7 +1,4 @@
-import React, {
-  useState, useCallback, useEffect, type JSX,
-} from 'react';
-
+import React, { type JSX, useCallback, useEffect, useState } from 'react';
 import type { IPageHasId } from '@growi/core';
 
 import { apiv3Get } from '~/client/util/apiv3-client';
@@ -14,11 +11,10 @@ import PaginationWrapper from '../PaginationWrapper';
 const logger = loggerFactory('growi:RecentCreated');
 
 type RecentCreatedProps = {
-  userId: string,
-}
+  userId: string;
+};
 
 export const RecentCreated = (props: RecentCreatedProps): JSX.Element => {
-
   const { userId } = props;
 
   const [pages, setPages] = useState<IPageHasId[]>([]);
@@ -26,42 +22,45 @@ export const RecentCreated = (props: RecentCreatedProps): JSX.Element => {
   const [totalPages, setTotalPages] = useState(0);
   const [pagingLimit, setPagingLimit] = useState(10);
 
-  const getMyRecentCreatedList = useCallback(async(selectedPage) => {
-    const page = selectedPage;
-
-    try {
-      const res = await apiv3Get(`/users/${userId}/recent`, { page });
-      const { totalCount, pages, limit } = res.data;
-
-      setPages(pages);
-      setActivePage(selectedPage);
-      setTotalPages(totalCount);
-      setPagingLimit(limit);
-    }
-    catch (error) {
-      logger.error('failed to fetch data', error);
-      toastError(error);
-    }
-  }, [userId]);
+  const getMyRecentCreatedList = useCallback(
+    async (selectedPage) => {
+      const page = selectedPage;
+
+      try {
+        const res = await apiv3Get(`/users/${userId}/recent`, { page });
+        const { totalCount, pages, limit } = res.data;
+
+        setPages(pages);
+        setActivePage(selectedPage);
+        setTotalPages(totalCount);
+        setPagingLimit(limit);
+      } catch (error) {
+        logger.error('failed to fetch data', error);
+        toastError(error);
+      }
+    },
+    [userId],
+  );
 
   useEffect(() => {
     getMyRecentCreatedList(1);
   }, [getMyRecentCreatedList]);
 
-  const handlePage = useCallback(async(selectedPage) => {
-    await getMyRecentCreatedList(selectedPage);
-  }, [getMyRecentCreatedList]);
+  const handlePage = useCallback(
+    async (selectedPage) => {
+      await getMyRecentCreatedList(selectedPage);
+    },
+    [getMyRecentCreatedList],
+  );
 
   return (
     <div className="page-list-container-create">
       <ul className="page-list-ul page-list-ul-flat mb-3">
-
-        {pages.map(page => (
+        {pages.map((page) => (
           <li key={`recent-created:list-view:${page._id}`} className="mt-4">
             <PageListItemS page={page} />
           </li>
         ))}
-
       </ul>
       <PaginationWrapper
         activePage={activePage}
@@ -73,5 +72,4 @@ export const RecentCreated = (props: RecentCreatedProps): JSX.Element => {
       />
     </div>
   );
-
 };

+ 51 - 34
apps/app/src/client/components/RevisionComparer/RevisionComparer.tsx

@@ -1,11 +1,13 @@
-import React, { useState, type JSX } from 'react';
-
+import React, { type JSX, useState } from 'react';
 import type { IRevisionHasId } from '@growi/core';
 import { pagePathUtils } from '@growi/core/dist/utils';
 import { useTranslation } from 'next-i18next';
 import { CopyToClipboard } from 'react-copy-to-clipboard';
 import {
-  Dropdown, DropdownToggle, DropdownMenu, DropdownItem,
+  Dropdown,
+  DropdownItem,
+  DropdownMenu,
+  DropdownToggle,
 } from 'reactstrap';
 
 import { RevisionDiff } from '../PageHistory/RevisionDiff';
@@ -16,29 +18,34 @@ const { encodeSpaces } = pagePathUtils;
 
 const DropdownItemContents = ({ title, contents }) => (
   <>
-    <div className="h6 mt-1 mb-2"><strong>{title}</strong></div>
+    <div className="h6 mt-1 mb-2">
+      <strong>{title}</strong>
+    </div>
     <div className="card mb-1 p-2">{contents}</div>
   </>
 );
 
 type RevisionComparerProps = {
-  sourceRevision: IRevisionHasId
-  targetRevision: IRevisionHasId
-  currentPageId?: string
-  currentPagePath: string
-  onClose: () => void
-}
+  sourceRevision: IRevisionHasId;
+  targetRevision: IRevisionHasId;
+  currentPageId?: string;
+  currentPagePath: string;
+  onClose: () => void;
+};
 
 export const RevisionComparer = (props: RevisionComparerProps): JSX.Element => {
   const { t } = useTranslation(['translation', 'commons']);
 
   const {
-    sourceRevision, targetRevision, onClose, currentPageId, currentPagePath,
+    sourceRevision,
+    targetRevision,
+    onClose,
+    currentPageId,
+    currentPagePath,
   } = props;
 
   const [dropdownOpen, setDropdownOpen] = useState(false);
 
-
   const toggleDropdown = () => {
     setDropdownOpen(!dropdownOpen);
   };
@@ -56,18 +63,23 @@ export const RevisionComparer = (props: RevisionComparerProps): JSX.Element => {
     return encodeSpaces(decodeURI(url.href));
   };
 
-  const isNodiff = (sourceRevision == null || targetRevision == null) ? true : sourceRevision._id === targetRevision._id;
+  const isNodiff =
+    sourceRevision == null || targetRevision == null
+      ? true
+      : sourceRevision._id === targetRevision._id;
 
   if (currentPageId == null || currentPagePath == null) {
-    return <>{ t('not_found_page.page_not_exist')}</>;
+    return <>{t('not_found_page.page_not_exist')}</>;
   }
 
   return (
     <div className={`${styles['revision-compare']} revision-compare`}>
       <div className="d-flex">
-        <h4 className="align-self-center">{ t('page_history.comparing_revisions') }</h4>
+        <h4 className="align-self-center">
+          {t('page_history.comparing_revisions')}
+        </h4>
 
-        { !isNodiff && (
+        {!isNodiff && (
           <Dropdown
             className="grw-copy-dropdown align-self-center ms-auto"
             isOpen={dropdownOpen}
@@ -80,37 +92,42 @@ export const RevisionComparer = (props: RevisionComparerProps): JSX.Element => {
               {/* Page path URL */}
               <CopyToClipboard text={generateURL(currentPagePath)}>
                 <DropdownItem className="px-3">
-                  <DropdownItemContents title={t('copy_to_clipboard.Page URL', { ns: 'commons' })} contents={generateURL(currentPagePath)} />
+                  <DropdownItemContents
+                    title={t('copy_to_clipboard.Page URL', { ns: 'commons' })}
+                    contents={generateURL(currentPagePath)}
+                  />
                 </DropdownItem>
               </CopyToClipboard>
               {/* Permanent Link URL */}
               <CopyToClipboard text={generateURL(currentPageId)}>
                 <DropdownItem className="px-3">
-                  <DropdownItemContents title={t('copy_to_clipboard.Permanent link', { ns: 'commons' })} contents={generateURL(currentPageId)} />
+                  <DropdownItemContents
+                    title={t('copy_to_clipboard.Permanent link', {
+                      ns: 'commons',
+                    })}
+                    contents={generateURL(currentPageId)}
+                  />
                 </DropdownItem>
               </CopyToClipboard>
               <DropdownItem divider className="my-0"></DropdownItem>
             </DropdownMenu>
           </Dropdown>
-        ) }
+        )}
       </div>
 
       <div className={`revision-compare-container ${isNodiff ? 'nodiff' : ''}`}>
-        { isNodiff
-          ? (
-            <span className="h3 text-muted">{t('No diff')}</span>
-          )
-          : (
-            <RevisionDiff
-              revisionDiffOpened
-              previousRevision={sourceRevision}
-              currentRevision={targetRevision}
-              currentPageId={currentPageId}
-              currentPagePath={currentPagePath}
-              onClose={onClose}
-            />
-          )
-        }
+        {isNodiff ? (
+          <span className="h3 text-muted">{t('No diff')}</span>
+        ) : (
+          <RevisionDiff
+            revisionDiffOpened
+            previousRevision={sourceRevision}
+            currentRevision={targetRevision}
+            currentPageId={currentPageId}
+            currentPagePath={currentPagePath}
+            onClose={onClose}
+          />
+        )}
       </div>
     </div>
   );

+ 173 - 58
apps/app/src/client/components/ShortcutsModal/ShortcutsModal.tsx

@@ -1,13 +1,15 @@
-import React, { useMemo } from 'react';
-
+import type React from 'react';
+import { useMemo } from 'react';
 import { useTranslation } from 'next-i18next';
-import { Modal, ModalHeader, ModalBody } from 'reactstrap';
+import { Modal, ModalBody, ModalHeader } from 'reactstrap';
 
-import { useShortcutsModalStatus, useShortcutsModalActions } from '~/states/ui/modal/shortcuts';
+import {
+  useShortcutsModalActions,
+  useShortcutsModalStatus,
+} from '~/states/ui/modal/shortcuts';
 
 import styles from './ShortcutsModal.module.scss';
 
-
 /**
  * ShortcutsModalSubstance - Presentation component (heavy logic, rendered only when isOpen)
  */
@@ -18,7 +20,7 @@ const ShortcutsModalSubstance = (): React.JSX.Element => {
   // Memoize OS-specific class
   const additionalClassByOs = useMemo(() => {
     const platform = window.navigator.platform.toLowerCase();
-    const isMac = (platform.indexOf('mac') > -1);
+    const isMac = platform.indexOf('mac') > -1;
     return isMac ? 'mac' : 'win';
   }, []);
 
@@ -39,7 +41,12 @@ const ShortcutsModalSubstance = (): React.JSX.Element => {
                   <span
                     className="text-nowrap"
                     // eslint-disable-next-line react/no-danger
-                    dangerouslySetInnerHTML={{ __html: t('modal_shortcuts.global.Open/Close shortcut help') }}
+                    // biome-ignore lint/security/noDangerouslySetInnerHtml: i18n content includes markup
+                    dangerouslySetInnerHTML={{
+                      __html: t(
+                        'modal_shortcuts.global.Open/Close shortcut help',
+                      ),
+                    }}
                   />
                 </div>
                 <div className="d-flex align-items-center">
@@ -50,21 +57,27 @@ const ShortcutsModalSubstance = (): React.JSX.Element => {
               </li>
               {/* Create Page */}
               <li className="d-flex align-items-center p-3 border-bottom">
-                <div className="flex-grow-1">{t('modal_shortcuts.global.Create Page')}</div>
+                <div className="flex-grow-1">
+                  {t('modal_shortcuts.global.Create Page')}
+                </div>
                 <div>
                   <span className="key">C</span>
                 </div>
               </li>
               {/* Edit Page */}
               <li className="d-flex align-items-center p-3 border-bottom">
-                <div className="flex-grow-1">{t('modal_shortcuts.global.Edit Page')}</div>
+                <div className="flex-grow-1">
+                  {t('modal_shortcuts.global.Edit Page')}
+                </div>
                 <div>
                   <span className="key">E</span>
                 </div>
               </li>
               {/* Search */}
               <li className="d-flex align-items-center p-3 border-bottom">
-                <div className="flex-grow-1">{t('modal_shortcuts.global.Search')}</div>
+                <div className="flex-grow-1">
+                  {t('modal_shortcuts.global.Search')}
+                </div>
                 <div>
                   <span className="key">/</span>
                 </div>
@@ -75,27 +88,50 @@ const ShortcutsModalSubstance = (): React.JSX.Element => {
                   <span
                     className="text-nowrap"
                     // eslint-disable-next-line react/no-danger
-                    dangerouslySetInnerHTML={{ __html: t('modal_shortcuts.global.Show Contributors') }}
+                    // biome-ignore lint/security/noDangerouslySetInnerHtml: i18n content includes markup
+                    dangerouslySetInnerHTML={{
+                      __html: t('modal_shortcuts.global.Show Contributors'),
+                    }}
                   />
                 </div>
                 <div className="text-start">
-                  <a href={t('modal_shortcuts.global.konami_code_url')} target="_blank" rel="noreferrer">
+                  <a
+                    href={t('modal_shortcuts.global.konami_code_url')}
+                    target="_blank"
+                    rel="noreferrer"
+                  >
                     <span className="text-secondary small">
                       {t('modal_shortcuts.global.Konami Code')}
                     </span>
                   </a>
                   <div className="d-flex gap-2 flex-column align-items-start mt-1">
                     <div className="d-flex gap-1">
-                      <span className="key material-symbols-outlined fs-5 px-0">arrow_upward</span>
-                      <span className="key material-symbols-outlined fs-5 px-0">arrow_upward</span>
-                      <span className="key material-symbols-outlined fs-5 px-0">arrow_downward</span>
-                      <span className="key material-symbols-outlined fs-5 px-0">arrow_downward</span>
+                      <span className="key material-symbols-outlined fs-5 px-0">
+                        arrow_upward
+                      </span>
+                      <span className="key material-symbols-outlined fs-5 px-0">
+                        arrow_upward
+                      </span>
+                      <span className="key material-symbols-outlined fs-5 px-0">
+                        arrow_downward
+                      </span>
+                      <span className="key material-symbols-outlined fs-5 px-0">
+                        arrow_downward
+                      </span>
                     </div>
                     <div className="d-flex gap-1">
-                      <span className="key material-symbols-outlined fs-5 px-0">arrow_back</span>
-                      <span className="key material-symbols-outlined fs-5 px-0">arrow_forward</span>
-                      <span className="key material-symbols-outlined fs-5 px-0">arrow_back</span>
-                      <span className="key material-symbols-outlined fs-5 px-0">arrow_forward</span>
+                      <span className="key material-symbols-outlined fs-5 px-0">
+                        arrow_back
+                      </span>
+                      <span className="key material-symbols-outlined fs-5 px-0">
+                        arrow_forward
+                      </span>
+                      <span className="key material-symbols-outlined fs-5 px-0">
+                        arrow_back
+                      </span>
+                      <span className="key material-symbols-outlined fs-5 px-0">
+                        arrow_forward
+                      </span>
                     </div>
                     <div className="d-flex gap-1">
                       <span className="key">B</span>
@@ -106,9 +142,15 @@ const ShortcutsModalSubstance = (): React.JSX.Element => {
               </li>
               {/* Mirror Mode */}
               <li className="d-flex align-items-center p-3">
-                <div className="flex-grow-1">{t('modal_shortcuts.global.MirrorMode')}</div>
+                <div className="flex-grow-1">
+                  {t('modal_shortcuts.global.MirrorMode')}
+                </div>
                 <div className="text-start">
-                  <a href={t('modal_shortcuts.global.konami_code_url')} target="_blank" rel="noreferrer">
+                  <a
+                    href={t('modal_shortcuts.global.konami_code_url')}
+                    target="_blank"
+                    rel="noreferrer"
+                  >
                     <span className="text-secondary small">
                       {t('modal_shortcuts.global.Konami Code')}
                     </span>
@@ -127,8 +169,12 @@ const ShortcutsModalSubstance = (): React.JSX.Element => {
                       <span className="key">Y</span>
                     </div>
                     <div className="d-flex gap-1">
-                      <span className="key material-symbols-outlined fs-5 px-0">arrow_downward</span>
-                      <span className="key material-symbols-outlined fs-5 px-0">arrow_back</span>
+                      <span className="key material-symbols-outlined fs-5 px-0">
+                        arrow_downward
+                      </span>
+                      <span className="key material-symbols-outlined fs-5 px-0">
+                        arrow_back
+                      </span>
                     </div>
                   </div>
                 </div>
@@ -143,7 +189,9 @@ const ShortcutsModalSubstance = (): React.JSX.Element => {
             <ul className="list-unstyled m-0">
               {/* Search in Editor */}
               <li className="d-flex align-items-center p-3 border-bottom">
-                <div className="flex-grow-1">{t('modal_shortcuts.editor.Search in Editor')}</div>
+                <div className="flex-grow-1">
+                  {t('modal_shortcuts.editor.Search in Editor')}
+                </div>
                 <div className="text-nowrap">
                   <span className={`key cmd-key ${additionalClassByOs}`}></span>
                   <span className="text-secondary mx-2">+</span>
@@ -154,7 +202,9 @@ const ShortcutsModalSubstance = (): React.JSX.Element => {
               <li className="d-flex align-items-center p-3 border-bottom">
                 <div className="flex-grow-1">
                   {t('modal_shortcuts.editor.Save Page')}
-                  <span className="small text-secondary ms-1">{t('modal_shortcuts.editor.Only Editor')}</span>
+                  <span className="small text-secondary ms-1">
+                    {t('modal_shortcuts.editor.Only Editor')}
+                  </span>
                 </div>
                 <div className="text-nowrap">
                   <span className={`key cmd-key ${additionalClassByOs}`}></span>
@@ -164,14 +214,18 @@ const ShortcutsModalSubstance = (): React.JSX.Element => {
               </li>
               {/* Indent */}
               <li className="d-flex align-items-center p-3 border-bottom">
-                <div className="flex-grow-1">{t('modal_shortcuts.editor.Indent')}</div>
+                <div className="flex-grow-1">
+                  {t('modal_shortcuts.editor.Indent')}
+                </div>
                 <div>
                   <span className="key">Tab</span>
                 </div>
               </li>
               {/* Outdent */}
               <li className="d-flex align-items-center p-3 border-bottom">
-                <div className="flex-grow-1">{t('modal_shortcuts.editor.Outdent')}</div>
+                <div className="flex-grow-1">
+                  {t('modal_shortcuts.editor.Outdent')}
+                </div>
                 <div className="text-nowrap gap-1">
                   <span className="key">Shift</span>
                   <span className="text-secondary mx-2">+</span>
@@ -180,7 +234,9 @@ const ShortcutsModalSubstance = (): React.JSX.Element => {
               </li>
               {/* Delete Line */}
               <li className="d-flex align-items-center p-3 border-bottom">
-                <div className="flex-grow-1">{t('modal_shortcuts.editor.Delete Line')}</div>
+                <div className="flex-grow-1">
+                  {t('modal_shortcuts.editor.Delete Line')}
+                </div>
                 <div className="text-nowrap">
                   <span className={`key cmd-key ${additionalClassByOs}`}></span>
                   <span className="text-secondary mx-2">+</span>
@@ -194,10 +250,15 @@ const ShortcutsModalSubstance = (): React.JSX.Element => {
                 <div className="flex-grow-1">
                   <span
                     // eslint-disable-next-line react/no-danger
-                    dangerouslySetInnerHTML={{ __html: t('modal_shortcuts.editor.Insert Line') }}
+                    // biome-ignore lint/security/noDangerouslySetInnerHtml: i18n content includes markup
+                    dangerouslySetInnerHTML={{
+                      __html: t('modal_shortcuts.editor.Insert Line'),
+                    }}
                   />
                   <br />
-                  <span className="small text-secondary ms-1">{t('modal_shortcuts.editor.Post Comment')}</span>
+                  <span className="small text-secondary ms-1">
+                    {t('modal_shortcuts.editor.Post Comment')}
+                  </span>
                 </div>
                 <div className="text-nowrap">
                   <span className={`key cmd-key ${additionalClassByOs}`}></span>
@@ -207,30 +268,44 @@ const ShortcutsModalSubstance = (): React.JSX.Element => {
               </li>
               {/* Move Line */}
               <li className="d-flex align-items-center p-3 border-bottom">
-                <div className="flex-grow-1">{t('modal_shortcuts.editor.Move Line')}</div>
+                <div className="flex-grow-1">
+                  {t('modal_shortcuts.editor.Move Line')}
+                </div>
                 <div className="text-nowrap">
                   <span className={`key alt-key ${additionalClassByOs}`}></span>
                   <span className="text-secondary mx-2">+</span>
-                  <span className="key material-symbols-outlined fs-5 px-0">arrow_downward</span>
+                  <span className="key material-symbols-outlined fs-5 px-0">
+                    arrow_downward
+                  </span>
                   <span className="text-secondary mx-2">or</span>
-                  <span className="key material-symbols-outlined fs-5 px-0">arrow_upward</span>
+                  <span className="key material-symbols-outlined fs-5 px-0">
+                    arrow_upward
+                  </span>
                 </div>
               </li>
               {/* Copy Line */}
               <li className="d-flex align-items-center p-3 border-bottom">
-                <div className="flex-grow-1">{t('modal_shortcuts.editor.Copy Line')}</div>
+                <div className="flex-grow-1">
+                  {t('modal_shortcuts.editor.Copy Line')}
+                </div>
                 <div className="text-nowrap">
                   <div className="text-start">
                     <div>
-                      <span className={`key alt-key ${additionalClassByOs}`}></span>
+                      <span
+                        className={`key alt-key ${additionalClassByOs}`}
+                      ></span>
                       <span className="text-secondary mx-2">+</span>
                       <span className="key">Shift</span>
                       <span className="text-secondary ms-2">+</span>
                     </div>
                     <div className="mt-1">
-                      <span className="key material-symbols-outlined fs-5 px-0">arrow_downward</span>
+                      <span className="key material-symbols-outlined fs-5 px-0">
+                        arrow_downward
+                      </span>
                       <span className="text-secondary mx-2">or</span>
-                      <span className="key material-symbols-outlined fs-5 px-0">arrow_upward</span>
+                      <span className="key material-symbols-outlined fs-5 px-0">
+                        arrow_upward
+                      </span>
                     </div>
                   </div>
                 </div>
@@ -243,18 +318,27 @@ const ShortcutsModalSubstance = (): React.JSX.Element => {
                 <div className="text-nowrap">
                   <div className="text-end">
                     <div>
-                      <span className={`key cmd-key ${additionalClassByOs}`}></span>
+                      <span
+                        className={`key cmd-key ${additionalClassByOs}`}
+                      ></span>
                       <span className="text-secondary mx-2">+</span>
-                      <span className={`key alt-key ${additionalClassByOs}`}></span>
+                      <span
+                        className={`key alt-key ${additionalClassByOs}`}
+                      ></span>
                       <span className="text-secondary ms-2">+</span>
                     </div>
                     <div className="mt-1">
-                      <span className="key material-symbols-outlined fs-5 px-0">arrow_downward</span>
+                      <span className="key material-symbols-outlined fs-5 px-0">
+                        arrow_downward
+                      </span>
                       <span className="text-secondary mx-2">or</span>
-                      <span className="key material-symbols-outlined fs-5 px-0">arrow_upward</span>
+                      <span className="key material-symbols-outlined fs-5 px-0">
+                        arrow_upward
+                      </span>
                     </div>
-                    <span className="small text-secondary">{t('modal_shortcuts.editor.Or Alt Click')}</span>
-
+                    <span className="small text-secondary">
+                      {t('modal_shortcuts.editor.Or Alt Click')}
+                    </span>
                   </div>
                 </div>
               </li>
@@ -271,7 +355,9 @@ const ShortcutsModalSubstance = (): React.JSX.Element => {
             <ul className="list-unstyled m-0">
               {/* Bold */}
               <li className="d-flex align-items-center p-3 border-bottom">
-                <div className="flex-grow-1">{t('modal_shortcuts.format.Bold')}</div>
+                <div className="flex-grow-1">
+                  {t('modal_shortcuts.format.Bold')}
+                </div>
                 <div className="text-nowrap">
                   <span className={`key cmd-key ${additionalClassByOs}`}></span>
                   <span className="text-secondary mx-2">+</span>
@@ -280,7 +366,9 @@ const ShortcutsModalSubstance = (): React.JSX.Element => {
               </li>
               {/* Italic */}
               <li className="d-flex align-items-center p-3 border-bottom">
-                <div className="flex-grow-1">{t('modal_shortcuts.format.Italic')}</div>
+                <div className="flex-grow-1">
+                  {t('modal_shortcuts.format.Italic')}
+                </div>
                 <div className="text-nowrap">
                   <span className={`key cmd-key ${additionalClassByOs}`}></span>
                   <span className="text-secondary mx-2">+</span>
@@ -291,7 +379,9 @@ const ShortcutsModalSubstance = (): React.JSX.Element => {
               </li>
               {/* Strikethrough */}
               <li className="d-flex align-items-center p-3 border-bottom">
-                <div className="flex-grow-1">{t('modal_shortcuts.format.Strikethrough')}</div>
+                <div className="flex-grow-1">
+                  {t('modal_shortcuts.format.Strikethrough')}
+                </div>
                 <div className="text-nowrap">
                   <span className={`key cmd-key ${additionalClassByOs}`}></span>
                   <span className="text-secondary mx-2">+</span>
@@ -302,7 +392,9 @@ const ShortcutsModalSubstance = (): React.JSX.Element => {
               </li>
               {/* Code Text */}
               <li className="d-flex align-items-center p-3 border-bottom">
-                <div className="flex-grow-1">{t('modal_shortcuts.format.Code Text')}</div>
+                <div className="flex-grow-1">
+                  {t('modal_shortcuts.format.Code Text')}
+                </div>
                 <div className="text-nowrap">
                   <span className={`key cmd-key ${additionalClassByOs}`}></span>
                   <span className="text-secondary mx-2">+</span>
@@ -313,7 +405,9 @@ const ShortcutsModalSubstance = (): React.JSX.Element => {
               </li>
               {/* Hyperlink */}
               <li className="d-flex align-items-center p-3">
-                <div className="flex-grow-1">{t('modal_shortcuts.format.Hyperlink')}</div>
+                <div className="flex-grow-1">
+                  {t('modal_shortcuts.format.Hyperlink')}
+                </div>
                 <div className="text-nowrap">
                   <span className={`key cmd-key ${additionalClassByOs}`}></span>
                   <span className="text-secondary mx-2">+</span>
@@ -332,7 +426,9 @@ const ShortcutsModalSubstance = (): React.JSX.Element => {
             <ul className="list-unstyled m-0">
               {/* Simple List */}
               <li className="d-flex align-items-center p-3 border-bottom">
-                <div className="flex-grow-1">{t('modal_shortcuts.line_settings.Numbered List')}</div>
+                <div className="flex-grow-1">
+                  {t('modal_shortcuts.line_settings.Numbered List')}
+                </div>
                 <div className="text-nowrap">
                   <span className={`key cmd-key ${additionalClassByOs}`}></span>
                   <span className="text-secondary mx-2">+</span>
@@ -343,7 +439,9 @@ const ShortcutsModalSubstance = (): React.JSX.Element => {
               </li>
               {/* Numbered List */}
               <li className="d-flex align-items-center p-3 border-bottom">
-                <div className="flex-grow-1">{t('modal_shortcuts.line_settings.Bullet List')}</div>
+                <div className="flex-grow-1">
+                  {t('modal_shortcuts.line_settings.Bullet List')}
+                </div>
                 <div className="text-nowrap">
                   <span className={`key cmd-key ${additionalClassByOs}`}></span>
                   <span className="text-secondary mx-2">+</span>
@@ -354,7 +452,9 @@ const ShortcutsModalSubstance = (): React.JSX.Element => {
               </li>
               {/* Quote */}
               <li className="d-flex align-items-center p-3 border-bottom">
-                <div className="flex-grow-1">{t('modal_shortcuts.line_settings.Quote')}</div>
+                <div className="flex-grow-1">
+                  {t('modal_shortcuts.line_settings.Quote')}
+                </div>
                 <div className="text-nowrap">
                   <span className={`key cmd-key ${additionalClassByOs}`}></span>
                   <span className="text-secondary mx-2">+</span>
@@ -365,13 +465,19 @@ const ShortcutsModalSubstance = (): React.JSX.Element => {
               </li>
               {/* Code Block */}
               <li className="d-flex align-items-center p-3 border-bottom">
-                <div className="flex-grow-1">{t('modal_shortcuts.line_settings.Code Block')}</div>
+                <div className="flex-grow-1">
+                  {t('modal_shortcuts.line_settings.Code Block')}
+                </div>
                 <div className="text-nowrap">
                   <div className="text-start">
                     <div>
-                      <span className={`key cmd-key ${additionalClassByOs}`}></span>
+                      <span
+                        className={`key cmd-key ${additionalClassByOs}`}
+                      ></span>
                       <span className="text-secondary mx-2">+</span>
-                      <span className={`key alt-key ${additionalClassByOs}`}></span>
+                      <span
+                        className={`key alt-key ${additionalClassByOs}`}
+                      ></span>
                       <span className="text-secondary ms-2">+</span>
                     </div>
                     <div className="mt-1">
@@ -385,8 +491,11 @@ const ShortcutsModalSubstance = (): React.JSX.Element => {
               {/* Hide comments */}
               <li className="d-flex align-items-center p-3">
                 <div className="flex-grow-1">
-                  {t('modal_shortcuts.line_settings.Comment Out')}<br />
-                  <span className="small text-secondary">{t('modal_shortcuts.line_settings.Comment Out Desc')}</span>
+                  {t('modal_shortcuts.line_settings.Comment Out')}
+                  <br />
+                  <span className="small text-secondary">
+                    {t('modal_shortcuts.line_settings.Comment Out Desc')}
+                  </span>
                 </div>
                 <div className="text-nowrap">
                   <span className={`key cmd-key ${additionalClassByOs}`}></span>
@@ -421,7 +530,13 @@ export const ShortcutsModal = (): React.JSX.Element => {
   const { close } = useShortcutsModalActions();
 
   return (
-    <Modal id="shortcuts-modal" size="lg" isOpen={status?.isOpened ?? false} toggle={close} className={`shortcuts-modal ${styles['shortcuts-modal']}`}>
+    <Modal
+      id="shortcuts-modal"
+      size="lg"
+      isOpen={status?.isOpened ?? false}
+      toggle={close}
+      className={`shortcuts-modal ${styles['shortcuts-modal']}`}
+    >
       {status?.isOpened && <ShortcutsModalSubstance />}
     </Modal>
   );

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

@@ -10,7 +10,10 @@ export const ShortcutsModalLazyLoaded = (): JSX.Element => {
 
   const ShortcutsModal = useLazyLoader<ShortcutsModalProps>(
     'shortcuts-modal',
-    () => import('./ShortcutsModal').then(mod => ({ default: mod.ShortcutsModal })),
+    () =>
+      import('./ShortcutsModal').then((mod) => ({
+        default: mod.ShortcutsModal,
+      })),
     status?.isOpened ?? false,
   );
 

+ 39 - 31
apps/app/src/client/components/StaffCredit/StaffCredit.tsx

@@ -1,22 +1,16 @@
-import React, { useCallback, useState, type JSX } from 'react';
-
+import React, { type JSX, useCallback, useState } from 'react';
 import localFont from 'next/font/local';
 import { animateScroll } from 'react-scroll';
-import {
-  Modal, ModalBody,
-} from 'reactstrap';
+import { Modal, ModalBody } from 'reactstrap';
 
 import { useSWRxStaffs } from '~/stores/staff';
 import loggerFactory from '~/utils/logger';
 
-
 import styles from './StaffCredit.module.scss';
 
-
 // eslint-disable-next-line @typescript-eslint/no-unused-vars
 const logger = loggerFactory('growi:cli:StaffCredit');
 
-
 // define fonts
 const pressStart2P = localFont({
   src: '../../../../resource/fonts/PressStart2P-latin.woff2',
@@ -24,20 +18,17 @@ const pressStart2P = localFont({
   preload: false,
 });
 
-
 type Props = {
-  onClosed?: () => void,
-}
+  onClosed?: () => void;
+};
 
 const StaffCredit = (props: Props): JSX.Element => {
-
   const { onClosed } = props;
 
   const { data: contributors } = useSWRxStaffs();
 
   const [isScrolling, setScrolling] = useState(false);
 
-
   const closeHandler = useCallback(() => {
     if (onClosed != null) {
       onClosed();
@@ -47,8 +38,7 @@ const StaffCredit = (props: Props): JSX.Element => {
   const contentsClickedHandler = useCallback(() => {
     if (isScrolling) {
       setScrolling(false);
-    }
-    else {
+    } else {
       closeHandler();
     }
   }, [closeHandler, isScrolling]);
@@ -57,19 +47,25 @@ const StaffCredit = (props: Props): JSX.Element => {
     // construct members elements
     const members = memberGroup.members.map((member) => {
       return (
-        <div className={memberGroup.additionalClass} key={`${keyPrefix}-${member.name}-container`}>
-          <span className="dev-position" key={`${keyPrefix}-${member.name}-position`}>
+        <div
+          className={memberGroup.additionalClass}
+          key={`${keyPrefix}-${member.name}-container`}
+        >
+          <span
+            className="dev-position"
+            key={`${keyPrefix}-${member.name}-position`}
+          >
             {/* position or '&nbsp;' */}
-            { member.position || '\u00A0' }
+            {member.position || '\u00A0'}
           </span>
-          <p className="dev-name" key={`${keyPrefix}-${member.name}`}>{member.name}</p>
+          <p className="dev-name" key={`${keyPrefix}-${member.name}`}>
+            {member.name}
+          </p>
         </div>
       );
     });
     return (
-      <React.Fragment key={`${keyPrefix}-fragment`}>
-        {members}
-      </React.Fragment>
+      <React.Fragment key={`${keyPrefix}-fragment`}>{members}</React.Fragment>
     );
   }, []);
 
@@ -81,12 +77,23 @@ const StaffCredit = (props: Props): JSX.Element => {
     const credit = contributors.map((contributor) => {
       // construct members elements
       const memberGroups = contributor.memberGroups.map((memberGroup, idx) => {
-        return renderMembers(memberGroup, `${contributor.sectionName}-group${idx}`);
+        return renderMembers(
+          memberGroup,
+          `${contributor.sectionName}-group${idx}`,
+        );
       });
       return (
         <React.Fragment key={`${contributor.sectionName}-fragment`}>
-          <div className={`row ${contributor.additionalClass}`} key={`${contributor.sectionName}-row`}>
-            <h2 className="col-md-12 dev-team staff-credit-mt-10rem staff-credit-mb-6rem" key={contributor.sectionName}>{contributor.sectionName}</h2>
+          <div
+            className={`row ${contributor.additionalClass}`}
+            key={`${contributor.sectionName}-row`}
+          >
+            <h2
+              className="col-md-12 dev-team staff-credit-mt-10rem staff-credit-mb-6rem"
+              key={contributor.sectionName}
+            >
+              {contributor.sectionName}
+            </h2>
             {memberGroups}
           </div>
           <div className="clearfix"></div>
@@ -94,15 +101,18 @@ const StaffCredit = (props: Props): JSX.Element => {
       );
     });
     return (
-      <div className="text-center staff-credit-content" onClick={contentsClickedHandler}>
+      <button
+        type="button"
+        className="text-center staff-credit-content btn btn-link p-0 border-0"
+        onClick={contentsClickedHandler}
+      >
         <h1 className="staff-credit-mb-6rem">GROWI Contributors</h1>
         <div className="clearfix"></div>
         {credit}
-      </div>
+      </button>
     );
   }, [contentsClickedHandler, contributors, renderMembers]);
 
-
   const openedHandler = useCallback(() => {
     // init
     animateScroll.scrollTo(0, { containerId: 'modalBody', duration: 0 });
@@ -116,12 +126,11 @@ const StaffCredit = (props: Props): JSX.Element => {
       delay: 200,
       duration: (scrollDistanceInPx: number) => {
         const scrollSpeed = 200;
-        return scrollDistanceInPx / scrollSpeed * 1000;
+        return (scrollDistanceInPx / scrollSpeed) * 1000;
       },
     });
   }, []);
 
-
   const isLoaded = contributors !== undefined;
 
   if (contributors == null) {
@@ -142,7 +151,6 @@ const StaffCredit = (props: Props): JSX.Element => {
       <div className="background"></div>
     </Modal>
   );
-
 };
 
 export default StaffCredit;

+ 237 - 144
apps/app/src/client/components/TemplateModal/TemplateModal.tsx

@@ -1,25 +1,33 @@
 import React, {
-  useCallback, useEffect, useState, useMemo, type JSX,
+  type JSX,
+  useCallback,
+  useEffect,
+  useMemo,
+  useState,
 } from 'react';
-
-import assert from 'assert';
-
 import type { Lang } from '@growi/core';
-import { useTemplateModalStatus, useTemplateModalActions, type TemplateModalState } from '@growi/editor';
 import {
-  extractSupportedLocales, getLocalizedTemplate, type TemplateSummary,
+  type TemplateModalState,
+  useTemplateModalActions,
+  useTemplateModalStatus,
+} from '@growi/editor';
+import {
+  extractSupportedLocales,
+  getLocalizedTemplate,
+  type TemplateSummary,
 } from '@growi/pluginkit/dist/v4';
 import { LoadingSpinner } from '@growi/ui/dist/components';
+import assert from 'assert';
 import { useTranslation } from 'next-i18next';
 import {
+  DropdownItem,
+  DropdownMenu,
+  DropdownToggle,
   Modal,
-  ModalHeader,
   ModalBody,
   ModalFooter,
+  ModalHeader,
   UncontrolledDropdown,
-  DropdownToggle,
-  DropdownMenu,
-  DropdownItem,
 } from 'reactstrap';
 
 import { useSWRxTemplate, useSWRxTemplates } from '~/features/templates/stores';
@@ -28,16 +36,12 @@ import { usePreviewOptions } from '~/stores/renderer';
 import loggerFactory from '~/utils/logger';
 
 import Preview from '../PageEditor/Preview';
-
 import { useFormatter } from './use-formatter';
 
-
 import styles from './TemplateModal.module.scss';
 
-
 const logger = loggerFactory('growi:components:TemplateModal');
 
-
 function constructTemplateId(templateSummary: TemplateSummary): string {
   const defaultTemplate = templateSummary.default;
 
@@ -45,12 +49,12 @@ function constructTemplateId(templateSummary: TemplateSummary): string {
 }
 
 type TemplateSummaryItemProps = {
-  templateSummary: TemplateSummary,
-  selectedLocale?: string,
-  onClick?: () => void,
-  isSelected?: boolean,
-  usersDefaultLang?: Lang,
-}
+  templateSummary: TemplateSummary;
+  selectedLocale?: string;
+  onClick?: () => void;
+  isSelected?: boolean;
+  usersDefaultLang?: Lang;
+};
 
 const TemplateListGroupItem: React.FC<TemplateSummaryItemProps> = ({
   templateSummary,
@@ -58,63 +62,95 @@ const TemplateListGroupItem: React.FC<TemplateSummaryItemProps> = ({
   isSelected,
   usersDefaultLang,
 }) => {
-  const localizedTemplate = getLocalizedTemplate(templateSummary, usersDefaultLang);
+  const localizedTemplate = getLocalizedTemplate(
+    templateSummary,
+    usersDefaultLang,
+  );
   const templateLocales = extractSupportedLocales(templateSummary);
 
   assert(localizedTemplate?.isValid);
 
   return (
-    <a
+    <button
+      type="button"
       className={`list-group-item list-group-item-action ${isSelected ? 'active' : ''}`}
       onClick={onClick}
     >
       <h4 className="mb-1 d-flex">
-        <span className="d-inline-block text-truncate">{localizedTemplate.title}</span>
-        {localizedTemplate.pluginId != null ? <span className="material-symbols-outlined me-1 ms-2 text-muted small">extension</span> : ''}
+        <span className="d-inline-block text-truncate">
+          {localizedTemplate.title}
+        </span>
+        {localizedTemplate.pluginId != null ? (
+          <span className="material-symbols-outlined me-1 ms-2 text-muted small">
+            extension
+          </span>
+        ) : (
+          ''
+        )}
       </h4>
       <p className="mb-2">{localizedTemplate.desc}</p>
-      { templateLocales != null && Array.from(templateLocales).map(locale => (
-        <span key={locale} className="badge border rounded-pill text-muted me-1">{locale}</span>
-      ))}
-    </a>
+      {templateLocales != null &&
+        Array.from(templateLocales).map((locale) => (
+          <span
+            key={locale}
+            className="badge border rounded-pill text-muted me-1"
+          >
+            {locale}
+          </span>
+        ))}
+    </button>
   );
 };
 
-
 const TemplateDropdownItem: React.FC<TemplateSummaryItemProps> = ({
   templateSummary,
   onClick,
   usersDefaultLang,
 }) => {
-
-  const localizedTemplate = getLocalizedTemplate(templateSummary, usersDefaultLang);
+  const localizedTemplate = getLocalizedTemplate(
+    templateSummary,
+    usersDefaultLang,
+  );
   const templateLocales = extractSupportedLocales(templateSummary);
 
   assert(localizedTemplate?.isValid);
 
   return (
-    <DropdownItem
-      onClick={onClick}
-      className="px-4 py-3"
-    >
+    <DropdownItem onClick={onClick} className="px-4 py-3">
       <h4 className="mb-1 d-flex">
-        <span className="d-inline-block text-truncate">{localizedTemplate.title}</span>
-        {localizedTemplate.pluginId != null ? <span className="material-symbols-outlined me-1 ms-2 text-muted small">extension</span> : ''}
+        <span className="d-inline-block text-truncate">
+          {localizedTemplate.title}
+        </span>
+        {localizedTemplate.pluginId != null ? (
+          <span className="material-symbols-outlined me-1 ms-2 text-muted small">
+            extension
+          </span>
+        ) : (
+          ''
+        )}
       </h4>
       <p className="mb-1 text-wrap">{localizedTemplate.desc}</p>
-      { templateLocales != null && Array.from(templateLocales).map(locale => (
-        <span key={locale} className="badge border rounded-pill text-muted me-1">{locale}</span>
-      ))}
+      {templateLocales != null &&
+        Array.from(templateLocales).map((locale) => (
+          <span
+            key={locale}
+            className="badge border rounded-pill text-muted me-1"
+          >
+            {locale}
+          </span>
+        ))}
     </DropdownItem>
   );
 };
 
 type TemplateModalSubstanceProps = {
-  templateModalStatus: TemplateModalState,
-  close: () => void,
-}
+  templateModalStatus: TemplateModalState;
+  close: () => void;
+};
 
-const TemplateModalSubstance = (props: TemplateModalSubstanceProps): JSX.Element => {
+const TemplateModalSubstance = (
+  props: TemplateModalSubstanceProps,
+): JSX.Element => {
   const { templateModalStatus, close } = props;
 
   const { t } = useTranslation(['translation', 'commons']);
@@ -123,66 +159,87 @@ const TemplateModalSubstance = (props: TemplateModalSubstanceProps): JSX.Element
   const { data: rendererOptions } = usePreviewOptions();
   const { data: templateSummaries, isLoading } = useSWRxTemplates();
 
-  const [selectedTemplateSummary, setSelectedTemplateSummary] = useState<TemplateSummary>();
-  const [selectedTemplateLocale, setSelectedTemplateLocale] = useState<string>();
+  const [selectedTemplateSummary, setSelectedTemplateSummary] =
+    useState<TemplateSummary>();
+  const [selectedTemplateLocale, setSelectedTemplateLocale] =
+    useState<string>();
 
-  const { data: selectedTemplateMarkdown } = useSWRxTemplate(selectedTemplateSummary, selectedTemplateLocale);
+  const { data: selectedTemplateMarkdown } = useSWRxTemplate(
+    selectedTemplateSummary,
+    selectedTemplateLocale,
+  );
 
   const { format } = useFormatter();
 
   const usersDefaultLang = personalSettingsInfo?.lang;
 
   // Memoize heavy calculations
-  const selectedLocalizedTemplate = useMemo(() => (
-    getLocalizedTemplate(selectedTemplateSummary, usersDefaultLang)
-  ), [selectedTemplateSummary, usersDefaultLang]);
-
-  const selectedTemplateLocales = useMemo(() => (
-    extractSupportedLocales(selectedTemplateSummary)
-  ), [selectedTemplateSummary]);
-
-  const submitHandler = useCallback((markdown?: string) => {
-    if (markdown == null) {
-      return;
-    }
+  const selectedLocalizedTemplate = useMemo(
+    () => getLocalizedTemplate(selectedTemplateSummary, usersDefaultLang),
+    [selectedTemplateSummary, usersDefaultLang],
+  );
 
-    if (templateModalStatus.onSubmit == null) {
-      close();
-      return;
-    }
+  const selectedTemplateLocales = useMemo(
+    () => extractSupportedLocales(selectedTemplateSummary),
+    [selectedTemplateSummary],
+  );
 
-    templateModalStatus.onSubmit(format(selectedTemplateMarkdown));
-    close();
-  }, [close, format, selectedTemplateMarkdown, templateModalStatus]);
+  const submitHandler = useCallback(
+    (markdown?: string) => {
+      if (markdown == null) {
+        return;
+      }
 
-  const onClickHandler = useCallback((
-      templateSummary: TemplateSummary,
-  ) => {
-    let localeToSet: string | Lang | undefined;
+      if (templateModalStatus.onSubmit == null) {
+        close();
+        return;
+      }
 
-    if (selectedTemplateLocale != null && selectedTemplateLocale in templateSummary) {
-      localeToSet = selectedTemplateLocale;
-    }
-    else if (usersDefaultLang != null && usersDefaultLang in templateSummary) {
-      localeToSet = usersDefaultLang;
-    }
-    else {
-      localeToSet = undefined;
-    }
+      templateModalStatus.onSubmit(format(selectedTemplateMarkdown));
+      close();
+    },
+    [close, format, selectedTemplateMarkdown, templateModalStatus],
+  );
 
-    setSelectedTemplateLocale(localeToSet);
-    setSelectedTemplateSummary(templateSummary);
-  }, [selectedTemplateLocale, usersDefaultLang]);
+  const onClickHandler = useCallback(
+    (templateSummary: TemplateSummary) => {
+      let localeToSet: string | Lang | undefined;
+
+      if (
+        selectedTemplateLocale != null &&
+        selectedTemplateLocale in templateSummary
+      ) {
+        localeToSet = selectedTemplateLocale;
+      } else if (
+        usersDefaultLang != null &&
+        usersDefaultLang in templateSummary
+      ) {
+        localeToSet = usersDefaultLang;
+      } else {
+        localeToSet = undefined;
+      }
+
+      setSelectedTemplateLocale(localeToSet);
+      setSelectedTemplateSummary(templateSummary);
+    },
+    [selectedTemplateLocale, usersDefaultLang],
+  );
 
   // Memoize handler creator to avoid recreating onClick functions in map
-  const createOnClickHandler = useCallback((templateSummary: TemplateSummary) => () => {
-    onClickHandler(templateSummary);
-  }, [onClickHandler]);
+  const createOnClickHandler = useCallback(
+    (templateSummary: TemplateSummary) => () => {
+      onClickHandler(templateSummary);
+    },
+    [onClickHandler],
+  );
 
   // Memoize locale handler creator
-  const createLocaleHandler = useCallback((locale: string) => () => {
-    setSelectedTemplateLocale(locale);
-  }, []);
+  const createLocaleHandler = useCallback(
+    (locale: string) => () => {
+      setSelectedTemplateLocale(locale);
+    },
+    [],
+  );
 
   useEffect(() => {
     if (!templateModalStatus.isOpened) {
@@ -200,59 +257,72 @@ const TemplateModalSubstance = (props: TemplateModalSubstanceProps): JSX.Element
         <div className="row">
           {/* List Group */}
           <div className="d-none d-lg-block col-lg-4">
-
-            { isLoading && (
+            {isLoading && (
               <div className="h-100 d-flex justify-content-center align-items-center">
                 <LoadingSpinner className="mx-auto text-muted fs-3" />
               </div>
-            ) }
+            )}
 
             <div className="list-group">
-              { templateSummaries != null && templateSummaries.map((templateSummary) => {
-                const templateId = constructTemplateId(templateSummary);
-                const isSelected = selectedTemplateSummary != null && constructTemplateId(selectedTemplateSummary) === templateId;
-
-                return (
-                  <TemplateListGroupItem
-                    key={templateId}
-                    templateSummary={templateSummary}
-                    onClick={createOnClickHandler(templateSummary)}
-                    isSelected={isSelected}
-                    usersDefaultLang={usersDefaultLang}
-                  />
-                );
-              }) }
+              {templateSummaries != null &&
+                templateSummaries.map((templateSummary) => {
+                  const templateId = constructTemplateId(templateSummary);
+                  const isSelected =
+                    selectedTemplateSummary != null &&
+                    constructTemplateId(selectedTemplateSummary) === templateId;
+
+                  return (
+                    <TemplateListGroupItem
+                      key={templateId}
+                      templateSummary={templateSummary}
+                      onClick={createOnClickHandler(templateSummary)}
+                      isSelected={isSelected}
+                      usersDefaultLang={usersDefaultLang}
+                    />
+                  );
+                })}
             </div>
           </div>
           {/* Dropdown */}
           <div className="d-lg-none col mb-3">
             <UncontrolledDropdown>
-              <DropdownToggle caret type="button" outline className="w-100 text-end" disabled={isLoading}>
+              <DropdownToggle
+                caret
+                type="button"
+                outline
+                className="w-100 text-end"
+                disabled={isLoading}
+              >
                 <span className="float-start">
-                  { (() => {
+                  {(() => {
                     if (isLoading) {
                       return 'Loading..';
                     }
 
-                    return selectedLocalizedTemplate != null && selectedLocalizedTemplate.isValid
+                    return selectedLocalizedTemplate != null &&
+                      selectedLocalizedTemplate.isValid
                       ? selectedLocalizedTemplate.title
                       : t('Select template');
-                  })() }
+                  })()}
                 </span>
               </DropdownToggle>
-              <DropdownMenu role="menu" className={`p-0 mw-100 ${styles['dm-templates']}`}>
-                { templateSummaries != null && templateSummaries.map((templateSummary) => {
-                  const templateId = constructTemplateId(templateSummary);
-
-                  return (
-                    <TemplateDropdownItem
-                      key={templateId}
-                      templateSummary={templateSummary}
-                      onClick={createOnClickHandler(templateSummary)}
-                      usersDefaultLang={usersDefaultLang}
-                    />
-                  );
-                }) }
+              <DropdownMenu
+                role="menu"
+                className={`p-0 mw-100 ${styles['dm-templates']}`}
+              >
+                {templateSummaries != null &&
+                  templateSummaries.map((templateSummary) => {
+                    const templateId = constructTemplateId(templateSummary);
+
+                    return (
+                      <TemplateDropdownItem
+                        key={templateId}
+                        templateSummary={templateSummary}
+                        onClick={createOnClickHandler(templateSummary)}
+                        usersDefaultLang={usersDefaultLang}
+                      />
+                    );
+                  })}
               </DropdownMenu>
             </UncontrolledDropdown>
           </div>
@@ -271,36 +341,51 @@ const TemplateModalSubstance = (props: TemplateModalSubstanceProps): JSX.Element
                     disabled={selectedTemplateSummary == null}
                     data-testid="select-locale-dropdown-toggle"
                   >
-                    <span className="float-start">{selectedTemplateLocale != null ? selectedTemplateLocale : t('Language')}</span>
+                    <span className="float-start">
+                      {selectedTemplateLocale != null
+                        ? selectedTemplateLocale
+                        : t('Language')}
+                    </span>
                   </DropdownToggle>
                   <DropdownMenu className="dropdown-menu" role="menu">
-                    { selectedTemplateLocales != null && Array.from(selectedTemplateLocales).map((locale) => {
-                      return (
-                        <DropdownItem
-                          data-testid="select-locale-dropdown-item"
-                          key={locale}
-                          onClick={createLocaleHandler(locale)}
-                        >
-                          <span>{locale}</span>
-                        </DropdownItem>
-                      );
-                    }) }
+                    {selectedTemplateLocales != null &&
+                      Array.from(selectedTemplateLocales).map((locale) => {
+                        return (
+                          <DropdownItem
+                            data-testid="select-locale-dropdown-item"
+                            key={locale}
+                            onClick={createLocaleHandler(locale)}
+                          >
+                            <span>{locale}</span>
+                          </DropdownItem>
+                        );
+                      })}
                   </DropdownMenu>
                 </UncontrolledDropdown>
               </div>
             </div>
             <div className="card">
-              <div className="card-body" style={{ height: '400px', overflowY: 'auto' }}>
-                { rendererOptions != null && selectedTemplateSummary != null && (
-                  <Preview rendererOptions={rendererOptions} markdown={format(selectedTemplateMarkdown)} />
-                ) }
+              <div
+                className="card-body"
+                style={{ height: '400px', overflowY: 'auto' }}
+              >
+                {rendererOptions != null && selectedTemplateSummary != null && (
+                  <Preview
+                    rendererOptions={rendererOptions}
+                    markdown={format(selectedTemplateMarkdown)}
+                  />
+                )}
               </div>
             </div>
           </div>
         </div>
       </ModalBody>
       <ModalFooter>
-        <button type="button" className="btn btn-outline-secondary mx-1" onClick={close}>
+        <button
+          type="button"
+          className="btn btn-outline-secondary mx-1"
+          onClick={close}
+        >
           {t('Cancel')}
         </button>
         <button
@@ -316,7 +401,6 @@ const TemplateModalSubstance = (props: TemplateModalSubstanceProps): JSX.Element
   );
 };
 
-
 export const TemplateModal = (): JSX.Element => {
   const templateModalStatus = useTemplateModalStatus();
   const { close } = useTemplateModalActions();
@@ -326,10 +410,19 @@ export const TemplateModal = (): JSX.Element => {
   }
 
   return (
-    <Modal className="link-edit-modal" isOpen={templateModalStatus.isOpened} toggle={close} size="xl" autoFocus={false}>
-      { templateModalStatus.isOpened && (
-        <TemplateModalSubstance templateModalStatus={templateModalStatus} close={close} />
-      ) }
+    <Modal
+      className="link-edit-modal"
+      isOpen={templateModalStatus.isOpened}
+      toggle={close}
+      size="xl"
+      autoFocus={false}
+    >
+      {templateModalStatus.isOpened && (
+        <TemplateModalSubstance
+          templateModalStatus={templateModalStatus}
+          close={close}
+        />
+      )}
     </Modal>
   );
 };

+ 2 - 2
apps/app/src/client/components/TemplateModal/dynamic.tsx

@@ -1,5 +1,4 @@
 import type { JSX } from 'react';
-
 import { useTemplateModalStatus } from '@growi/editor';
 
 import { useLazyLoader } from '~/components/utils/use-lazy-loader';
@@ -11,7 +10,8 @@ export const TemplateModalLazyLoaded = (): JSX.Element => {
 
   const TemplateModal = useLazyLoader<TemplateModalProps>(
     'template-modal',
-    () => import('./TemplateModal').then(mod => ({ default: mod.TemplateModal })),
+    () =>
+      import('./TemplateModal').then((mod) => ({ default: mod.TemplateModal })),
     status?.isOpened ?? false,
   );
 

+ 3 - 7
apps/app/src/client/components/TemplateModal/use-formatter.spec.tsx

@@ -2,7 +2,6 @@ import { renderHook } from '@testing-library/react';
 
 import { useFormatter } from './use-formatter';
 
-
 const mocks = vi.hoisted(() => {
   return {
     useCurrentPagePathMock: vi.fn<() => string | undefined>(() => undefined),
@@ -13,11 +12,8 @@ vi.mock('~/states/page', () => {
   return { useCurrentPagePath: mocks.useCurrentPagePathMock };
 });
 
-
 describe('useFormatter', () => {
-
   describe('format()', () => {
-
     it('returns an empty string when the argument is undefined', () => {
       // setup
       const mastacheMock = {
@@ -35,13 +31,14 @@ describe('useFormatter', () => {
       expect(markdown).toBe('');
       expect(mastacheMock.render).not.toHaveBeenCalled();
     });
-
   });
 
   it('returns markdown as-is when mustache.render throws an error', () => {
     // setup
     const mastacheMock = {
-      render: vi.fn(() => { throw new Error() }),
+      render: vi.fn(() => {
+        throw new Error();
+      }),
     };
     vi.doMock('mustache', () => mastacheMock);
 
@@ -95,5 +92,4 @@ path: /Sandbox
 date: 2023/05/31 15:01
 `);
   });
-
 });

+ 4 - 7
apps/app/src/client/components/TemplateModal/use-formatter.tsx

@@ -1,18 +1,16 @@
-import path from 'path';
-
 import { format as dateFnsFormat } from 'date-fns/format';
 import mustache from 'mustache';
+import path from 'path';
 
 import { useCurrentPagePath } from '~/states/page';
 import loggerFactory from '~/utils/logger';
 
 const logger = loggerFactory('growi:components:TemplateModal:use-formatter');
 
-
 type FormatMethod = (markdown?: string) => string;
 type FormatterData = {
-  format: FormatMethod,
-}
+  format: FormatMethod;
+};
 
 export const useFormatter = (): FormatterData => {
   const currentPagePath = useCurrentPagePath();
@@ -34,8 +32,7 @@ export const useFormatter = (): FormatterData => {
         HH: dateFnsFormat(now, 'HH'),
         mm: dateFnsFormat(now, 'mm'),
       });
-    }
-    catch (err) {
+    } catch (err) {
       logger.warn('An error occured while ejs processing.', err);
       return markdown;
     }

+ 1 - 17
biome.json

@@ -29,24 +29,16 @@
       "!packages/pdf-converter-client/src/index.ts",
       "!packages/pdf-converter-client/specs",
       "!apps/app/src/client/components/Admin",
-      "!apps/app/src/client/components/AuthorInfo",
       "!apps/app/src/client/components/Bookmarks",
-      "!apps/app/src/client/components/CreateTemplateModal",
-      "!apps/app/src/client/components/CustomNavigation",
-      "!apps/app/src/client/components/DeleteBookmarkFolderModal",
       "!apps/app/src/client/components/DescendantsPageListModal",
-      "!apps/app/src/client/components/EmptyTrashModal",
-      "!apps/app/src/client/components/GrantedGroupsInheritanceSelectModal",
       "!apps/app/src/client/components/InAppNotification",
       "!apps/app/src/client/components/ItemsTree",
       "!apps/app/src/client/components/LoginForm",
-      "!apps/app/src/client/components/Maintenance",
       "!apps/app/src/client/components/Me",
       "!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/PageHistory",
       "!apps/app/src/client/components/PageList",
       "!apps/app/src/client/components/PageManagement",
       "!apps/app/src/client/components/PagePathNavSticky",
@@ -55,15 +47,7 @@
       "!apps/app/src/client/components/PageSelectModal",
       "!apps/app/src/client/components/PageSideContents",
       "!apps/app/src/client/components/PageTags",
-      "!apps/app/src/client/components/Presentation",
-      "!apps/app/src/client/components/PutbackPageModal",
-      "!apps/app/src/client/components/ReactMarkdownComponents",
-      "!apps/app/src/client/components/RecentActivity",
-      "!apps/app/src/client/components/RecentCreated",
-      "!apps/app/src/client/components/RevisionComparer",
-      "!apps/app/src/client/components/ShortcutsModal",
-      "!apps/app/src/client/components/StaffCredit",
-      "!apps/app/src/client/components/TemplateModal"
+      "!apps/app/src/client/components/ReactMarkdownComponents"
     ]
   },
   "formatter": {