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

configure biome for admin import/export and misc admin pages

Futa Arai 3 месяцев назад
Родитель
Сommit
03c6c237ec
41 измененных файлов с 1818 добавлено и 979 удалено
  1. 10 0
      apps/app/.eslintrc.js
  2. 55 27
      apps/app/src/client/components/Admin/AdminHome/AdminHome.jsx
  3. 8 8
      apps/app/src/client/components/Admin/AdminHome/EnvVarsTable.tsx
  4. 22 15
      apps/app/src/client/components/Admin/AdminHome/SystemInfomationTable.tsx
  5. 142 66
      apps/app/src/client/components/Admin/AuditLogManagement.tsx
  6. 2 6
      apps/app/src/client/components/Admin/Common/Accordion.jsx
  7. 11 5
      apps/app/src/client/components/Admin/Common/AdminInstallButtonRow.tsx
  8. 5 6
      apps/app/src/client/components/Admin/Common/AdminUpdateButtonRow.tsx
  9. 17 12
      apps/app/src/client/components/Admin/Common/LabeledProgressBar.tsx
  10. 29 34
      apps/app/src/client/components/Admin/ElasticsearchManagement/ElasticsearchManagement.tsx
  11. 12 9
      apps/app/src/client/components/Admin/ElasticsearchManagement/NormalizeIndicesControls.tsx
  12. 11 15
      apps/app/src/client/components/Admin/ElasticsearchManagement/RebuildIndexControls.jsx
  13. 11 11
      apps/app/src/client/components/Admin/ElasticsearchManagement/ReconnectControls.tsx
  14. 82 43
      apps/app/src/client/components/Admin/ElasticsearchManagement/StatusTable.jsx
  15. 13 6
      apps/app/src/client/components/Admin/ExportArchiveData/ArchiveFilesTable.tsx
  16. 36 12
      apps/app/src/client/components/Admin/ExportArchiveData/ArchiveFilesTableMenu.tsx
  17. 136 82
      apps/app/src/client/components/Admin/ExportArchiveData/SelectCollectionsModal.tsx
  18. 40 27
      apps/app/src/client/components/Admin/ExportArchiveDataPage.tsx
  19. 0 2
      apps/app/src/client/components/Admin/ForbiddenPage.tsx
  20. 4 2
      apps/app/src/client/components/Admin/FullTextSearchManagement.tsx
  21. 101 47
      apps/app/src/client/components/Admin/G2GDataTransfer.tsx
  22. 115 49
      apps/app/src/client/components/Admin/G2GDataTransferExportForm.tsx
  23. 45 10
      apps/app/src/client/components/Admin/G2GDataTransferStatusIcon.tsx
  24. 12 7
      apps/app/src/client/components/Admin/ImportData/GrowiArchive/ErrorViewer.tsx
  25. 100 36
      apps/app/src/client/components/Admin/ImportData/GrowiArchive/ImportCollectionConfigurationModal.jsx
  26. 86 34
      apps/app/src/client/components/Admin/ImportData/GrowiArchive/ImportCollectionItem.jsx
  27. 161 73
      apps/app/src/client/components/Admin/ImportData/GrowiArchive/ImportForm.jsx
  28. 21 15
      apps/app/src/client/components/Admin/ImportData/GrowiArchive/UploadForm.jsx
  29. 24 24
      apps/app/src/client/components/Admin/ImportData/GrowiArchiveSection.jsx
  30. 4 2
      apps/app/src/client/components/Admin/ImportData/ImportDataPageContents.jsx
  31. 23 15
      apps/app/src/client/components/Admin/LegacySlackIntegration/LegacySlackIntegration.jsx
  32. 137 64
      apps/app/src/client/components/Admin/LegacySlackIntegration/SlackConfiguration.jsx
  33. 32 27
      apps/app/src/client/components/Admin/ManageExternalAccount.tsx
  34. 58 24
      apps/app/src/client/components/Admin/MarkdownSetting/IndentForm.tsx
  35. 55 21
      apps/app/src/client/components/Admin/MarkdownSetting/LineBreakForm.jsx
  36. 28 18
      apps/app/src/client/components/Admin/MarkdownSetting/MarkDownSettingContents.tsx
  37. 29 17
      apps/app/src/client/components/Admin/MarkdownSetting/WhitelistInput.tsx
  38. 70 37
      apps/app/src/client/components/Admin/MarkdownSetting/XssForm.jsx
  39. 1 4
      apps/app/src/client/components/Admin/NotFoundPage.tsx
  40. 65 57
      apps/app/src/client/components/Admin/UserManagement.tsx
  41. 5 10
      biome.json

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

@@ -41,6 +41,16 @@ module.exports = {
     'src/client/components/*.jsx',
     'src/client/components/*.ts',
     'src/client/components/*.js',
+    'src/client/components/Admin/*.ts',
+    'src/client/components/Admin/*.tsx',
+    'src/client/components/Admin/*.scss',
+    'src/client/components/Admin/AdminHome/**',
+    'src/client/components/Admin/Common/**',
+    'src/client/components/Admin/ElasticsearchManagement/**',
+    'src/client/components/Admin/ExportArchiveData/**',
+    'src/client/components/Admin/ImportData/**',
+    'src/client/components/Admin/LegacySlackIntegration/**',
+    'src/client/components/Admin/MarkdownSetting/**',
     'src/client/components/Admin/App/**',
     'src/client/components/Admin/SlackIntegration/**',
     'src/client/components/Admin/Users/**',

+ 55 - 27
apps/app/src/client/components/Admin/AdminHome/AdminHome.jsx

@@ -1,5 +1,4 @@
-import React, { useEffect, useCallback } from 'react';
-
+import React, { useCallback, useEffect } from 'react';
 import { useTranslation } from 'next-i18next';
 import PropTypes from 'prop-types';
 import { CopyToClipboard } from 'react-copy-to-clipboard';
@@ -10,14 +9,10 @@ import { toastError } from '~/client/util/toastr';
 import { useSWRxV5MigrationStatus } from '~/stores/page-listing';
 import loggerFactory from '~/utils/logger';
 
-
 import { withUnstatedContainers } from '../../UnstatedUtils';
-
-
 import { EnvVarsTable } from './EnvVarsTable';
 import SystemInfomationTable from './SystemInfomationTable';
 
-
 const logger = loggerFactory('growi:admin');
 
 const AdminHome = (props) => {
@@ -25,11 +20,10 @@ const AdminHome = (props) => {
   const { t } = useTranslation();
   const { data: migrationStatus } = useSWRxV5MigrationStatus();
 
-  const fetchAdminHomeData = useCallback(async() => {
+  const fetchAdminHomeData = useCallback(async () => {
     try {
       await adminHomeContainer.retrieveAdminHomeData();
-    }
-    catch (err) {
+    } catch (err) {
       toastError(err);
       logger.error(err);
     }
@@ -48,25 +42,36 @@ const AdminHome = (props) => {
             <h3 className="alert-heading">
               {t('admin:maintenance_mode.maintenance_mode')}
             </h3>
-            <p>
-              {t('admin:maintenance_mode.description')}
-            </p>
+            <p>{t('admin:maintenance_mode.description')}</p>
             <hr />
             <a className="btn-link" href="/admin/app" rel="noopener noreferrer">
-              <span className="material-symbols-outlined ms-1" aria-hidden="true">link</span>
-              <strong>{t('admin:maintenance_mode.end_maintenance_mode')}</strong>
+              <span
+                className="material-symbols-outlined ms-1"
+                aria-hidden="true"
+              >
+                link
+              </span>
+              <strong>
+                {t('admin:maintenance_mode.end_maintenance_mode')}
+              </strong>
             </a>
           </div>
         )
       }
       {
         // Alert message will be displayed in case that V5 migration has not been compleated
-        (migrationStatus != null && !migrationStatus.isV5Compatible)
-        && (
-          <div className={`alert ${migrationStatus.isV5Compatible == null ? 'alert-warning' : 'alert-info'}`}>
+        migrationStatus != null && !migrationStatus.isV5Compatible && (
+          <div
+            className={`alert ${migrationStatus.isV5Compatible == null ? 'alert-warning' : 'alert-info'}`}
+          >
             {t('admin:v5_page_migration.migration_desc')}
             <a className="btn-link" href="/admin/app" rel="noopener noreferrer">
-              <span className="material-symbols-outlined ms-1" aria-hidden="true">link</span>
+              <span
+                className="material-symbols-outlined ms-1"
+                aria-hidden="true"
+              >
+                link
+              </span>
               <strong>{t('admin:v5_page_migration.upgrade_to_v5')}</strong>
             </a>
           </div>
@@ -80,43 +85,65 @@ const AdminHome = (props) => {
 
       <div className="row mb-5">
         <div className="col-lg-12">
-          <h2 className="admin-setting-header">{t('admin:admin_top.system_information')}</h2>
+          <h2 className="admin-setting-header">
+            {t('admin:admin_top.system_information')}
+          </h2>
           <SystemInfomationTable />
         </div>
       </div>
 
       <div className="row mb-5">
         <div className="col-md-12">
-          <h2 className="admin-setting-header">{t('admin:admin_top.list_of_env_vars')}</h2>
+          <h2 className="admin-setting-header">
+            {t('admin:admin_top.list_of_env_vars')}
+          </h2>
           <p>{t('admin:admin_top.env_var_priority')}</p>
           {/* eslint-disable-next-line react/no-danger */}
-          <p dangerouslySetInnerHTML={{ __html: t('admin:admin_top.about_security') }} />
+          <p
+            dangerouslySetInnerHTML={{
+              __html: t('admin:admin_top.about_security'),
+            }}
+          />
           <EnvVarsTable envVars={adminHomeContainer.state.envVars} />
         </div>
       </div>
 
       <div className="row mb-5">
         <div className="col-md-12">
-          <h2 className="admin-setting-header">{t('admin:admin_top.bug_report')}</h2>
+          <h2 className="admin-setting-header">
+            {t('admin:admin_top.bug_report')}
+          </h2>
           <div className="d-flex align-items-center">
             <CopyToClipboard
               text={adminHomeContainer.generatePrefilledHostInformationMarkdown()}
               onCopy={() => adminHomeContainer.onCopyPrefilledHostInformation()}
             >
-              <button id="prefilledHostInformationButton" type="button" className="btn btn-primary">
+              <button
+                id="prefilledHostInformationButton"
+                type="button"
+                className="btn btn-primary"
+              >
                 {t('admin:admin_top:copy_prefilled_host_information:default')}
               </button>
             </CopyToClipboard>
             <Tooltip
               placement="bottom"
-              isOpen={adminHomeContainer.state.copyState === adminHomeContainer.copyStateValues.DONE}
+              isOpen={
+                adminHomeContainer.state.copyState ===
+                adminHomeContainer.copyStateValues.DONE
+              }
               target="prefilledHostInformationButton"
               fade={false}
             >
               {t('admin:admin_top:copy_prefilled_host_information:done')}
             </Tooltip>
             {/* eslint-disable-next-line react/no-danger */}
-            <span className="ms-2" dangerouslySetInnerHTML={{ __html: t('admin:admin_top:submit_bug_report') }} />
+            <span
+              className="ms-2"
+              dangerouslySetInnerHTML={{
+                __html: t('admin:admin_top:submit_bug_report'),
+              }}
+            />
           </div>
         </div>
       </div>
@@ -124,8 +151,9 @@ const AdminHome = (props) => {
   );
 };
 
-
-const AdminHomeWrapper = withUnstatedContainers(AdminHome, [AdminHomeContainer]);
+const AdminHomeWrapper = withUnstatedContainers(AdminHome, [
+  AdminHomeContainer,
+]);
 
 AdminHome.propTypes = {
   adminHomeContainer: PropTypes.instanceOf(AdminHomeContainer).isRequired,

+ 8 - 8
apps/app/src/client/components/Admin/AdminHome/EnvVarsTable.tsx

@@ -1,12 +1,14 @@
-import React, { type JSX } from 'react';
-
+import type React from 'react';
+import type { JSX } from 'react';
 import { LoadingSpinner } from '@growi/ui/dist/components';
 
 type EnvVarsTableProps = {
-  envVars?: Record<string, string | number | boolean>,
-}
+  envVars?: Record<string, string | number | boolean>;
+};
 
-export const EnvVarsTable: React.FC<EnvVarsTableProps> = (props: EnvVarsTableProps) => {
+export const EnvVarsTable: React.FC<EnvVarsTableProps> = (
+  props: EnvVarsTableProps,
+) => {
   const { envVars } = props;
   if (envVars == null) {
     return <LoadingSpinner />;
@@ -27,9 +29,7 @@ export const EnvVarsTable: React.FC<EnvVarsTableProps> = (props: EnvVarsTablePro
 
   return (
     <table className="table table-bordered">
-      <tbody>
-        {envVarRows}
-      </tbody>
+      <tbody>{envVarRows}</tbody>
     </table>
   );
 };

+ 22 - 15
apps/app/src/client/components/Admin/AdminHome/SystemInfomationTable.tsx

@@ -1,55 +1,62 @@
 import React from 'react';
-
 import { LoadingSpinner } from '@growi/ui/dist/components';
 
 import AdminHomeContainer from '~/client/services/AdminHomeContainer';
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 
-
 type Props = {
-  adminHomeContainer: AdminHomeContainer,
-}
+  adminHomeContainer: AdminHomeContainer;
+};
 
 const SystemInformationTable = (props: Props) => {
   const { adminHomeContainer } = props;
 
-  const {
-    growiVersion, nodeVersion, npmVersion, pnpmVersion,
-  } = adminHomeContainer.state;
+  const { growiVersion, nodeVersion, npmVersion, pnpmVersion } =
+    adminHomeContainer.state;
 
-  if (growiVersion == null || nodeVersion == null || npmVersion == null || pnpmVersion == null) {
+  if (
+    growiVersion == null ||
+    nodeVersion == null ||
+    npmVersion == null ||
+    pnpmVersion == null
+  ) {
     return <LoadingSpinner />;
   }
 
   return (
-    <table data-testid="admin-system-information-table" className="table table-bordered">
+    <table
+      data-testid="admin-system-information-table"
+      className="table table-bordered"
+    >
       <tbody>
         <tr>
           <th>GROWI</th>
-          <td data-vrt-blackout>{ growiVersion }</td>
+          <td data-vrt-blackout>{growiVersion}</td>
         </tr>
         <tr>
           <th>node.js</th>
-          <td>{ nodeVersion }</td>
+          <td>{nodeVersion}</td>
         </tr>
         <tr>
           <th>npm</th>
-          <td>{ npmVersion }</td>
+          <td>{npmVersion}</td>
         </tr>
         <tr>
           <th>pnpm</th>
-          <td>{ pnpmVersion }</td>
+          <td>{pnpmVersion}</td>
         </tr>
       </tbody>
     </table>
   );
-
 };
 
 /**
  * Wrapper component for using unstated
  */
-const SystemInformationTableWrapper = withUnstatedContainers(SystemInformationTable, [AdminHomeContainer]);
+const SystemInformationTableWrapper = withUnstatedContainers(
+  SystemInformationTable,
+  [AdminHomeContainer],
+);
 
 export default SystemInformationTableWrapper;

+ 142 - 66
apps/app/src/client/components/Admin/AuditLogManagement.tsx

@@ -1,6 +1,6 @@
+import type React from 'react';
 import type { FC } from 'react';
-import React, { useState, useCallback, useRef } from 'react';
-
+import { useCallback, useRef, useState } from 'react';
 import { LoadingSpinner } from '@growi/ui/dist/components';
 import { format } from 'date-fns/format';
 import { useAtomValue } from 'jotai';
@@ -9,11 +9,13 @@ import { useTranslation } from 'react-i18next';
 import type { IClearable } from '~/client/interfaces/clearable';
 import { toastError } from '~/client/util/toastr';
 import type { SupportedActionType } from '~/interfaces/activity';
-import { auditLogEnabledAtom, auditLogAvailableActionsAtom } from '~/states/server-configurations';
+import {
+  auditLogAvailableActionsAtom,
+  auditLogEnabledAtom,
+} from '~/states/server-configurations';
 import { useSWRxActivity } from '~/stores/activity';
 
 import PaginationWrapper from '../PaginationWrapper';
-
 import { ActivityTable } from './AuditLog/ActivityTable';
 import { AuditLogDisableMode } from './AuditLog/AuditLogDisableMode';
 import { AuditLogSettings } from './AuditLog/AuditLogSettings';
@@ -35,7 +37,9 @@ export const AuditLogManagement: FC = () => {
 
   const typeaheadRef = useRef<IClearable>(null);
 
-  const auditLogAvailableActionsData = useAtomValue(auditLogAvailableActionsAtom);
+  const auditLogAvailableActionsData = useAtomValue(
+    auditLogAvailableActionsAtom,
+  );
 
   /*
    * State
@@ -48,20 +52,39 @@ export const AuditLogManagement: FC = () => {
   const [endDate, setEndDate] = useState<Date | null>(null);
   const [selectedUsernames, setSelectedUsernames] = useState<string[]>([]);
   const [actionMap, setActionMap] = useState(
-    new Map<SupportedActionType, boolean>(auditLogAvailableActionsData != null ? auditLogAvailableActionsData.map(action => [action, true]) : []),
+    new Map<SupportedActionType, boolean>(
+      auditLogAvailableActionsData != null
+        ? auditLogAvailableActionsData.map((action) => [action, true])
+        : [],
+    ),
   );
 
   /*
    * Fetch
    */
-  const selectedDate = { startDate: formatDate(startDate), endDate: formatDate(endDate) };
-  const selectedActionList = Array.from(actionMap.entries()).filter(v => v[1]).map(v => v[0]);
-  const searchFilter = { actions: selectedActionList, dates: selectedDate, usernames: selectedUsernames };
-
-  const { data: activityData, mutate: mutateActivity, error } = useSWRxActivity(PAGING_LIMIT, offset, searchFilter);
+  const selectedDate = {
+    startDate: formatDate(startDate),
+    endDate: formatDate(endDate),
+  };
+  const selectedActionList = Array.from(actionMap.entries())
+    .filter((v) => v[1])
+    .map((v) => v[0]);
+  const searchFilter = {
+    actions: selectedActionList,
+    dates: selectedDate,
+    usernames: selectedUsernames,
+  };
+
+  const {
+    data: activityData,
+    mutate: mutateActivity,
+    error,
+  } = useSWRxActivity(PAGING_LIMIT, offset, searchFilter);
   const activityList = activityData?.docs != null ? activityData.docs : [];
-  const totalActivityNum = activityData?.totalDocs != null ? activityData.totalDocs : 0;
-  const totalPagingPages = activityData?.totalPages != null ? activityData.totalPages : 0;
+  const totalActivityNum =
+    activityData?.totalDocs != null ? activityData.totalDocs : 0;
+  const totalPagingPages =
+    activityData?.totalPages != null ? activityData.totalPages : 0;
   const isLoading = activityData === undefined && error == null;
 
   if (error != null) {
@@ -83,17 +106,23 @@ export const AuditLogManagement: FC = () => {
     setEndDate(dateList[1]);
   }, []);
 
-  const actionCheckboxChangedHandler = useCallback((action: SupportedActionType) => {
-    setActivePageNumber(1);
-    actionMap.set(action, !actionMap.get(action));
-    setActionMap(new Map(actionMap.entries()));
-  }, [actionMap, setActionMap]);
+  const actionCheckboxChangedHandler = useCallback(
+    (action: SupportedActionType) => {
+      setActivePageNumber(1);
+      actionMap.set(action, !actionMap.get(action));
+      setActionMap(new Map(actionMap.entries()));
+    },
+    [actionMap, setActionMap],
+  );
 
-  const multipleActionCheckboxChangedHandler = useCallback((actions: SupportedActionType[], isChecked) => {
-    setActivePageNumber(1);
-    actions.forEach(action => actionMap.set(action, isChecked));
-    setActionMap(new Map(actionMap.entries()));
-  }, [actionMap, setActionMap]);
+  const multipleActionCheckboxChangedHandler = useCallback(
+    (actions: SupportedActionType[], isChecked) => {
+      setActivePageNumber(1);
+      actions.forEach((action) => actionMap.set(action, isChecked));
+      setActionMap(new Map(actionMap.entries()));
+    },
+    [actionMap, setActionMap],
+  );
 
   const setUsernamesHandler = useCallback((usernames: string[]) => {
     setActivePageNumber(1);
@@ -108,41 +137,62 @@ export const AuditLogManagement: FC = () => {
     typeaheadRef.current?.clear();
 
     if (auditLogAvailableActionsData != null) {
-      setActionMap(new Map<SupportedActionType, boolean>(auditLogAvailableActionsData.map(action => [action, true])));
+      setActionMap(
+        new Map<SupportedActionType, boolean>(
+          auditLogAvailableActionsData.map((action) => [action, true]),
+        ),
+      );
     }
-  }, [setActivePageNumber, setStartDate, setEndDate, setSelectedUsernames, setActionMap, auditLogAvailableActionsData]);
+  }, [
+    setActivePageNumber,
+    setStartDate,
+    setEndDate,
+    setSelectedUsernames,
+    setActionMap,
+    auditLogAvailableActionsData,
+  ]);
 
   const reloadButtonPushedHandler = useCallback(() => {
     setActivePageNumber(1);
     mutateActivity();
   }, [mutateActivity]);
 
-  const jumpPageInputChangeHandler = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
-    const inputNumber = Number(e.target.value);
-    const isNan = Number.isNaN(inputNumber);
-
-    if (!isNan) {
-      // eslint-disable-next-line no-nested-ternary
-      const jumpPageNumber = inputNumber > totalPagingPages ? totalPagingPages : inputNumber <= 0 ? activePageNumber : inputNumber;
-      setJumpPageNumber(jumpPageNumber);
-    }
-    else {
-      setJumpPageNumber(activePageNumber);
-    }
-  }, [totalPagingPages, activePageNumber, setJumpPageNumber]);
+  const jumpPageInputChangeHandler = useCallback(
+    (e: React.ChangeEvent<HTMLInputElement>) => {
+      const inputNumber = Number(e.target.value);
+      const isNan = Number.isNaN(inputNumber);
+
+      if (!isNan) {
+        // eslint-disable-next-line no-nested-ternary
+        const jumpPageNumber =
+          inputNumber > totalPagingPages
+            ? totalPagingPages
+            : inputNumber <= 0
+              ? activePageNumber
+              : inputNumber;
+        setJumpPageNumber(jumpPageNumber);
+      } else {
+        setJumpPageNumber(activePageNumber);
+      }
+    },
+    [totalPagingPages, activePageNumber, setJumpPageNumber],
+  );
 
-  const jumpPageInputKeyDownHandler = useCallback((e) => {
-    if (e.key === 'Enter') {
-      setActivePageNumber(jumpPageNumber);
-    }
-  }, [setActivePageNumber, jumpPageNumber]);
+  const jumpPageInputKeyDownHandler = useCallback(
+    (e) => {
+      if (e.key === 'Enter') {
+        setActivePageNumber(jumpPageNumber);
+      }
+    },
+    [setActivePageNumber, jumpPageNumber],
+  );
 
   const jumpPageButtonPushedHandler = useCallback(() => {
     setActivePageNumber(jumpPageNumber);
   }, [jumpPageNumber]);
 
   // eslint-disable-next-line max-len
-  const activityCounter = `<b>${activityList.length === 0 ? 0 : offset + 1}</b> - <b>${(PAGING_LIMIT * activePageNumber) - (PAGING_LIMIT - activityList.length)}</b> of <b>${totalActivityNum}<b/>`;
+  const activityCounter = `<b>${activityList.length === 0 ? 0 : offset + 1}</b> - <b>${PAGING_LIMIT * activePageNumber - (PAGING_LIMIT - activityList.length)}</b> of <b>${totalActivityNum}<b/>`;
 
   if (!auditLogEnabled) {
     return <AuditLogDisableMode />;
@@ -150,20 +200,36 @@ export const AuditLogManagement: FC = () => {
 
   return (
     <div data-testid="admin-auditlog">
-      <button type="button" className="btn btn-outline-secondary mb-4" onClick={() => setIsSettingPage(!isSettingPage)}>
-        {
-          isSettingPage
-            ? <><span className="material-symbols-outlined">arrow_left_alt</span>{t('admin:audit_log_management.return')}</>
-            : <><span className="material-symbols-outlined">settings</span>{t('admin:audit_log_management.settings')}</>
-        }
+      <button
+        type="button"
+        className="btn btn-outline-secondary mb-4"
+        onClick={() => setIsSettingPage(!isSettingPage)}
+      >
+        {isSettingPage ? (
+          <>
+            <span className="material-symbols-outlined">arrow_left_alt</span>
+            {t('admin:audit_log_management.return')}
+          </>
+        ) : (
+          <>
+            <span className="material-symbols-outlined">settings</span>
+            {t('admin:audit_log_management.settings')}
+          </>
+        )}
       </button>
 
       <h2 className="admin-setting-header mb-3">
         <span>
-          {isSettingPage ? t('audit_log_management.audit_log_settings') : t('audit_log_management.audit_log')}
+          {isSettingPage
+            ? t('audit_log_management.audit_log_settings')
+            : t('audit_log_management.audit_log')}
         </span>
-        { !isSettingPage && (
-          <button type="button" className="btn btn-sm ms-auto grw-btn-reload" onClick={reloadButtonPushedHandler}>
+        {!isSettingPage && (
+          <button
+            type="button"
+            className="btn btn-sm ms-auto grw-btn-reload"
+            onClick={reloadButtonPushedHandler}
+          >
             <span className="material-symbols-outlined">refresh</span>
           </button>
         )}
@@ -199,7 +265,11 @@ export const AuditLogManagement: FC = () => {
             </div>
 
             <div className="col-12">
-              <button type="button" className="btn btn-link" onClick={clearButtonPushedHandler}>
+              <button
+                type="button"
+                className="btn btn-link"
+                onClick={clearButtonPushedHandler}
+              >
                 {t('admin:audit_log_management.clear')}
               </button>
             </div>
@@ -211,16 +281,13 @@ export const AuditLogManagement: FC = () => {
             dangerouslySetInnerHTML={{ __html: activityCounter }}
           />
 
-          { isLoading
-            ? (
-              <div className="text-muted text-center mb-5">
-                <LoadingSpinner className="me-1 fs-3" />
-              </div>
-            )
-            : (
-              <ActivityTable activityList={activityList} />
-            )
-          }
+          {isLoading ? (
+            <div className="text-muted text-center mb-5">
+              <LoadingSpinner className="me-1 fs-3" />
+            </div>
+          ) : (
+            <ActivityTable activityList={activityList} />
+          )}
 
           <div className="d-flex flex-row justify-content-center">
             <PaginationWrapper
@@ -233,7 +300,12 @@ export const AuditLogManagement: FC = () => {
             />
 
             <div className="admin-audit-log ms-3">
-              <label htmlFor="jumpPageInput" className="form-label me-1 text-secondary">Jump To Page</label>
+              <label
+                htmlFor="jumpPageInput"
+                className="form-label me-1 text-secondary"
+              >
+                Jump To Page
+              </label>
               <input
                 id="jumpPageInput"
                 type="text"
@@ -241,7 +313,11 @@ export const AuditLogManagement: FC = () => {
                 onChange={jumpPageInputChangeHandler}
                 onKeyDown={jumpPageInputKeyDownHandler}
               />
-              <button className="btn btn-sm" type="button" onClick={jumpPageButtonPushedHandler}>
+              <button
+                className="btn btn-sm"
+                type="button"
+                onClick={jumpPageButtonPushedHandler}
+              >
                 <b>Go</b>
               </button>
             </div>

+ 2 - 6
apps/app/src/client/components/Admin/Common/Accordion.jsx

@@ -1,9 +1,7 @@
 import React, { useState } from 'react';
-
 import PropTypes from 'prop-types';
 import { Collapse } from 'reactstrap';
 
-
 const Accordion = (props) => {
   const [isOpen, setIsOpen] = useState(props.isOpenDefault);
   return (
@@ -14,15 +12,13 @@ const Accordion = (props) => {
           type="button"
           data-bs-toggle="collapse"
           aria-expanded="true"
-          onClick={() => setIsOpen(prevState => !prevState)}
+          onClick={() => setIsOpen((prevState) => !prevState)}
         >
           {props.title}
         </button>
       </p>
       <Collapse isOpen={isOpen}>
-        <div className="accordion-body">
-          {props.children}
-        </div>
+        <div className="accordion-body">{props.children}</div>
       </Collapse>
     </div>
   );

+ 11 - 5
apps/app/src/client/components/Admin/Common/AdminInstallButtonRow.tsx

@@ -1,16 +1,22 @@
 import React, { type JSX } from 'react';
 
 type Props = {
-  onClick: () => void,
-  disabled: boolean,
-
-}
+  onClick: () => void;
+  disabled: boolean;
+};
 
 export const AdminInstallButtonRow = (props: Props): JSX.Element => {
   return (
     <div className="row my-3">
       <div className="mx-auto">
-        <button type="button" className="btn btn-primary" onClick={props.onClick} disabled={props.disabled}>Install</button>
+        <button
+          type="button"
+          className="btn btn-primary"
+          onClick={props.onClick}
+          disabled={props.disabled}
+        >
+          Install
+        </button>
       </div>
     </div>
   );

+ 5 - 6
apps/app/src/client/components/Admin/Common/AdminUpdateButtonRow.tsx

@@ -1,12 +1,11 @@
 import React, { type JSX } from 'react';
-
 import { useTranslation } from 'next-i18next';
 
 type Props = {
-  onClick?: () => void,
-  disabled?: boolean,
-  type?: 'button' | 'submit' | 'reset',
-}
+  onClick?: () => void;
+  disabled?: boolean;
+  type?: 'button' | 'submit' | 'reset';
+};
 
 const AdminUpdateButtonRow = (props: Props): JSX.Element => {
   const { t } = useTranslation('admin');
@@ -22,7 +21,7 @@ const AdminUpdateButtonRow = (props: Props): JSX.Element => {
           onClick={props.onClick}
           disabled={props.disabled ?? false}
         >
-          { t('Update') }
+          {t('Update')}
         </button>
       </div>
     </div>

+ 17 - 12
apps/app/src/client/components/Admin/Common/LabeledProgressBar.tsx

@@ -1,18 +1,15 @@
 import React, { type JSX } from 'react';
-
 import { Progress } from 'reactstrap';
 
 type Props = {
-  header: string,
-  currentCount: number,
-  totalCount: number,
-  isInProgress?: boolean,
-}
+  header: string;
+  currentCount: number;
+  totalCount: number;
+  isInProgress?: boolean;
+};
 
 const LabeledProgressBar = (props: Props): JSX.Element => {
-  const {
-    header, currentCount, totalCount, isInProgress,
-  } = props;
+  const { header, currentCount, totalCount, isInProgress } = props;
 
   const progressingColor = isInProgress ? 'info' : 'success';
 
@@ -20,14 +17,22 @@ const LabeledProgressBar = (props: Props): JSX.Element => {
     <>
       <h6 className="my-1">
         {header}
-        <div className="float-end">{currentCount} / {totalCount}</div>
+        <div className="float-end">
+          {currentCount} / {totalCount}
+        </div>
       </h6>
       <Progress multi>
-        <Progress bar max={totalCount} color={progressingColor} striped={isInProgress} animated={isInProgress} value={currentCount} />
+        <Progress
+          bar
+          max={totalCount}
+          color={progressingColor}
+          striped={isInProgress}
+          animated={isInProgress}
+          value={currentCount}
+        />
       </Progress>
     </>
   );
-
 };
 
 export default LabeledProgressBar;

+ 29 - 34
apps/app/src/client/components/Admin/ElasticsearchManagement/ElasticsearchManagement.tsx

@@ -1,10 +1,9 @@
-import React, { useEffect, useState, useCallback } from 'react';
-
+import React, { useCallback, useEffect, useState } from 'react';
 import { useAtomValue } from 'jotai';
 import { useTranslation } from 'next-i18next';
 
 import { apiv3Get, apiv3Post, apiv3Put } from '~/client/util/apiv3-client';
-import { toastSuccess, toastError } from '~/client/util/toastr';
+import { toastError, toastSuccess } from '~/client/util/toastr';
 import { useAdminSocket } from '~/features/admin/states/socket-io';
 import { SocketEventName } from '~/interfaces/websocket';
 import { isSearchServiceReachableAtom } from '~/states/server-configurations';
@@ -24,7 +23,8 @@ const ElasticsearchManagement = (): JSX.Element => {
 
   const [isConnected, setIsConnected] = useState(false);
   const [isConfigured, setIsConfigured] = useState(false);
-  const [isReconnectingProcessing, setIsReconnectingProcessing] = useState(false);
+  const [isReconnectingProcessing, setIsReconnectingProcessing] =
+    useState(false);
   const [isRebuildingProcessing, setIsRebuildingProcessing] = useState(false);
   const [isRebuildingCompleted, setIsRebuildingCompleted] = useState(false);
 
@@ -32,8 +32,7 @@ const ElasticsearchManagement = (): JSX.Element => {
   const [indicesData, setIndicesData] = useState(null);
   const [aliasesData, setAliasesData] = useState(null);
 
-
-  const retrieveIndicesStatus = useCallback(async() => {
+  const retrieveIndicesStatus = useCallback(async () => {
     try {
       const { data } = await apiv3Get('/search/indices');
       const { info } = data;
@@ -46,8 +45,7 @@ const ElasticsearchManagement = (): JSX.Element => {
       setIsNormalized(info.isNormalized);
 
       return info.isNormalized;
-    }
-    catch (errors: unknown) {
+    } catch (errors: unknown) {
       setIsConnected(false);
 
       // evaluate whether configured or not
@@ -58,14 +56,12 @@ const ElasticsearchManagement = (): JSX.Element => {
           }
         }
         toastError(errors as Error[]);
-      }
-      else {
+      } else {
         toastError(errors as Error);
       }
 
       return false;
-    }
-    finally {
+    } finally {
       setIsInitialized(true);
     }
   }, []);
@@ -82,12 +78,12 @@ const ElasticsearchManagement = (): JSX.Element => {
       setIsRebuildingProcessing(true);
     });
 
-    socket.on(SocketEventName.FinishAddPage, async(data) => {
+    socket.on(SocketEventName.FinishAddPage, async (data) => {
       let retryCount = 0;
       const maxRetries = 5;
       const retryDelay = 500;
 
-      const retrieveIndicesStatusWithRetry = async() => {
+      const retrieveIndicesStatusWithRetry = async () => {
         const isNormalizedResult = await retrieveIndicesStatus();
         if (!isNormalizedResult && retryCount < maxRetries) {
           retryCount++;
@@ -111,13 +107,12 @@ const ElasticsearchManagement = (): JSX.Element => {
     };
   }, [retrieveIndicesStatus, socket]);
 
-  const reconnect = async() => {
+  const reconnect = async () => {
     setIsReconnectingProcessing(true);
 
     try {
       await apiv3Post('/search/connection');
-    }
-    catch (e) {
+    } catch (e) {
       toastError(e);
       return;
     }
@@ -126,12 +121,10 @@ const ElasticsearchManagement = (): JSX.Element => {
     window.location.reload();
   };
 
-  const normalizeIndices = async() => {
-
+  const normalizeIndices = async () => {
     try {
       await apiv3Put('/search/indices', { operation: 'normalize' });
-    }
-    catch (e) {
+    } catch (e) {
       toastError(e);
     }
 
@@ -140,14 +133,13 @@ const ElasticsearchManagement = (): JSX.Element => {
     toastSuccess('Normalizing has succeeded');
   };
 
-  const rebuildIndices = async() => {
+  const rebuildIndices = async () => {
     setIsRebuildingProcessing(true);
 
     try {
       await apiv3Put('/search/indices', { operation: 'rebuild' });
       toastSuccess('Rebuilding is requested');
-    }
-    catch (e) {
+    } catch (e) {
       toastError(e);
     }
 
@@ -156,7 +148,9 @@ const ElasticsearchManagement = (): JSX.Element => {
 
   const isErrorOccuredOnSearchService = !isSearchServiceReachable;
 
-  const isReconnectBtnEnabled = !isReconnectingProcessing && (!isInitialized || !isConnected || isErrorOccuredOnSearchService);
+  const isReconnectBtnEnabled =
+    !isReconnectingProcessing &&
+    (!isInitialized || !isConnected || isErrorOccuredOnSearchService);
 
   return (
     <>
@@ -178,7 +172,9 @@ const ElasticsearchManagement = (): JSX.Element => {
 
       {/* Controls */}
       <div className="row">
-        <label className="col-md-3 col-form-label text-start text-md-end">{ t('full_text_search_management.reconnect') }</label>
+        <label className="col-md-3 col-form-label text-start text-md-end">
+          {t('full_text_search_management.reconnect')}
+        </label>
         <div className="col-md-6">
           <ReconnectControls
             isEnabled={isReconnectBtnEnabled}
@@ -191,7 +187,9 @@ const ElasticsearchManagement = (): JSX.Element => {
       <hr />
 
       <div className="row">
-        <label className="col-md-3 col-form-label text-start text-md-end">{ t('full_text_search_management.normalize') }</label>
+        <label className="col-md-3 col-form-label text-start text-md-end">
+          {t('full_text_search_management.normalize')}
+        </label>
         <div className="col-md-6">
           <NormalizeIndicesControls
             isRebuildingProcessing={isRebuildingProcessing}
@@ -204,7 +202,9 @@ const ElasticsearchManagement = (): JSX.Element => {
       <hr />
 
       <div className="row">
-        <label className="col-md-3 col-form-label text-start text-md-end">{ t('full_text_search_management.rebuild') }</label>
+        <label className="col-md-3 col-form-label text-start text-md-end">
+          {t('full_text_search_management.rebuild')}
+        </label>
         <div className="col-md-6">
           <RebuildIndexControls
             isRebuildingProcessing={isRebuildingProcessing}
@@ -214,15 +214,10 @@ const ElasticsearchManagement = (): JSX.Element => {
           />
         </div>
       </div>
-
     </>
   );
-
 };
 
-
-ElasticsearchManagement.propTypes = {
-
-};
+ElasticsearchManagement.propTypes = {};
 
 export default ElasticsearchManagement;

+ 12 - 9
apps/app/src/client/components/Admin/ElasticsearchManagement/NormalizeIndicesControls.tsx

@@ -1,32 +1,35 @@
 import React, { type JSX } from 'react';
-
 import { useTranslation } from 'next-i18next';
 
 type Props = {
-  isRebuildingProcessing: boolean,
-  onNormalizingRequested: () => void,
-  isNormalized?: boolean,
-}
+  isRebuildingProcessing: boolean;
+  onNormalizingRequested: () => void;
+  isNormalized?: boolean;
+};
 
 const NormalizeIndicesControls = (props: Props): JSX.Element => {
   const { t } = useTranslation('admin');
   const { isNormalized, isRebuildingProcessing } = props;
 
-  const isEnabled = (isNormalized != null) && !isNormalized && !isRebuildingProcessing;
+  const isEnabled =
+    isNormalized != null && !isNormalized && !isRebuildingProcessing;
 
   return (
     <>
       <button
         type="submit"
         className={`btn ${isEnabled ? 'btn-outline-info' : 'btn-outline-secondary'}`}
-        onClick={() => { props.onNormalizingRequested() }}
+        onClick={() => {
+          props.onNormalizingRequested();
+        }}
         disabled={!isEnabled}
       >
-        { t('full_text_search_management.normalize_button') }
+        {t('full_text_search_management.normalize_button')}
       </button>
 
       <p className="form-text text-muted">
-        { t('full_text_search_management.normalize_description') }<br />
+        {t('full_text_search_management.normalize_description')}
+        <br />
       </p>
     </>
   );

+ 11 - 15
apps/app/src/client/components/Admin/ElasticsearchManagement/RebuildIndexControls.jsx

@@ -1,5 +1,4 @@
 import React from 'react';
-
 import { useTranslation } from 'next-i18next';
 import PropTypes from 'prop-types';
 
@@ -9,7 +8,6 @@ import { SocketEventName } from '~/interfaces/websocket';
 import LabeledProgressBar from '../Common/LabeledProgressBar';
 
 class RebuildIndexControls extends React.Component {
-
   constructor(props) {
     super(props);
 
@@ -44,12 +42,8 @@ class RebuildIndexControls extends React.Component {
   }
 
   renderProgressBar() {
-    const {
-      isRebuildingProcessing, isRebuildingCompleted,
-    } = this.props;
-    const {
-      total, current,
-    } = this.state;
+    const { isRebuildingProcessing, isRebuildingCompleted } = this.props;
+    const { total, current } = this.state;
     const showProgressBar = isRebuildingProcessing || isRebuildingCompleted;
 
     if (!showProgressBar) {
@@ -76,25 +70,28 @@ class RebuildIndexControls extends React.Component {
 
     return (
       <>
-        { this.renderProgressBar() }
+        {this.renderProgressBar()}
 
         <button
           type="submit"
           className="btn btn-primary"
-          onClick={() => { this.props.onRebuildingRequested() }}
+          onClick={() => {
+            this.props.onRebuildingRequested();
+          }}
           disabled={!isEnabled}
         >
-          { t('full_text_search_management.rebuild_button') }
+          {t('full_text_search_management.rebuild_button')}
         </button>
 
         <p className="form-text text-muted">
-          { t('full_text_search_management.rebuild_description_1') }<br />
-          { t('full_text_search_management.rebuild_description_2') }<br />
+          {t('full_text_search_management.rebuild_description_1')}
+          <br />
+          {t('full_text_search_management.rebuild_description_2')}
+          <br />
         </p>
       </>
     );
   }
-
 }
 
 const RebuildIndexControlsFC = (props) => {
@@ -103,7 +100,6 @@ const RebuildIndexControlsFC = (props) => {
   return <RebuildIndexControls t={t} socket={socket} {...props} />;
 };
 
-
 RebuildIndexControls.propTypes = {
   t: PropTypes.func.isRequired, // i18next
 

+ 11 - 11
apps/app/src/client/components/Admin/ElasticsearchManagement/ReconnectControls.tsx

@@ -1,14 +1,12 @@
 import React, { type JSX } from 'react';
-
 import { LoadingSpinner } from '@growi/ui/dist/components';
 import { useTranslation } from 'next-i18next';
 
-
 type Props = {
-  isEnabled?: boolean,
-  isProcessing?: boolean,
-  onReconnectingRequested: () => void,
-}
+  isEnabled?: boolean;
+  isProcessing?: boolean;
+  onReconnectingRequested: () => void;
+};
 
 const ReconnectControls = (props: Props): JSX.Element => {
   const { t } = useTranslation('admin');
@@ -20,19 +18,21 @@ const ReconnectControls = (props: Props): JSX.Element => {
       <button
         type="submit"
         className={`btn ${isEnabled ? 'btn-outline-success' : 'btn-outline-secondary'}`}
-        onClick={() => { props.onReconnectingRequested() }}
+        onClick={() => {
+          props.onReconnectingRequested();
+        }}
         disabled={!isEnabled}
       >
-        { isProcessing && <LoadingSpinner className="me-2" /> }
-        { t('full_text_search_management.reconnect_button') }
+        {isProcessing && <LoadingSpinner className="me-2" />}
+        {t('full_text_search_management.reconnect_button')}
       </button>
 
       <p className="form-text text-muted">
-        { t('full_text_search_management.reconnect_description') }<br />
+        {t('full_text_search_management.reconnect_description')}
+        <br />
       </p>
     </>
   );
-
 };
 
 export default ReconnectControls;

+ 82 - 43
apps/app/src/client/components/Admin/ElasticsearchManagement/StatusTable.jsx

@@ -1,43 +1,54 @@
 import React from 'react';
-
 import { useTranslation } from 'next-i18next';
 import PropTypes from 'prop-types';
 
 class StatusTable extends React.PureComponent {
-
   renderPreInitializedLabel() {
     return <span className="badge text-bg-default">――</span>;
   }
 
   renderConnectionStatusLabels() {
     const { t } = this.props;
-    const {
-      isErrorOccuredOnSearchService,
-      isConnected, isConfigured,
-    } = this.props;
+    const { isErrorOccuredOnSearchService, isConnected, isConfigured } =
+      this.props;
 
-    const errorOccuredLabel = isErrorOccuredOnSearchService
-      ? <span className="badge text-bg-danger ms-2">{ t('full_text_search_management.connection_status_label_erroroccured') }</span>
-      : null;
+    const errorOccuredLabel = isErrorOccuredOnSearchService ? (
+      <span className="badge text-bg-danger ms-2">
+        {t('full_text_search_management.connection_status_label_erroroccured')}
+      </span>
+    ) : null;
 
     let connectionStatusLabel = null;
     if (!isConfigured) {
       connectionStatusLabel = (
         <span className="badge text-bg-default">
-          { t('full_text_search_management.connection_status_label_unconfigured') }
+          {t(
+            'full_text_search_management.connection_status_label_unconfigured',
+          )}
         </span>
       );
-    }
-    else {
-      connectionStatusLabel = isConnected
+    } else {
+      connectionStatusLabel = isConnected ? (
         // eslint-disable-next-line max-len
-        ? <span data-testid="connection-status-badge-connected" className="badge text-bg-success">{ t('full_text_search_management.connection_status_label_connected') }</span>
-        : <span className="badge text-bg-danger">{ t('full_text_search_management.connection_status_label_disconnected') }</span>;
+        <span
+          data-testid="connection-status-badge-connected"
+          className="badge text-bg-success"
+        >
+          {t('full_text_search_management.connection_status_label_connected')}
+        </span>
+      ) : (
+        <span className="badge text-bg-danger">
+          {t(
+            'full_text_search_management.connection_status_label_disconnected',
+          )}
+        </span>
+      );
     }
 
     return (
       <>
-        {connectionStatusLabel}{errorOccuredLabel}
+        {connectionStatusLabel}
+        {errorOccuredLabel}
       </>
     );
   }
@@ -45,9 +56,15 @@ class StatusTable extends React.PureComponent {
   renderIndicesStatusLabel() {
     const { t, isNormalized } = this.props;
 
-    return isNormalized
-      ? <span className="badge text-bg-info">{ t('full_text_search_management.indices_status_label_normalized') }</span>
-      : <span className="badge text-bg-warning">{ t('full_text_search_management.indices_status_label_unnormalized') }</span>;
+    return isNormalized ? (
+      <span className="badge text-bg-info">
+        {t('full_text_search_management.indices_status_label_normalized')}
+      </span>
+    ) : (
+      <span className="badge text-bg-warning">
+        {t('full_text_search_management.indices_status_label_unnormalized')}
+      </span>
+    );
   }
 
   renderIndexInfoPanel(indexName, body = {}, aliases = []) {
@@ -55,7 +72,10 @@ class StatusTable extends React.PureComponent {
 
     const aliasLabels = aliases.map((aliasName) => {
       return (
-        <span key={`badge-${indexName}-${aliasName}`} className="badge text-bg-primary me-2">
+        <span
+          key={`badge-${indexName}-${aliasName}`}
+          className="badge text-bg-primary me-2"
+        >
           <span className="material-symbols-outlined">sell</span>
           <span>{aliasName}</span>
         </span>
@@ -65,17 +85,22 @@ class StatusTable extends React.PureComponent {
     return (
       <div className="card">
         <div className="card-header">
-
-          <a role="button" className="text-nowrap me-2" data-bs-toggle="collapse" href={`#${collapseId}`} aria-expanded="true" aria-controls={collapseId}>
-            <span className="material-symbols-outlined">database</span> {indexName}
+          <a
+            role="button"
+            className="text-nowrap me-2"
+            data-bs-toggle="collapse"
+            href={`#${collapseId}`}
+            aria-expanded="true"
+            aria-controls={collapseId}
+          >
+            <span className="material-symbols-outlined">database</span>{' '}
+            {indexName}
           </a>
           <span className="ms-md-3">{aliasLabels}</span>
         </div>
         <div id={collapseId} className="collapse">
           <div className="card-body">
-            <pre>
-              {JSON.stringify(body, null, 2)}
-            </pre>
+            <pre>{JSON.stringify(body, null, 2)}</pre>
           </div>
         </div>
       </div>
@@ -83,10 +108,7 @@ class StatusTable extends React.PureComponent {
   }
 
   renderIndexInfoPanels() {
-    const {
-      indicesData,
-      aliasesData,
-    } = this.props;
+    const { indicesData, aliasesData } = this.props;
 
     // data is null
     if (indicesData == null) {
@@ -126,43 +148,60 @@ class StatusTable extends React.PureComponent {
 
     return (
       <div className="row">
-        { Object.keys(indexNameToDataMap).map((indexName) => {
+        {Object.keys(indexNameToDataMap).map((indexName) => {
           return (
             <div key={`col-${indexName}`} className="col-md-6">
-              { this.renderIndexInfoPanel(indexName, indexNameToDataMap[indexName], indexNameToAliasMap[indexName]) }
+              {this.renderIndexInfoPanel(
+                indexName,
+                indexNameToDataMap[indexName],
+                indexNameToAliasMap[indexName],
+              )}
             </div>
           );
-        }) }
+        })}
       </div>
     );
   }
 
   render() {
     const { t } = this.props;
-    const {
-      isInitialized,
-    } = this.props;
+    const { isInitialized } = this.props;
 
     return (
       <table className="table table-bordered">
         <tbody>
           <tr>
-            <th className="w-25">{t('full_text_search_management.connection_status')}</th>
-            <td className="w-75">{ isInitialized ? this.renderConnectionStatusLabels() : this.renderPreInitializedLabel() }</td>
+            <th className="w-25">
+              {t('full_text_search_management.connection_status')}
+            </th>
+            <td className="w-75">
+              {isInitialized
+                ? this.renderConnectionStatusLabels()
+                : this.renderPreInitializedLabel()}
+            </td>
           </tr>
           <tr>
-            <th className="w-25">{t('full_text_search_management.indices_status')}</th>
-            <td className="w-75">{ isInitialized ? this.renderIndicesStatusLabel() : this.renderPreInitializedLabel() }</td>
+            <th className="w-25">
+              {t('full_text_search_management.indices_status')}
+            </th>
+            <td className="w-75">
+              {isInitialized
+                ? this.renderIndicesStatusLabel()
+                : this.renderPreInitializedLabel()}
+            </td>
           </tr>
           <tr>
-            <th className="w-25">{t('full_text_search_management.indices_summary')}</th>
-            <td className="p-4 w-75">{ isInitialized && this.renderIndexInfoPanels() }</td>
+            <th className="w-25">
+              {t('full_text_search_management.indices_summary')}
+            </th>
+            <td className="p-4 w-75">
+              {isInitialized && this.renderIndexInfoPanels()}
+            </td>
           </tr>
         </tbody>
       </table>
     );
   }
-
 }
 
 const StatusTableWrapperFC = (props) => {

+ 13 - 6
apps/app/src/client/components/Admin/ExportArchiveData/ArchiveFilesTable.tsx

@@ -1,14 +1,13 @@
 import React, { type JSX } from 'react';
-
 import { format } from 'date-fns/format';
 import { useTranslation } from 'next-i18next';
 
 import ArchiveFilesTableMenu from './ArchiveFilesTableMenu';
 
 type ArchiveFilesTableProps = {
-  zipFileStats: any[],
-  onZipFileStatRemove: (fileName: string) => void,
-}
+  zipFileStats: any[];
+  onZipFileStatRemove: (fileName: string) => void;
+};
 
 const ArchiveFilesTable = (props: ArchiveFilesTableProps): JSX.Element => {
   const { t } = useTranslation();
@@ -30,8 +29,16 @@ const ArchiveFilesTable = (props: ArchiveFilesTableProps): JSX.Element => {
             <tr key={fileName}>
               <th>{fileName}</th>
               <td>{meta.version}</td>
-              <td className="text-capitalize">{innerFileStats.map(fileStat => fileStat.collectionName).join(', ')}</td>
-              <td>{meta.exportedAt ? format(new Date(meta.exportedAt), 'yyyy/MM/dd HH:mm:ss') : ''}</td>
+              <td className="text-capitalize">
+                {innerFileStats
+                  .map((fileStat) => fileStat.collectionName)
+                  .join(', ')}
+              </td>
+              <td>
+                {meta.exportedAt
+                  ? format(new Date(meta.exportedAt), 'yyyy/MM/dd HH:mm:ss')
+                  : ''}
+              </td>
               <td>
                 <ArchiveFilesTableMenu
                   fileName={fileName}

+ 36 - 12
apps/app/src/client/components/Admin/ExportArchiveData/ArchiveFilesTableMenu.tsx

@@ -1,29 +1,53 @@
 import React, { type JSX } from 'react';
-
 import { useTranslation } from 'next-i18next';
 
 // import { toastSuccess, toastError } from '~/client/util/toastr';
 
 type ArchiveFilesTableMenuProps = {
-  fileName: string,
-  onZipFileStatRemove: (fileName: string) => void,
-}
+  fileName: string;
+  onZipFileStatRemove: (fileName: string) => void;
+};
 
-const ArchiveFilesTableMenu = (props: ArchiveFilesTableMenuProps):JSX.Element => {
+const ArchiveFilesTableMenu = (
+  props: ArchiveFilesTableMenuProps,
+): JSX.Element => {
   const { t } = useTranslation();
 
   return (
     <div className="dropdown">
-      <button type="button" className="btn btn-sm btn-outline-secondary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
-        <span className="material-symbols-outlined">settings</span> <span className="caret"></span>
+      <button
+        type="button"
+        className="btn btn-sm btn-outline-secondary dropdown-toggle"
+        data-bs-toggle="dropdown"
+        aria-expanded="false"
+      >
+        <span className="material-symbols-outlined">settings</span>{' '}
+        <span className="caret"></span>
       </button>
       <ul className="dropdown-menu dropdown-menu-end">
-        <li className="dropdown-header">{t('admin:export_management.export_menu')}</li>
-        <button type="button" className="dropdown-item" onClick={() => { window.location.href = `/admin/export/${props.fileName}` }}>
-          <span className="material-symbols-outlined">cloud_download</span> {t('admin:export_management.download')}
+        <li className="dropdown-header">
+          {t('admin:export_management.export_menu')}
+        </li>
+        <button
+          type="button"
+          className="dropdown-item"
+          onClick={() => {
+            window.location.href = `/admin/export/${props.fileName}`;
+          }}
+        >
+          <span className="material-symbols-outlined">cloud_download</span>{' '}
+          {t('admin:export_management.download')}
         </button>
-        <button type="button" className="dropdown-item" role="button" onClick={() => props.onZipFileStatRemove(props.fileName)}>
-          <span className="text-danger"><span className="material-symbols-outlined">delete</span> {t('admin:export_management.delete')}</span>
+        <button
+          type="button"
+          className="dropdown-item"
+          role="button"
+          onClick={() => props.onZipFileStatRemove(props.fileName)}
+        >
+          <span className="text-danger">
+            <span className="material-symbols-outlined">delete</span>{' '}
+            {t('admin:export_management.delete')}
+          </span>
         </button>
       </ul>
     </div>

+ 136 - 82
apps/app/src/client/components/Admin/ExportArchiveData/SelectCollectionsModal.tsx

@@ -1,47 +1,61 @@
-import React, {
-  useCallback, useState, useEffect, type JSX,
-} from 'react';
-
+import React, { type JSX, useCallback, useEffect, 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 { toastError, toastSuccess } from '~/client/util/toastr';
 
-
 const GROUPS_PAGE = [
-  'pages', 'revisions', 'tags', 'pagetagrelations', 'pageredirects', 'comments', 'sharelinks',
+  'pages',
+  'revisions',
+  'tags',
+  'pagetagrelations',
+  'pageredirects',
+  'comments',
+  'sharelinks',
 ];
 const GROUPS_USER = [
-  'users', 'externalaccounts', 'usergroups', 'usergrouprelations',
-  'externalusergroups', 'externalusergrouprelations',
-  'useruisettings', 'editorsettings', 'bookmarks', 'bookmarkfolders', 'subscriptions',
+  'users',
+  'externalaccounts',
+  'usergroups',
+  'usergrouprelations',
+  'externalusergroups',
+  'externalusergrouprelations',
+  'useruisettings',
+  'editorsettings',
+  'bookmarks',
+  'bookmarkfolders',
+  'subscriptions',
   'inappnotificationsettings',
 ];
 const GROUPS_CONFIG = [
-  'configs', 'migrations', 'updateposts', 'globalnotificationsettings', 'slackappintegrations',
+  'configs',
+  'migrations',
+  'updateposts',
+  'globalnotificationsettings',
+  'slackappintegrations',
   'growiplugins',
 ];
-const ALL_GROUPED_COLLECTIONS = GROUPS_PAGE.concat(GROUPS_USER).concat(GROUPS_CONFIG);
+const ALL_GROUPED_COLLECTIONS =
+  GROUPS_PAGE.concat(GROUPS_USER).concat(GROUPS_CONFIG);
 
 type Props = {
-  isOpen: boolean,
-  onExportingRequested: () => void,
-  onClose: () => void,
-  collections: string[],
-  isAllChecked?: boolean,
+  isOpen: boolean;
+  onExportingRequested: () => void;
+  onClose: () => void;
+  collections: string[];
+  isAllChecked?: boolean;
 };
 
 const SelectCollectionsModal = (props: Props): JSX.Element => {
   const { t } = useTranslation();
 
-  const {
-    isOpen, onExportingRequested, onClose, collections, isAllChecked,
-  } = props;
+  const { isOpen, onExportingRequested, onClose, collections, isAllChecked } =
+    props;
 
-  const [selectedCollections, setSelectedCollections] = useState<Set<string>>(new Set());
+  const [selectedCollections, setSelectedCollections] = useState<Set<string>>(
+    new Set(),
+  );
 
   const toggleCheckbox = useCallback((e) => {
     const { target } = e;
@@ -51,8 +65,7 @@ const SelectCollectionsModal = (props: Props): JSX.Element => {
       const selectedCollections = new Set(prevState);
       if (checked) {
         selectedCollections.add(name);
-      }
-      else {
+      } else {
         selectedCollections.delete(name);
       }
 
@@ -68,27 +81,31 @@ const SelectCollectionsModal = (props: Props): JSX.Element => {
     setSelectedCollections(new Set());
   }, []);
 
-  const doExport = useCallback(async(e) => {
-    e.preventDefault();
+  const doExport = useCallback(
+    async (e) => {
+      e.preventDefault();
 
-    try {
-      // TODO: use apiv3Post
-      const result = await apiPost<any>('/v3/export', { collections: Array.from(selectedCollections) });
+      try {
+        // TODO: use apiv3Post
+        const result = await apiPost<any>('/v3/export', {
+          collections: Array.from(selectedCollections),
+        });
 
-      if (!result.ok) {
-        throw new Error('Error occured.');
-      }
+        if (!result.ok) {
+          throw new Error('Error occured.');
+        }
 
-      toastSuccess('Export process has requested.');
+        toastSuccess('Export process has requested.');
 
-      onExportingRequested();
-      onClose();
-      uncheckAll();
-    }
-    catch (err) {
-      toastError(err);
-    }
-  }, [onClose, onExportingRequested, selectedCollections, uncheckAll]);
+        onExportingRequested();
+        onClose();
+        uncheckAll();
+      } catch (err) {
+        toastError(err);
+      }
+    },
+    [onClose, onExportingRequested, selectedCollections, uncheckAll],
+  );
 
   const validateForm = useCallback(() => {
     return selectedCollections.size > 0;
@@ -116,42 +133,51 @@ const SelectCollectionsModal = (props: Props): JSX.Element => {
     );
   }, [selectedCollections, t]);
 
-  const renderCheckboxes = useCallback((collectionNames, color?) => {
-    const checkboxColor = color ? `form-check-${color}` : 'form-check-info';
+  const renderCheckboxes = useCallback(
+    (collectionNames, color?) => {
+      const checkboxColor = color ? `form-check-${color}` : 'form-check-info';
 
-    return (
-      <div className={`form-check ${checkboxColor}`}>
-        <div className="row">
-          {collectionNames.map((collectionName) => {
-            return (
-              <div className="col-sm-6 my-1" key={collectionName}>
-                <input
-                  type="checkbox"
-                  className="form-check-input"
-                  id={collectionName}
-                  name={collectionName}
-                  value={collectionName}
-                  checked={selectedCollections.has(collectionName)}
-                  onChange={toggleCheckbox}
-                />
-                <label className="form-label text-capitalize form-check-label ms-3" htmlFor={collectionName}>
-                  {collectionName}
-                </label>
-              </div>
-            );
-          })}
+      return (
+        <div className={`form-check ${checkboxColor}`}>
+          <div className="row">
+            {collectionNames.map((collectionName) => {
+              return (
+                <div className="col-sm-6 my-1" key={collectionName}>
+                  <input
+                    type="checkbox"
+                    className="form-check-input"
+                    id={collectionName}
+                    name={collectionName}
+                    value={collectionName}
+                    checked={selectedCollections.has(collectionName)}
+                    onChange={toggleCheckbox}
+                  />
+                  <label
+                    className="form-label text-capitalize form-check-label ms-3"
+                    htmlFor={collectionName}
+                  >
+                    {collectionName}
+                  </label>
+                </div>
+              );
+            })}
+          </div>
         </div>
-      </div>
-    );
-  }, [selectedCollections, toggleCheckbox]);
+      );
+    },
+    [selectedCollections, toggleCheckbox],
+  );
 
-  const renderGroups = useCallback((groupList, color?) => {
-    const collectionNames = groupList.filter((collectionName) => {
-      return collections.includes(collectionName);
-    });
+  const renderGroups = useCallback(
+    (groupList, color?) => {
+      const collectionNames = groupList.filter((collectionName) => {
+        return collections.includes(collectionName);
+      });
 
-    return renderCheckboxes(collectionNames, color);
-  }, [collections, renderCheckboxes]);
+      return renderCheckboxes(collectionNames, color);
+    },
+    [collections, renderCheckboxes],
+  );
 
   const renderOthers = useCallback(() => {
     const collectionNames = collections.filter((collectionName) => {
@@ -175,11 +201,23 @@ const SelectCollectionsModal = (props: Props): JSX.Element => {
         <ModalBody>
           <div className="row">
             <div className="col-sm-12">
-              <button type="button" className="btn btn-sm btn-outline-secondary me-2" onClick={checkAll}>
-                <span className="material-symbols-outlined">check_box</span> {t('admin:export_management.check_all')}
+              <button
+                type="button"
+                className="btn btn-sm btn-outline-secondary me-2"
+                onClick={checkAll}
+              >
+                <span className="material-symbols-outlined">check_box</span>{' '}
+                {t('admin:export_management.check_all')}
               </button>
-              <button type="button" className="btn btn-sm btn-outline-secondary me-2" onClick={uncheckAll}>
-                <span className="material-symbols-outlined">check_box_outline_blank</span> {t('admin:export_management.uncheck_all')}
+              <button
+                type="button"
+                className="btn btn-sm btn-outline-secondary me-2"
+                onClick={uncheckAll}
+              >
+                <span className="material-symbols-outlined">
+                  check_box_outline_blank
+                </span>{' '}
+                {t('admin:export_management.uncheck_all')}
               </button>
             </div>
           </div>
@@ -198,21 +236,37 @@ const SelectCollectionsModal = (props: Props): JSX.Element => {
           </div>
           <div className="row mt-4">
             <div className="col-sm-12">
-              <h3 className="admin-setting-header">MongoDB Config Collections</h3>
+              <h3 className="admin-setting-header">
+                MongoDB Config Collections
+              </h3>
               {renderGroups(GROUPS_CONFIG)}
             </div>
           </div>
           <div className="row mt-4">
             <div className="col-sm-12">
-              <h3 className="admin-setting-header">MongoDB Other Collections</h3>
+              <h3 className="admin-setting-header">
+                MongoDB Other Collections
+              </h3>
               {renderOthers()}
             </div>
           </div>
         </ModalBody>
 
         <ModalFooter>
-          <button type="button" className="btn btn-sm btn-outline-secondary" onClick={onClose}>{t('admin:export_management.cancel')}</button>
-          <button type="submit" className="btn btn-sm btn-primary" disabled={!validateForm()}>{t('admin:export_management.export')}</button>
+          <button
+            type="button"
+            className="btn btn-sm btn-outline-secondary"
+            onClick={onClose}
+          >
+            {t('admin:export_management.cancel')}
+          </button>
+          <button
+            type="submit"
+            className="btn btn-sm btn-primary"
+            disabled={!validateForm()}
+          >
+            {t('admin:export_management.export')}
+          </button>
         </ModalFooter>
       </form>
     </Modal>

+ 40 - 27
apps/app/src/client/components/Admin/ExportArchiveDataPage.tsx

@@ -1,10 +1,6 @@
-import React, {
-  useCallback, useEffect, useState, type JSX,
-} from 'react';
-
+import React, { type JSX, useCallback, useEffect, useState } from 'react';
 import { useTranslation } from 'react-i18next';
 
-
 import { apiDelete } from '~/client/util/apiv1-client';
 import { apiv3Get } from '~/client/util/apiv3-client';
 import { toastError, toastSuccess } from '~/client/util/toastr';
@@ -14,9 +10,11 @@ import LabeledProgressBar from './Common/LabeledProgressBar';
 import ArchiveFilesTable from './ExportArchiveData/ArchiveFilesTable';
 import SelectCollectionsModal from './ExportArchiveData/SelectCollectionsModal';
 
-
 const IGNORED_COLLECTION_NAMES = [
-  'sessions', 'rlflx', 'yjs-writings', 'transferkeys',
+  'sessions',
+  'rlflx',
+  'yjs-writings',
+  'transferkeys',
 ];
 
 const ExportArchiveDataPage = (): JSX.Element => {
@@ -31,16 +29,26 @@ const ExportArchiveDataPage = (): JSX.Element => {
   const [isZipping, setZipping] = useState(false);
   const [isExported, setExported] = useState(false);
 
-  const fetchData = useCallback(async() => {
-    const [{ data: collectionsData }, { data: statusData }] = await Promise.all([
-      apiv3Get<{collections: any[]}>('/mongo/collections', {}),
-      apiv3Get<{status: { zipFileStats: any[], isExporting: boolean, progressList: any[] }}>('/export/status', {}),
-    ]);
+  const fetchData = useCallback(async () => {
+    const [{ data: collectionsData }, { data: statusData }] = await Promise.all(
+      [
+        apiv3Get<{ collections: any[] }>('/mongo/collections', {}),
+        apiv3Get<{
+          status: {
+            zipFileStats: any[];
+            isExporting: boolean;
+            progressList: any[];
+          };
+        }>('/export/status', {}),
+      ],
+    );
 
     // filter only not ignored collection names
-    const filteredCollections = collectionsData.collections.filter((collectionName) => {
-      return !IGNORED_COLLECTION_NAMES.includes(collectionName);
-    });
+    const filteredCollections = collectionsData.collections.filter(
+      (collectionName) => {
+        return !IGNORED_COLLECTION_NAMES.includes(collectionName);
+      },
+    );
 
     const { zipFileStats, isExporting, progressList } = statusData.status;
     setCollections(filteredCollections);
@@ -67,7 +75,7 @@ const ExportArchiveDataPage = (): JSX.Element => {
       setExporting(false);
       setZipping(false);
       setExported(true);
-      setZipFileStats(prev => prev.concat([addedZipFileStat]));
+      setZipFileStats((prev) => prev.concat([addedZipFileStat]));
 
       toastSuccess(`New Archive Data '${addedZipFileStat.fileName}' is added`);
     };
@@ -83,18 +91,18 @@ const ExportArchiveDataPage = (): JSX.Element => {
       socket.off('admin:onStartZippingForExport', onStartZipping);
       socket.off('admin:onTerminateForExport', onTerminateForExport);
     };
-
   }, [socket]);
 
-  const onZipFileStatRemove = useCallback(async(fileName) => {
+  const onZipFileStatRemove = useCallback(async (fileName) => {
     try {
       await apiDelete(`/v3/export/${fileName}`, {});
 
-      setZipFileStats(prev => prev.filter(stat => stat.fileName !== fileName));
+      setZipFileStats((prev) =>
+        prev.filter((stat) => stat.fileName !== fileName),
+      );
 
       toastSuccess(`Deleted ${fileName}`);
-    }
-    catch (err) {
+    } catch (err) {
       toastError(err);
     }
   }, []);
@@ -148,23 +156,28 @@ const ExportArchiveDataPage = (): JSX.Element => {
     };
   }, [fetchData, setupWebsocketEventHandler]);
 
-  const showExportingData = (isExported || isExporting) && (progressList != null);
+  const showExportingData = (isExported || isExporting) && progressList != null;
 
   return (
     <div data-testid="admin-export-archive-data">
       <h2>{t('export_management.export_archive_data')}</h2>
 
-      <button type="button" className="btn btn-outline-secondary" disabled={isExporting} onClick={() => setExportModalOpen(true)}>
+      <button
+        type="button"
+        className="btn btn-outline-secondary"
+        disabled={isExporting}
+        onClick={() => setExportModalOpen(true)}
+      >
         {t('export_management.create_new_archive_data')}
       </button>
 
-      { showExportingData && (
+      {showExportingData && (
         <div className="mt-5">
           <h3>{t('export_management.exporting_collection_list')}</h3>
-          { renderProgressBarsForCollections() }
-          { renderProgressBarForZipping() }
+          {renderProgressBarsForCollections()}
+          {renderProgressBarForZipping()}
         </div>
-      ) }
+      )}
 
       <div className="mt-5">
         <h3 className="mb-3">{t('export_management.exported_data_list')}</h3>

+ 0 - 2
apps/app/src/client/components/Admin/ForbiddenPage.tsx

@@ -1,9 +1,7 @@
 import React, { type JSX } from 'react';
-
 import DefaultErrorPage from 'next/error';
 import { useTranslation } from 'react-i18next';
 
-
 export const ForbiddenPage = (): JSX.Element => {
   const { t } = useTranslation('admin');
 

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

@@ -1,5 +1,4 @@
 import React, { type JSX } from 'react';
-
 import { useTranslation } from 'next-i18next';
 
 import ElasticsearchManagement from './ElasticsearchManagement/ElasticsearchManagement';
@@ -9,7 +8,10 @@ export const FullTextSearchManagement = (): JSX.Element => {
 
   return (
     <div data-testid="admin-full-text-search">
-      <h2 className="mb-4"> { t('full_text_search_management.elasticsearch_management') } </h2>
+      <h2 className="mb-4">
+        {' '}
+        {t('full_text_search_management.elasticsearch_management')}{' '}
+      </h2>
       <ElasticsearchManagement />
     </div>
   );

+ 101 - 47
apps/app/src/client/components/Admin/G2GDataTransfer.tsx

@@ -1,24 +1,27 @@
-import React, {
-  useCallback, useEffect, useState, type JSX,
-} from 'react';
-
+import React, { type JSX, useCallback, useEffect, useState } from 'react';
 import { useTranslation } from 'next-i18next';
 
 import { useGenerateTransferKey } from '~/client/services/g2g-transfer';
 import { apiv3Get, apiv3Post } from '~/client/util/apiv3-client';
 import { toastError, toastSuccess } from '~/client/util/toastr';
 import { useAdminSocket } from '~/features/admin/states/socket-io';
-import { G2G_PROGRESS_STATUS, type G2GProgress } from '~/interfaces/g2g-transfer';
+import {
+  G2G_PROGRESS_STATUS,
+  type G2GProgress,
+} from '~/interfaces/g2g-transfer';
 import { useGrowiDocumentationUrl } from '~/states/context';
 
 import CustomCopyToClipBoard from '../Common/CustomCopyToClipBoard';
-
 // import { FileUploadSettingMolecule } from './App/FileUploadSetting';
 import G2GDataTransferExportForm from './G2GDataTransferExportForm';
 import G2GDataTransferStatusIcon from './G2GDataTransferStatusIcon';
 
 const IGNORED_COLLECTION_NAMES = [
-  'sessions', 'rlflx', 'activities', 'attachmentFiles.files', 'attachmentFiles.chunks',
+  'sessions',
+  'rlflx',
+  'activities',
+  'attachmentFiles.files',
+  'attachmentFiles.chunks',
 ];
 
 const G2GDataTransfer = (): JSX.Element => {
@@ -27,7 +30,9 @@ const G2GDataTransfer = (): JSX.Element => {
 
   const [startTransferKey, setStartTransferKey] = useState('');
   const [collections, setCollections] = useState<string[]>([]);
-  const [selectedCollections, setSelectedCollections] = useState<Set<string>>(new Set());
+  const [selectedCollections, setSelectedCollections] = useState<Set<string>>(
+    new Set(),
+  );
   const [optionsMap, setOptionsMap] = useState<any>({});
   const [isShowExportForm, setShowExportForm] = useState(false);
   const [isTransferring, setTransferring] = useState(false);
@@ -61,13 +66,18 @@ const G2GDataTransfer = (): JSX.Element => {
     setStartTransferKey(e.target.value);
   }, []);
 
-  const setCollectionsAndSelectedCollections = useCallback(async() => {
-    const { data: collectionsData } = await apiv3Get<{collections: any[]}>('/mongo/collections', {});
+  const setCollectionsAndSelectedCollections = useCallback(async () => {
+    const { data: collectionsData } = await apiv3Get<{ collections: any[] }>(
+      '/mongo/collections',
+      {},
+    );
 
     // filter only not ignored collection names
-    const filteredCollections = collectionsData.collections.filter((collectionName) => {
-      return !IGNORED_COLLECTION_NAMES.includes(collectionName);
-    });
+    const filteredCollections = collectionsData.collections.filter(
+      (collectionName) => {
+        return !IGNORED_COLLECTION_NAMES.includes(collectionName);
+      },
+    );
 
     setCollections(filteredCollections);
     setSelectedCollections(new Set(filteredCollections));
@@ -78,7 +88,10 @@ const G2GDataTransfer = (): JSX.Element => {
       socket.on('admin:g2gProgress', (g2gProgress: G2GProgress) => {
         setG2GProgress(g2gProgress);
 
-        if (g2gProgress.mongo === G2G_PROGRESS_STATUS.COMPLETED && g2gProgress.attachments === G2G_PROGRESS_STATUS.COMPLETED) {
+        if (
+          g2gProgress.mongo === G2G_PROGRESS_STATUS.COMPLETED &&
+          g2gProgress.attachments === G2G_PROGRESS_STATUS.COMPLETED
+        ) {
           toastSuccess(t('admin:g2g:transfer_success'));
         }
       });
@@ -99,30 +112,31 @@ const G2GDataTransfer = (): JSX.Element => {
 
   const { transferKey, generateTransferKey } = useGenerateTransferKey();
 
-  const onClickHandler = useCallback(async() => {
+  const onClickHandler = useCallback(async () => {
     try {
       await generateTransferKey();
-    }
-    catch (errs) {
+    } catch (errs) {
       toastError(errs);
     }
   }, [generateTransferKey]);
 
-  const startTransfer = useCallback(async(e) => {
-    e.preventDefault();
-    setTransferring(true);
-
-    try {
-      await apiv3Post('/g2g-transfer/transfer', {
-        transferKey: startTransferKey,
-        collections: Array.from(selectedCollections),
-        optionsMap,
-      });
-    }
-    catch (errs) {
-      toastError(errs);
-    }
-  }, [setTransferring, startTransferKey, selectedCollections, optionsMap]);
+  const startTransfer = useCallback(
+    async (e) => {
+      e.preventDefault();
+      setTransferring(true);
+
+      try {
+        await apiv3Post('/g2g-transfer/transfer', {
+          transferKey: startTransferKey,
+          collections: Array.from(selectedCollections),
+          optionsMap,
+        });
+      } catch (errs) {
+        toastError(errs);
+      }
+    },
+    [setTransferring, startTransferKey, selectedCollections, optionsMap],
+  );
 
   const documentationUrl = useGrowiDocumentationUrl();
 
@@ -173,7 +187,6 @@ const G2GDataTransfer = (): JSX.Element => {
   //   setGcsUploadNamespace(val);
   // }, []);
 
-
   useEffect(() => {
     setCollectionsAndSelectedCollections();
     setupWebsocketEventHandler();
@@ -181,13 +194,24 @@ const G2GDataTransfer = (): JSX.Element => {
     return () => {
       cleanUpWebsocketEventHandler();
     };
-  }, [setCollectionsAndSelectedCollections, setupWebsocketEventHandler, cleanUpWebsocketEventHandler]);
+  }, [
+    setCollectionsAndSelectedCollections,
+    setupWebsocketEventHandler,
+    cleanUpWebsocketEventHandler,
+  ]);
 
   return (
     <div data-testid="admin-export-archive-data">
-      <h2 className="border-bottom">{t('admin:g2g_data_transfer.transfer_data_to_another_growi')}</h2>
-
-      <button type="button" className="btn btn-outline-secondary mt-4" disabled={isTransferring} onClick={() => setShowExportForm(!isShowExportForm)}>
+      <h2 className="border-bottom">
+        {t('admin:g2g_data_transfer.transfer_data_to_another_growi')}
+      </h2>
+
+      <button
+        type="button"
+        className="btn btn-outline-secondary mt-4"
+        disabled={isTransferring}
+        onClick={() => setShowExportForm(!isShowExportForm)}
+      >
         {t('admin:g2g_data_transfer.advanced_options')}
       </button>
 
@@ -243,7 +267,9 @@ const G2GDataTransfer = (): JSX.Element => {
             />
           </div>
           <div className="col-3">
-            <button type="submit" className="btn btn-primary w-100">{t('admin:g2g_data_transfer.start_transfer')}</button>
+            <button type="submit" className="btn btn-primary w-100">
+              {t('admin:g2g_data_transfer.start_transfer')}
+            </button>
           </div>
         </div>
       </form>
@@ -251,38 +277,66 @@ const G2GDataTransfer = (): JSX.Element => {
       {isTransferring && (
         <div className="border rounded p-4">
           <div className="my-2">
-            <G2GDataTransferStatusIcon className="me-2" status={g2gProgress.mongo} /> MongoDB
+            <G2GDataTransferStatusIcon
+              className="me-2"
+              status={g2gProgress.mongo}
+            />{' '}
+            MongoDB
           </div>
           <div className="my-2">
-            <G2GDataTransferStatusIcon className="me-2" status={g2gProgress.attachments} /> Attachments
+            <G2GDataTransferStatusIcon
+              className="me-2"
+              status={g2gProgress.attachments}
+            />{' '}
+            Attachments
           </div>
         </div>
       )}
 
-      <h2 className="border-bottom mt-5">{t('commons:g2g_data_transfer.transfer_data_to_this_growi')}</h2>
+      <h2 className="border-bottom mt-5">
+        {t('commons:g2g_data_transfer.transfer_data_to_this_growi')}
+      </h2>
 
       <div className="row mt-4">
         <div className="col-md-3">
-          <button type="button" className="btn btn-primary w-100" onClick={onClickHandler}>
+          <button
+            type="button"
+            className="btn btn-primary w-100"
+            onClick={onClickHandler}
+          >
             {t('commons:g2g_data_transfer.publish_transfer_key')}
           </button>
         </div>
         <div className="col-md-9">
           <div className=" mx-1">
-            <input className="form-control" type="text" value={transferKey} readOnly />
-            <CustomCopyToClipBoard textToBeCopied={transferKey} message="admin:slack_integration.copied_to_clipboard" />
+            <input
+              className="form-control"
+              type="text"
+              value={transferKey}
+              readOnly
+            />
+            <CustomCopyToClipBoard
+              textToBeCopied={transferKey}
+              message="admin:slack_integration.copied_to_clipboard"
+            />
           </div>
         </div>
       </div>
 
       <div className="alert alert-warning mt-4">
-        <p className="mb-1">{t('commons:g2g_data_transfer.transfer_key_limit')}</p>
-        <p className="mb-1">{t('commons:g2g_data_transfer.once_transfer_key_used')}</p>
+        <p className="mb-1">
+          {t('commons:g2g_data_transfer.transfer_key_limit')}
+        </p>
+        <p className="mb-1">
+          {t('commons:g2g_data_transfer.once_transfer_key_used')}
+        </p>
         <p
           className="mb-0"
           // eslint-disable-next-line react/no-danger
           dangerouslySetInnerHTML={{
-            __html: t('commons:g2g_data_transfer.transfer_to_growi_cloud', { documentationUrl }),
+            __html: t('commons:g2g_data_transfer.transfer_to_growi_cloud', {
+              documentationUrl,
+            }),
           }}
         />
       </div>

+ 115 - 49
apps/app/src/client/components/Admin/G2GDataTransferExportForm.tsx

@@ -1,7 +1,10 @@
 import React, {
-  useState, useEffect, useCallback, useMemo, type JSX,
+  type JSX,
+  useCallback,
+  useEffect,
+  useMemo,
+  useState,
 } from 'react';
-
 import { useTranslation } from 'next-i18next';
 
 import { GrowiArchiveImportOption } from '~/models/admin/growi-archive-import-option';
@@ -9,41 +12,52 @@ import { ImportOptionForPages } from '~/models/admin/import-option-for-pages';
 import { ImportOptionForRevisions } from '~/models/admin/import-option-for-revisions';
 
 import ImportCollectionConfigurationModal from './ImportData/GrowiArchive/ImportCollectionConfigurationModal';
-import ImportCollectionItem, { DEFAULT_MODE, MODE_RESTRICTED_COLLECTION } from './ImportData/GrowiArchive/ImportCollectionItem';
+import ImportCollectionItem, {
+  DEFAULT_MODE,
+  MODE_RESTRICTED_COLLECTION,
+} from './ImportData/GrowiArchive/ImportCollectionItem';
 
-const GROUPS_PAGE = [
-  'pages', 'revisions', 'tags', 'pagetagrelations',
-];
+const GROUPS_PAGE = ['pages', 'revisions', 'tags', 'pagetagrelations'];
 const GROUPS_USER = [
-  'users', 'externalaccounts', 'usergroups', 'usergrouprelations',
-];
-const GROUPS_CONFIG = [
-  'configs', 'updateposts', 'globalnotificationsettings',
+  'users',
+  'externalaccounts',
+  'usergroups',
+  'usergrouprelations',
 ];
-const ALL_GROUPED_COLLECTIONS = GROUPS_PAGE.concat(GROUPS_USER).concat(GROUPS_CONFIG);
-
-const IMPORT_OPTION_CLASS_MAPPING: Record<string, typeof GrowiArchiveImportOption> = {
+const GROUPS_CONFIG = ['configs', 'updateposts', 'globalnotificationsettings'];
+const ALL_GROUPED_COLLECTIONS =
+  GROUPS_PAGE.concat(GROUPS_USER).concat(GROUPS_CONFIG);
+
+const IMPORT_OPTION_CLASS_MAPPING: Record<
+  string,
+  typeof GrowiArchiveImportOption
+> = {
   pages: ImportOptionForPages,
   revisions: ImportOptionForRevisions,
 };
 
 type Props = {
-  allCollectionNames: string[],
-  selectedCollections: Set<string>,
-  updateSelectedCollections: (newSelectedCollections: Set<string>) => void,
-  optionsMap: any,
-  updateOptionsMap: (newOptionsMap: any) => void,
+  allCollectionNames: string[];
+  selectedCollections: Set<string>;
+  updateSelectedCollections: (newSelectedCollections: Set<string>) => void;
+  optionsMap: any;
+  updateOptionsMap: (newOptionsMap: any) => void;
 };
 
 const G2GDataTransferExportForm = (props: Props): JSX.Element => {
   const { t } = useTranslation('admin');
 
   const {
-    allCollectionNames, selectedCollections, updateSelectedCollections, optionsMap, updateOptionsMap,
+    allCollectionNames,
+    selectedCollections,
+    updateSelectedCollections,
+    optionsMap,
+    updateOptionsMap,
   } = props;
 
   const [isConfigurationModalOpen, setConfigurationModalOpen] = useState(false);
-  const [collectionNameForConfiguration, setCollectionNameForConfiguration] = useState<any>();
+  const [collectionNameForConfiguration, setCollectionNameForConfiguration] =
+    useState<any>();
 
   const checkAll = useCallback(() => {
     updateSelectedCollections(new Set(allCollectionNames));
@@ -53,26 +67,28 @@ const G2GDataTransferExportForm = (props: Props): JSX.Element => {
     updateSelectedCollections(new Set());
   }, [updateSelectedCollections]);
 
-  const updateOption = useCallback((collectionName, data) => {
-    const options = optionsMap[collectionName];
+  const updateOption = useCallback(
+    (collectionName, data) => {
+      const options = optionsMap[collectionName];
 
-    // merge
-    Object.assign(options, data);
+      // merge
+      Object.assign(options, data);
 
-    const updatedOptionsMap = {};
-    updatedOptionsMap[collectionName] = options;
-    updateOptionsMap((prev) => {
-      return { ...prev, updatedOptionsMap };
-    });
-  }, [optionsMap, updateOptionsMap]);
+      const updatedOptionsMap = {};
+      updatedOptionsMap[collectionName] = options;
+      updateOptionsMap((prev) => {
+        return { ...prev, updatedOptionsMap };
+      });
+    },
+    [optionsMap, updateOptionsMap],
+  );
 
   const ImportItems = ({ collectionNames }): JSX.Element => {
     const toggleCheckbox = (collectionName, bool) => {
       const collections = new Set(selectedCollections);
       if (bool) {
         collections.add(collectionName);
-      }
-      else {
+      } else {
         collections.delete(collectionName);
       }
 
@@ -90,7 +106,9 @@ const G2GDataTransferExportForm = (props: Props): JSX.Element => {
     return (
       <div className="row">
         {collectionNames.map((collectionName) => {
-          const isConfigButtonAvailable = Object.keys(IMPORT_OPTION_CLASS_MAPPING).includes(collectionName);
+          const isConfigButtonAvailable = Object.keys(
+            IMPORT_OPTION_CLASS_MAPPING,
+          ).includes(collectionName);
 
           if (optionsMap[collectionName] == null) {
             return null;
@@ -162,7 +180,13 @@ const G2GDataTransferExportForm = (props: Props): JSX.Element => {
     });
 
     // TODO: エラー対応
-    return <GroupImportItems groupList={collectionNames} groupName="Other" errors={[]} />;
+    return (
+      <GroupImportItems
+        groupList={collectionNames}
+        groupName="Other"
+        errors={[]}
+      />
+    );
   };
 
   const configurationModal = useMemo(() => {
@@ -179,16 +203,26 @@ const G2GDataTransferExportForm = (props: Props): JSX.Element => {
         option={optionsMap[collectionNameForConfiguration]}
       />
     );
-  }, [collectionNameForConfiguration, isConfigurationModalOpen, optionsMap, updateOption]);
+  }, [
+    collectionNameForConfiguration,
+    isConfigurationModalOpen,
+    optionsMap,
+    updateOption,
+  ]);
 
   const setInitialOptionsMap = useCallback(() => {
     const initialOptionsMap = {};
     allCollectionNames.forEach((collectionName) => {
-      const initialMode = (MODE_RESTRICTED_COLLECTION[collectionName] != null)
-        ? MODE_RESTRICTED_COLLECTION[collectionName][0]
-        : DEFAULT_MODE;
-      const ImportOption = IMPORT_OPTION_CLASS_MAPPING[collectionName] || GrowiArchiveImportOption;
-      initialOptionsMap[collectionName] = new ImportOption(collectionName, initialMode);
+      const initialMode =
+        MODE_RESTRICTED_COLLECTION[collectionName] != null
+          ? MODE_RESTRICTED_COLLECTION[collectionName][0]
+          : DEFAULT_MODE;
+      const ImportOption =
+        IMPORT_OPTION_CLASS_MAPPING[collectionName] || GrowiArchiveImportOption;
+      initialOptionsMap[collectionName] = new ImportOption(
+        collectionName,
+        initialMode,
+      );
     });
     updateOptionsMap(initialOptionsMap);
   }, [allCollectionNames, updateOptionsMap]);
@@ -201,24 +235,52 @@ const G2GDataTransferExportForm = (props: Props): JSX.Element => {
     <>
       <form className="mt-3 row row-cols-lg-auto g-3 align-items-center">
         <div className="col-12">
-          <button type="button" className="btn btn-sm btn-outline-secondary me-2" onClick={checkAll}>
-            <span className="material-symbols-outlined">check_box</span>, {t('admin:export_management.check_all')}
+          <button
+            type="button"
+            className="btn btn-sm btn-outline-secondary me-2"
+            onClick={checkAll}
+          >
+            <span className="material-symbols-outlined">check_box</span>,{' '}
+            {t('admin:export_management.check_all')}
           </button>
         </div>
         <div className="col-12">
-          <button type="button" className="btn btn-sm btn-outline-secondary me-2" onClick={uncheckAll}>
-            <span className="material-symbols-outlined">check_box_outline_blank</span> {t('admin:export_management.uncheck_all')}
+          <button
+            type="button"
+            className="btn btn-sm btn-outline-secondary me-2"
+            onClick={uncheckAll}
+          >
+            <span className="material-symbols-outlined">
+              check_box_outline_blank
+            </span>{' '}
+            {t('admin:export_management.uncheck_all')}
           </button>
         </div>
       </form>
 
       <div className="card custom-card small my-4">
         <ul>
-          <li>{t('admin:importer_management.growi_settings.description_of_import_mode.about')}</li>
+          <li>
+            {t(
+              'admin:importer_management.growi_settings.description_of_import_mode.about',
+            )}
+          </li>
           <ul>
-            <li>{t('admin:importer_management.growi_settings.description_of_import_mode.insert')}</li>
-            <li>{t('admin:importer_management.growi_settings.description_of_import_mode.upsert')}</li>
-            <li>{t('admin:importer_management.growi_settings.description_of_import_mode.flash_and_insert')}</li>
+            <li>
+              {t(
+                'admin:importer_management.growi_settings.description_of_import_mode.insert',
+              )}
+            </li>
+            <li>
+              {t(
+                'admin:importer_management.growi_settings.description_of_import_mode.upsert',
+              )}
+            </li>
+            <li>
+              {t(
+                'admin:importer_management.growi_settings.description_of_import_mode.flash_and_insert',
+              )}
+            </li>
           </ul>
         </ul>
       </div>
@@ -226,7 +288,11 @@ const G2GDataTransferExportForm = (props: Props): JSX.Element => {
       {/* TODO: エラー追加 */}
       <GroupImportItems groupList={GROUPS_PAGE} groupName="Page" errors={[]} />
       <GroupImportItems groupList={GROUPS_USER} groupName="User" errors={[]} />
-      <GroupImportItems groupList={GROUPS_CONFIG} groupName="Config" errors={[]} />
+      <GroupImportItems
+        groupList={GROUPS_CONFIG}
+        groupName="Config"
+        errors={[]}
+      />
       <OtherImportItems />
 
       {configurationModal}

+ 45 - 10
apps/app/src/client/components/Admin/G2GDataTransferStatusIcon.tsx

@@ -1,46 +1,81 @@
 import React, { type ComponentPropsWithoutRef, type JSX } from 'react';
-
 import { LoadingSpinner } from '@growi/ui/dist/components';
 
-import { G2G_PROGRESS_STATUS, type G2GProgressStatus } from '~/interfaces/g2g-transfer';
-
+import {
+  G2G_PROGRESS_STATUS,
+  type G2GProgressStatus,
+} from '~/interfaces/g2g-transfer';
 
 /**
  * Props for {@link G2GDataTransferStatusIcon}
  */
-interface Props extends ComponentPropsWithoutRef<'span'>{
+interface Props extends ComponentPropsWithoutRef<'span'> {
   status: G2GProgressStatus;
 }
 
 /**
  * Icon for G2G transfer status
  */
-const G2GDataTransferStatusIcon = ({ status, className, ...props }: Props): JSX.Element => {
+const G2GDataTransferStatusIcon = ({
+  status,
+  className,
+  ...props
+}: Props): JSX.Element => {
   if (status === G2G_PROGRESS_STATUS.IN_PROGRESS) {
     return (
-      <LoadingSpinner className={`${className}`} aria-label="in progress" {...props} />
+      <LoadingSpinner
+        className={`${className}`}
+        aria-label="in progress"
+        {...props}
+      />
     );
   }
 
   if (status === G2G_PROGRESS_STATUS.COMPLETED) {
     return (
-      <span className={`material-symbols-outlined text-info ${className}`} aria-label="completed" {...props}>check_circle</span>
+      <span
+        className={`material-symbols-outlined text-info ${className}`}
+        aria-label="completed"
+        {...props}
+      >
+        check_circle
+      </span>
     );
   }
 
   if (status === G2G_PROGRESS_STATUS.ERROR) {
     return (
-      <span className={`material-symbols-outlined text-danger ${className}`} aria-label="error" {...props}>error</span>
+      <span
+        className={`material-symbols-outlined text-danger ${className}`}
+        aria-label="error"
+        {...props}
+      >
+        error
+      </span>
     );
   }
 
   if (status === G2G_PROGRESS_STATUS.SKIPPED) {
     return (
-      <span className={`material-symbols-outlined ${className}`} aria-label="skipped" {...props}>block</span>
+      <span
+        className={`material-symbols-outlined ${className}`}
+        aria-label="skipped"
+        {...props}
+      >
+        block
+      </span>
     );
   }
 
-  return <span className={`material-symbols-outlined ${className}`} aria-label="pending" {...props}>circle</span>;
+  return (
+    <span
+      className={`material-symbols-outlined ${className}`}
+      aria-label="pending"
+      {...props}
+    >
+      circle
+    </span>
+  );
 };
 
 export default G2GDataTransferStatusIcon;

+ 12 - 7
apps/app/src/client/components/Admin/ImportData/GrowiArchive/ErrorViewer.tsx

@@ -1,12 +1,11 @@
 import React, { type JSX } from 'react';
-
-import { Modal, ModalHeader, ModalBody } from 'reactstrap';
+import { Modal, ModalBody, ModalHeader } from 'reactstrap';
 
 type ErrorViewerProps = {
-  isOpen: boolean,
-  errors: any[],
-  onClose: () => void,
-}
+  isOpen: boolean;
+  errors: any[];
+  onClose: () => void;
+};
 
 const ErrorViewer = (props: ErrorViewerProps): JSX.Element => {
   const { errors } = props;
@@ -25,7 +24,13 @@ const ErrorViewer = (props: ErrorViewerProps): JSX.Element => {
         Errors
       </ModalHeader>
       <ModalBody>
-        <textarea className="form-control" rows={8} readOnly wrap="off" defaultValue={value}></textarea>
+        <textarea
+          className="form-control"
+          rows={8}
+          readOnly
+          wrap="off"
+          defaultValue={value}
+        ></textarea>
       </ModalBody>
     </Modal>
   );

+ 100 - 36
apps/app/src/client/components/Admin/ImportData/GrowiArchive/ImportCollectionConfigurationModal.jsx

@@ -1,23 +1,15 @@
 /* eslint-disable react/no-danger */
 
 import React from 'react';
-
 import { useTranslation } from 'next-i18next';
 import PropTypes from 'prop-types';
-import {
-  Modal,
-  ModalHeader,
-  ModalBody,
-  ModalFooter,
-} from 'reactstrap';
+import { Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap';
 
 import { GrowiArchiveImportOption } from '~/models/admin/growi-archive-import-option';
 
 // import { toastSuccess, toastError } from '~/client/util/toastr';
 
-
 class ImportCollectionConfigurationModal extends React.Component {
-
   constructor(props) {
     super(props);
 
@@ -46,9 +38,7 @@ class ImportCollectionConfigurationModal extends React.Component {
   }
 
   updateOption() {
-    const {
-      collectionName, onOptionChange, onClose,
-    } = this.props;
+    const { collectionName, onOptionChange, onClose } = this.props;
 
     if (onOptionChange != null) {
       onOptionChange(collectionName, this.state.option);
@@ -61,7 +51,8 @@ class ImportCollectionConfigurationModal extends React.Component {
     const { t } = this.props;
     const { option } = this.state;
 
-    const translationBase = 'admin:importer_management.growi_settings.configuration.pages';
+    const translationBase =
+      'admin:importer_management.growi_settings.configuration.pages';
 
     /* eslint-disable react/no-unescaped-entities */
     return (
@@ -72,11 +63,21 @@ class ImportCollectionConfigurationModal extends React.Component {
             type="checkbox"
             className="form-check-input"
             checked={option.isOverwriteAuthorWithCurrentUser || false} // add ' || false' to avoid uncontrolled input warning
-            onChange={() => this.changeHandler({ isOverwriteAuthorWithCurrentUser: !option.isOverwriteAuthorWithCurrentUser })}
+            onChange={() =>
+              this.changeHandler({
+                isOverwriteAuthorWithCurrentUser:
+                  !option.isOverwriteAuthorWithCurrentUser,
+              })
+            }
           />
           <label htmlFor="cbOpt4" className="form-label form-check-label">
             {t(`${translationBase}.overwrite_author.label`)}
-            <p className="form-text text-muted mt-0" dangerouslySetInnerHTML={{ __html: t(`${translationBase}.overwrite_author.desc`) }} />
+            <p
+              className="form-text text-muted mt-0"
+              dangerouslySetInnerHTML={{
+                __html: t(`${translationBase}.overwrite_author.desc`),
+              }}
+            />
           </label>
         </div>
         <div className="form-check form-check-warning">
@@ -85,13 +86,23 @@ class ImportCollectionConfigurationModal extends React.Component {
             type="checkbox"
             className="form-check-input"
             checked={option.makePublicForGrant2 || false} // add ' || false' to avoid uncontrolled input warning
-            onChange={() => this.changeHandler({ makePublicForGrant2: !option.makePublicForGrant2 })}
+            onChange={() =>
+              this.changeHandler({
+                makePublicForGrant2: !option.makePublicForGrant2,
+              })
+            }
           />
           <label htmlFor="cbOpt1" className="form-label form-check-label">
-            {t(`${translationBase}.set_public_to_page.label`, { from: t('Anyone with the link') })}
+            {t(`${translationBase}.set_public_to_page.label`, {
+              from: t('Anyone with the link'),
+            })}
             <p
               className="form-text text-muted mt-0"
-              dangerouslySetInnerHTML={{ __html: t(`${translationBase}.set_public_to_page.desc`, { from: t('Anyone with the link') }) }}
+              dangerouslySetInnerHTML={{
+                __html: t(`${translationBase}.set_public_to_page.desc`, {
+                  from: t('Anyone with the link'),
+                }),
+              }}
             />
           </label>
         </div>
@@ -101,13 +112,23 @@ class ImportCollectionConfigurationModal extends React.Component {
             type="checkbox"
             className="form-check-input"
             checked={option.makePublicForGrant4 || false} // add ' || false' to avoid uncontrolled input warning
-            onChange={() => this.changeHandler({ makePublicForGrant4: !option.makePublicForGrant4 })}
+            onChange={() =>
+              this.changeHandler({
+                makePublicForGrant4: !option.makePublicForGrant4,
+              })
+            }
           />
           <label htmlFor="cbOpt2" className="form-label form-check-label">
-            {t(`${translationBase}.set_public_to_page.label`, { from: t('Only me') })}
+            {t(`${translationBase}.set_public_to_page.label`, {
+              from: t('Only me'),
+            })}
             <p
               className="form-text text-muted mt-0"
-              dangerouslySetInnerHTML={{ __html: t(`${translationBase}.set_public_to_page.desc`, { from: t('Only me') }) }}
+              dangerouslySetInnerHTML={{
+                __html: t(`${translationBase}.set_public_to_page.desc`, {
+                  from: t('Only me'),
+                }),
+              }}
             />
           </label>
         </div>
@@ -117,13 +138,23 @@ class ImportCollectionConfigurationModal extends React.Component {
             type="checkbox"
             className="form-check-input"
             checked={option.makePublicForGrant5 || false} // add ' || false' to avoid uncontrolled input warning
-            onChange={() => this.changeHandler({ makePublicForGrant5: !option.makePublicForGrant5 })}
+            onChange={() =>
+              this.changeHandler({
+                makePublicForGrant5: !option.makePublicForGrant5,
+              })
+            }
           />
           <label htmlFor="cbOpt3" className="form-label form-check-label">
-            {t(`${translationBase}.set_public_to_page.label`, { from: t('Only inside the group') })}
+            {t(`${translationBase}.set_public_to_page.label`, {
+              from: t('Only inside the group'),
+            })}
             <p
               className="form-text text-muted mt-0"
-              dangerouslySetInnerHTML={{ __html: t(`${translationBase}.set_public_to_page.desc`, { from: t('Only inside the group') }) }}
+              dangerouslySetInnerHTML={{
+                __html: t(`${translationBase}.set_public_to_page.desc`, {
+                  from: t('Only inside the group'),
+                }),
+              }}
             />
           </label>
         </div>
@@ -133,11 +164,20 @@ class ImportCollectionConfigurationModal extends React.Component {
             type="checkbox"
             className="form-check-input"
             checked={option.initPageMetadatas || false} // add ' || false' to avoid uncontrolled input warning
-            onChange={() => this.changeHandler({ initPageMetadatas: !option.initPageMetadatas })}
+            onChange={() =>
+              this.changeHandler({
+                initPageMetadatas: !option.initPageMetadatas,
+              })
+            }
           />
           <label htmlFor="cbOpt5" className="form-label form-check-label">
             {t(`${translationBase}.initialize_meta_datas.label`)}
-            <p className="form-text text-muted mt-0" dangerouslySetInnerHTML={{ __html: t(`${translationBase}.initialize_meta_datas.desc`) }} />
+            <p
+              className="form-text text-muted mt-0"
+              dangerouslySetInnerHTML={{
+                __html: t(`${translationBase}.initialize_meta_datas.desc`),
+              }}
+            />
           </label>
         </div>
       </>
@@ -149,7 +189,8 @@ class ImportCollectionConfigurationModal extends React.Component {
     const { t } = this.props;
     const { option } = this.state;
 
-    const translationBase = 'admin:importer_management.growi_settings.configuration.revisions';
+    const translationBase =
+      'admin:importer_management.growi_settings.configuration.revisions';
 
     /* eslint-disable react/no-unescaped-entities */
     return (
@@ -160,11 +201,21 @@ class ImportCollectionConfigurationModal extends React.Component {
             type="checkbox"
             className="form-check-input"
             checked={option.isOverwriteAuthorWithCurrentUser || false} // add ' || false' to avoid uncontrolled input warning
-            onChange={() => this.changeHandler({ isOverwriteAuthorWithCurrentUser: !option.isOverwriteAuthorWithCurrentUser })}
+            onChange={() =>
+              this.changeHandler({
+                isOverwriteAuthorWithCurrentUser:
+                  !option.isOverwriteAuthorWithCurrentUser,
+              })
+            }
           />
           <label htmlFor="cbOpt1" className="form-label form-check-label">
             {t(`${translationBase}.overwrite_author.label`)}
-            <p className="form-text text-muted mt-0" dangerouslySetInnerHTML={{ __html: t(`${translationBase}.overwrite_author.desc`) }} />
+            <p
+              className="form-text text-muted mt-0"
+              dangerouslySetInnerHTML={{
+                __html: t(`${translationBase}.overwrite_author.desc`),
+              }}
+            />
           </label>
         </div>
       </>
@@ -189,23 +240,36 @@ class ImportCollectionConfigurationModal extends React.Component {
     }
 
     return (
-      <Modal isOpen={this.props.isOpen} toggle={this.props.onClose} onEnter={this.initialize}>
+      <Modal
+        isOpen={this.props.isOpen}
+        toggle={this.props.onClose}
+        onEnter={this.initialize}
+      >
         <ModalHeader tag="h4" toggle={this.props.onClose} className="text-info">
           {`'${collectionName}'`} Configuration
         </ModalHeader>
 
-        <ModalBody>
-          {contents}
-        </ModalBody>
+        <ModalBody>{contents}</ModalBody>
 
         <ModalFooter>
-          <button type="button" className="btn btn-sm btn-outline-secondary" onClick={this.props.onClose}>{t('Cancel')}</button>
-          <button type="button" className="btn btn-sm btn-primary" onClick={this.updateOption}>{t('Update')}</button>
+          <button
+            type="button"
+            className="btn btn-sm btn-outline-secondary"
+            onClick={this.props.onClose}
+          >
+            {t('Cancel')}
+          </button>
+          <button
+            type="button"
+            className="btn btn-sm btn-primary"
+            onClick={this.updateOption}
+          >
+            {t('Update')}
+          </button>
         </ModalFooter>
       </Modal>
     );
   }
-
 }
 
 ImportCollectionConfigurationModal.propTypes = {

+ 86 - 34
apps/app/src/client/components/Admin/ImportData/GrowiArchive/ImportCollectionItem.jsx

@@ -1,17 +1,23 @@
 import React from 'react';
-
 import PropTypes from 'prop-types';
 import {
-  Progress, UncontrolledDropdown, DropdownToggle, DropdownMenu, DropdownItem,
+  DropdownItem,
+  DropdownMenu,
+  DropdownToggle,
+  Progress,
+  UncontrolledDropdown,
 } from 'reactstrap';
 
 import { GrowiArchiveImportOption } from '~/models/admin/growi-archive-import-option';
 
-
 const MODE_ATTR_MAP = {
   insert: { color: 'info', icon: 'add_circle', label: 'Insert' },
   upsert: { color: 'success', icon: 'add_circle', label: 'Upsert' },
-  flushAndInsert: { color: 'danger', icon: 'autorenew', label: 'Flush and Insert' },
+  flushAndInsert: {
+    color: 'danger',
+    icon: 'autorenew',
+    label: 'Flush and Insert',
+  },
 };
 
 export const DEFAULT_MODE = 'insert';
@@ -23,13 +29,13 @@ export const MODE_RESTRICTED_COLLECTION = {
 };
 
 export default class ImportCollectionItem extends React.Component {
-
   constructor(props) {
     super(props);
 
     this.changeHandler = this.changeHandler.bind(this);
     this.modeSelectedHandler = this.modeSelectedHandler.bind(this);
-    this.configButtonClickedHandler = this.configButtonClickedHandler.bind(this);
+    this.configButtonClickedHandler =
+      this.configButtonClickedHandler.bind(this);
     this.errorLinkClickedHandler = this.errorLinkClickedHandler.bind(this);
   }
 
@@ -76,13 +82,16 @@ export default class ImportCollectionItem extends React.Component {
   renderModeLabel(mode, isColorized = false) {
     const attrMap = MODE_ATTR_MAP[mode];
     const className = isColorized ? `text-${attrMap.color}` : '';
-    return <span className={`text-nowrap ${className}`}><span className="material-symbols-outlined">{attrMap.icon}</span> {attrMap.label}</span>;
+    return (
+      <span className={`text-nowrap ${className}`}>
+        <span className="material-symbols-outlined">{attrMap.icon}</span>{' '}
+        {attrMap.label}
+      </span>
+    );
   }
 
   renderCheckbox() {
-    const {
-      collectionName, isSelected, isImporting,
-    } = this.props;
+    const { collectionName, isSelected, isImporting } = this.props;
 
     return (
       <div className="form-check form-check-info my-0">
@@ -96,7 +105,10 @@ export default class ImportCollectionItem extends React.Component {
           disabled={isImporting}
           onChange={this.changeHandler}
         />
-        <label className="form-label text-capitalize form-check-label" htmlFor={collectionName}>
+        <label
+          className="form-label text-capitalize form-check-label"
+          htmlFor={collectionName}
+        >
           {collectionName}
         </label>
       </div>
@@ -104,22 +116,26 @@ export default class ImportCollectionItem extends React.Component {
   }
 
   renderModeSelector() {
-    const {
-      collectionName, option, isImporting,
-    } = this.props;
+    const { collectionName, option, isImporting } = this.props;
     const currentMode = option?.mode || 'insert';
     const attrMap = MODE_ATTR_MAP[currentMode];
-    const modes = MODE_RESTRICTED_COLLECTION[collectionName] || Object.keys(MODE_ATTR_MAP);
+    const modes =
+      MODE_RESTRICTED_COLLECTION[collectionName] || Object.keys(MODE_ATTR_MAP);
 
     return (
       <span className="d-inline-flex align-items-center">
         Mode:&nbsp;
         <UncontrolledDropdown size="sm" className="d-inline-block">
-          <DropdownToggle color={attrMap.color} caret disabled={isImporting} id={`ddmMode-${collectionName}`}>
+          <DropdownToggle
+            color={attrMap.color}
+            caret
+            disabled={isImporting}
+            id={`ddmMode-${collectionName}`}
+          >
             {this.renderModeLabel(currentMode)}
           </DropdownToggle>
           <DropdownMenu>
-            {modes.map(mode => (
+            {modes.map((mode) => (
               <DropdownItem
                 key={`buttonMode_${mode}`}
                 onClick={() => this.modeSelectedHandler(mode)}
@@ -141,7 +157,9 @@ export default class ImportCollectionItem extends React.Component {
         type="button"
         className="btn btn-outline-secondary btn-sm p-1 ms-2"
         disabled={isImporting || !isConfigButtonAvailable}
-        onClick={isConfigButtonAvailable ? this.configButtonClickedHandler : null}
+        onClick={
+          isConfigButtonAvailable ? this.configButtonClickedHandler : null
+        }
       >
         <span className="material-symbols-outlined">settings</span>
       </button>
@@ -149,17 +167,37 @@ export default class ImportCollectionItem extends React.Component {
   }
 
   renderProgressBar() {
-    const {
-      isImporting, insertedCount, modifiedCount, errorsCount,
-    } = this.props;
+    const { isImporting, insertedCount, modifiedCount, errorsCount } =
+      this.props;
 
     const total = insertedCount + modifiedCount + errorsCount;
 
     return (
       <Progress multi className="mb-0">
-        <Progress bar max={total} color="info" striped={isImporting} animated={isImporting} value={insertedCount} />
-        <Progress bar max={total} color="success" striped={isImporting} animated={isImporting} value={modifiedCount} />
-        <Progress bar max={total} color="danger" striped={isImporting} animated={isImporting} value={errorsCount} />
+        <Progress
+          bar
+          max={total}
+          color="info"
+          striped={isImporting}
+          animated={isImporting}
+          value={insertedCount}
+        />
+        <Progress
+          bar
+          max={total}
+          color="success"
+          striped={isImporting}
+          animated={isImporting}
+          value={modifiedCount}
+        />
+        <Progress
+          bar
+          max={total}
+          color="danger"
+          striped={isImporting}
+          animated={isImporting}
+          value={errorsCount}
+        />
       </Progress>
     );
   }
@@ -174,20 +212,35 @@ export default class ImportCollectionItem extends React.Component {
     const { insertedCount, modifiedCount, errorsCount } = this.props;
     return (
       <div className="w-100 text-center">
-        <span className="text-info"><strong>{insertedCount}</strong> Inserted</span>,&nbsp;
-        <span className="text-success"><strong>{modifiedCount}</strong> Modified</span>,&nbsp;
-        { errorsCount > 0
-          ? <a className="text-danger" role="button" onClick={this.errorLinkClickedHandler}><u><strong>{errorsCount}</strong> Failed</u></a>
-          : <span className="text-muted"><strong>0</strong> Failed</span>
-        }
+        <span className="text-info">
+          <strong>{insertedCount}</strong> Inserted
+        </span>
+        ,&nbsp;
+        <span className="text-success">
+          <strong>{modifiedCount}</strong> Modified
+        </span>
+        ,&nbsp;
+        {errorsCount > 0 ? (
+          <a
+            className="text-danger"
+            role="button"
+            onClick={this.errorLinkClickedHandler}
+          >
+            <u>
+              <strong>{errorsCount}</strong> Failed
+            </u>
+          </a>
+        ) : (
+          <span className="text-muted">
+            <strong>0</strong> Failed
+          </span>
+        )}
       </div>
     );
   }
 
   render() {
-    const {
-      isSelected, isHideProgress,
-    } = this.props;
+    const { isSelected, isHideProgress } = this.props;
 
     return (
       <div className="card border-light">
@@ -211,7 +264,6 @@ export default class ImportCollectionItem extends React.Component {
       </div>
     );
   }
-
 }
 
 ImportCollectionItem.propTypes = {

+ 161 - 73
apps/app/src/client/components/Admin/ImportData/GrowiArchive/ImportForm.jsx

@@ -1,31 +1,31 @@
 import React from 'react';
-
 import { useTranslation } from 'next-i18next';
 import PropTypes from 'prop-types';
 
 import { apiv3Post } from '~/client/util/apiv3-client';
-import { toastSuccess, toastError } from '~/client/util/toastr';
+import { toastError, toastSuccess } from '~/client/util/toastr';
 import { useAdminSocket } from '~/features/admin/states/socket-io';
 import { GrowiArchiveImportOption } from '~/models/admin/growi-archive-import-option';
 import { ImportOptionForPages } from '~/models/admin/import-option-for-pages';
 import { ImportOptionForRevisions } from '~/models/admin/import-option-for-revisions';
 
-
 import ErrorViewer from './ErrorViewer';
 import ImportCollectionConfigurationModal from './ImportCollectionConfigurationModal';
-import ImportCollectionItem, { DEFAULT_MODE, MODE_RESTRICTED_COLLECTION } from './ImportCollectionItem';
+import ImportCollectionItem, {
+  DEFAULT_MODE,
+  MODE_RESTRICTED_COLLECTION,
+} from './ImportCollectionItem';
 
-
-const GROUPS_PAGE = [
-  'pages', 'revisions', 'tags', 'pagetagrelations',
-];
+const GROUPS_PAGE = ['pages', 'revisions', 'tags', 'pagetagrelations'];
 const GROUPS_USER = [
-  'users', 'externalaccounts', 'usergroups', 'usergrouprelations',
+  'users',
+  'externalaccounts',
+  'usergroups',
+  'usergrouprelations',
 ];
-const GROUPS_CONFIG = [
-  'configs', 'updateposts', 'globalnotificationsettings',
-];
-const ALL_GROUPED_COLLECTIONS = GROUPS_PAGE.concat(GROUPS_USER).concat(GROUPS_CONFIG);
+const GROUPS_CONFIG = ['configs', 'updateposts', 'globalnotificationsettings'];
+const ALL_GROUPED_COLLECTIONS =
+  GROUPS_PAGE.concat(GROUPS_USER).concat(GROUPS_CONFIG);
 
 /** @type Record<string, typeof GrowiArchiveImportOption> */
 const IMPORT_OPTION_CLASS_MAPPING = {
@@ -34,7 +34,6 @@ const IMPORT_OPTION_CLASS_MAPPING = {
 };
 
 class ImportForm extends React.Component {
-
   constructor(props) {
     super(props);
 
@@ -69,12 +68,17 @@ class ImportForm extends React.Component {
       this.initialState.collectionNameToFileNameMap[collectionName] = fileName;
 
       // determine initial mode
-      const initialMode = (MODE_RESTRICTED_COLLECTION[collectionName] != null)
-        ? MODE_RESTRICTED_COLLECTION[collectionName][0]
-        : DEFAULT_MODE;
+      const initialMode =
+        MODE_RESTRICTED_COLLECTION[collectionName] != null
+          ? MODE_RESTRICTED_COLLECTION[collectionName][0]
+          : DEFAULT_MODE;
       // create GrowiArchiveImportOption instance
-      const ImportOption = IMPORT_OPTION_CLASS_MAPPING[collectionName] || GrowiArchiveImportOption;
-      this.initialState.optionsMap[collectionName] = new ImportOption(collectionName, initialMode);
+      const ImportOption =
+        IMPORT_OPTION_CLASS_MAPPING[collectionName] || GrowiArchiveImportOption;
+      this.initialState.optionsMap[collectionName] = new ImportOption(
+        collectionName,
+        initialMode,
+      );
     });
 
     this.state = this.initialState;
@@ -106,21 +110,24 @@ class ImportForm extends React.Component {
 
     // websocket event
     // eslint-disable-next-line object-curly-newline
-    socket.on('admin:onProgressForImport', ({ collectionName, collectionProgress, appendedErrors }) => {
-      const { progressMap, errorsMap } = this.state;
-      progressMap[collectionName] = collectionProgress;
-
-      if (appendedErrors != null) {
-        const errors = errorsMap[collectionName] || [];
-        errorsMap[collectionName] = errors.concat(appendedErrors);
-      }
-
-      this.setState({
-        isImporting: true,
-        progressMap,
-        errorsMap,
-      });
-    });
+    socket.on(
+      'admin:onProgressForImport',
+      ({ collectionName, collectionProgress, appendedErrors }) => {
+        const { progressMap, errorsMap } = this.state;
+        progressMap[collectionName] = collectionProgress;
+
+        if (appendedErrors != null) {
+          const errors = errorsMap[collectionName] || [];
+          errorsMap[collectionName] = errors.concat(appendedErrors);
+        }
+
+        this.setState({
+          isImporting: true,
+          progressMap,
+          errorsMap,
+        });
+      },
+    );
 
     // websocket event
     socket.on('admin:onTerminateForImport', () => {
@@ -154,8 +161,7 @@ class ImportForm extends React.Component {
     const selectedCollections = new Set(this.state.selectedCollections);
     if (bool) {
       selectedCollections.add(collectionName);
-    }
-    else {
+    } else {
       selectedCollections.delete(collectionName);
     }
 
@@ -165,7 +171,9 @@ class ImportForm extends React.Component {
   }
 
   async checkAll() {
-    await this.setState({ selectedCollections: new Set(this.allCollectionNames) });
+    await this.setState({
+      selectedCollections: new Set(this.allCollectionNames),
+    });
     this.validate();
   }
 
@@ -186,11 +194,17 @@ class ImportForm extends React.Component {
   }
 
   openConfigurationModal(collectionName) {
-    this.setState({ isConfigurationModalOpen: true, collectionNameForConfiguration: collectionName });
+    this.setState({
+      isConfigurationModalOpen: true,
+      collectionNameForConfiguration: collectionName,
+    });
   }
 
   showErrorsViewer(collectionName) {
-    this.setState({ isErrorsViewerOpen: true, collectionNameForErrorsViewer: collectionName });
+    this.setState({
+      isErrorsViewerOpen: true,
+      collectionNameForErrorsViewer: collectionName,
+    });
   }
 
   async validate() {
@@ -224,7 +238,9 @@ class ImportForm extends React.Component {
     const { warnForOtherGroups, selectedCollections } = this.state;
 
     if (selectedCollections.size === 0) {
-      warnForOtherGroups.push(t('admin:importer_management.growi_settings.errors.at_least_one'));
+      warnForOtherGroups.push(
+        t('admin:importer_management.growi_settings.errors.at_least_one'),
+      );
     }
 
     this.setState({ warnForOtherGroups });
@@ -234,13 +250,20 @@ class ImportForm extends React.Component {
     const { t } = this.props;
     const { warnForPageGroups, selectedCollections } = this.state;
 
-    const pageRelatedCollectionsLength = ['pages', 'revisions'].filter((collectionName) => {
-      return selectedCollections.has(collectionName);
-    }).length;
+    const pageRelatedCollectionsLength = ['pages', 'revisions'].filter(
+      (collectionName) => {
+        return selectedCollections.has(collectionName);
+      },
+    ).length;
 
     // MUST be included both or neither when importing
-    if (pageRelatedCollectionsLength !== 0 && pageRelatedCollectionsLength !== 2) {
-      warnForPageGroups.push(t('admin:importer_management.growi_settings.errors.page_and_revision'));
+    if (
+      pageRelatedCollectionsLength !== 0 &&
+      pageRelatedCollectionsLength !== 2
+    ) {
+      warnForPageGroups.push(
+        t('admin:importer_management.growi_settings.errors.page_and_revision'),
+      );
     }
 
     this.setState({ warnForPageGroups });
@@ -253,7 +276,12 @@ class ImportForm extends React.Component {
     // MUST include also 'users' if 'externalaccounts' is selected
     if (selectedCollections.has('externalaccounts')) {
       if (!selectedCollections.has('users')) {
-        warnForUserGroups.push(t('admin:importer_management.growi_settings.errors.depends', { target: 'Users', condition: 'Externalaccounts' }));
+        warnForUserGroups.push(
+          t('admin:importer_management.growi_settings.errors.depends', {
+            target: 'Users',
+            condition: 'Externalaccounts',
+          }),
+        );
       }
     }
 
@@ -267,7 +295,12 @@ class ImportForm extends React.Component {
     // MUST include also 'users' if 'usergroups' is selected
     if (selectedCollections.has('usergroups')) {
       if (!selectedCollections.has('users')) {
-        warnForUserGroups.push(t('admin:importer_management.growi_settings.errors.depends', { target: 'Users', condition: 'Usergroups' }));
+        warnForUserGroups.push(
+          t('admin:importer_management.growi_settings.errors.depends', {
+            target: 'Users',
+            condition: 'Usergroups',
+          }),
+        );
       }
     }
 
@@ -281,7 +314,12 @@ class ImportForm extends React.Component {
     // MUST include also 'usergroups' if 'usergrouprelations' is selected
     if (selectedCollections.has('usergrouprelations')) {
       if (!selectedCollections.has('usergroups')) {
-        warnForUserGroups.push(t('admin:importer_management.growi_settings.errors.depends', { target: 'Usergroups', condition: 'Usergrouprelations' }));
+        warnForUserGroups.push(
+          t('admin:importer_management.growi_settings.errors.depends', {
+            target: 'Usergroups',
+            condition: 'Usergrouprelations',
+          }),
+        );
       }
     }
 
@@ -289,9 +327,7 @@ class ImportForm extends React.Component {
   }
 
   async import() {
-    const {
-      fileName, onPostImport, t,
-    } = this.props;
+    const { fileName, onPostImport, t } = this.props;
     const { selectedCollections, optionsMap } = this.state;
 
     // init progress data
@@ -314,8 +350,7 @@ class ImportForm extends React.Component {
       }
 
       toastSuccess(undefined, 'Import process has requested.');
-    }
-    catch (err) {
+    } catch (err) {
       if (err.code === 'only_upsert_available') {
         toastError(t('admin:importer_management.error.only_upsert_available'));
       }
@@ -363,7 +398,11 @@ class ImportForm extends React.Component {
       return !ALL_GROUPED_COLLECTIONS.includes(collectionName);
     });
 
-    return this.renderGroups(collectionNames, 'Other', this.state.warnForOtherGroups);
+    return this.renderGroups(
+      collectionNames,
+      'Other',
+      this.state.warnForOtherGroups,
+    );
   }
 
   renderImportItems(collectionNames) {
@@ -382,15 +421,21 @@ class ImportForm extends React.Component {
         {collectionNames.map((collectionName) => {
           const collectionProgress = progressMap[collectionName];
           const errorsCount = errorsMap[collectionName]?.length ?? 0;
-          const isConfigButtonAvailable = Object.keys(IMPORT_OPTION_CLASS_MAPPING).includes(collectionName);
+          const isConfigButtonAvailable = Object.keys(
+            IMPORT_OPTION_CLASS_MAPPING,
+          ).includes(collectionName);
 
           return (
             <div className="col-md-6 my-1" key={collectionName}>
               <ImportCollectionItem
                 isImporting={isImporting}
                 isImported={collectionProgress ? isImported : false}
-                insertedCount={collectionProgress ? collectionProgress.insertedCount : 0}
-                modifiedCount={collectionProgress ? collectionProgress.modifiedCount : 0}
+                insertedCount={
+                  collectionProgress ? collectionProgress.insertedCount : 0
+                }
+                modifiedCount={
+                  collectionProgress ? collectionProgress.modifiedCount : 0
+                }
                 errorsCount={errorsCount}
                 collectionName={collectionName}
                 isSelected={selectedCollections.has(collectionName)}
@@ -410,7 +455,11 @@ class ImportForm extends React.Component {
   }
 
   renderConfigurationModal() {
-    const { isConfigurationModalOpen, collectionNameForConfiguration: collectionName, optionsMap } = this.state;
+    const {
+      isConfigurationModalOpen,
+      collectionNameForConfiguration: collectionName,
+      optionsMap,
+    } = this.state;
 
     if (collectionName == null) {
       return null;
@@ -428,7 +477,8 @@ class ImportForm extends React.Component {
   }
 
   renderErrorsViewer() {
-    const { isErrorsViewerOpen, errorsMap, collectionNameForErrorsViewer } = this.state;
+    const { isErrorsViewerOpen, errorsMap, collectionNameForErrorsViewer } =
+      this.state;
     const errors = errorsMap[collectionNameForErrorsViewer];
 
     return (
@@ -443,32 +493,63 @@ class ImportForm extends React.Component {
   render() {
     const { t } = this.props;
     const {
-      canImport, isImporting,
-      warnForPageGroups, warnForUserGroups, warnForConfigGroups,
+      canImport,
+      isImporting,
+      warnForPageGroups,
+      warnForUserGroups,
+      warnForConfigGroups,
     } = this.state;
 
     return (
       <>
         <form className="row row-cols-lg-auto g-3 align-items-center">
           <div className="col-12">
-            <button type="button" className="btn btn-sm btn-outline-secondary me-2" onClick={this.checkAll}>
-              <span className="material-symbols-outlined">check_box</span> {t('admin:export_management.check_all')}
+            <button
+              type="button"
+              className="btn btn-sm btn-outline-secondary me-2"
+              onClick={this.checkAll}
+            >
+              <span className="material-symbols-outlined">check_box</span>{' '}
+              {t('admin:export_management.check_all')}
             </button>
           </div>
           <div className="col-12">
-            <button type="button" className="btn btn-sm btn-outline-secondary me-2" onClick={this.uncheckAll}>
-              <span className="material-symbols-outlined">check_box_outline_blank</span> {t('admin:export_management.uncheck_all')}
+            <button
+              type="button"
+              className="btn btn-sm btn-outline-secondary me-2"
+              onClick={this.uncheckAll}
+            >
+              <span className="material-symbols-outlined">
+                check_box_outline_blank
+              </span>{' '}
+              {t('admin:export_management.uncheck_all')}
             </button>
           </div>
         </form>
 
         <div className="card custom-card small my-4">
           <ul>
-            <li>{t('admin:importer_management.growi_settings.description_of_import_mode.about')}</li>
+            <li>
+              {t(
+                'admin:importer_management.growi_settings.description_of_import_mode.about',
+              )}
+            </li>
             <ul>
-              <li>{t('admin:importer_management.growi_settings.description_of_import_mode.insert')}</li>
-              <li>{t('admin:importer_management.growi_settings.description_of_import_mode.upsert')}</li>
-              <li>{t('admin:importer_management.growi_settings.description_of_import_mode.flash_and_insert')}</li>
+              <li>
+                {t(
+                  'admin:importer_management.growi_settings.description_of_import_mode.insert',
+                )}
+              </li>
+              <li>
+                {t(
+                  'admin:importer_management.growi_settings.description_of_import_mode.upsert',
+                )}
+              </li>
+              <li>
+                {t(
+                  'admin:importer_management.growi_settings.description_of_import_mode.flash_and_insert',
+                )}
+              </li>
             </ul>
           </ul>
         </div>
@@ -479,10 +560,19 @@ class ImportForm extends React.Component {
         {this.renderOthers()}
 
         <div className="mt-4 text-center">
-          <button type="button" className="btn btn-outline-secondary mx-1" onClick={this.props.onDiscard}>
+          <button
+            type="button"
+            className="btn btn-outline-secondary mx-1"
+            onClick={this.props.onDiscard}
+          >
             {t('admin:importer_management.growi_settings.discard')}
           </button>
-          <button type="button" className="btn btn-primary mx-1" onClick={this.import} disabled={!canImport || isImporting}>
+          <button
+            type="button"
+            className="btn btn-primary mx-1"
+            onClick={this.import}
+            disabled={!canImport || isImporting}
+          >
             {t('admin:importer_management.import')}
           </button>
         </div>
@@ -492,7 +582,6 @@ class ImportForm extends React.Component {
       </>
     );
   }
-
 }
 
 ImportForm.propTypes = {
@@ -516,5 +605,4 @@ const ImportFormWrapperFc = (props) => {
   return <ImportForm t={t} socket={socket} {...props} />;
 };
 
-
 export default ImportFormWrapperFc;

+ 21 - 15
apps/app/src/client/components/Admin/ImportData/GrowiArchive/UploadForm.jsx

@@ -1,5 +1,4 @@
 import React from 'react';
-
 import { useTranslation } from 'next-i18next';
 import PropTypes from 'prop-types';
 
@@ -7,7 +6,6 @@ import { apiv3PostForm } from '~/client/util/apiv3-client';
 import { toastError } from '~/client/util/toastr';
 
 class UploadForm extends React.Component {
-
   constructor(props) {
     super(props);
 
@@ -33,14 +31,12 @@ class UploadForm extends React.Component {
     try {
       const { data } = await apiv3PostForm('/import/upload', formData);
       this.props.onUpload(data);
-    }
-    catch (err) {
+    } catch (err) {
       if (err[0].code === 'versions-are-not-met') {
         if (this.props.onVersionMismatch !== null) {
           this.props.onVersionMismatch(err[0].code);
         }
-      }
-      else {
+      } else {
         toastError(err);
       }
     }
@@ -48,9 +44,9 @@ class UploadForm extends React.Component {
 
   validateForm() {
     return (
-      this.inputRef.current // null check
-      && this.inputRef.current.files[0] // null check
-      && /\.zip$/.test(this.inputRef.current.files[0].name) // validate extension
+      this.inputRef.current && // null check
+      this.inputRef.current.files[0] && // null check
+      /\.zip$/.test(this.inputRef.current.files[0].name) // validate extension
     );
   }
 
@@ -61,7 +57,10 @@ class UploadForm extends React.Component {
       <form onSubmit={this.uploadZipFile}>
         <fieldset>
           <div className="row">
-            <label htmlFor="file" className="col-md-3 col-form-label col-form-label-sm">
+            <label
+              htmlFor="file"
+              className="col-md-3 col-form-label col-form-label-sm"
+            >
               {t('admin:importer_management.growi_settings.growi_archive_file')}
             </label>
             <div className="col-md-6">
@@ -76,12 +75,20 @@ class UploadForm extends React.Component {
           </div>
           <div className="row">
             <div className="mt-4 text-center">
-              { this.props.onDiscard && (
-                <button type="button" className="btn btn-outline-secondary mx-1" onClick={this.props.onDiscard}>
+              {this.props.onDiscard && (
+                <button
+                  type="button"
+                  className="btn btn-outline-secondary mx-1"
+                  onClick={this.props.onDiscard}
+                >
                   {t('admin:importer_management.growi_settings.discard')}
                 </button>
-              ) }
-              <button type="submit" className="btn btn-primary" disabled={!this.validateForm()}>
+              )}
+              <button
+                type="submit"
+                className="btn btn-primary"
+                disabled={!this.validateForm()}
+              >
                 {t('admin:importer_management.growi_settings.upload')}
               </button>
             </div>
@@ -90,7 +97,6 @@ class UploadForm extends React.Component {
       </form>
     );
   }
-
 }
 
 UploadForm.propTypes = {

+ 24 - 24
apps/app/src/client/components/Admin/ImportData/GrowiArchiveSection.jsx

@@ -1,17 +1,14 @@
 import React, { Fragment } from 'react';
-
 import { useTranslation } from 'next-i18next';
 import PropTypes from 'prop-types';
 
 import { apiv3Delete, apiv3Get } from '~/client/util/apiv3-client';
 import { toastError, toastSuccess } from '~/client/util/toastr';
 
-
 import ImportForm from './GrowiArchive/ImportForm';
 import UploadForm from './GrowiArchive/UploadForm';
 
 class GrowiArchiveSection extends React.Component {
-
   constructor(props) {
     super(props);
 
@@ -27,7 +24,8 @@ class GrowiArchiveSection extends React.Component {
     this.discardData = this.discardData.bind(this);
     this.resetState = this.resetState.bind(this);
     this.handleMismatchedVersions = this.handleMismatchedVersions.bind(this);
-    this.renderDefferentVersionAlert = this.renderDefferentVersionAlert.bind(this);
+    this.renderDefferentVersionAlert =
+      this.renderDefferentVersionAlert.bind(this);
   }
 
   async UNSAFE_componentWillMount() {
@@ -42,9 +40,7 @@ class GrowiArchiveSection extends React.Component {
     }
   }
 
-  handleUpload({
-    meta, fileName, innerFileStats,
-  }) {
+  handleUpload({ meta, fileName, innerFileStats }) {
     this.setState({
       fileName,
       innerFileStats,
@@ -59,18 +55,15 @@ class GrowiArchiveSection extends React.Component {
       this.resetState();
 
       toastSuccess(`Deleted ${fileName}`);
-    }
-    catch (err) {
+    } catch (err) {
       toastError(err);
     }
   }
 
-
   handleMismatchedVersions(err) {
     this.setState({
       isTheSameVersion: false,
     });
-
   }
 
   renderDefferentVersionAlert() {
@@ -92,17 +85,24 @@ class GrowiArchiveSection extends React.Component {
 
     return (
       <Fragment>
-        <h2 className="mb-3">{t('importer_management.import_growi_archive')}</h2>
+        <h2 className="mb-3">
+          {t('importer_management.import_growi_archive')}
+        </h2>
         <div className="card custom-card bg-body-tertiary mb-4 small">
           <ul>
-            <li>{t('importer_management.skip_username_and_email_when_overlapped')}</li>
-            <li>{t('importer_management.prepare_new_account_for_migration')}</li>
+            <li>
+              {t('importer_management.skip_username_and_email_when_overlapped')}
+            </li>
+            <li>
+              {t('importer_management.prepare_new_account_for_migration')}
+            </li>
             <li>
               <a
                 href={`${t('importer_management.admin_archive_data_import_guide_url')}`}
                 target="_blank"
                 rel="noopener noreferrer"
-              >{t('importer_management.archive_data_import_detail')}
+              >
+                {t('importer_management.archive_data_import_detail')}
               </a>
             </li>
           </ul>
@@ -117,18 +117,18 @@ class GrowiArchiveSection extends React.Component {
               onDiscard={this.discardData}
             />
           </div>
-        )
-          : (
-            <UploadForm
-              onUpload={this.handleUpload}
-              onDiscard={this.state.fileName != null ? this.discardData : undefined}
-              onVersionMismatch={this.handleMismatchedVersions}
-            />
-          )}
+        ) : (
+          <UploadForm
+            onUpload={this.handleUpload}
+            onDiscard={
+              this.state.fileName != null ? this.discardData : undefined
+            }
+            onVersionMismatch={this.handleMismatchedVersions}
+          />
+        )}
       </Fragment>
     );
   }
-
 }
 
 GrowiArchiveSection.propTypes = {

+ 4 - 2
apps/app/src/client/components/Admin/ImportData/ImportDataPageContents.jsx

@@ -1,5 +1,4 @@
 import { withUnstatedContainers } from '../../UnstatedUtils';
-
 import GrowiArchiveSection from './GrowiArchiveSection';
 
 const ImportDataPageContents = () => {
@@ -13,6 +12,9 @@ const ImportDataPageContents = () => {
 /**
  * Wrapper component for using unstated
  */
-const ImportDataPageContentsWrapper = withUnstatedContainers(ImportDataPageContents, []);
+const ImportDataPageContentsWrapper = withUnstatedContainers(
+  ImportDataPageContents,
+  [],
+);
 
 export default ImportDataPageContentsWrapper;

+ 23 - 15
apps/app/src/client/components/Admin/LegacySlackIntegration/LegacySlackIntegration.jsx

@@ -1,5 +1,4 @@
 import React, { useEffect } from 'react';
-
 import { useTranslation } from 'next-i18next';
 import PropTypes from 'prop-types';
 
@@ -9,8 +8,6 @@ import { toArrayIfNot } from '~/utils/array-utils';
 import loggerFactory from '~/utils/logger';
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
-
-
 import SlackConfiguration from './SlackConfiguration';
 
 const logger = loggerFactory('growi:NotificationSetting');
@@ -19,39 +16,45 @@ const LegacySlackIntegration = (props) => {
   const { t } = useTranslation();
   const { adminSlackIntegrationLegacyContainer } = props;
 
-
   useEffect(() => {
-    const fetchLegacySlackIntegrationData = async() => {
+    const fetchLegacySlackIntegrationData = async () => {
       await adminSlackIntegrationLegacyContainer.retrieveData();
     };
 
     try {
       fetchLegacySlackIntegrationData();
-    }
-    catch (err) {
+    } catch (err) {
       const errs = toArrayIfNot(err);
       toastError(errs);
       logger.error(errs);
     }
   }, [adminSlackIntegrationLegacyContainer]);
 
-
-  const isDisabled = adminSlackIntegrationLegacyContainer.state.isSlackbotConfigured;
+  const isDisabled =
+    adminSlackIntegrationLegacyContainer.state.isSlackbotConfigured;
 
   return (
     <div data-testid="admin-slack-integration-legacy">
-      { isDisabled && (
+      {isDisabled && (
         <div className="alert alert-danger">
           <span className="material-symbols-outlined">remove</span>
           {/* eslint-disable-next-line react/no-danger */}
-          <span dangerouslySetInnerHTML={{ __html: t('admin:slack_integration_legacy.alert_disabled') }}></span>
+          <span
+            dangerouslySetInnerHTML={{
+              __html: t('admin:slack_integration_legacy.alert_disabled'),
+            }}
+          ></span>
         </div>
-      ) }
+      )}
 
       <div className="alert alert-warning">
         <span className="material-symbols-outlined">info</span>
         {/* eslint-disable-next-line react/no-danger */}
-        <span dangerouslySetInnerHTML={{ __html: t('admin:slack_integration_legacy.alert_deplicated') }}></span>
+        <span
+          dangerouslySetInnerHTML={{
+            __html: t('admin:slack_integration_legacy.alert_deplicated'),
+          }}
+        ></span>
       </div>
 
       <SlackConfiguration />
@@ -59,10 +62,15 @@ const LegacySlackIntegration = (props) => {
   );
 };
 
-const LegacySlackIntegrationWithUnstatedContainer = withUnstatedContainers(LegacySlackIntegration, [AdminSlackIntegrationLegacyContainer]);
+const LegacySlackIntegrationWithUnstatedContainer = withUnstatedContainers(
+  LegacySlackIntegration,
+  [AdminSlackIntegrationLegacyContainer],
+);
 
 LegacySlackIntegration.propTypes = {
-  adminSlackIntegrationLegacyContainer: PropTypes.instanceOf(AdminSlackIntegrationLegacyContainer).isRequired,
+  adminSlackIntegrationLegacyContainer: PropTypes.instanceOf(
+    AdminSlackIntegrationLegacyContainer,
+  ).isRequired,
 };
 
 export default LegacySlackIntegrationWithUnstatedContainer;

+ 137 - 64
apps/app/src/client/components/Admin/LegacySlackIntegration/SlackConfiguration.jsx

@@ -1,11 +1,10 @@
 import React, { useCallback, useEffect } from 'react';
-
 import { useTranslation } from 'next-i18next';
 import PropTypes from 'prop-types';
 import { useForm } from 'react-hook-form';
 
 import AdminSlackIntegrationLegacyContainer from '~/client/services/AdminSlackIntegrationLegacyContainer';
-import { toastSuccess, toastError } from '~/client/util/toastr';
+import { toastError, toastSuccess } from '~/client/util/toastr';
 import loggerFactory from '~/utils/logger';
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
@@ -15,7 +14,8 @@ const logger = loggerFactory('growi:slackAppConfiguration');
 
 const SlackConfiguration = (props) => {
   const { t, adminSlackIntegrationLegacyContainer } = props;
-  const { webhookUrl, slackToken, retrieveError } = adminSlackIntegrationLegacyContainer.state;
+  const { webhookUrl, slackToken, retrieveError } =
+    adminSlackIntegrationLegacyContainer.state;
 
   const { register, handleSubmit, reset } = useForm();
 
@@ -27,18 +27,24 @@ const SlackConfiguration = (props) => {
     });
   }, [reset, webhookUrl, slackToken]);
 
-  const onClickSubmit = useCallback(async(data) => {
-    try {
-      await adminSlackIntegrationLegacyContainer.changeWebhookUrl(data.webhookUrl ?? '');
-      await adminSlackIntegrationLegacyContainer.changeSlackToken(data.slackToken ?? '');
-      await adminSlackIntegrationLegacyContainer.updateSlackAppConfiguration();
-      toastSuccess(t('notification_settings.updated_slackApp'));
-    }
-    catch (err) {
-      toastError(err);
-      logger.error(err);
-    }
-  }, [adminSlackIntegrationLegacyContainer, t]);
+  const onClickSubmit = useCallback(
+    async (data) => {
+      try {
+        await adminSlackIntegrationLegacyContainer.changeWebhookUrl(
+          data.webhookUrl ?? '',
+        );
+        await adminSlackIntegrationLegacyContainer.changeSlackToken(
+          data.slackToken ?? '',
+        );
+        await adminSlackIntegrationLegacyContainer.updateSlackAppConfiguration();
+        toastSuccess(t('notification_settings.updated_slackApp'));
+      } catch (err) {
+        toastError(err);
+        logger.error(err);
+      }
+    },
+    [adminSlackIntegrationLegacyContainer, t],
+  );
 
   return (
     <form onSubmit={handleSubmit(onClickSubmit)}>
@@ -56,21 +62,47 @@ const SlackConfiguration = (props) => {
               >
                 {`Slack ${adminSlackIntegrationLegacyContainer.state.selectSlackOption}`}
               </button>
-              <div className="dropdown-menu" aria-labelledby="dropdownMenuButton">
-                <button className="dropdown-item" type="button" onClick={() => adminSlackIntegrationLegacyContainer.switchSlackOption('Incoming Webhooks')}>
+              <div
+                className="dropdown-menu"
+                aria-labelledby="dropdownMenuButton"
+              >
+                <button
+                  className="dropdown-item"
+                  type="button"
+                  onClick={() =>
+                    adminSlackIntegrationLegacyContainer.switchSlackOption(
+                      'Incoming Webhooks',
+                    )
+                  }
+                >
                   Slack Incoming Webhooks
                 </button>
-                <button className="dropdown-item" type="button" onClick={() => adminSlackIntegrationLegacyContainer.switchSlackOption('App')}>Slack App</button>
+                <button
+                  className="dropdown-item"
+                  type="button"
+                  onClick={() =>
+                    adminSlackIntegrationLegacyContainer.switchSlackOption(
+                      'App',
+                    )
+                  }
+                >
+                  Slack App
+                </button>
               </div>
             </div>
           </div>
         </div>
-        {adminSlackIntegrationLegacyContainer.state.selectSlackOption === 'Incoming Webhooks' ? (
+        {adminSlackIntegrationLegacyContainer.state.selectSlackOption ===
+        'Incoming Webhooks' ? (
           <React.Fragment>
-            <h2 className="border-bottom mb-5">{t('notification_settings.slack_incoming_configuration')}</h2>
+            <h2 className="border-bottom mb-5">
+              {t('notification_settings.slack_incoming_configuration')}
+            </h2>
 
             <div className="row mb-3">
-              <label className="form-label col-md-3 text-start text-md-end">Webhook URL</label>
+              <label className="form-label col-md-3 text-start text-md-end">
+                Webhook URL
+              </label>
               <div className="col-md-6">
                 <input
                   className="form-control"
@@ -87,10 +119,18 @@ const SlackConfiguration = (props) => {
                     type="checkbox"
                     className="form-check-input"
                     id="cbPrioritizeIWH"
-                    checked={adminSlackIntegrationLegacyContainer.state.isIncomingWebhookPrioritized || false}
-                    onChange={() => { adminSlackIntegrationLegacyContainer.switchIsIncomingWebhookPrioritized() }}
+                    checked={
+                      adminSlackIntegrationLegacyContainer.state
+                        .isIncomingWebhookPrioritized || false
+                    }
+                    onChange={() => {
+                      adminSlackIntegrationLegacyContainer.switchIsIncomingWebhookPrioritized();
+                    }}
                   />
-                  <label className="form-label form-check-label" htmlFor="cbPrioritizeIWH">
+                  <label
+                    className="form-label form-check-label"
+                    htmlFor="cbPrioritizeIWH"
+                  >
                     {t('notification_settings.prioritize_webhook')}
                   </label>
                 </div>
@@ -100,40 +140,54 @@ const SlackConfiguration = (props) => {
               </div>
             </div>
           </React.Fragment>
-        )
-          : (
-            <React.Fragment>
-              <h2 className="border-bottom mb-3">{t('notification_settings.slack_app_configuration')}</h2>
-
-              <div className="card custom-card bg-danger-subtle">
-                <span className="text-danger"><span className="material-symbols-outlined">error</span>NOT RECOMMENDED</span>
-                <br />
-                {/* eslint-disable-next-line react/no-danger */}
-                <span dangerouslySetInnerHTML={{ __html: t('notification_settings.slack_app_configuration_desc') }} />
-                <br />
-                <a
-                  href="#slack-incoming-webhooks"
-                  data-bs-toggle="tab"
-                  onClick={() => adminSlackIntegrationLegacyContainer.switchSlackOption('Incoming Webhooks')}
-                >
-                  {t('notification_settings.use_instead')}
-                </a>
-              </div>
+        ) : (
+          <React.Fragment>
+            <h2 className="border-bottom mb-3">
+              {t('notification_settings.slack_app_configuration')}
+            </h2>
 
-              <div className="row mb-5 mt-4">
-                <label className="form-label col-md-3 text-start text-md-end">OAuth access token</label>
-                <div className="col-md-6">
-                  <input
-                    className="form-control"
-                    type="text"
-                    {...register('slackToken')}
-                  />
-                </div>
-              </div>
+            <div className="card custom-card bg-danger-subtle">
+              <span className="text-danger">
+                <span className="material-symbols-outlined">error</span>NOT
+                RECOMMENDED
+              </span>
+              <br />
+              {/* eslint-disable-next-line react/no-danger */}
+              <span
+                dangerouslySetInnerHTML={{
+                  __html: t(
+                    'notification_settings.slack_app_configuration_desc',
+                  ),
+                }}
+              />
+              <br />
+              <a
+                href="#slack-incoming-webhooks"
+                data-bs-toggle="tab"
+                onClick={() =>
+                  adminSlackIntegrationLegacyContainer.switchSlackOption(
+                    'Incoming Webhooks',
+                  )
+                }
+              >
+                {t('notification_settings.use_instead')}
+              </a>
+            </div>
 
-            </React.Fragment>
-          )
-        }
+            <div className="row mb-5 mt-4">
+              <label className="form-label col-md-3 text-start text-md-end">
+                OAuth access token
+              </label>
+              <div className="col-md-6">
+                <input
+                  className="form-control"
+                  type="text"
+                  {...register('slackToken')}
+                />
+              </div>
+            </div>
+          </React.Fragment>
+        )}
 
         <AdminUpdateButtonRow
           disabled={retrieveError != null}
@@ -143,16 +197,27 @@ const SlackConfiguration = (props) => {
         <hr />
 
         <h3>
-          <span className="material-symbols-outlined" aria-hidden="true">help</span>{' '}
-          <a href="#collapseHelpForIwh" data-bs-toggle="collapse">{t('notification_settings.how_to.header')}</a>
+          <span className="material-symbols-outlined" aria-hidden="true">
+            help
+          </span>{' '}
+          <a href="#collapseHelpForIwh" data-bs-toggle="collapse">
+            {t('notification_settings.how_to.header')}
+          </a>
         </h3>
 
-        <ol id="collapseHelpForIwh" className="collapse card custom-card bg-body-tertiary">
+        <ol
+          id="collapseHelpForIwh"
+          className="collapse card custom-card bg-body-tertiary"
+        >
           <li className="ms-3">
             {t('notification_settings.how_to.workspace')}
             <ol>
               {/* eslint-disable-next-line react/no-danger */}
-              <li dangerouslySetInnerHTML={{ __html: t('notification_settings.how_to.workspace_desc1') }} />
+              <li
+                dangerouslySetInnerHTML={{
+                  __html: t('notification_settings.how_to.workspace_desc1'),
+                }}
+              />
               <li>{t('notification_settings.how_to.workspace_desc2')}</li>
               <li>{t('notification_settings.how_to.workspace_desc3')}</li>
             </ol>
@@ -161,11 +226,14 @@ const SlackConfiguration = (props) => {
             {t('notification_settings.how_to.at_growi')}
             <ol>
               {/* eslint-disable-next-line react/no-danger */}
-              <li dangerouslySetInnerHTML={{ __html: t('notification_settings.how_to.at_growi_desc') }} />
+              <li
+                dangerouslySetInnerHTML={{
+                  __html: t('notification_settings.how_to.at_growi_desc'),
+                }}
+              />
             </ol>
           </li>
         </ol>
-
       </React.Fragment>
     </form>
   );
@@ -173,7 +241,9 @@ const SlackConfiguration = (props) => {
 
 SlackConfiguration.propTypes = {
   t: PropTypes.func.isRequired, // i18next
-  adminSlackIntegrationLegacyContainer: PropTypes.instanceOf(AdminSlackIntegrationLegacyContainer).isRequired,
+  adminSlackIntegrationLegacyContainer: PropTypes.instanceOf(
+    AdminSlackIntegrationLegacyContainer,
+  ).isRequired,
 };
 
 const SlackConfigurationWrapperFc = (props) => {
@@ -182,6 +252,9 @@ const SlackConfigurationWrapperFc = (props) => {
   return <SlackConfiguration t={t} {...props} />;
 };
 
-const SlackConfigurationWrapper = withUnstatedContainers(SlackConfigurationWrapperFc, [AdminSlackIntegrationLegacyContainer]);
+const SlackConfigurationWrapper = withUnstatedContainers(
+  SlackConfigurationWrapperFc,
+  [AdminSlackIntegrationLegacyContainer],
+);
 
 export default SlackConfigurationWrapper;

+ 32 - 27
apps/app/src/client/components/Admin/ManageExternalAccount.tsx

@@ -1,34 +1,38 @@
-import React, { useCallback, useEffect, type JSX } from 'react';
-
-import { useTranslation } from 'next-i18next';
+import React, { type JSX, useCallback, useEffect } from 'react';
 import Link from 'next/link';
+import { useTranslation } from 'next-i18next';
 
 import AdminExternalAccountsContainer from '~/client/services/AdminExternalAccountsContainer';
 import { toastError } from '~/client/util/toastr';
 
 import PaginationWrapper from '../PaginationWrapper';
 import { withUnstatedContainers } from '../UnstatedUtils';
-
 import ExternalAccountTable from './Users/ExternalAccountTable';
 
 type ManageExternalAccountProps = {
-  adminExternalAccountsContainer: AdminExternalAccountsContainer,
-}
-
-const ManageExternalAccount = (props: ManageExternalAccountProps): JSX.Element => {
+  adminExternalAccountsContainer: AdminExternalAccountsContainer;
+};
 
+const ManageExternalAccount = (
+  props: ManageExternalAccountProps,
+): JSX.Element => {
   const { t } = useTranslation();
   const { adminExternalAccountsContainer } = props;
-  const { activePage, totalAccounts, pagingLimit } = adminExternalAccountsContainer.state;
+  const { activePage, totalAccounts, pagingLimit } =
+    adminExternalAccountsContainer.state;
 
-  const externalAccountPageHandler = useCallback(async(selectedPage) => {
-    try {
-      await adminExternalAccountsContainer.retrieveExternalAccountsByPagingNum(selectedPage);
-    }
-    catch (err) {
-      toastError(err);
-    }
-  }, [adminExternalAccountsContainer]);
+  const externalAccountPageHandler = useCallback(
+    async (selectedPage) => {
+      try {
+        await adminExternalAccountsContainer.retrieveExternalAccountsByPagingNum(
+          selectedPage,
+        );
+      } catch (err) {
+        toastError(err);
+      }
+    },
+    [adminExternalAccountsContainer],
+  );
 
   // for Next routing
   useEffect(() => {
@@ -54,28 +58,29 @@ const ManageExternalAccount = (props: ManageExternalAccountProps): JSX.Element =
           prefetch={false}
           className="btn btn-outline-secondary"
         >
-          <span className="material-symbols-outlined" aria-hidden="true">arrow_back</span>
+          <span className="material-symbols-outlined" aria-hidden="true">
+            arrow_back
+          </span>
           {t('admin:user_management.back_to_user_management')}
         </Link>
       </p>
       <h2>{t('admin:user_management.external_account_list')}</h2>
-      {(totalAccounts !== 0) ? (
+      {totalAccounts !== 0 ? (
         <>
           {pager}
           <ExternalAccountTable />
           {pager}
         </>
-      )
-        : (
-          <>
-            { t('admin:user_management.external_account_none') }
-          </>
-        )
-      }
+      ) : (
+        <>{t('admin:user_management.external_account_none')}</>
+      )}
     </>
   );
 };
 
-const ManageExternalAccountWrapper = withUnstatedContainers(ManageExternalAccount, [AdminExternalAccountsContainer]);
+const ManageExternalAccountWrapper = withUnstatedContainers(
+  ManageExternalAccount,
+  [AdminExternalAccountsContainer],
+);
 
 export default ManageExternalAccountWrapper;

+ 58 - 24
apps/app/src/client/components/Admin/MarkdownSetting/IndentForm.tsx

@@ -1,13 +1,15 @@
 /* eslint-disable react/no-danger */
 import React, { useCallback } from 'react';
-
 import { useTranslation } from 'next-i18next';
 import {
-  UncontrolledDropdown, DropdownToggle, DropdownMenu, DropdownItem,
+  DropdownItem,
+  DropdownMenu,
+  DropdownToggle,
+  UncontrolledDropdown,
 } from 'reactstrap';
 
 import AdminMarkDownContainer from '~/client/services/AdminMarkDownContainer';
-import { toastSuccess, toastError } from '~/client/util/toastr';
+import { toastError, toastSuccess } from '~/client/util/toastr';
 import loggerFactory from '~/utils/logger';
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
@@ -15,24 +17,30 @@ import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
 
 const logger = loggerFactory('growi:importer');
 
-
 type Props = {
   adminMarkDownContainer: AdminMarkDownContainer;
-}
+};
 
 const IndentForm = (props: Props) => {
   const { t } = useTranslation('admin');
 
-  const onClickSubmit = useCallback(async(props) => {
-    try {
-      await props.adminMarkDownContainer.updateIndentSetting();
-      toastSuccess(t('toaster.update_successed', { target: t('markdown_settings.indent_header'), ns: 'commons' }));
-    }
-    catch (err) {
-      toastError(err);
-      logger.error(err);
-    }
-  }, [t]);
+  const onClickSubmit = useCallback(
+    async (props) => {
+      try {
+        await props.adminMarkDownContainer.updateIndentSetting();
+        toastSuccess(
+          t('toaster.update_successed', {
+            target: t('markdown_settings.indent_header'),
+            ns: 'commons',
+          }),
+        );
+      } catch (err) {
+        toastError(err);
+        logger.error(err);
+      }
+    },
+    [t],
+  );
 
   const renderIndentSizeOption = (props) => {
     const { adminMarkDownContainer } = props;
@@ -41,9 +49,14 @@ const IndentForm = (props: Props) => {
     return (
       <div className="col">
         <div>
-          <label htmlFor="adminPreferredIndentSize" className="form-label">{t('markdown_settings.indent_options.indentSize')}</label>
+          <label htmlFor="adminPreferredIndentSize" className="form-label">
+            {t('markdown_settings.indent_options.indentSize')}
+          </label>
           <UncontrolledDropdown id="adminPreferredIndentSize">
-            <DropdownToggle caret className="col-3 col-sm-2 col-md-5 col-lg-5 col-xl-3 text-end">
+            <DropdownToggle
+              caret
+              className="col-3 col-sm-2 col-md-5 col-lg-5 col-xl-3 text-end"
+            >
               <span className="float-start">
                 {adminPreferredIndentSize || 4}
               </span>
@@ -51,7 +64,13 @@ const IndentForm = (props: Props) => {
             <DropdownMenu className="dropdown-menu" role="menu">
               {[2, 4].map((num) => {
                 return (
-                  <DropdownItem key={num} role="presentation" onClick={() => adminMarkDownContainer.setAdminPreferredIndentSize(num)}>
+                  <DropdownItem
+                    key={num}
+                    role="presentation"
+                    onClick={() =>
+                      adminMarkDownContainer.setAdminPreferredIndentSize(num)
+                    }
+                  >
                     <a role="menuitem">{num}</a>
                   </DropdownItem>
                 );
@@ -70,7 +89,9 @@ const IndentForm = (props: Props) => {
     const { adminMarkDownContainer } = props;
     const { isIndentSizeForced } = adminMarkDownContainer.state;
 
-    const helpIndentInComment = { __html: t('markdown_settings.indent_options.disallow_indent_change_desc') };
+    const helpIndentInComment = {
+      __html: t('markdown_settings.indent_options.disallow_indent_change_desc'),
+    };
 
     return (
       <div className="col">
@@ -81,14 +102,22 @@ const IndentForm = (props: Props) => {
             id="isIndentSizeForced"
             checked={isIndentSizeForced || false}
             onChange={() => {
-              adminMarkDownContainer.setState({ isIndentSizeForced: !isIndentSizeForced });
+              adminMarkDownContainer.setState({
+                isIndentSizeForced: !isIndentSizeForced,
+              });
             }}
           />
-          <label className="form-label form-check-label" htmlFor="isIndentSizeForced">
+          <label
+            className="form-label form-check-label"
+            htmlFor="isIndentSizeForced"
+          >
             {t('markdown_settings.indent_options.disallow_indent_change')}
           </label>
         </div>
-        <p className="form-text text-muted" dangerouslySetInnerHTML={helpIndentInComment} />
+        <p
+          className="form-text text-muted"
+          dangerouslySetInnerHTML={helpIndentInComment}
+        />
       </div>
     );
   };
@@ -101,7 +130,10 @@ const IndentForm = (props: Props) => {
         {renderIndentSizeOption(props)}
         {renderIndentForceOption(props)}
       </fieldset>
-      <AdminUpdateButtonRow onClick={() => onClickSubmit(props)} disabled={adminMarkDownContainer.state.retrieveError != null} />
+      <AdminUpdateButtonRow
+        onClick={() => onClickSubmit(props)}
+        disabled={adminMarkDownContainer.state.retrieveError != null}
+      />
     </React.Fragment>
   );
 };
@@ -109,6 +141,8 @@ const IndentForm = (props: Props) => {
 /**
  * Wrapper component for using unstated
  */
-const IndentFormWrapper = withUnstatedContainers(IndentForm, [AdminMarkDownContainer]);
+const IndentFormWrapper = withUnstatedContainers(IndentForm, [
+  AdminMarkDownContainer,
+]);
 
 export default IndentFormWrapper;

+ 55 - 21
apps/app/src/client/components/Admin/MarkdownSetting/LineBreakForm.jsx

@@ -1,11 +1,10 @@
 /* eslint-disable react/no-danger */
 import React from 'react';
-
 import { useTranslation } from 'next-i18next';
 import PropTypes from 'prop-types';
 
 import AdminMarkDownContainer from '~/client/services/AdminMarkDownContainer';
-import { toastSuccess, toastError } from '~/client/util/toastr';
+import { toastError, toastSuccess } from '~/client/util/toastr';
 import loggerFactory from '~/utils/logger';
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
@@ -14,22 +13,24 @@ import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
 const logger = loggerFactory('growi:importer');
 
 class LineBreakForm extends React.Component {
-
   constructor(props) {
     super(props);
 
     this.onClickSubmit = this.onClickSubmit.bind(this);
   }
 
-
   async onClickSubmit() {
     const { t } = this.props;
 
     try {
       await this.props.adminMarkDownContainer.updateLineBreakSetting();
-      toastSuccess(t('toaster.update_successed', { target: t('markdown_settings.lineBreak_header'), ns: 'commons' }));
-    }
-    catch (err) {
+      toastSuccess(
+        t('toaster.update_successed', {
+          target: t('markdown_settings.lineBreak_header'),
+          ns: 'commons',
+        }),
+      );
+    } catch (err) {
       toastError(err);
       logger.error(err);
     }
@@ -39,7 +40,9 @@ class LineBreakForm extends React.Component {
     const { t, adminMarkDownContainer } = this.props;
     const { isEnabledLinebreaks } = adminMarkDownContainer.state;
 
-    const helpLineBreak = { __html: t('markdown_settings.lineBreak_options.enable_lineBreak_desc') };
+    const helpLineBreak = {
+      __html: t('markdown_settings.lineBreak_options.enable_lineBreak_desc'),
+    };
 
     return (
       <div className="col">
@@ -49,13 +52,23 @@ class LineBreakForm extends React.Component {
             className="form-check-input"
             id="isEnabledLinebreaks"
             checked={isEnabledLinebreaks}
-            onChange={() => { adminMarkDownContainer.setState({ isEnabledLinebreaks: !isEnabledLinebreaks }) }}
+            onChange={() => {
+              adminMarkDownContainer.setState({
+                isEnabledLinebreaks: !isEnabledLinebreaks,
+              });
+            }}
           />
-          <label className="form-label form-check-label" htmlFor="isEnabledLinebreaks">
-            {t('markdown_settings.lineBreak_options.enable_lineBreak') }
+          <label
+            className="form-label form-check-label"
+            htmlFor="isEnabledLinebreaks"
+          >
+            {t('markdown_settings.lineBreak_options.enable_lineBreak')}
           </label>
         </div>
-        <p className="form-text text-muted" dangerouslySetInnerHTML={helpLineBreak} />
+        <p
+          className="form-text text-muted"
+          dangerouslySetInnerHTML={helpLineBreak}
+        />
       </div>
     );
   }
@@ -64,7 +77,11 @@ class LineBreakForm extends React.Component {
     const { t, adminMarkDownContainer } = this.props;
     const { isEnabledLinebreaksInComments } = adminMarkDownContainer.state;
 
-    const helpLineBreakInComment = { __html: t('markdown_settings.lineBreak_options.enable_lineBreak_for_comment_desc') };
+    const helpLineBreakInComment = {
+      __html: t(
+        'markdown_settings.lineBreak_options.enable_lineBreak_for_comment_desc',
+      ),
+    };
 
     return (
       <div className="col">
@@ -74,13 +91,25 @@ class LineBreakForm extends React.Component {
             className="form-check-input"
             id="isEnabledLinebreaksInComments"
             checked={isEnabledLinebreaksInComments}
-            onChange={() => { adminMarkDownContainer.setState({ isEnabledLinebreaksInComments: !isEnabledLinebreaksInComments }) }}
+            onChange={() => {
+              adminMarkDownContainer.setState({
+                isEnabledLinebreaksInComments: !isEnabledLinebreaksInComments,
+              });
+            }}
           />
-          <label className="form-label form-check-label" htmlFor="isEnabledLinebreaksInComments">
-            {t('markdown_settings.lineBreak_options.enable_lineBreak_for_comment') }
+          <label
+            className="form-label form-check-label"
+            htmlFor="isEnabledLinebreaksInComments"
+          >
+            {t(
+              'markdown_settings.lineBreak_options.enable_lineBreak_for_comment',
+            )}
           </label>
         </div>
-        <p className="form-text text-muted" dangerouslySetInnerHTML={helpLineBreakInComment} />
+        <p
+          className="form-text text-muted"
+          dangerouslySetInnerHTML={helpLineBreakInComment}
+        />
       </div>
     );
   }
@@ -94,11 +123,13 @@ class LineBreakForm extends React.Component {
           {this.renderLineBreakOption()}
           {this.renderLineBreakInCommentOption()}
         </fieldset>
-        <AdminUpdateButtonRow onClick={this.onClickSubmit} disabled={adminMarkDownContainer.state.retrieveError != null} />
+        <AdminUpdateButtonRow
+          onClick={this.onClickSubmit}
+          disabled={adminMarkDownContainer.state.retrieveError != null}
+        />
       </React.Fragment>
     );
   }
-
 }
 
 const LineBreakFormFC = (props) => {
@@ -109,11 +140,14 @@ const LineBreakFormFC = (props) => {
 /**
  * Wrapper component for using unstated
  */
-const LineBreakFormWrapper = withUnstatedContainers(LineBreakFormFC, [AdminMarkDownContainer]);
+const LineBreakFormWrapper = withUnstatedContainers(LineBreakFormFC, [
+  AdminMarkDownContainer,
+]);
 
 LineBreakForm.propTypes = {
   t: PropTypes.func.isRequired,
-  adminMarkDownContainer: PropTypes.instanceOf(AdminMarkDownContainer).isRequired,
+  adminMarkDownContainer: PropTypes.instanceOf(AdminMarkDownContainer)
+    .isRequired,
 };
 
 export default LineBreakFormWrapper;

+ 28 - 18
apps/app/src/client/components/Admin/MarkdownSetting/MarkDownSettingContents.tsx

@@ -1,5 +1,4 @@
-import React, { useEffect, type JSX } from 'react';
-
+import React, { type JSX, useEffect } from 'react';
 import { useTranslation } from 'next-i18next';
 import { Card, CardBody } from 'reactstrap';
 
@@ -9,30 +8,28 @@ import { toArrayIfNot } from '~/utils/array-utils';
 import loggerFactory from '~/utils/logger';
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
-
 import IndentForm from './IndentForm';
 import LineBreakForm from './LineBreakForm';
 import XssForm from './XssForm';
 
 const logger = loggerFactory('growi:MarkDown');
 
-type Props ={
-  adminMarkDownContainer: AdminMarkDownContainer
-}
+type Props = {
+  adminMarkDownContainer: AdminMarkDownContainer;
+};
 
 const MarkDownSettingContents = React.memo((props: Props): JSX.Element => {
   const { t } = useTranslation('admin');
   const { adminMarkDownContainer } = props;
 
   useEffect(() => {
-    const fetchMarkdownData = async() => {
+    const fetchMarkdownData = async () => {
       await adminMarkDownContainer.retrieveMarkdownData();
     };
 
     try {
       fetchMarkdownData();
-    }
-    catch (err) {
+    } catch (err) {
       const errs = toArrayIfNot(err);
       toastError(errs);
       logger.error(errs);
@@ -42,23 +39,35 @@ const MarkDownSettingContents = React.memo((props: Props): JSX.Element => {
   return (
     <div data-testid="admin-markdown" className="mb-5">
       {/* Line Break Setting */}
-      <h2 className="admin-setting-header">{t('markdown_settings.lineBreak_header')}</h2>
+      <h2 className="admin-setting-header">
+        {t('markdown_settings.lineBreak_header')}
+      </h2>
       <Card className="card custom-card bg-body-tertiary my-3">
-        <CardBody className="px-0 py-2">{ t('markdown_settings.lineBreak_desc') }</CardBody>
+        <CardBody className="px-0 py-2">
+          {t('markdown_settings.lineBreak_desc')}
+        </CardBody>
       </Card>
       <LineBreakForm />
 
       {/* Indent Setting */}
-      <h2 className="admin-setting-header mt-5">{t('markdown_settings.indent_header')}</h2>
+      <h2 className="admin-setting-header mt-5">
+        {t('markdown_settings.indent_header')}
+      </h2>
       <Card className="card custom-card bg-body-tertiary my-3">
-        <CardBody className="px-0 py-2">{t('markdown_settings.indent_desc') }</CardBody>
+        <CardBody className="px-0 py-2">
+          {t('markdown_settings.indent_desc')}
+        </CardBody>
       </Card>
       <IndentForm />
 
       {/* XSS Setting */}
-      <h2 className="admin-setting-header mt-5">{ t('markdown_settings.xss_header') }</h2>
+      <h2 className="admin-setting-header mt-5">
+        {t('markdown_settings.xss_header')}
+      </h2>
       <Card className="card custom-card bg-body-tertiary my-3">
-        <CardBody className="px-0 py-2">{ t('markdown_settings.xss_desc') }</CardBody>
+        <CardBody className="px-0 py-2">
+          {t('markdown_settings.xss_desc')}
+        </CardBody>
       </Card>
       <XssForm />
     </div>
@@ -66,8 +75,9 @@ const MarkDownSettingContents = React.memo((props: Props): JSX.Element => {
 });
 MarkDownSettingContents.displayName = 'MarkDownSettingContents';
 
-
-const MarkdownSettingWithUnstatedContainer = withUnstatedContainers(MarkDownSettingContents, [AdminMarkDownContainer]);
-
+const MarkdownSettingWithUnstatedContainer = withUnstatedContainers(
+  MarkDownSettingContents,
+  [AdminMarkDownContainer],
+);
 
 export default MarkdownSettingWithUnstatedContainer;

+ 29 - 17
apps/app/src/client/components/Admin/MarkdownSetting/WhitelistInput.tsx

@@ -1,24 +1,25 @@
-import { useCallback, type JSX } from 'react';
-
+import { type JSX, useCallback } from 'react';
 import { useTranslation } from 'next-i18next';
 import type { UseFormRegister, UseFormSetValue } from 'react-hook-form';
 
 import type AdminMarkDownContainer from '~/client/services/AdminMarkDownContainer';
-import { tagNames as recommendedTagNames, attributes as recommendedAttributes } from '~/services/renderer/recommended-whitelist';
+import {
+  attributes as recommendedAttributes,
+  tagNames as recommendedTagNames,
+} from '~/services/renderer/recommended-whitelist';
 
 type FormValues = {
-  tagWhitelist: string,
-  attrWhitelist: string,
-}
+  tagWhitelist: string;
+  attrWhitelist: string;
+};
 
-type Props ={
-  adminMarkDownContainer: AdminMarkDownContainer,
-  register: UseFormRegister<FormValues>,
-  setValue: UseFormSetValue<FormValues>,
-}
+type Props = {
+  adminMarkDownContainer: AdminMarkDownContainer;
+  register: UseFormRegister<FormValues>;
+  setValue: UseFormSetValue<FormValues>;
+};
 
 export const WhitelistInput = (props: Props): JSX.Element => {
-
   const { t } = useTranslation('admin');
   const { adminMarkDownContainer, register, setValue } = props;
 
@@ -39,8 +40,14 @@ export const WhitelistInput = (props: Props): JSX.Element => {
       <div className="mt-4">
         <div className="d-flex justify-content-between">
           {t('markdown_settings.xss_options.tag_names')}
-          <p id="btn-import-tags" className="btn btn-sm btn-primary" onClick={clickRecommendTagButtonHandler}>
-            {t('markdown_settings.xss_options.import_recommended', { target: 'Tags' })}
+          <p
+            id="btn-import-tags"
+            className="btn btn-sm btn-primary"
+            onClick={clickRecommendTagButtonHandler}
+          >
+            {t('markdown_settings.xss_options.import_recommended', {
+              target: 'Tags',
+            })}
           </p>
         </div>
         <textarea
@@ -53,8 +60,14 @@ export const WhitelistInput = (props: Props): JSX.Element => {
       <div className="mt-4">
         <div className="d-flex justify-content-between">
           {t('markdown_settings.xss_options.tag_attributes')}
-          <p id="btn-import-tags" className="btn btn-sm btn-primary" onClick={clickRecommendAttrButtonHandler}>
-            {t('markdown_settings.xss_options.import_recommended', { target: 'Attrs' })}
+          <p
+            id="btn-import-tags"
+            className="btn btn-sm btn-primary"
+            onClick={clickRecommendAttrButtonHandler}
+          >
+            {t('markdown_settings.xss_options.import_recommended', {
+              target: 'Attrs',
+            })}
           </p>
         </div>
         <textarea
@@ -66,5 +79,4 @@ export const WhitelistInput = (props: Props): JSX.Element => {
       </div>
     </>
   );
-
 };

+ 70 - 37
apps/app/src/client/components/Admin/MarkdownSetting/XssForm.jsx

@@ -1,31 +1,29 @@
 import React, { useCallback, useEffect } from 'react';
-
 import { useTranslation } from 'next-i18next';
 import PropTypes from 'prop-types';
 import { useForm } from 'react-hook-form';
 
 import AdminMarkDownContainer from '~/client/services/AdminMarkDownContainer';
-import { toastSuccess, toastError } from '~/client/util/toastr';
+import { toastError, toastSuccess } from '~/client/util/toastr';
 import { RehypeSanitizeType } from '~/interfaces/services/rehype-sanitize';
-import { tagNames as recommendedTagNames, attributes as recommendedAttributes } from '~/services/renderer/recommended-whitelist';
+import {
+  attributes as recommendedAttributes,
+  tagNames as recommendedTagNames,
+} from '~/services/renderer/recommended-whitelist';
 import loggerFactory from '~/utils/logger';
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
-
 import { WhitelistInput } from './WhitelistInput';
 
 const logger = loggerFactory('growi:importer');
 
 const XssForm = (props) => {
   const { t, adminMarkDownContainer } = props;
-  const {
-    xssOption, tagWhitelist, attrWhitelist, retrieveError,
-  } = adminMarkDownContainer.state;
+  const { xssOption, tagWhitelist, attrWhitelist, retrieveError } =
+    adminMarkDownContainer.state;
 
-  const {
-    register, handleSubmit, reset, setValue,
-  } = useForm();
+  const { register, handleSubmit, reset, setValue } = useForm();
 
   // Sync form with container state
   useEffect(() => {
@@ -35,28 +33,37 @@ const XssForm = (props) => {
     });
   }, [reset, tagWhitelist, attrWhitelist]);
 
-  const onClickSubmit = useCallback(async(data) => {
-    try {
-      await adminMarkDownContainer.setState({ tagWhitelist: data.tagWhitelist ?? '' });
-      await adminMarkDownContainer.setState({ attrWhitelist: data.attrWhitelist ?? '' });
-      await adminMarkDownContainer.updateXssSetting();
-      toastSuccess(t('toaster.update_successed', { target: t('markdown_settings.xss_header'), ns: 'commons' }));
-    }
-    catch (err) {
-      toastError(err);
-      logger.error(err);
-    }
-  }, [adminMarkDownContainer, t]);
+  const onClickSubmit = useCallback(
+    async (data) => {
+      try {
+        await adminMarkDownContainer.setState({
+          tagWhitelist: data.tagWhitelist ?? '',
+        });
+        await adminMarkDownContainer.setState({
+          attrWhitelist: data.attrWhitelist ?? '',
+        });
+        await adminMarkDownContainer.updateXssSetting();
+        toastSuccess(
+          t('toaster.update_successed', {
+            target: t('markdown_settings.xss_header'),
+            ns: 'commons',
+          }),
+        );
+      } catch (err) {
+        toastError(err);
+        logger.error(err);
+      }
+    },
+    [adminMarkDownContainer, t],
+  );
 
   const xssOptions = useCallback(() => {
-
     const rehypeRecommendedTags = recommendedTagNames.join(',');
     const rehypeRecommendedAttributes = JSON.stringify(recommendedAttributes);
 
     return (
       <div className="col-12 mt-3">
         <div className="row">
-
           <div className="col-md-6 col-sm-12 align-self-start">
             <div className="form-check">
               <input
@@ -65,10 +72,19 @@ const XssForm = (props) => {
                 id="xssOption1"
                 name="XssOption"
                 checked={xssOption === RehypeSanitizeType.RECOMMENDED}
-                onChange={() => { adminMarkDownContainer.setState({ xssOption: RehypeSanitizeType.RECOMMENDED }) }}
+                onChange={() => {
+                  adminMarkDownContainer.setState({
+                    xssOption: RehypeSanitizeType.RECOMMENDED,
+                  });
+                }}
               />
-              <label className="form-label form-check-label w-100" htmlFor="xssOption1">
-                <p className="fw-bold">{t('markdown_settings.xss_options.recommended_setting')}</p>
+              <label
+                className="form-label form-check-label w-100"
+                htmlFor="xssOption1"
+              >
+                <p className="fw-bold">
+                  {t('markdown_settings.xss_options.recommended_setting')}
+                </p>
                 <div className="mt-4">
                   <div className="d-flex justify-content-between">
                     {t('markdown_settings.xss_options.tag_names')}
@@ -107,11 +123,24 @@ const XssForm = (props) => {
                 id="xssOption2"
                 name="XssOption"
                 checked={xssOption === RehypeSanitizeType.CUSTOM}
-                onChange={() => { adminMarkDownContainer.setState({ xssOption: RehypeSanitizeType.CUSTOM }) }}
+                onChange={() => {
+                  adminMarkDownContainer.setState({
+                    xssOption: RehypeSanitizeType.CUSTOM,
+                  });
+                }}
               />
-              <label className="form-label form-check-label w-100" htmlFor="xssOption2">
-                <p className="fw-bold">{t('markdown_settings.xss_options.custom_whitelist')}</p>
-                <WhitelistInput adminMarkDownContainer={adminMarkDownContainer} register={register} setValue={setValue} />
+              <label
+                className="form-label form-check-label w-100"
+                htmlFor="xssOption2"
+              >
+                <p className="fw-bold">
+                  {t('markdown_settings.xss_options.custom_whitelist')}
+                </p>
+                <WhitelistInput
+                  adminMarkDownContainer={adminMarkDownContainer}
+                  register={register}
+                  setValue={setValue}
+                />
               </label>
             </div>
           </div>
@@ -137,16 +166,17 @@ const XssForm = (props) => {
                   checked={isEnabledXss}
                   onChange={adminMarkDownContainer.switchEnableXss}
                 />
-                <label className="form-label form-check-label w-100" htmlFor="XssEnable">
+                <label
+                  className="form-label form-check-label w-100"
+                  htmlFor="XssEnable"
+                >
                   {t('markdown_settings.xss_options.enable_xss_prevention')}
                 </label>
               </div>
             </div>
           </div>
 
-          <div className="col-12">
-            {isEnabledXss && xssOptions()}
-          </div>
+          <div className="col-12">{isEnabledXss && xssOptions()}</div>
         </fieldset>
         <AdminUpdateButtonRow
           disabled={retrieveError != null}
@@ -159,7 +189,8 @@ const XssForm = (props) => {
 
 XssForm.propTypes = {
   t: PropTypes.func.isRequired, // i18next
-  adminMarkDownContainer: PropTypes.instanceOf(AdminMarkDownContainer).isRequired,
+  adminMarkDownContainer: PropTypes.instanceOf(AdminMarkDownContainer)
+    .isRequired,
 };
 
 const XssFormWrapperFC = (props) => {
@@ -168,6 +199,8 @@ const XssFormWrapperFC = (props) => {
   return <XssForm t={t} {...props} />;
 };
 
-const XssFormWrapper = withUnstatedContainers(XssFormWrapperFC, [AdminMarkDownContainer]);
+const XssFormWrapper = withUnstatedContainers(XssFormWrapperFC, [
+  AdminMarkDownContainer,
+]);
 
 export default XssFormWrapper;

+ 1 - 4
apps/app/src/client/components/Admin/NotFoundPage.tsx

@@ -1,11 +1,8 @@
 import React, { type JSX } from 'react';
-
 import { useTranslation } from 'next-i18next';
 
 export const AdminNotFoundPage = (): JSX.Element => {
   const { t } = useTranslation('commons');
 
-  return (
-    <h1 className="title">{t('not_found_page.page_not_exist')}</h1>
-  );
+  return <h1 className="title">{t('not_found_page.page_not_exist')}</h1>;
 };

+ 65 - 57
apps/app/src/client/components/Admin/UserManagement.tsx

@@ -1,16 +1,13 @@
-import React, {
-  useEffect, useState, useRef, useCallback,
-} from 'react';
-
-import { useTranslation } from 'next-i18next';
+import type React from 'react';
+import { useCallback, useEffect, useRef, useState } from 'react';
 import Link from 'next/link';
+import { useTranslation } from 'next-i18next';
 
 import AdminUsersContainer from '~/client/services/AdminUsersContainer';
 import { toastError } from '~/client/util/toastr';
 
 import PaginationWrapper from '../PaginationWrapper';
 import { withUnstatedContainers } from '../UnstatedUtils';
-
 import InviteUserControl from './Users/InviteUserControl';
 import PasswordResetModal from './Users/PasswordResetModal';
 import UserStatisticsTable from './Users/UserStatisticsTable';
@@ -19,24 +16,25 @@ import UserTable from './Users/UserTable';
 import styles from './UserManagement.module.scss';
 
 type UserManagementProps = {
-  adminUsersContainer: AdminUsersContainer
-}
+  adminUsersContainer: AdminUsersContainer;
+};
 
 const UserManagement = (props: UserManagementProps) => {
-
   const { t } = useTranslation('admin');
   const { adminUsersContainer } = props;
   const [isNotifyCommentShow, setIsNotifyCommentShow] = useState(false);
   const inputRef = useRef<HTMLInputElement>(null);
 
-  const pagingHandler = useCallback(async(selectedPage: number) => {
-    try {
-      await adminUsersContainer.retrieveUsersByPagingNum(selectedPage);
-    }
-    catch (err) {
-      toastError(err);
-    }
-  }, [adminUsersContainer]);
+  const pagingHandler = useCallback(
+    async (selectedPage: number) => {
+      try {
+        await adminUsersContainer.retrieveUsersByPagingNum(selectedPage);
+      } catch (err) {
+        toastError(err);
+      }
+    },
+    [adminUsersContainer],
+  );
 
   // for Next routing
   useEffect(() => {
@@ -45,12 +43,9 @@ const UserManagement = (props: UserManagementProps) => {
   }, [pagingHandler, adminUsersContainer]);
 
   const validateToggleStatus = (statusType: string) => {
-    return (adminUsersContainer.isSelected(statusType)) ? (
-      adminUsersContainer.state.selectedStatusList.size > 1
-    )
-      : (
-        true
-      );
+    return adminUsersContainer.isSelected(statusType)
+      ? adminUsersContainer.state.selectedStatusList.size > 1
+      : true;
   };
 
   const clickHandler = (statusType: string) => {
@@ -64,24 +59,30 @@ const UserManagement = (props: UserManagementProps) => {
     adminUsersContainer.handleClick(statusType);
   };
 
-  const resetButtonClickHandler = useCallback(async() => {
+  const resetButtonClickHandler = useCallback(async () => {
     try {
       await adminUsersContainer.resetAllChanges();
       setIsNotifyCommentShow(false);
       if (inputRef.current != null) {
         inputRef.current.value = '';
       }
-    }
-    catch (err) {
+    } catch (err) {
       toastError(err);
     }
   }, [adminUsersContainer]);
 
-  const changeSearchTextHandler = useCallback(async(e: React.FormEvent<HTMLInputElement>) => {
-    await adminUsersContainer.handleChangeSearchText(e?.currentTarget.value);
-  }, [adminUsersContainer]);
+  const changeSearchTextHandler = useCallback(
+    async (e: React.FormEvent<HTMLInputElement>) => {
+      await adminUsersContainer.handleChangeSearchText(e?.currentTarget.value);
+    },
+    [adminUsersContainer],
+  );
 
-  const renderCheckbox = (status: string, statusLabel: string, statusColor: string) => {
+  const renderCheckbox = (
+    status: string,
+    statusLabel: string,
+    statusColor: string,
+  ) => {
     return (
       <div className={`form-check form-check-${statusColor} me-2`}>
         <input
@@ -92,7 +93,9 @@ const UserManagement = (props: UserManagementProps) => {
           onChange={() => clickHandler(status)}
         />
         <label className="form-label form-check-label" htmlFor={`c_${status}`}>
-          <span className={`badge text-bg-${statusColor} d-inline-block vt mt-1`}>
+          <span
+            className={`badge text-bg-${statusColor} d-inline-block vt mt-1`}
+          >
             {statusLabel}
           </span>
         </label>
@@ -115,14 +118,15 @@ const UserManagement = (props: UserManagementProps) => {
 
   return (
     <div data-testid="admin-users">
-      { adminUsersContainer.state.userForPasswordResetModal != null
-      && (
+      {adminUsersContainer.state.userForPasswordResetModal != null && (
         <PasswordResetModal
           isOpen={adminUsersContainer.state.isPasswordResetModalShown}
           onClose={adminUsersContainer.hidePasswordResetModal}
-          userForPasswordResetModal={adminUsersContainer.state.userForPasswordResetModal}
+          userForPasswordResetModal={
+            adminUsersContainer.state.userForPasswordResetModal
+          }
         />
-      ) }
+      )}
       <p>
         <InviteUserControl />
         <Link
@@ -130,7 +134,9 @@ const UserManagement = (props: UserManagementProps) => {
           className="btn btn-outline-secondary ms-2"
           role="button"
         >
-          <span className="material-symbols-outlined" aria-hidden="true">person_add</span>
+          <span className="material-symbols-outlined" aria-hidden="true">
+            person_add
+          </span>
           {t('admin:user_management.external_account')}
         </Link>
       </p>
@@ -140,7 +146,6 @@ const UserManagement = (props: UserManagementProps) => {
         userStatistics={adminUsersContainer.state.userStatistics}
       />
       <div className="border-top border-bottom">
-
         <div className="row d-flex justify-content-start align-items-center my-2">
           <div className="col-md-3 d-flex align-items-center my-2">
             <span className="material-symbols-outlined">search</span>
@@ -151,22 +156,21 @@ const UserManagement = (props: UserManagementProps) => {
                 ref={inputRef}
                 onChange={changeSearchTextHandler}
               />
-              {
-                adminUsersContainer.state.searchText.length > 0
-                  ? (
-                    <span
-                      className="material-symbols-outlined me-1 search-clear"
-                      onClick={async() => {
-                        await adminUsersContainer.clearSearchText();
-                        if (inputRef.current != null) {
-                          inputRef.current.value = '';
-                        }
-                      }}
-                    >cancel
-                    </span>
-                  )
-                  : ''
-              }
+              {adminUsersContainer.state.searchText.length > 0 ? (
+                <span
+                  className="material-symbols-outlined me-1 search-clear"
+                  onClick={async () => {
+                    await adminUsersContainer.clearSearchText();
+                    if (inputRef.current != null) {
+                      inputRef.current.value = '';
+                    }
+                  }}
+                >
+                  cancel
+                </span>
+              ) : (
+                ''
+              )}
             </span>
           </div>
 
@@ -179,7 +183,11 @@ const UserManagement = (props: UserManagementProps) => {
               {renderCheckbox('invited', 'Invited', 'secondary')}
             </div>
             <div>
-              { isNotifyCommentShow && <span className="text-warning">{t('admin:user_management.click_twice_same_checkbox')}</span> }
+              {isNotifyCommentShow && (
+                <span className="text-warning">
+                  {t('admin:user_management.click_twice_same_checkbox')}
+                </span>
+              )}
             </div>
           </div>
 
@@ -199,12 +207,12 @@ const UserManagement = (props: UserManagementProps) => {
       {pager}
       <UserTable />
       {pager}
-
     </div>
   );
-
 };
 
-const UserManagementWrapper = withUnstatedContainers(UserManagement, [AdminUsersContainer]);
+const UserManagementWrapper = withUnstatedContainers(UserManagement, [
+  AdminUsersContainer,
+]);
 
 export default UserManagementWrapper;

+ 5 - 10
biome.json

@@ -28,20 +28,15 @@
       "!apps/slackbot-proxy/src/public/bootstrap",
       "!packages/pdf-converter-client/src/index.ts",
       "!packages/pdf-converter-client/specs",
-      "!apps/app/src/client/components/Admin/*.ts",
-      "!apps/app/src/client/components/Admin/*.tsx",
-      "!apps/app/src/client/components/Admin/*.scss",
-      "!apps/app/src/client/components/Admin/AdminHome",
       "!apps/app/src/client/components/Admin/AuditLog",
-      "!apps/app/src/client/components/Admin/Common",
+      "!apps/app/src/client/components/Admin/App",
       "!apps/app/src/client/components/Admin/Customize",
-      "!apps/app/src/client/components/Admin/ElasticsearchManagement",
-      "!apps/app/src/client/components/Admin/ExportArchiveData",
-      "!apps/app/src/client/components/Admin/ImportData",
-      "!apps/app/src/client/components/Admin/LegacySlackIntegration",
-      "!apps/app/src/client/components/Admin/MarkdownSetting",
       "!apps/app/src/client/components/Admin/Notification",
       "!apps/app/src/client/components/Admin/Security",
+      "!apps/app/src/client/components/Admin/SlackIntegration",
+      "!apps/app/src/client/components/Admin/UserGroup",
+      "!apps/app/src/client/components/Admin/UserGroupDetail",
+      "!apps/app/src/client/components/Admin/Users",
       "!apps/app/src/client/components/Bookmarks",
       "!apps/app/src/client/components/DescendantsPageListModal",
       "!apps/app/src/client/components/InAppNotification",