Browse Source

configure biome for Admin security/compliance dirs

Futa Arai 3 tháng trước cách đây
mục cha
commit
b4946f2c0f
58 tập tin đã thay đổi với 3604 bổ sung1789 xóa
  1. 4 0
      apps/app/.eslintrc.js
  2. 32 13
      apps/app/src/client/components/Admin/AuditLog/ActivityTable.tsx
  3. 9 4
      apps/app/src/client/components/Admin/AuditLog/AuditLogDisableMode.tsx
  4. 35 14
      apps/app/src/client/components/Admin/AuditLog/AuditLogSettings.tsx
  5. 49 44
      apps/app/src/client/components/Admin/AuditLog/DateRangePicker.tsx
  6. 52 26
      apps/app/src/client/components/Admin/AuditLog/SearchUsernameTypeahead.tsx
  7. 147 88
      apps/app/src/client/components/Admin/AuditLog/SelectActionDropdown.tsx
  8. 8 10
      apps/app/src/client/components/Admin/Customize/Customize.jsx
  9. 36 28
      apps/app/src/client/components/Admin/Customize/CustomizeCssSetting.tsx
  10. 7 11
      apps/app/src/client/components/Admin/Customize/CustomizeFunctionOption.tsx
  11. 105 44
      apps/app/src/client/components/Admin/Customize/CustomizeFunctionSetting.tsx
  12. 28 15
      apps/app/src/client/components/Admin/Customize/CustomizeLayoutSetting.tsx
  13. 87 42
      apps/app/src/client/components/Admin/Customize/CustomizeLogoSetting.tsx
  14. 48 33
      apps/app/src/client/components/Admin/Customize/CustomizeNoscriptSetting.tsx
  15. 36 18
      apps/app/src/client/components/Admin/Customize/CustomizePresentationSetting.tsx
  16. 47 33
      apps/app/src/client/components/Admin/Customize/CustomizeScriptSetting.tsx
  17. 24 21
      apps/app/src/client/components/Admin/Customize/CustomizeSidebarSetting.tsx
  18. 19 15
      apps/app/src/client/components/Admin/Customize/CustomizeThemeOptions.tsx
  19. 25 19
      apps/app/src/client/components/Admin/Customize/CustomizeThemeSetting.tsx
  20. 58 30
      apps/app/src/client/components/Admin/Customize/CustomizeTitle.tsx
  21. 10 9
      apps/app/src/client/components/Admin/Customize/PagingSizeUncontrolledDropdown.jsx
  22. 45 19
      apps/app/src/client/components/Admin/Customize/ThemeColorBox.tsx
  23. 67 24
      apps/app/src/client/components/Admin/Notification/GlobalNotification.jsx
  24. 91 40
      apps/app/src/client/components/Admin/Notification/GlobalNotificationList.jsx
  25. 156 101
      apps/app/src/client/components/Admin/Notification/ManageGlobalNotification.tsx
  26. 18 11
      apps/app/src/client/components/Admin/Notification/NotificationDeleteModal.jsx
  27. 77 42
      apps/app/src/client/components/Admin/Notification/NotificationSetting.jsx
  28. 8 6
      apps/app/src/client/components/Admin/Notification/NotificationTypeIcon.tsx
  29. 5 5
      apps/app/src/client/components/Admin/Notification/TriggerEventCheckBox.jsx
  30. 18 13
      apps/app/src/client/components/Admin/Notification/UserNotificationRow.jsx
  31. 64 29
      apps/app/src/client/components/Admin/Notification/UserTriggerNotification.jsx
  32. 7 10
      apps/app/src/client/components/Admin/Security/DeleteAllShareLinksModal.jsx
  33. 10 12
      apps/app/src/client/components/Admin/Security/GitHubSecuritySetting.jsx
  34. 146 57
      apps/app/src/client/components/Admin/Security/GitHubSecuritySettingContents.tsx
  35. 10 12
      apps/app/src/client/components/Admin/Security/GoogleSecuritySetting.jsx
  36. 157 59
      apps/app/src/client/components/Admin/Security/GoogleSecuritySettingContents.tsx
  37. 53 31
      apps/app/src/client/components/Admin/Security/LdapAuthTest.tsx
  38. 1 12
      apps/app/src/client/components/Admin/Security/LdapAuthTestModal.jsx
  39. 9 10
      apps/app/src/client/components/Admin/Security/LdapSecuritySetting.jsx
  40. 296 130
      apps/app/src/client/components/Admin/Security/LdapSecuritySettingContents.tsx
  41. 9 11
      apps/app/src/client/components/Admin/Security/LocalSecuritySetting.jsx
  42. 118 55
      apps/app/src/client/components/Admin/Security/LocalSecuritySettingContents.tsx
  43. 9 10
      apps/app/src/client/components/Admin/Security/OidcSecuritySetting.jsx
  44. 360 106
      apps/app/src/client/components/Admin/Security/OidcSecuritySettingContents.tsx
  45. 9 10
      apps/app/src/client/components/Admin/Security/SamlSecuritySetting.jsx
  46. 332 114
      apps/app/src/client/components/Admin/Security/SamlSecuritySettingContents.tsx
  47. 9 8
      apps/app/src/client/components/Admin/Security/SecurityManagement.tsx
  48. 27 15
      apps/app/src/client/components/Admin/Security/SecurityManagementContents.jsx
  49. 19 6
      apps/app/src/client/components/Admin/Security/SecuritySetting/CommentManageRightsSettings.tsx
  50. 17 6
      apps/app/src/client/components/Admin/Security/SecuritySetting/PageAccessRightsSettings.tsx
  51. 320 170
      apps/app/src/client/components/Admin/Security/SecuritySetting/PageDeleteRightsSettings.tsx
  52. 45 16
      apps/app/src/client/components/Admin/Security/SecuritySetting/PageListDisplaySettings.tsx
  53. 9 4
      apps/app/src/client/components/Admin/Security/SecuritySetting/SessionMaxAgeSettings.tsx
  54. 43 13
      apps/app/src/client/components/Admin/Security/SecuritySetting/UserHomepageDeletionSettings.tsx
  55. 81 37
      apps/app/src/client/components/Admin/Security/SecuritySetting/index.tsx
  56. 22 7
      apps/app/src/client/components/Admin/Security/SecuritySetting/types.ts
  57. 71 57
      apps/app/src/client/components/Admin/Security/ShareLinkSetting.tsx
  58. 0 4
      biome.json

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

@@ -42,6 +42,10 @@ module.exports = {
     'src/client/components/*.ts',
     'src/client/components/*.js',
     'src/client/components/Admin/App/**',
+    'src/client/components/Admin/AuditLog/**',
+    'src/client/components/Admin/Customize/**',
+    'src/client/components/Admin/Notification/**',
+    'src/client/components/Admin/Security/**',
     'src/client/components/Admin/SlackIntegration/**',
     'src/client/components/Admin/Users/**',
     'src/client/components/Admin/UserGroup/**',

+ 32 - 13
apps/app/src/client/components/Admin/AuditLog/ActivityTable.tsx

@@ -1,6 +1,5 @@
 import type { FC } from 'react';
-import React, { useState, useCallback } from 'react';
-
+import React, { useCallback, useState } from 'react';
 import { isPopulated } from '@growi/core';
 import { pagePathUtils } from '@growi/core/dist/utils';
 import { UserPicture } from '@growi/ui/dist/components';
@@ -11,19 +10,18 @@ import { Tooltip } from 'reactstrap';
 
 import type { IActivityHasId } from '~/interfaces/activity';
 
- type Props = {
-   activityList: IActivityHasId[]
- }
+type Props = {
+  activityList: IActivityHasId[];
+};
 
 const formatDate = (date: Date): string => {
   return format(new Date(date), 'yyyy/MM/dd HH:mm:ss');
 };
 
-export const ActivityTable : FC<Props> = (props: Props) => {
+export const ActivityTable: FC<Props> = (props: Props) => {
   const { t } = useTranslation();
   const [activeTooltipId, setActiveTooltipId] = useState<string | null>(null);
 
-
   const showToolTip = useCallback((id: string) => {
     setActiveTooltipId(id);
     setTimeout(() => {
@@ -48,12 +46,16 @@ export const ActivityTable : FC<Props> = (props: Props) => {
             return (
               <tr data-testid="activity-table" key={activity._id}>
                 <td>
-                  { activity.user != null && (
+                  {activity.user != null && (
                     <>
                       <UserPicture user={activity.user} />
                       <a
                         className="ms-2"
-                        href={isPopulated(activity.user) ? pagePathUtils.userHomepagePath(activity.user) : undefined}
+                        href={
+                          isPopulated(activity.user)
+                            ? pagePathUtils.userHomepagePath(activity.user)
+                            : undefined
+                        }
                       >
                         {activity.snapshot?.username}
                       </a>
@@ -68,12 +70,29 @@ export const ActivityTable : FC<Props> = (props: Props) => {
                     <span className="flex-grow-1 text-truncate">
                       {activity.endpoint}
                     </span>
-                    <CopyToClipboard text={activity.endpoint} onCopy={() => showToolTip(activity._id)}>
-                      <button type="button" className="btn btn-outline-secondary border-0 ms-2" id={`tooltipTarget-${activity._id}`}>
-                        <span className="material-symbols-outlined" aria-hidden="true">content_paste</span>
+                    <CopyToClipboard
+                      text={activity.endpoint}
+                      onCopy={() => showToolTip(activity._id)}
+                    >
+                      <button
+                        type="button"
+                        className="btn btn-outline-secondary border-0 ms-2"
+                        id={`tooltipTarget-${activity._id}`}
+                      >
+                        <span
+                          className="material-symbols-outlined"
+                          aria-hidden="true"
+                        >
+                          content_paste
+                        </span>
                       </button>
                     </CopyToClipboard>
-                    <Tooltip placement="top" isOpen={activeTooltipId === activity._id} fade={false} target={`tooltipTarget-${activity._id}`}>
+                    <Tooltip
+                      placement="top"
+                      isOpen={activeTooltipId === activity._id}
+                      fade={false}
+                      target={`tooltipTarget-${activity._id}`}
+                    >
                       copied!
                     </Tooltip>
                   </div>

+ 9 - 4
apps/app/src/client/components/Admin/AuditLog/AuditLogDisableMode.tsx

@@ -1,6 +1,5 @@
 import type { FC } from 'react';
 import React from 'react';
-
 import { useTranslation } from 'react-i18next';
 
 export const AuditLogDisableMode: FC = () => {
@@ -13,11 +12,17 @@ export const AuditLogDisableMode: FC = () => {
           <div className="col-md-6 mt-5">
             <div className="text-center">
               {/* error icon large */}
-              <h1><span className="material-symbols-outlined">error</span></h1>
-              <h1 className="text-center">{t('audit_log_management.audit_log')}</h1>
+              <h1>
+                <span className="material-symbols-outlined">error</span>
+              </h1>
+              <h1 className="text-center">
+                {t('audit_log_management.audit_log')}
+              </h1>
               <h3
                 // eslint-disable-next-line react/no-danger
-                dangerouslySetInnerHTML={{ __html: t('audit_log_management.disable_mode_explanation') }}
+                dangerouslySetInnerHTML={{
+                  __html: t('audit_log_management.disable_mode_explanation'),
+                }}
               />
             </div>
           </div>

+ 35 - 14
apps/app/src/client/components/Admin/AuditLog/AuditLogSettings.tsx

@@ -1,36 +1,44 @@
 import type { FC } from 'react';
 import React, { useState } from 'react';
-
 import { useAtomValue } from 'jotai';
 import { useTranslation } from 'react-i18next';
 import { Collapse } from 'reactstrap';
 
 import { AllSupportedActions } from '~/interfaces/activity';
-import { activityExpirationSecondsAtom, auditLogAvailableActionsAtom } from '~/states/server-configurations';
+import {
+  activityExpirationSecondsAtom,
+  auditLogAvailableActionsAtom,
+} from '~/states/server-configurations';
 
 export const AuditLogSettings: FC = () => {
   const { t } = useTranslation();
 
   const [isExpandActionList, setIsExpandActionList] = useState(false);
 
-  const activityExpirationSeconds = useAtomValue(activityExpirationSecondsAtom) || 2592000;
+  const activityExpirationSeconds =
+    useAtomValue(activityExpirationSecondsAtom) || 2592000;
 
   const availableActions = useAtomValue(auditLogAvailableActionsAtom);
 
   return (
     <>
-      <h4 className="mt-4">{t('admin:audit_log_management.activity_expiration_date')}</h4>
+      <h4 className="mt-4">
+        {t('admin:audit_log_management.activity_expiration_date')}
+      </h4>
       <p className="form-text text-muted">
         {t('admin:audit_log_management.activity_expiration_date_explanation')}
       </p>
       <p className="alert alert-warning col-6">
         <span className="material-symbols-outlined">error</span>
-        <b>FIXED</b><br />
+        <b>FIXED</b>
+        <br />
         <b
           // eslint-disable-next-line react/no-danger
           dangerouslySetInnerHTML={{
-            __html: t('admin:audit_log_management.fixed_by_env_var',
-              { key: 'ACTIVITY_EXPIRATION_SECONDS', value: activityExpirationSeconds }),
+            __html: t('admin:audit_log_management.fixed_by_env_var', {
+              key: 'ACTIVITY_EXPIRATION_SECONDS',
+              value: activityExpirationSeconds,
+            }),
           }}
         />
       </p>
@@ -46,23 +54,36 @@ export const AuditLogSettings: FC = () => {
           target="_blank"
           rel="noopener noreferrer"
         >
-          <span className="material-symbols-outlined" aria-hidden="true">help</span>
+          <span className="material-symbols-outlined" aria-hidden="true">
+            help
+          </span>
         </a>
       </h4>
       <p className="form-text text-muted">
         {t('admin:audit_log_management.available_action_list_explanation')}
       </p>
       <p className="mt-1">
-        <button type="button" className="btn btn-link p-0" aria-expanded="false" onClick={() => setIsExpandActionList(!isExpandActionList)}>
-          <span className={`material-symbols-outlined me-1 ${isExpandActionList ? 'rotate-90' : ''}`}>navigate_next</span>
-          { t('admin:audit_log_management.action_list') }
+        <button
+          type="button"
+          className="btn btn-link p-0"
+          aria-expanded="false"
+          onClick={() => setIsExpandActionList(!isExpandActionList)}
+        >
+          <span
+            className={`material-symbols-outlined me-1 ${isExpandActionList ? 'rotate-90' : ''}`}
+          >
+            navigate_next
+          </span>
+          {t('admin:audit_log_management.action_list')}
         </button>
       </p>
       <Collapse isOpen={isExpandActionList}>
         <ul className="list-group">
-          { availableActions.map(action => (
-            <li key={action} className="list-group-item">{t(`admin:audit_log_action.${action}`)}</li>
-          )) }
+          {availableActions.map((action) => (
+            <li key={action} className="list-group-item">
+              {t(`admin:audit_log_action.${action}`)}
+            </li>
+          ))}
         </ul>
       </Collapse>
     </>

+ 49 - 44
apps/app/src/client/components/Admin/AuditLog/DateRangePicker.tsx

@@ -1,64 +1,69 @@
 import type { FC } from 'react';
 import React, { forwardRef, useCallback } from 'react';
-
 import { addDays, format } from 'date-fns';
 import DatePicker from 'react-datepicker';
 import 'react-datepicker/dist/react-datepicker.css';
 
-
 type CustomInputProps = {
-  value?: string
-  onChange?: () => void
-  onFocus?: () => void
-}
+  value?: string;
+  onChange?: () => void;
+  onFocus?: () => void;
+};
 
-const CustomInput = forwardRef<HTMLInputElement, CustomInputProps>((props: CustomInputProps, ref) => {
-  const dateFormat = 'MM/dd/yyyy';
-  const date = new Date();
-  const placeholder = `${format(date, dateFormat)} - ${format(addDays(date, 1), dateFormat)}`;
+const CustomInput = forwardRef<HTMLInputElement, CustomInputProps>(
+  (props: CustomInputProps, ref) => {
+    const dateFormat = 'MM/dd/yyyy';
+    const date = new Date();
+    const placeholder = `${format(date, dateFormat)} - ${format(addDays(date, 1), dateFormat)}`;
 
-  return (
-    <div className="input-group admin-audit-log">
-      <span className="input-group-text">
-        <span className="material-symbols-outlined me-1">calendar_month</span>
-      </span>
-      <input
-        ref={ref}
-        type="text"
-        value={props?.value}
-        onFocus={props?.onFocus}
-        onChange={props?.onChange}
-        placeholder={placeholder}
-        className="form-control date-range-picker"
-        aria-describedby="basic-addon1"
-      />
-    </div>
-  );
-});
+    return (
+      <div className="input-group admin-audit-log">
+        <span className="input-group-text">
+          <span className="material-symbols-outlined me-1">calendar_month</span>
+        </span>
+        <input
+          ref={ref}
+          type="text"
+          value={props?.value}
+          onFocus={props?.onFocus}
+          onChange={props?.onChange}
+          placeholder={placeholder}
+          className="form-control date-range-picker"
+          aria-describedby="basic-addon1"
+        />
+      </div>
+    );
+  },
+);
 
 CustomInput.displayName = 'CustomInput';
 
 type DateRangePickerProps = {
-  startDate: Date | null
-  endDate: Date | null
-  onChange: (dateList: Date[] | null[]) => void
-}
+  startDate: Date | null;
+  endDate: Date | null;
+  onChange: (dateList: Date[] | null[]) => void;
+};
 
-export const DateRangePicker: FC<DateRangePickerProps> = (props: DateRangePickerProps) => {
+export const DateRangePicker: FC<DateRangePickerProps> = (
+  props: DateRangePickerProps,
+) => {
   const { startDate, endDate, onChange } = props;
 
-  const changeHandler = useCallback((dateList: Date[] | null[]) => {
-    if (onChange != null) {
-      const [start, end] = dateList;
-      const isSameTime = (start != null && end != null) && (start.getTime() === end.getTime());
-      if (isSameTime) {
-        onChange([null, null]);
+  const changeHandler = useCallback(
+    (dateList: Date[] | null[]) => {
+      if (onChange != null) {
+        const [start, end] = dateList;
+        const isSameTime =
+          start != null && end != null && start.getTime() === end.getTime();
+        if (isSameTime) {
+          onChange([null, null]);
+        } else {
+          onChange(dateList);
+        }
       }
-      else {
-        onChange(dateList);
-      }
-    }
-  }, [onChange]);
+    },
+    [onChange],
+  );
 
   return (
     <div className="me-2">

+ 52 - 26
apps/app/src/client/components/Admin/AuditLog/SearchUsernameTypeahead.tsx

@@ -1,8 +1,12 @@
 import type { ForwardRefRenderFunction } from 'react';
 import React, {
-  Fragment, useState, useCallback, forwardRef, useRef, useImperativeHandle,
+  Fragment,
+  forwardRef,
+  useCallback,
+  useImperativeHandle,
+  useRef,
+  useState,
 } from 'react';
-
 import type { TypeaheadRef } from 'react-bootstrap-typeahead';
 import { AsyncTypeahead, Menu, MenuItem } from 'react-bootstrap-typeahead';
 import { useTranslation } from 'react-i18next';
@@ -10,25 +14,27 @@ import { useTranslation } from 'react-i18next';
 import type { IClearable } from '~/client/interfaces/clearable';
 import { useSWRxUsernames } from '~/stores/user';
 
-
 const Categories = {
   activeUser: 'Active User',
   inactiveUser: 'Inactive User',
   activitySnapshotUser: 'Activity Snapshot User',
 } as const;
 
-type CategoryType = typeof Categories[keyof typeof Categories]
+type CategoryType = (typeof Categories)[keyof typeof Categories];
 
 type UserDataType = {
-  username: string
-  category: CategoryType
-}
+  username: string;
+  category: CategoryType;
+};
 
 type Props = {
-  onChange: (text: string[]) => void
-}
+  onChange: (text: string[]) => void;
+};
 
-const SearchUsernameTypeaheadSubstance: ForwardRefRenderFunction<IClearable, Props> = ((props: Props, ref) => {
+const SearchUsernameTypeaheadSubstance: ForwardRefRenderFunction<
+  IClearable,
+  Props
+> = (props: Props, ref) => {
   const { onChange } = props;
   const { t } = useTranslation();
 
@@ -42,16 +48,33 @@ const SearchUsernameTypeaheadSubstance: ForwardRefRenderFunction<IClearable, Pro
   /*
    * Fetch
    */
-  const requestOptions = { isIncludeActiveUser: true, isIncludeInactiveUser: true, isIncludeActivitySnapshotUser: true };
-  const { data: usernameData, error, isLoading: _isLoading } = useSWRxUsernames(searchKeyword, 0, 5, requestOptions);
-  const activeUsernames = usernameData?.activeUser?.usernames != null ? usernameData.activeUser.usernames : [];
-  const inactiveUsernames = usernameData?.inactiveUser?.usernames != null ? usernameData.inactiveUser.usernames : [];
-  const activitySnapshotUsernames = usernameData?.activitySnapshotUser?.usernames != null ? usernameData.activitySnapshotUser.usernames : [];
+  const requestOptions = {
+    isIncludeActiveUser: true,
+    isIncludeInactiveUser: true,
+    isIncludeActivitySnapshotUser: true,
+  };
+  const {
+    data: usernameData,
+    error,
+    isLoading: _isLoading,
+  } = useSWRxUsernames(searchKeyword, 0, 5, requestOptions);
+  const activeUsernames =
+    usernameData?.activeUser?.usernames != null
+      ? usernameData.activeUser.usernames
+      : [];
+  const inactiveUsernames =
+    usernameData?.inactiveUser?.usernames != null
+      ? usernameData.inactiveUser.usernames
+      : [];
+  const activitySnapshotUsernames =
+    usernameData?.activitySnapshotUser?.usernames != null
+      ? usernameData.activitySnapshotUser.usernames
+      : [];
   const isLoading = _isLoading === true && error == null;
 
   const allUser: UserDataType[] = [];
   const pushToAllUser = (usernames: string[], category: CategoryType) => {
-    usernames.forEach(username => allUser.push({ username, category }));
+    usernames.forEach((username) => allUser.push({ username, category }));
   };
   pushToAllUser(activeUsernames, Categories.activeUser);
   pushToAllUser(inactiveUsernames, Categories.inactiveUser);
@@ -60,10 +83,13 @@ const SearchUsernameTypeaheadSubstance: ForwardRefRenderFunction<IClearable, Pro
   /*
    * Functions
    */
-  const changeHandler = useCallback((userData: UserDataType[]) => {
-    const usernames = userData.map(user => user.username);
-    onChange(usernames);
-  }, [onChange]);
+  const changeHandler = useCallback(
+    (userData: UserDataType[]) => {
+      const usernames = userData.map((user) => user.username);
+      onChange(usernames);
+    },
+    [onChange],
+  );
 
   const searchHandler = useCallback((text: string) => {
     setSearchKeyword(text);
@@ -76,7 +102,7 @@ const SearchUsernameTypeaheadSubstance: ForwardRefRenderFunction<IClearable, Pro
 
     let index = 0;
     const items = Object.values(Categories).map((category) => {
-      const userData = allUser.filter(user => user.category === category);
+      const userData = allUser.filter((user) => user.category === category);
       return (
         <Fragment key={category}>
           {index !== 0 && <Menu.Divider />}
@@ -94,9 +120,7 @@ const SearchUsernameTypeaheadSubstance: ForwardRefRenderFunction<IClearable, Pro
       );
     });
 
-    return (
-      <Menu {...menuProps}>{items}</Menu>
-    );
+    return <Menu {...menuProps}>{items}</Menu>;
   }, []);
 
   useImperativeHandle(ref, () => ({
@@ -129,6 +153,8 @@ const SearchUsernameTypeaheadSubstance: ForwardRefRenderFunction<IClearable, Pro
       />
     </div>
   );
-});
+};
 
-export const SearchUsernameTypeahead = forwardRef(SearchUsernameTypeaheadSubstance);
+export const SearchUsernameTypeahead = forwardRef(
+  SearchUsernameTypeaheadSubstance,
+);

+ 147 - 88
apps/app/src/client/components/Admin/AuditLog/SelectActionDropdown.tsx

@@ -1,89 +1,141 @@
 import type { FC } from 'react';
-import React, { useMemo, useCallback } from 'react';
-
+import React, { useCallback, useMemo } from 'react';
 import { useTranslation } from 'react-i18next';
 
-import type { SupportedActionType, SupportedActionCategoryType } from '~/interfaces/activity';
+import type {
+  SupportedActionCategoryType,
+  SupportedActionType,
+} from '~/interfaces/activity';
 import {
+  AdminActions,
+  AttachmentActions,
+  CommentActions,
+  InAppNotificationActions,
+  PageActions,
+  SearchActions,
+  ShareLinkActions,
   SupportedActionCategory,
-  PageActions, CommentActions, TagActions, ShareLinkActions, AttachmentActions, InAppNotificationActions, SearchActions, UserActions, AdminActions,
+  TagActions,
+  UserActions,
 } from '~/interfaces/activity';
 
 type Props = {
-  actionMap: Map<SupportedActionType, boolean>
-  availableActions: SupportedActionType[]
-  onChangeAction: (action: SupportedActionType) => void
-  onChangeMultipleAction: (actions: SupportedActionType[], isChecked: boolean) => void
-}
+  actionMap: Map<SupportedActionType, boolean>;
+  availableActions: SupportedActionType[];
+  onChangeAction: (action: SupportedActionType) => void;
+  onChangeMultipleAction: (
+    actions: SupportedActionType[],
+    isChecked: boolean,
+  ) => void;
+};
 
 export const SelectActionDropdown: FC<Props> = (props: Props) => {
   const { t } = useTranslation();
   const {
-    actionMap, availableActions, onChangeAction, onChangeMultipleAction,
+    actionMap,
+    availableActions,
+    onChangeAction,
+    onChangeMultipleAction,
   } = props;
 
-  const dropdownItems = useMemo<Array<{actionCategory: SupportedActionCategoryType, actions: SupportedActionType[]}>>(() => {
-    return (
-      [
-        {
-          actionCategory: SupportedActionCategory.PAGE,
-          actions: PageActions.filter(action => availableActions.includes(action)),
-        },
-        {
-          actionCategory: SupportedActionCategory.COMMENT,
-          actions: CommentActions.filter(action => availableActions.includes(action)),
-        },
-        {
-          actionCategory: SupportedActionCategory.TAG,
-          actions: TagActions.filter(action => availableActions.includes(action)),
-        },
-        {
-          actionCategory: SupportedActionCategory.ATTACHMENT,
-          actions: AttachmentActions.filter(action => availableActions.includes(action)),
-        },
-        {
-          actionCategory: SupportedActionCategory.SHARE_LINK,
-          actions: ShareLinkActions.filter(action => availableActions.includes(action)),
-        },
-        {
-          actionCategory: SupportedActionCategory.IN_APP_NOTIFICATION,
-          actions: InAppNotificationActions.filter(action => availableActions.includes(action)),
-        },
-        {
-          actionCategory: SupportedActionCategory.SEARCH,
-          actions: SearchActions.filter(action => availableActions.includes(action)),
-        },
-        {
-          actionCategory: SupportedActionCategory.USER,
-          actions: UserActions.filter(action => availableActions.includes(action)),
-        },
-        {
-          actionCategory: SupportedActionCategory.ADMIN,
-          actions: AdminActions.filter(action => availableActions.includes(action)),
-        },
-      ]
-    );
-  }, [availableActions]).filter(item => item.actions.length !== 0);
+  const dropdownItems = useMemo<
+    Array<{
+      actionCategory: SupportedActionCategoryType;
+      actions: SupportedActionType[];
+    }>
+  >(() => {
+    return [
+      {
+        actionCategory: SupportedActionCategory.PAGE,
+        actions: PageActions.filter((action) =>
+          availableActions.includes(action),
+        ),
+      },
+      {
+        actionCategory: SupportedActionCategory.COMMENT,
+        actions: CommentActions.filter((action) =>
+          availableActions.includes(action),
+        ),
+      },
+      {
+        actionCategory: SupportedActionCategory.TAG,
+        actions: TagActions.filter((action) =>
+          availableActions.includes(action),
+        ),
+      },
+      {
+        actionCategory: SupportedActionCategory.ATTACHMENT,
+        actions: AttachmentActions.filter((action) =>
+          availableActions.includes(action),
+        ),
+      },
+      {
+        actionCategory: SupportedActionCategory.SHARE_LINK,
+        actions: ShareLinkActions.filter((action) =>
+          availableActions.includes(action),
+        ),
+      },
+      {
+        actionCategory: SupportedActionCategory.IN_APP_NOTIFICATION,
+        actions: InAppNotificationActions.filter((action) =>
+          availableActions.includes(action),
+        ),
+      },
+      {
+        actionCategory: SupportedActionCategory.SEARCH,
+        actions: SearchActions.filter((action) =>
+          availableActions.includes(action),
+        ),
+      },
+      {
+        actionCategory: SupportedActionCategory.USER,
+        actions: UserActions.filter((action) =>
+          availableActions.includes(action),
+        ),
+      },
+      {
+        actionCategory: SupportedActionCategory.ADMIN,
+        actions: AdminActions.filter((action) =>
+          availableActions.includes(action),
+        ),
+      },
+    ];
+  }, [availableActions]).filter((item) => item.actions.length !== 0);
 
-  const actionCheckboxChangedHandler = useCallback((action) => {
-    if (onChangeAction != null) {
-      onChangeAction(action);
-    }
-  }, [onChangeAction]);
+  const actionCheckboxChangedHandler = useCallback(
+    (action) => {
+      if (onChangeAction != null) {
+        onChangeAction(action);
+      }
+    },
+    [onChangeAction],
+  );
 
-  const multipleActionCheckboxChangedHandler = useCallback((actions, isChecked) => {
-    if (onChangeMultipleAction != null) {
-      onChangeMultipleAction(actions, isChecked);
-    }
-  }, [onChangeMultipleAction]);
+  const multipleActionCheckboxChangedHandler = useCallback(
+    (actions, isChecked) => {
+      if (onChangeMultipleAction != null) {
+        onChangeMultipleAction(actions, isChecked);
+      }
+    },
+    [onChangeMultipleAction],
+  );
 
   return (
     <div className="btn-group me-2 admin-audit-log">
-      <button className="btn btn-outline-secondary dropdown-toggle" type="button" id="dropdownMenuButton" data-bs-toggle="dropdown">
-        <span className="material-symbols-outlined me-1">bolt</span>{t('admin:audit_log_management.action')}
+      <button
+        className="btn btn-outline-secondary dropdown-toggle"
+        type="button"
+        id="dropdownMenuButton"
+        data-bs-toggle="dropdown"
+      >
+        <span className="material-symbols-outlined me-1">bolt</span>
+        {t('admin:audit_log_management.action')}
       </button>
-      <ul className="dropdown-menu select-action-dropdown" aria-labelledby="dropdownMenuButton">
-        {dropdownItems.map(item => (
+      <ul
+        className="dropdown-menu select-action-dropdown"
+        aria-labelledby="dropdownMenuButton"
+      >
+        {dropdownItems.map((item) => (
           <div key={item.actionCategory}>
             <div className="dropdown-item">
               <div className="px-2 m-0">
@@ -91,32 +143,39 @@ export const SelectActionDropdown: FC<Props> = (props: Props) => {
                   type="checkbox"
                   className="form-check-input"
                   defaultChecked
-                  onChange={(e) => { multipleActionCheckboxChangedHandler(item.actions, e.target.checked) }}
+                  onChange={(e) => {
+                    multipleActionCheckboxChangedHandler(
+                      item.actions,
+                      e.target.checked,
+                    );
+                  }}
                 />
-                <label className="form-label form-check-label">{t(`admin:audit_log_action_category.${item.actionCategory}`)}</label>
+                <label className="form-label form-check-label">
+                  {t(`admin:audit_log_action_category.${item.actionCategory}`)}
+                </label>
               </div>
             </div>
-            {
-              item.actions.map(action => (
-                <div className="dropdown-item" key={action}>
-                  <div className="px-4 m-0">
-                    <input
-                      type="checkbox"
-                      className="form-check-input"
-                      id={`checkbox${action}`}
-                      onChange={() => { actionCheckboxChangedHandler(action) }}
-                      checked={actionMap.get(action)}
-                    />
-                    <label
-                      className="form-check-label"
-                      htmlFor={`checkbox${action}`}
-                    >
-                      {t(`admin:audit_log_action.${action}`)}
-                    </label>
-                  </div>
+            {item.actions.map((action) => (
+              <div className="dropdown-item" key={action}>
+                <div className="px-4 m-0">
+                  <input
+                    type="checkbox"
+                    className="form-check-input"
+                    id={`checkbox${action}`}
+                    onChange={() => {
+                      actionCheckboxChangedHandler(action);
+                    }}
+                    checked={actionMap.get(action)}
+                  />
+                  <label
+                    className="form-check-label"
+                    htmlFor={`checkbox${action}`}
+                  >
+                    {t(`admin:audit_log_action.${action}`)}
+                  </label>
                 </div>
-              ))
-            }
+              </div>
+            ))}
           </div>
         ))}
       </ul>

+ 8 - 10
apps/app/src/client/components/Admin/Customize/Customize.jsx

@@ -1,6 +1,4 @@
-
-import React, { useEffect, useCallback } from 'react';
-
+import React, { useCallback, useEffect } from 'react';
 import PropTypes from 'prop-types';
 
 import AdminCustomizeContainer from '~/client/services/AdminCustomizeContainer';
@@ -9,7 +7,6 @@ import { toArrayIfNot } from '~/utils/array-utils';
 import loggerFactory from '~/utils/logger';
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
-
 import CustomizeCssSetting from './CustomizeCssSetting';
 import CustomizeFunctionSetting from './CustomizeFunctionSetting';
 import CustomizeLayoutSetting from './CustomizeLayoutSetting';
@@ -26,11 +23,10 @@ const logger = loggerFactory('growi:services:AdminCustomizePage');
 function Customize(props) {
   const { adminCustomizeContainer } = props;
 
-  const fetchCustomizeSettingsData = useCallback(async() => {
+  const fetchCustomizeSettingsData = useCallback(async () => {
     try {
       await adminCustomizeContainer.retrieveCustomizeData();
-    }
-    catch (err) {
+    } catch (err) {
       const errs = toArrayIfNot(err);
       toastError(errs);
       logger.error(errs);
@@ -41,7 +37,6 @@ function Customize(props) {
     fetchCustomizeSettingsData();
   }, [fetchCustomizeSettingsData]);
 
-
   return (
     <div data-testid="admin-customize">
       <div className="mb-5">
@@ -78,10 +73,13 @@ function Customize(props) {
   );
 }
 
-const CustomizePageWithUnstatedContainer = withUnstatedContainers(Customize, [AdminCustomizeContainer]);
+const CustomizePageWithUnstatedContainer = withUnstatedContainers(Customize, [
+  AdminCustomizeContainer,
+]);
 
 Customize.propTypes = {
-  adminCustomizeContainer: PropTypes.instanceOf(AdminCustomizeContainer).isRequired,
+  adminCustomizeContainer: PropTypes.instanceOf(AdminCustomizeContainer)
+    .isRequired,
 };
 
 export default CustomizePageWithUnstatedContainer;

+ 36 - 28
apps/app/src/client/components/Admin/Customize/CustomizeCssSetting.tsx

@@ -1,29 +1,23 @@
-import React, { useCallback, useEffect, type JSX } from 'react';
-
+import React, { type JSX, useCallback, useEffect } from 'react';
 import { useTranslation } from 'next-i18next';
 import { useForm } from 'react-hook-form';
 import { Card, CardBody } from 'reactstrap';
 
 import AdminCustomizeContainer from '~/client/services/AdminCustomizeContainer';
-import { toastSuccess, toastError } from '~/client/util/toastr';
+import { toastError, toastSuccess } from '~/client/util/toastr';
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
 
 type Props = {
-  adminCustomizeContainer: AdminCustomizeContainer
-}
+  adminCustomizeContainer: AdminCustomizeContainer;
+};
 
 const CustomizeCssSetting = (props: Props): JSX.Element => {
-
   const { adminCustomizeContainer } = props;
   const { t } = useTranslation();
 
-  const {
-    register,
-    handleSubmit,
-    reset,
-  } = useForm();
+  const { register, handleSubmit, reset } = useForm();
 
   // Sync form with container state
   useEffect(() => {
@@ -32,28 +26,38 @@ const CustomizeCssSetting = (props: Props): JSX.Element => {
     });
   }, [adminCustomizeContainer.state.currentCustomizeCss, reset]);
 
-  const onSubmit = useCallback(async(data) => {
-    try {
-      // Update container state before API call
-      await adminCustomizeContainer.changeCustomizeCss(data.customizeCss);
-      await adminCustomizeContainer.updateCustomizeCss();
-      toastSuccess(t('toaster.update_successed', { target: t('admin:customize_settings.custom_css'), ns: 'commons' }));
-    }
-    catch (err) {
-      toastError(err);
-    }
-  }, [t, adminCustomizeContainer]);
+  const onSubmit = useCallback(
+    async (data) => {
+      try {
+        // Update container state before API call
+        await adminCustomizeContainer.changeCustomizeCss(data.customizeCss);
+        await adminCustomizeContainer.updateCustomizeCss();
+        toastSuccess(
+          t('toaster.update_successed', {
+            target: t('admin:customize_settings.custom_css'),
+            ns: 'commons',
+          }),
+        );
+      } catch (err) {
+        toastError(err);
+      }
+    },
+    [t, adminCustomizeContainer],
+  );
 
   return (
     <React.Fragment>
       <div className="row">
         <div className="col-12">
-          <h2 className="admin-setting-header">{t('admin:customize_settings.custom_css')}</h2>
+          <h2 className="admin-setting-header">
+            {t('admin:customize_settings.custom_css')}
+          </h2>
 
           <Card className="card custom-card bg-body-tertiary my-3">
             <CardBody className="px-0 py-2">
-              { t('admin:customize_settings.write_css') }<br />
-              { t('admin:customize_settings.reflect_change') }
+              {t('admin:customize_settings.write_css')}
+              <br />
+              {t('admin:customize_settings.reflect_change')}
             </CardBody>
           </Card>
 
@@ -66,15 +70,19 @@ const CustomizeCssSetting = (props: Props): JSX.Element => {
               />
             </div>
 
-            <AdminUpdateButtonRow type="submit" disabled={adminCustomizeContainer.state.retrieveError != null} />
+            <AdminUpdateButtonRow
+              type="submit"
+              disabled={adminCustomizeContainer.state.retrieveError != null}
+            />
           </form>
         </div>
       </div>
     </React.Fragment>
   );
-
 };
 
-const CustomizeCssSettingWrapper = withUnstatedContainers(CustomizeCssSetting, [AdminCustomizeContainer]);
+const CustomizeCssSettingWrapper = withUnstatedContainers(CustomizeCssSetting, [
+  AdminCustomizeContainer,
+]);
 
 export default CustomizeCssSettingWrapper;

+ 7 - 11
apps/app/src/client/components/Admin/Customize/CustomizeFunctionOption.tsx

@@ -1,18 +1,15 @@
 import React, { type JSX } from 'react';
 
 type Props = {
-  optionId: string
-  label: string,
-  isChecked: boolean,
-  onChecked: () => void,
-  children: React.ReactNode,
-}
+  optionId: string;
+  label: string;
+  isChecked: boolean;
+  onChecked: () => void;
+  children: React.ReactNode;
+};
 
 const CustomizeFunctionOption = (props: Props): JSX.Element => {
-
-  const {
-    optionId, label, isChecked, onChecked, children,
-  } = props;
+  const { optionId, label, isChecked, onChecked, children } = props;
 
   return (
     <React.Fragment>
@@ -31,7 +28,6 @@ const CustomizeFunctionOption = (props: Props): JSX.Element => {
       {children}
     </React.Fragment>
   );
-
 };
 
 export default CustomizeFunctionOption;

+ 105 - 44
apps/app/src/client/components/Admin/Customize/CustomizeFunctionSetting.tsx

@@ -1,33 +1,33 @@
-import React, { useCallback, type JSX } from 'react';
-
+import React, { type JSX, useCallback } from 'react';
 import { useTranslation } from 'next-i18next';
 import { Card, CardBody } from 'reactstrap';
 
 import AdminCustomizeContainer from '~/client/services/AdminCustomizeContainer';
-import { toastSuccess, toastError } from '~/client/util/toastr';
+import { toastError, toastSuccess } from '~/client/util/toastr';
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
-
 import CustomizeFunctionOption from './CustomizeFunctionOption';
 import PagingSizeUncontrolledDropdown from './PagingSizeUncontrolledDropdown';
 
 type Props = {
-  adminCustomizeContainer: AdminCustomizeContainer
-}
+  adminCustomizeContainer: AdminCustomizeContainer;
+};
 
 const CustomizeFunctionSetting = (props: Props): JSX.Element => {
-
   const { adminCustomizeContainer } = props;
   const { t } = useTranslation();
 
-  const onClickSubmit = useCallback(async() => {
-
+  const onClickSubmit = useCallback(async () => {
     try {
       await adminCustomizeContainer.updateCustomizeFunction();
-      toastSuccess(t('toaster.update_successed', { target: t('admin:customize_settings.function'), ns: 'commons' }));
-    }
-    catch (err) {
+      toastSuccess(
+        t('toaster.update_successed', {
+          target: t('admin:customize_settings.function'),
+          ns: 'commons',
+        }),
+      );
+    } catch (err) {
       toastError(err);
     }
   }, [t, adminCustomizeContainer]);
@@ -36,24 +36,33 @@ const CustomizeFunctionSetting = (props: Props): JSX.Element => {
     <React.Fragment>
       <div className="row">
         <div className="col-12">
-          <h2 className="admin-setting-header">{t('admin:customize_settings.function')}</h2>
+          <h2 className="admin-setting-header">
+            {t('admin:customize_settings.function')}
+          </h2>
           <Card className="card custom-card bg-body-tertiary my-3">
             <CardBody className="px-0 py-2">
               {t('admin:customize_settings.function_desc')}
             </CardBody>
           </Card>
 
-
           <div className="row mt-4">
             <div className="offset-md-2 col-md-7 text-start">
               <CustomizeFunctionOption
                 optionId="isEnabledAttachTitleHeader"
-                label={t('admin:customize_settings.function_options.attach_title_header')}
-                isChecked={adminCustomizeContainer.state.isEnabledAttachTitleHeader}
-                onChecked={() => { adminCustomizeContainer.switchEnabledAttachTitleHeader() }}
+                label={t(
+                  'admin:customize_settings.function_options.attach_title_header',
+                )}
+                isChecked={
+                  adminCustomizeContainer.state.isEnabledAttachTitleHeader
+                }
+                onChecked={() => {
+                  adminCustomizeContainer.switchEnabledAttachTitleHeader();
+                }}
               >
                 <p className="form-text text-muted">
-                  {t('admin:customize_settings.function_options.attach_title_header_desc')}
+                  {t(
+                    'admin:customize_settings.function_options.attach_title_header_desc',
+                  )}
                 </p>
               </CustomizeFunctionOption>
             </div>
@@ -61,43 +70,67 @@ const CustomizeFunctionSetting = (props: Props): JSX.Element => {
 
           <PagingSizeUncontrolledDropdown
             label={t('admin:customize_settings.function_options.list_num_s')}
-            desc={t('admin:customize_settings.function_options.list_num_desc_s')}
+            desc={t(
+              'admin:customize_settings.function_options.list_num_desc_s',
+            )}
             toggleLabel={adminCustomizeContainer.state.pageLimitationS || 20}
             dropdownItemSize={[10, 20, 50, 100]}
-            onChangeDropdownItem={adminCustomizeContainer.switchPageListLimitationS}
+            onChangeDropdownItem={
+              adminCustomizeContainer.switchPageListLimitationS
+            }
           />
           <PagingSizeUncontrolledDropdown
             label={t('admin:customize_settings.function_options.list_num_m')}
-            desc={t('admin:customize_settings.function_options.list_num_desc_m')}
+            desc={t(
+              'admin:customize_settings.function_options.list_num_desc_m',
+            )}
             toggleLabel={adminCustomizeContainer.state.pageLimitationM || 10}
             dropdownItemSize={[5, 10, 20, 50, 100]}
-            onChangeDropdownItem={adminCustomizeContainer.switchPageListLimitationM}
+            onChangeDropdownItem={
+              adminCustomizeContainer.switchPageListLimitationM
+            }
           />
           <PagingSizeUncontrolledDropdown
             label={t('admin:customize_settings.function_options.list_num_l')}
-            desc={t('admin:customize_settings.function_options.list_num_desc_l')}
+            desc={t(
+              'admin:customize_settings.function_options.list_num_desc_l',
+            )}
             toggleLabel={adminCustomizeContainer.state.pageLimitationL || 50}
             dropdownItemSize={[20, 50, 100, 200]}
-            onChangeDropdownItem={adminCustomizeContainer.switchPageListLimitationL}
+            onChangeDropdownItem={
+              adminCustomizeContainer.switchPageListLimitationL
+            }
           />
           <PagingSizeUncontrolledDropdown
             label={t('admin:customize_settings.function_options.list_num_xl')}
-            desc={t('admin:customize_settings.function_options.list_num_desc_xl')}
+            desc={t(
+              'admin:customize_settings.function_options.list_num_desc_xl',
+            )}
             toggleLabel={adminCustomizeContainer.state.pageLimitationXL || 20}
             dropdownItemSize={[5, 10, 20, 50, 100]}
-            onChangeDropdownItem={adminCustomizeContainer.switchPageListLimitationXL}
+            onChangeDropdownItem={
+              adminCustomizeContainer.switchPageListLimitationXL
+            }
           />
 
           <div className="row">
             <div className="offset-md-2 col-md-7 text-start">
               <CustomizeFunctionOption
                 optionId="isEnabledStaleNotification"
-                label={t('admin:customize_settings.function_options.stale_notification')}
-                isChecked={adminCustomizeContainer.state.isEnabledStaleNotification}
-                onChecked={() => { adminCustomizeContainer.switchEnableStaleNotification() }}
+                label={t(
+                  'admin:customize_settings.function_options.stale_notification',
+                )}
+                isChecked={
+                  adminCustomizeContainer.state.isEnabledStaleNotification
+                }
+                onChecked={() => {
+                  adminCustomizeContainer.switchEnableStaleNotification();
+                }}
               >
                 <p className="form-text text-muted">
-                  {t('admin:customize_settings.function_options.stale_notification_desc')}
+                  {t(
+                    'admin:customize_settings.function_options.stale_notification_desc',
+                  )}
                 </p>
               </CustomizeFunctionOption>
             </div>
@@ -107,12 +140,20 @@ const CustomizeFunctionSetting = (props: Props): JSX.Element => {
             <div className="offset-md-2 col-md-7 text-start">
               <CustomizeFunctionOption
                 optionId="isAllReplyShown"
-                label={t('admin:customize_settings.function_options.show_all_reply_comments')}
-                isChecked={adminCustomizeContainer.state.isAllReplyShown || false}
-                onChecked={() => { adminCustomizeContainer.switchIsAllReplyShown() }}
+                label={t(
+                  'admin:customize_settings.function_options.show_all_reply_comments',
+                )}
+                isChecked={
+                  adminCustomizeContainer.state.isAllReplyShown || false
+                }
+                onChecked={() => {
+                  adminCustomizeContainer.switchIsAllReplyShown();
+                }}
               >
                 <p className="form-text text-muted">
-                  {t('admin:customize_settings.function_options.show_all_reply_comments_desc')}
+                  {t(
+                    'admin:customize_settings.function_options.show_all_reply_comments_desc',
+                  )}
                 </p>
               </CustomizeFunctionOption>
             </div>
@@ -122,12 +163,21 @@ const CustomizeFunctionSetting = (props: Props): JSX.Element => {
             <div className="offset-md-2 col-md-7 text-start">
               <CustomizeFunctionOption
                 optionId="isSearchScopeChildrenAsDefault"
-                label={t('admin:customize_settings.function_options.select_search_scope_children_as_default')}
-                isChecked={adminCustomizeContainer.state.isSearchScopeChildrenAsDefault || false}
-                onChecked={() => { adminCustomizeContainer.switchIsSearchScopeChildrenAsDefault() }}
+                label={t(
+                  'admin:customize_settings.function_options.select_search_scope_children_as_default',
+                )}
+                isChecked={
+                  adminCustomizeContainer.state
+                    .isSearchScopeChildrenAsDefault || false
+                }
+                onChecked={() => {
+                  adminCustomizeContainer.switchIsSearchScopeChildrenAsDefault();
+                }}
               >
                 <p className="form-text text-muted">
-                  {t('admin:customize_settings.function_options.select_search_scope_children_as_default_desc')}
+                  {t(
+                    'admin:customize_settings.function_options.select_search_scope_children_as_default_desc',
+                  )}
                 </p>
               </CustomizeFunctionOption>
             </div>
@@ -137,25 +187,36 @@ const CustomizeFunctionSetting = (props: Props): JSX.Element => {
             <div className="offset-md-2 col-md-7 text-start">
               <CustomizeFunctionOption
                 optionId="showPageSideAuthors"
-                label={t('admin:customize_settings.function_options.show_page_side_authors')}
+                label={t(
+                  'admin:customize_settings.function_options.show_page_side_authors',
+                )}
                 isChecked={adminCustomizeContainer.state.showPageSideAuthors}
-                onChecked={() => { adminCustomizeContainer.switchShowPageSideAuthors() }}
+                onChecked={() => {
+                  adminCustomizeContainer.switchShowPageSideAuthors();
+                }}
               >
                 <p className="form-text text-muted">
-                  {t('admin:customize_settings.function_options.show_page_side_authors_desc')}
+                  {t(
+                    'admin:customize_settings.function_options.show_page_side_authors_desc',
+                  )}
                 </p>
               </CustomizeFunctionOption>
             </div>
           </div>
 
-          <AdminUpdateButtonRow onClick={onClickSubmit} disabled={adminCustomizeContainer.state.retrieveError != null} />
+          <AdminUpdateButtonRow
+            onClick={onClickSubmit}
+            disabled={adminCustomizeContainer.state.retrieveError != null}
+          />
         </div>
       </div>
     </React.Fragment>
   );
-
 };
 
-const CustomizeFunctionSettingWrapper = withUnstatedContainers(CustomizeFunctionSetting, [AdminCustomizeContainer]);
+const CustomizeFunctionSettingWrapper = withUnstatedContainers(
+  CustomizeFunctionSetting,
+  [AdminCustomizeContainer],
+);
 
 export default CustomizeFunctionSettingWrapper;

+ 28 - 15
apps/app/src/client/components/Admin/Customize/CustomizeLayoutSetting.tsx

@@ -1,16 +1,14 @@
-import React, {
-  useCallback, useEffect, useState, type JSX,
-} from 'react';
-
+import React, { type JSX, useCallback, useEffect, useState } from 'react';
 import { LoadingSpinner } from '@growi/ui/dist/components';
 import { useTranslation } from 'next-i18next';
 
-import { toastSuccess, toastError } from '~/client/util/toastr';
-import { useNextThemes } from '~/stores-universal/use-next-themes';
+import { toastError, toastSuccess } from '~/client/util/toastr';
 import { useSWRxLayoutSetting } from '~/stores/admin/customize';
+import { useNextThemes } from '~/stores-universal/use-next-themes';
 
 const useIsContainerFluid = () => {
-  const { data: layoutSetting, update: updateLayoutSetting } = useSWRxLayoutSetting();
+  const { data: layoutSetting, update: updateLayoutSetting } =
+    useSWRxLayoutSetting();
   const [isContainerFluid, setIsContainerFluid] = useState<boolean>();
 
   useEffect(() => {
@@ -29,15 +27,22 @@ const CustomizeLayoutSetting = (): JSX.Element => {
 
   const { resolvedTheme } = useNextThemes();
 
-  const { isContainerFluid, setIsContainerFluid, updateLayoutSetting } = useIsContainerFluid();
+  const { isContainerFluid, setIsContainerFluid, updateLayoutSetting } =
+    useIsContainerFluid();
 
-  const onClickSubmit = useCallback(async() => {
-    if (isContainerFluid == null) { return }
+  const onClickSubmit = useCallback(async () => {
+    if (isContainerFluid == null) {
+      return;
+    }
     try {
       await updateLayoutSetting({ isContainerFluid });
-      toastSuccess(t('toaster.update_successed', { target: t('customize_settings.layout'), ns: 'commons' }));
-    }
-    catch (err) {
+      toastSuccess(
+        t('toaster.update_successed', {
+          target: t('customize_settings.layout'),
+          ns: 'commons',
+        }),
+      );
+    } catch (err) {
       toastError(err);
     }
   }, [isContainerFluid, updateLayoutSetting, t]);
@@ -54,7 +59,9 @@ const CustomizeLayoutSetting = (): JSX.Element => {
     <React.Fragment>
       <div className="row">
         <div className="col-12">
-          <h2 className="admin-setting-header">{t('customize_settings.layout')}</h2>
+          <h2 className="admin-setting-header">
+            {t('customize_settings.layout')}
+          </h2>
 
           <div className="d-flex justify-content-around mt-5">
             <div className="row row-cols-2">
@@ -97,7 +104,13 @@ const CustomizeLayoutSetting = (): JSX.Element => {
 
           <div className="row my-3">
             <div className="mx-auto">
-              <button type="button" className="btn btn-primary" onClick={onClickSubmit}>{ t('Update') }</button>
+              <button
+                type="button"
+                className="btn btn-primary"
+                onClick={onClickSubmit}
+              >
+                {t('Update')}
+              </button>
             </div>
           </div>
         </div>

+ 87 - 42
apps/app/src/client/components/Admin/Customize/CustomizeLogoSetting.tsx

@@ -1,11 +1,12 @@
-import React, { useCallback, useState, type JSX } from 'react';
-
+import React, { type JSX, useCallback, useState } from 'react';
 import { useAtomValue, useSetAtom } from 'jotai';
 import { useTranslation } from 'react-i18next';
 
 import ImageCropModal from '~/client/components/Common/ImageCropModal';
 import {
-  apiv3Delete, apiv3PostForm, apiv3Put,
+  apiv3Delete,
+  apiv3PostForm,
+  apiv3Put,
 } from '~/client/util/apiv3-client';
 import { toastError, toastSuccess } from '~/client/util/toastr';
 import { useIsDefaultLogo } from '~/states/global';
@@ -13,20 +14,23 @@ import { isCustomizedLogoUploadedAtom } from '~/states/server-configurations';
 
 import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
 
-
 const DEFAULT_LOGO = '/images/logo.svg';
 const CUSTOMIZED_LOGO = '/attachment/brand-logo';
 
 const CustomizeLogoSetting = (): JSX.Element => {
-
   const { t } = useTranslation();
   const isDefaultLogo = useIsDefaultLogo();
   const isCustomizedLogoUploaded = useAtomValue(isCustomizedLogoUploadedAtom);
   const setIsCustomizedLogoUploaded = useSetAtom(isCustomizedLogoUploadedAtom);
 
-  const [uploadLogoSrc, setUploadLogoSrc] = useState<ArrayBuffer | string | null>(null);
-  const [isImageCropModalShow, setIsImageCropModalShow] = useState<boolean>(false);
-  const [isDefaultLogoSelected, setIsDefaultLogoSelected] = useState<boolean>(isDefaultLogo ?? true);
+  const [uploadLogoSrc, setUploadLogoSrc] = useState<
+    ArrayBuffer | string | null
+  >(null);
+  const [isImageCropModalShow, setIsImageCropModalShow] =
+    useState<boolean>(false);
+  const [isDefaultLogoSelected, setIsDefaultLogoSelected] = useState<boolean>(
+    isDefaultLogo ?? true,
+  );
   const [retrieveError, setRetrieveError] = useState<any>();
 
   const onSelectFile = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
@@ -40,10 +44,16 @@ const CustomizeLogoSetting = (): JSX.Element => {
 
   const onClickSubmit = useCallback(async () => {
     try {
-      await apiv3Put('/customize-setting/customize-logo', { isDefaultLogo: isDefaultLogoSelected });
-      toastSuccess(t('toaster.update_successed', { target: t('admin:customize_settings.custom_logo'), ns: 'commons' }));
-    }
-    catch (err) {
+      await apiv3Put('/customize-setting/customize-logo', {
+        isDefaultLogo: isDefaultLogoSelected,
+      });
+      toastSuccess(
+        t('toaster.update_successed', {
+          target: t('admin:customize_settings.custom_logo'),
+          ns: 'commons',
+        }),
+      );
+    } catch (err) {
       toastError(err);
     }
   }, [t, isDefaultLogoSelected]);
@@ -52,37 +62,49 @@ const CustomizeLogoSetting = (): JSX.Element => {
     try {
       await apiv3Delete('/customize-setting/delete-brand-logo');
       setIsCustomizedLogoUploaded(false);
-      toastSuccess(t('toaster.update_successed', { target: t('admin:customize_settings.current_logo'), ns: 'commons' }));
-    }
-    catch (err) {
+      toastSuccess(
+        t('toaster.update_successed', {
+          target: t('admin:customize_settings.current_logo'),
+          ns: 'commons',
+        }),
+      );
+    } catch (err) {
       toastError(err);
       setRetrieveError(err);
       throw new Error('Failed to delete logo');
     }
   }, [setIsCustomizedLogoUploaded, t]);
 
-
-  const processImageCompletedHandler = useCallback(async (croppedImage) => {
-    try {
-      const formData = new FormData();
-      formData.append('file', croppedImage);
-      await apiv3PostForm('/customize-setting/upload-brand-logo', formData);
-      setIsCustomizedLogoUploaded(true);
-      toastSuccess(t('toaster.update_successed', { target: t('admin:customize_settings.current_logo'), ns: 'commons' }));
-    }
-    catch (err) {
-      toastError(err);
-      setRetrieveError(err);
-      throw new Error('Failed to upload brand logo');
-    }
-  }, [setIsCustomizedLogoUploaded, t]);
+  const processImageCompletedHandler = useCallback(
+    async (croppedImage) => {
+      try {
+        const formData = new FormData();
+        formData.append('file', croppedImage);
+        await apiv3PostForm('/customize-setting/upload-brand-logo', formData);
+        setIsCustomizedLogoUploaded(true);
+        toastSuccess(
+          t('toaster.update_successed', {
+            target: t('admin:customize_settings.current_logo'),
+            ns: 'commons',
+          }),
+        );
+      } catch (err) {
+        toastError(err);
+        setRetrieveError(err);
+        throw new Error('Failed to upload brand logo');
+      }
+    },
+    [setIsCustomizedLogoUploaded, t],
+  );
 
   return (
     <React.Fragment>
       <div className="row">
         <div className="col-12">
           <div className="mb-5">
-            <h2 className="border-bottom my-4 admin-setting-header">{t('admin:customize_settings.custom_logo')}</h2>
+            <h2 className="border-bottom my-4 admin-setting-header">
+              {t('admin:customize_settings.custom_logo')}
+            </h2>
             <div className="row">
               <div className="col-md-6 col-12 mb-3 mb-md-0">
                 <h4>
@@ -94,9 +116,14 @@ const CustomizeLogoSetting = (): JSX.Element => {
                       form="formImageType"
                       name="imagetypeForm[isDefaultLogo]"
                       checked={isDefaultLogoSelected}
-                      onChange={() => { setIsDefaultLogoSelected(true) }}
+                      onChange={() => {
+                        setIsDefaultLogoSelected(true);
+                      }}
                     />
-                    <label className="form-check-label" htmlFor="radioDefaultLogo">
+                    <label
+                      className="form-check-label"
+                      htmlFor="radioDefaultLogo"
+                    >
                       {t('admin:customize_settings.default_logo')}
                     </label>
                   </div>
@@ -113,9 +140,14 @@ const CustomizeLogoSetting = (): JSX.Element => {
                       form="formImageType"
                       name="imagetypeForm[isDefaultLogo]"
                       checked={!isDefaultLogoSelected}
-                      onChange={() => { setIsDefaultLogoSelected(false) }}
+                      onChange={() => {
+                        setIsDefaultLogoSelected(false);
+                      }}
                     />
-                    <label className="form-check-label" htmlFor="radioUploadLogo">
+                    <label
+                      className="form-check-label"
+                      htmlFor="radioUploadLogo"
+                    >
                       {t('admin:customize_settings.upload_logo')}
                     </label>
                   </div>
@@ -128,9 +160,17 @@ const CustomizeLogoSetting = (): JSX.Element => {
                     {isCustomizedLogoUploaded && (
                       <>
                         <p>
-                          <img src={CUSTOMIZED_LOGO} id="settingBrandLogo" width="64" />
+                          <img
+                            src={CUSTOMIZED_LOGO}
+                            id="settingBrandLogo"
+                            width="64"
+                          />
                         </p>
-                        <button type="button" className="btn btn-danger" onClick={onClickDeleteBtn}>
+                        <button
+                          type="button"
+                          className="btn btn-danger"
+                          onClick={onClickDeleteBtn}
+                        >
                           {t('admin:customize_settings.delete_logo')}
                         </button>
                       </>
@@ -142,12 +182,20 @@ const CustomizeLogoSetting = (): JSX.Element => {
                     {t('admin:customize_settings.upload_new_logo')}
                   </label>
                   <div className="col-sm-8 col-12">
-                    <input type="file" onChange={onSelectFile} name="brandLogo" accept="image/*" />
+                    <input
+                      type="file"
+                      onChange={onSelectFile}
+                      name="brandLogo"
+                      accept="image/*"
+                    />
                   </div>
                 </div>
               </div>
             </div>
-            <AdminUpdateButtonRow onClick={onClickSubmit} disabled={retrieveError != null} />
+            <AdminUpdateButtonRow
+              onClick={onClickSubmit}
+              disabled={retrieveError != null}
+            />
           </div>
         </div>
       </div>
@@ -162,9 +210,6 @@ const CustomizeLogoSetting = (): JSX.Element => {
       />
     </React.Fragment>
   );
-
-
 };
 
-
 export default CustomizeLogoSetting;

+ 48 - 33
apps/app/src/client/components/Admin/Customize/CustomizeNoscriptSetting.tsx

@@ -1,5 +1,4 @@
-import React, { useCallback, useEffect, type JSX } from 'react';
-
+import React, { type JSX, useCallback, useEffect } from 'react';
 import { useTranslation } from 'next-i18next';
 import { useForm } from 'react-hook-form';
 import { PrismAsyncLight } from 'react-syntax-highlighter';
@@ -7,56 +6,65 @@ import { oneDark } from 'react-syntax-highlighter/dist/cjs/styles/prism';
 import { Card, CardBody } from 'reactstrap';
 
 import AdminCustomizeContainer from '~/client/services/AdminCustomizeContainer';
-import { toastSuccess, toastError } from '~/client/util/toastr';
+import { toastError, toastSuccess } from '~/client/util/toastr';
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
 
 type Props = {
-  adminCustomizeContainer: AdminCustomizeContainer
-}
+  adminCustomizeContainer: AdminCustomizeContainer;
+};
 
 const CustomizeNoscriptSetting = (props: Props): JSX.Element => {
-
   const { adminCustomizeContainer } = props;
   const { t } = useTranslation();
 
-  const {
-    register,
-    handleSubmit,
-    reset,
-  } = useForm();
+  const { register, handleSubmit, reset } = useForm();
 
   // Sync form with container state
   useEffect(() => {
     reset({
-      customizeNoscript: adminCustomizeContainer.state.currentCustomizeNoscript || '',
+      customizeNoscript:
+        adminCustomizeContainer.state.currentCustomizeNoscript || '',
     });
   }, [adminCustomizeContainer.state.currentCustomizeNoscript, reset]);
 
-  const onSubmit = useCallback(async(data) => {
-    try {
-      // Update container state before API call
-      await adminCustomizeContainer.changeCustomizeNoscript(data.customizeNoscript);
-      await adminCustomizeContainer.updateCustomizeNoscript();
-      toastSuccess(t('toaster.update_successed', { target: t('admin:customize_settings.custom_noscript'), ns: 'commons' }));
-    }
-    catch (err) {
-      toastError(err);
-    }
-  }, [t, adminCustomizeContainer]);
+  const onSubmit = useCallback(
+    async (data) => {
+      try {
+        // Update container state before API call
+        await adminCustomizeContainer.changeCustomizeNoscript(
+          data.customizeNoscript,
+        );
+        await adminCustomizeContainer.updateCustomizeNoscript();
+        toastSuccess(
+          t('toaster.update_successed', {
+            target: t('admin:customize_settings.custom_noscript'),
+            ns: 'commons',
+          }),
+        );
+      } catch (err) {
+        toastError(err);
+      }
+    },
+    [t, adminCustomizeContainer],
+  );
 
   return (
     <React.Fragment>
       <div className="row">
         <div className="col-12">
-          <h2 className="admin-setting-header">{t('admin:customize_settings.custom_noscript')}</h2>
+          <h2 className="admin-setting-header">
+            {t('admin:customize_settings.custom_noscript')}
+          </h2>
 
           <Card className="card custom-card bg-body-tertiary my-3">
             <CardBody className="px-0 py-2">
               <span
                 // eslint-disable-next-line react/no-danger
-                dangerouslySetInnerHTML={{ __html: t('admin:customize_settings.custom_noscript_detail') }}
+                dangerouslySetInnerHTML={{
+                  __html: t('admin:customize_settings.custom_noscript_detail'),
+                }}
               />
             </CardBody>
           </Card>
@@ -78,14 +86,16 @@ const CustomizeNoscriptSetting = (props: Props): JSX.Element => {
               aria-expanded="false"
               aria-controls="collapseExampleHtml"
             >
-              <span className="material-symbols-outlined me-1" aria-hidden="true">navigate_next</span>
+              <span
+                className="material-symbols-outlined me-1"
+                aria-hidden="true"
+              >
+                navigate_next
+              </span>
               Example for Google Tag Manager
             </a>
             <div className="collapse" id="collapseExampleHtml">
-              <PrismAsyncLight
-                style={oneDark}
-                language="javascript"
-              >
+              <PrismAsyncLight style={oneDark} language="javascript">
                 {`<iframe src="https://www.googletagmanager.com/ns.html?id=GTM-XXXXXXX"
   height="0"
   width="0"
@@ -93,15 +103,20 @@ const CustomizeNoscriptSetting = (props: Props): JSX.Element => {
               </PrismAsyncLight>
             </div>
 
-            <AdminUpdateButtonRow type="submit" disabled={adminCustomizeContainer.state.retrieveError != null} />
+            <AdminUpdateButtonRow
+              type="submit"
+              disabled={adminCustomizeContainer.state.retrieveError != null}
+            />
           </form>
         </div>
       </div>
     </React.Fragment>
   );
-
 };
 
-const CustomizeNoscriptSettingWrapper = withUnstatedContainers(CustomizeNoscriptSetting, [AdminCustomizeContainer]);
+const CustomizeNoscriptSettingWrapper = withUnstatedContainers(
+  CustomizeNoscriptSetting,
+  [AdminCustomizeContainer],
+);
 
 export default CustomizeNoscriptSettingWrapper;

+ 36 - 18
apps/app/src/client/components/Admin/Customize/CustomizePresentationSetting.tsx

@@ -1,69 +1,87 @@
-import React, { useCallback, type JSX } from 'react';
-
+import React, { type JSX, useCallback } from 'react';
 import { useTranslation } from 'next-i18next';
 
 import AdminCustomizeContainer from '~/client/services/AdminCustomizeContainer';
-import { toastSuccess, toastError } from '~/client/util/toastr';
+import { toastError, toastSuccess } from '~/client/util/toastr';
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
-
 import CustomizePresentationOption from './CustomizeFunctionOption';
 
 type Props = {
-  adminCustomizeContainer: AdminCustomizeContainer
-}
+  adminCustomizeContainer: AdminCustomizeContainer;
+};
 
 const CustomizePresentationSetting = (props: Props): JSX.Element => {
   const { adminCustomizeContainer } = props;
   const { t } = useTranslation();
 
-  const onClickSubmit = useCallback(async() => {
+  const onClickSubmit = useCallback(async () => {
     try {
       await adminCustomizeContainer.updateCustomizePresentation();
-      toastSuccess(t('toaster.update_successed', { target: t('admin:customize_settings.presentation'), ns: 'commons' }));
-    }
-    catch (err) {
+      toastSuccess(
+        t('toaster.update_successed', {
+          target: t('admin:customize_settings.presentation'),
+          ns: 'commons',
+        }),
+      );
+    } catch (err) {
       toastError(err);
     }
   }, [adminCustomizeContainer, t]);
 
   return (
     <React.Fragment>
-      <h2 className="admin-setting-header">{t('admin:customize_settings.custom_presentation')}</h2>
+      <h2 className="admin-setting-header">
+        {t('admin:customize_settings.custom_presentation')}
+      </h2>
       <div className="form-group row">
         <div className="offset-md-3 col-md-6 text-left">
           <CustomizePresentationOption
             optionId="isEnabledMarp"
-            label={t('admin:customize_settings.presentation_options.enable_marp')}
+            label={t(
+              'admin:customize_settings.presentation_options.enable_marp',
+            )}
             isChecked={adminCustomizeContainer?.state.isEnabledMarp || false}
-            onChecked={() => { adminCustomizeContainer.switchIsEnabledMarp() }}
+            onChecked={() => {
+              adminCustomizeContainer.switchIsEnabledMarp();
+            }}
           >
             <p className="form-text text-muted">
-              {t('admin:customize_settings.presentation_options.enable_marp_desc')}
+              {t(
+                'admin:customize_settings.presentation_options.enable_marp_desc',
+              )}
               <br></br>
               <a
                 href={`${t('admin:customize_settings.presentation_options.marp_official_site_link')}`}
                 target="_blank"
                 rel="noopener noreferrer"
-              >{`${t('admin:customize_settings.presentation_options.marp_official_site')}`}
+              >
+                {`${t('admin:customize_settings.presentation_options.marp_official_site')}`}
               </a>
               <br></br>
               <a
                 href={`${t('admin:customize_settings.presentation_options.marp_in_gorwi_link')}`}
                 target="_blank"
                 rel="noopener noreferrer"
-              >{`${t('admin:customize_settings.presentation_options.marp_in_growi')}`}
+              >
+                {`${t('admin:customize_settings.presentation_options.marp_in_growi')}`}
               </a>
             </p>
           </CustomizePresentationOption>
         </div>
       </div>
 
-      <AdminUpdateButtonRow onClick={onClickSubmit} disabled={adminCustomizeContainer.state.retrieveError != null} />
+      <AdminUpdateButtonRow
+        onClick={onClickSubmit}
+        disabled={adminCustomizeContainer.state.retrieveError != null}
+      />
     </React.Fragment>
   );
 };
-const CustomizePresentationSettingWrapper = withUnstatedContainers(CustomizePresentationSetting, [AdminCustomizeContainer]);
+const CustomizePresentationSettingWrapper = withUnstatedContainers(
+  CustomizePresentationSetting,
+  [AdminCustomizeContainer],
+);
 
 export default CustomizePresentationSettingWrapper;

+ 47 - 33
apps/app/src/client/components/Admin/Customize/CustomizeScriptSetting.tsx

@@ -1,5 +1,4 @@
-import React, { useCallback, useEffect, type JSX } from 'react';
-
+import React, { type JSX, useCallback, useEffect } from 'react';
 import { useTranslation } from 'next-i18next';
 import { useForm } from 'react-hook-form';
 import { PrismAsyncLight } from 'react-syntax-highlighter';
@@ -7,53 +6,61 @@ import { oneDark } from 'react-syntax-highlighter/dist/cjs/styles/prism';
 import { Card, CardBody } from 'reactstrap';
 
 import AdminCustomizeContainer from '~/client/services/AdminCustomizeContainer';
-import { toastSuccess, toastError } from '~/client/util/toastr';
+import { toastError, toastSuccess } from '~/client/util/toastr';
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
 
 type Props = {
-  adminCustomizeContainer: AdminCustomizeContainer
-}
+  adminCustomizeContainer: AdminCustomizeContainer;
+};
 
 const CustomizeScriptSetting = (props: Props): JSX.Element => {
-
   const { adminCustomizeContainer } = props;
   const { t } = useTranslation();
 
-  const {
-    register,
-    handleSubmit,
-    reset,
-  } = useForm();
+  const { register, handleSubmit, reset } = useForm();
 
   // Sync form with container state
   useEffect(() => {
     reset({
-      customizeScript: adminCustomizeContainer.state.currentCustomizeScript || '',
+      customizeScript:
+        adminCustomizeContainer.state.currentCustomizeScript || '',
     });
   }, [adminCustomizeContainer.state.currentCustomizeScript, reset]);
 
-  const onSubmit = useCallback(async(data) => {
-    try {
-      // Update container state before API call
-      await adminCustomizeContainer.changeCustomizeScript(data.customizeScript);
-      await adminCustomizeContainer.updateCustomizeScript();
-      toastSuccess(t('toaster.update_successed', { target: t('admin:customize_settings.custom_script'), ns: 'commons' }));
-    }
-    catch (err) {
-      toastError(err);
-    }
-  }, [t, adminCustomizeContainer]);
+  const onSubmit = useCallback(
+    async (data) => {
+      try {
+        // Update container state before API call
+        await adminCustomizeContainer.changeCustomizeScript(
+          data.customizeScript,
+        );
+        await adminCustomizeContainer.updateCustomizeScript();
+        toastSuccess(
+          t('toaster.update_successed', {
+            target: t('admin:customize_settings.custom_script'),
+            ns: 'commons',
+          }),
+        );
+      } catch (err) {
+        toastError(err);
+      }
+    },
+    [t, adminCustomizeContainer],
+  );
 
   return (
     <React.Fragment>
       <div className="row">
         <div className="col-12">
-          <h2 className="admin-setting-header">{t('admin:customize_settings.custom_script')}</h2>
+          <h2 className="admin-setting-header">
+            {t('admin:customize_settings.custom_script')}
+          </h2>
           <Card className="card custom-card bg-body-tertiary mb-3">
             <CardBody className="px-0 py-2">
-              {t('admin:customize_settings.write_java')}<br />
+              {t('admin:customize_settings.write_java')}
+              <br />
               {t('admin:customize_settings.reflect_change')}
             </CardBody>
           </Card>
@@ -75,14 +82,16 @@ const CustomizeScriptSetting = (props: Props): JSX.Element => {
               aria-expanded="false"
               aria-controls="collapseExampleScript"
             >
-              <span className="material-symbols-outlined me-1" aria-hidden="true">navigate_next</span>
+              <span
+                className="material-symbols-outlined me-1"
+                aria-hidden="true"
+              >
+                navigate_next
+              </span>
               Example for Google Tag Manager
             </a>
             <div className="collapse" id="collapseExampleScript">
-              <PrismAsyncLight
-                style={oneDark}
-                language="javascript"
-              >
+              <PrismAsyncLight style={oneDark} language="javascript">
                 {`(function(w,d,s,l,i){
 w[l]=w[l]||[];
 w[l].push({'gtm.start':new Date().getTime(),event:'gtm.js'});
@@ -95,15 +104,20 @@ j.src='https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefo
               </PrismAsyncLight>
             </div>
 
-            <AdminUpdateButtonRow type="submit" disabled={adminCustomizeContainer.state.retrieveError != null} />
+            <AdminUpdateButtonRow
+              type="submit"
+              disabled={adminCustomizeContainer.state.retrieveError != null}
+            />
           </form>
         </div>
       </div>
     </React.Fragment>
   );
-
 };
 
-const CustomizeScriptSettingWrapper = withUnstatedContainers(CustomizeScriptSetting, [AdminCustomizeContainer]);
+const CustomizeScriptSettingWrapper = withUnstatedContainers(
+  CustomizeScriptSetting,
+  [AdminCustomizeContainer],
+);
 
 export default CustomizeScriptSettingWrapper;

+ 24 - 21
apps/app/src/client/components/Admin/Customize/CustomizeSidebarSetting.tsx

@@ -1,30 +1,31 @@
-import React, { useCallback, type JSX } from 'react';
-
+import React, { type JSX, useCallback } from 'react';
 import { LoadingSpinner } from '@growi/ui/dist/components';
 import { useTranslation } from 'next-i18next';
 import { Card, CardBody } from 'reactstrap';
 
-import { toastSuccess, toastError } from '~/client/util/toastr';
-import { useNextThemes } from '~/stores-universal/use-next-themes';
+import { toastError, toastSuccess } from '~/client/util/toastr';
 import { useSWRxSidebarConfig } from '~/stores/admin/sidebar-config';
+import { useNextThemes } from '~/stores-universal/use-next-themes';
 
 const CustomizeSidebarsetting = (): JSX.Element => {
   const { t } = useTranslation(['admin', 'commons']);
 
-  const {
-    data, update, setIsSidebarCollapsedMode,
-  } = useSWRxSidebarConfig();
+  const { data, update, setIsSidebarCollapsedMode } = useSWRxSidebarConfig();
 
   const { resolvedTheme } = useNextThemes();
   const collapsedIconFileName = `/images/customize-settings/collapsed-${resolvedTheme}.svg`;
   const dockIconFileName = `/images/customize-settings/dock-${resolvedTheme}.svg`;
 
-  const onClickSubmit = useCallback(async() => {
+  const onClickSubmit = useCallback(async () => {
     try {
       await update();
-      toastSuccess(t('toaster.update_successed', { target: t('customize_settings.default_sidebar_mode.title'), ns: 'commons' }));
-    }
-    catch (err) {
+      toastSuccess(
+        t('toaster.update_successed', {
+          target: t('customize_settings.default_sidebar_mode.title'),
+          ns: 'commons',
+        }),
+      );
+    } catch (err) {
       toastError(err);
     }
   }, [t, update]);
@@ -39,8 +40,9 @@ const CustomizeSidebarsetting = (): JSX.Element => {
     <React.Fragment>
       <div className="row">
         <div className="col-12">
-
-          <h2 className="admin-setting-header">{t('customize_settings.default_sidebar_mode.title')}</h2>
+          <h2 className="admin-setting-header">
+            {t('customize_settings.default_sidebar_mode.title')}
+          </h2>
 
           <Card className="card custom-card bg-body-tertiary my-3">
             <CardBody className="px-0 py-2">
@@ -58,9 +60,7 @@ const CustomizeSidebarsetting = (): JSX.Element => {
                 >
                   {/* eslint-disable-next-line @next/next/no-img-element */}
                   <img src={collapsedIconFileName} alt="Collapsed Mode" />
-                  <div className="card-body text-center">
-                    Collapsed Mode
-                  </div>
+                  <div className="card-body text-center">Collapsed Mode</div>
                 </div>
               </div>
               <div className="col">
@@ -71,9 +71,7 @@ const CustomizeSidebarsetting = (): JSX.Element => {
                 >
                   {/* eslint-disable-next-line @next/next/no-img-element */}
                   <img src={dockIconFileName} alt="Dock Mode" />
-                  <div className="card-body  text-center">
-                    Dock Mode
-                  </div>
+                  <div className="card-body  text-center">Dock Mode</div>
                 </div>
               </div>
             </div>
@@ -81,10 +79,15 @@ const CustomizeSidebarsetting = (): JSX.Element => {
 
           <div className="row my-3">
             <div className="mx-auto">
-              <button type="button" onClick={onClickSubmit} className="btn btn-primary">{ t('Update') }</button>
+              <button
+                type="button"
+                onClick={onClickSubmit}
+                className="btn btn-primary"
+              >
+                {t('Update')}
+              </button>
             </div>
           </div>
-
         </div>
       </div>
     </React.Fragment>

+ 19 - 15
apps/app/src/client/components/Admin/Customize/CustomizeThemeOptions.tsx

@@ -1,15 +1,13 @@
-import React, { useMemo, type JSX } from 'react';
-
+import React, { type JSX, useMemo } from 'react';
 import { type GrowiThemeMetadata, GrowiThemeSchemeType } from '@growi/core';
 import { useTranslation } from 'next-i18next';
 
 import { ThemeColorBox } from './ThemeColorBox';
 
-
 type Props = {
-  availableThemes: GrowiThemeMetadata[],
-  selectedTheme?: string,
-  onSelected?: (themeName: string) => void,
+  availableThemes: GrowiThemeMetadata[];
+  selectedTheme?: string;
+  onSelected?: (themeName: string) => void;
 };
 
 const CustomizeThemeOptions = (props: Props): JSX.Element => {
@@ -18,24 +16,31 @@ const CustomizeThemeOptions = (props: Props): JSX.Element => {
   const { availableThemes, selectedTheme, onSelected } = props;
 
   const lightNDarkThemes = useMemo(() => {
-    return availableThemes.filter(s => s.schemeType === GrowiThemeSchemeType.BOTH);
+    return availableThemes.filter(
+      (s) => s.schemeType === GrowiThemeSchemeType.BOTH,
+    );
   }, [availableThemes]);
   const oneModeThemes = useMemo(() => {
-    return availableThemes.filter(s => s.schemeType !== GrowiThemeSchemeType.BOTH);
+    return availableThemes.filter(
+      (s) => s.schemeType !== GrowiThemeSchemeType.BOTH,
+    );
   }, [availableThemes]);
 
   return (
     <>
-
       {/* Light and Dark Themes */}
       <div>
-        <h3 className="mb-3">{t('customize_settings.theme_desc.light_and_dark')}</h3>
+        <h3 className="mb-3">
+          {t('customize_settings.theme_desc.light_and_dark')}
+        </h3>
         <div className="hstack gap-3 flex-wrap">
           {lightNDarkThemes.map((theme) => {
             return (
               <ThemeColorBox
                 key={theme.name}
-                isSelected={selectedTheme != null && selectedTheme === theme.name}
+                isSelected={
+                  selectedTheme != null && selectedTheme === theme.name
+                }
                 metadata={theme}
                 onSelected={() => onSelected?.(theme.name)}
               />
@@ -52,7 +57,9 @@ const CustomizeThemeOptions = (props: Props): JSX.Element => {
             return (
               <ThemeColorBox
                 key={theme.name}
-                isSelected={selectedTheme != null && selectedTheme === theme.name}
+                isSelected={
+                  selectedTheme != null && selectedTheme === theme.name
+                }
                 metadata={theme}
                 onSelected={() => onSelected?.(theme.name)}
               />
@@ -60,11 +67,8 @@ const CustomizeThemeOptions = (props: Props): JSX.Element => {
           })}
         </div>
       </div>
-
     </>
   );
-
 };
 
-
 export default CustomizeThemeOptions;

+ 25 - 19
apps/app/src/client/components/Admin/Customize/CustomizeThemeSetting.tsx

@@ -1,21 +1,15 @@
-import React, {
-  useCallback, useEffect, useState, type JSX,
-} from 'react';
-
+import React, { type JSX, useCallback, useEffect, useState } from 'react';
 import { PresetThemes, PresetThemesMetadatas } from '@growi/preset-themes';
 import { useTranslation } from 'next-i18next';
 
-import { toastSuccess, toastError, toastWarning } from '~/client/util/toastr';
+import { toastError, toastSuccess, toastWarning } from '~/client/util/toastr';
 import { useSWRxGrowiThemeSetting } from '~/stores/admin/customize';
 
 import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
-
 import CustomizeThemeOptions from './CustomizeThemeOptions';
 
-
 // eslint-disable-next-line @typescript-eslint/ban-types
-type Props = {
-}
+type Props = {};
 
 // eslint-disable-next-line @typescript-eslint/no-unused-vars
 const CustomizeThemeSetting = (props: Props): JSX.Element => {
@@ -32,7 +26,7 @@ const CustomizeThemeSetting = (props: Props): JSX.Element => {
     setCurrentTheme(themeName);
   }, []);
 
-  const submitHandler = useCallback(async() => {
+  const submitHandler = useCallback(async () => {
     if (currentTheme == null) {
       toastWarning('The selected theme is undefined');
       return;
@@ -43,29 +37,41 @@ const CustomizeThemeSetting = (props: Props): JSX.Element => {
         theme: currentTheme,
       });
 
-      toastSuccess(t('toaster.update_successed', { target: t('admin:customize_settings.theme'), ns: 'commons' }));
-    }
-    catch (err) {
+      toastSuccess(
+        t('toaster.update_successed', {
+          target: t('admin:customize_settings.theme'),
+          ns: 'commons',
+        }),
+      );
+    } catch (err) {
       toastError(err);
     }
   }, [currentTheme, t, update]);
 
-  const availableThemes = data?.pluginThemesMetadatas == null
-    ? PresetThemesMetadatas
-    : PresetThemesMetadatas.concat(data.pluginThemesMetadatas);
+  const availableThemes =
+    data?.pluginThemesMetadatas == null
+      ? PresetThemesMetadatas
+      : PresetThemesMetadatas.concat(data.pluginThemesMetadatas);
 
-  const selectedTheme = availableThemes.find(t => t.name === currentTheme)?.name ?? PresetThemes.DEFAULT;
+  const selectedTheme =
+    availableThemes.find((t) => t.name === currentTheme)?.name ??
+    PresetThemes.DEFAULT;
 
   return (
     <div className="row">
       <div className="col-12">
-        <h2 className="admin-setting-header">{t('admin:customize_settings.theme')}</h2>
+        <h2 className="admin-setting-header">
+          {t('admin:customize_settings.theme')}
+        </h2>
         <CustomizeThemeOptions
           onSelected={selectedHandler}
           availableThemes={availableThemes}
           selectedTheme={selectedTheme}
         />
-        <AdminUpdateButtonRow onClick={submitHandler} disabled={error != null} />
+        <AdminUpdateButtonRow
+          onClick={submitHandler}
+          disabled={error != null}
+        />
       </div>
     </div>
   );

+ 58 - 30
apps/app/src/client/components/Admin/Customize/CustomizeTitle.tsx

@@ -1,27 +1,21 @@
 import type { FC } from 'react';
 import React, { useCallback, useEffect } from 'react';
-
 import { useTranslation } from 'next-i18next';
 import { useForm } from 'react-hook-form';
 import { Card, CardBody } from 'reactstrap';
 
 import { apiv3Put } from '~/client/util/apiv3-client';
-import { toastSuccess, toastError } from '~/client/util/toastr';
+import { toastError, toastSuccess } from '~/client/util/toastr';
 import { useCustomTitleTemplate } from '~/states/global';
 
 import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
 
 export const CustomizeTitle: FC = () => {
-
   const { t } = useTranslation('admin');
 
   const customTitleTemplate = useCustomTitleTemplate();
 
-  const {
-    register,
-    handleSubmit,
-    reset,
-  } = useForm();
+  const { register, handleSubmit, reset } = useForm();
 
   // Sync form with store data
   useEffect(() => {
@@ -30,39 +24,70 @@ export const CustomizeTitle: FC = () => {
     });
   }, [customTitleTemplate, reset]);
 
-  const onSubmit = useCallback(async(data) => {
-    try {
-      await apiv3Put('/customize-setting/customize-title', {
-        customizeTitle: data.customizeTitle,
-      });
-      toastSuccess(t('toaster.update_successed', { target: t('admin:customize_settings.custom_title'), ns: 'commons' }));
-    }
-    catch (err) {
-      toastError(err);
-    }
-  }, [t]);
+  const onSubmit = useCallback(
+    async (data) => {
+      try {
+        await apiv3Put('/customize-setting/customize-title', {
+          customizeTitle: data.customizeTitle,
+        });
+        toastSuccess(
+          t('toaster.update_successed', {
+            target: t('admin:customize_settings.custom_title'),
+            ns: 'commons',
+          }),
+        );
+      } catch (err) {
+        toastError(err);
+      }
+    },
+    [t],
+  );
 
   return (
     <React.Fragment>
       <div className="row">
         <div className="col-12">
-          <h2 className="admin-setting-header">{t('admin:customize_settings.custom_title')}</h2>
+          <h2 className="admin-setting-header">
+            {t('admin:customize_settings.custom_title')}
+          </h2>
         </div>
 
         <div className="col-12">
           <Card className="card custom-card bg-body-tertiary mb-3">
             <CardBody className="px-0 py-2">
               {/* eslint-disable react/no-danger */}
-              <p dangerouslySetInnerHTML={{ __html: t('admin:customize_settings.custom_title_detail') }} />
+              <p
+                dangerouslySetInnerHTML={{
+                  __html: t('admin:customize_settings.custom_title_detail'),
+                }}
+              />
               <ul>
                 <li>
-                  <span dangerouslySetInnerHTML={{ __html: t('admin:customize_settings.custom_title_detail_placeholder1') }} />
+                  <span
+                    dangerouslySetInnerHTML={{
+                      __html: t(
+                        'admin:customize_settings.custom_title_detail_placeholder1',
+                      ),
+                    }}
+                  />
                 </li>
                 <li>
-                  <span dangerouslySetInnerHTML={{ __html: t('admin:customize_settings.custom_title_detail_placeholder2') }} />
+                  <span
+                    dangerouslySetInnerHTML={{
+                      __html: t(
+                        'admin:customize_settings.custom_title_detail_placeholder2',
+                      ),
+                    }}
+                  />
                 </li>
                 <li>
-                  <span dangerouslySetInnerHTML={{ __html: t('admin:customize_settings.custom_title_detail_placeholder3') }} />
+                  <span
+                    dangerouslySetInnerHTML={{
+                      __html: t(
+                        'admin:customize_settings.custom_title_detail_placeholder3',
+                      ),
+                    }}
+                  />
                 </li>
               </ul>
               {/* eslint-enable react/no-danger */}
@@ -72,16 +97,19 @@ export const CustomizeTitle: FC = () => {
 
         {/* TODO i18n */}
         <div className="form-text text-muted col-12 mb-3">
-          Default Value: <code>&#123;&#123;pagename&#125;&#125; - &#123;&#123;sitename&#125;&#125;</code>
+          Default Value:{' '}
+          <code>
+            &#123;&#123;pagename&#125;&#125; - &#123;&#123;sitename&#125;&#125;
+          </code>
           <br />
-          Default Output Example: <code className="xml">&lt;title&gt;Page name - My GROWI&lt;&#047;title&gt;</code>
+          Default Output Example:{' '}
+          <code className="xml">
+            &lt;title&gt;Page name - My GROWI&lt;&#047;title&gt;
+          </code>
         </div>
         <form onSubmit={handleSubmit(onSubmit)}>
           <div className="col-12">
-            <input
-              className="form-control"
-              {...register('customizeTitle')}
-            />
+            <input className="form-control" {...register('customizeTitle')} />
           </div>
           <div className="col-12">
             <AdminUpdateButtonRow type="submit" disabled={false} />

+ 10 - 9
apps/app/src/client/components/Admin/Customize/PagingSizeUncontrolledDropdown.jsx

@@ -1,13 +1,13 @@
 import React from 'react';
-
 import PropTypes from 'prop-types';
 import {
-  UncontrolledDropdown, DropdownToggle, DropdownMenu, DropdownItem,
+  DropdownItem,
+  DropdownMenu,
+  DropdownToggle,
+  UncontrolledDropdown,
 } from 'reactstrap';
 
-
 const PagingSizeUncontrolledDropdown = (props) => {
-
   function dropdownItemOnClickHandler(num) {
     if (props.onChangeDropdownItem === null) {
       return;
@@ -29,23 +29,24 @@ const PagingSizeUncontrolledDropdown = (props) => {
             <DropdownMenu className="dropdown-menu" role="menu">
               {props.dropdownItemSize.map((num) => {
                 return (
-                  <DropdownItem key={num} role="presentation" onClick={() => dropdownItemOnClickHandler(num)}>
+                  <DropdownItem
+                    key={num}
+                    role="presentation"
+                    onClick={() => dropdownItemOnClickHandler(num)}
+                  >
                     <a role="menuitem">{num}</a>
                   </DropdownItem>
                 );
               })}
             </DropdownMenu>
           </UncontrolledDropdown>
-          <p className="form-text text-muted">
-            {props.desc}
-          </p>
+          <p className="form-text text-muted">{props.desc}</p>
         </div>
       </div>
     </React.Fragment>
   );
 };
 
-
 PagingSizeUncontrolledDropdown.propTypes = {
   label: PropTypes.string,
   toggleLabel: PropTypes.number,

+ 45 - 19
apps/app/src/client/components/Admin/Customize/ThemeColorBox.tsx

@@ -1,25 +1,28 @@
 import React, { type JSX } from 'react';
-
 import type { GrowiThemeMetadata } from '@growi/core';
 
 import styles from './ThemeColorBox.module.scss';
 
 const themeOptionClass = styles['theme-option-container'];
 
-
 type Props = {
-  isSelected: boolean,
-  metadata: GrowiThemeMetadata,
-  onSelected?: () => void,
+  isSelected: boolean;
+  metadata: GrowiThemeMetadata;
+  onSelected?: () => void;
 };
 
 export const ThemeColorBox = (props: Props): JSX.Element => {
-
+  const { isSelected, metadata, onSelected } = props;
   const {
-    isSelected, metadata, onSelected,
-  } = props;
-  const {
-    name, lightBg, darkBg, lightSidebar, darkSidebar, lightIcon, darkIcon, createBtn, isPresetTheme,
+    name,
+    lightBg,
+    darkBg,
+    lightSidebar,
+    darkSidebar,
+    lightIcon,
+    darkIcon,
+    createBtn,
+    isPresetTheme,
   } = metadata;
 
   return (
@@ -33,10 +36,15 @@ export const ThemeColorBox = (props: Props): JSX.Element => {
         role="button"
         className={`
           m-0 rounded rounded-3
-          border border-4 border-primary ${isSelected ? '' : 'border-opacity-10'}`
-        }
+          border border-4 border-primary ${isSelected ? '' : 'border-opacity-10'}`}
       >
-        <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64" className="rounded">
+        <svg
+          xmlns="http://www.w3.org/2000/svg"
+          viewBox="0 0 64 64"
+          width="64"
+          height="64"
+          className="rounded"
+        >
           <path d="M32.5,0V36.364L64,20.437V0Z" fill={lightBg} />
           <path d="M32.5,36.364V64H64V20.438Z" fill={darkBg} />
           <path
@@ -47,17 +55,35 @@ export const ThemeColorBox = (props: Props): JSX.Element => {
             d="M6.436,53.44H26.065V55.5H6.436Zm14.831-11.4h4.8v2.061H17.189L10,47.743H26.065V49.8l-19.629,0v-.259L0,52.8V64H32.5V36.364Z"
             fill={darkSidebar}
           />
-          <path d="M22.338,31.19l6.087-10.543L22.338,10.1H10.163L4.077,20.647,10.163,31.19Z" fill={createBtn} />
+          <path
+            d="M22.338,31.19l6.087-10.543L22.338,10.1H10.163L4.077,20.647,10.163,31.19Z"
+            fill={createBtn}
+          />
           <path d="M6.436,49.543,10,47.742H6.436Z" fill={lightIcon} />
           <path d="M6.436,44.106H17.189l4.078-2.062H6.436Z" fill={lightIcon} />
-          <path d="M6.436,49.8l19.629,0V47.742H10l-3.561,1.8Z" fill={darkIcon} />
+          <path
+            d="M6.436,49.8l19.629,0V47.742H10l-3.561,1.8Z"
+            fill={darkIcon}
+          />
           <path d="M26.065,44.106V42.044h-4.8L17.19,44.106Z" fill={darkIcon} />
-          <rect width="19.629" height="2.062" transform="translate(6.436 53.439)" fill={darkIcon} />
+          <rect
+            width="19.629"
+            height="2.062"
+            transform="translate(6.436 53.439)"
+            fill={darkIcon}
+          />
         </svg>
       </a>
-      <span className={`mt-2 ${isSelected ? '' : 'opacity-25'}`}><b>{ name }</b></span>
-      { !isPresetTheme && <span className={`badge bg-primary mt-1 ${isSelected ? '' : 'opacity-25'}`}>Plugin</span> }
+      <span className={`mt-2 ${isSelected ? '' : 'opacity-25'}`}>
+        <b>{name}</b>
+      </span>
+      {!isPresetTheme && (
+        <span
+          className={`badge bg-primary mt-1 ${isSelected ? '' : 'opacity-25'}`}
+        >
+          Plugin
+        </span>
+      )}
     </div>
   );
-
 };

+ 67 - 24
apps/app/src/client/components/Admin/Notification/GlobalNotification.jsx

@@ -1,30 +1,31 @@
 import { React, useCallback } from 'react';
-
-import { useTranslation } from 'next-i18next';
 import { useRouter } from 'next/router';
+import { useTranslation } from 'next-i18next';
 import PropTypes from 'prop-types';
 
 import AdminNotificationContainer from '~/client/services/AdminNotificationContainer';
-import { toastSuccess, toastError } from '~/client/util/toastr';
+import { toastError, toastSuccess } from '~/client/util/toastr';
 import loggerFactory from '~/utils/logger';
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
-
 import GlobalNotificationList from './GlobalNotificationList';
 
 const logger = loggerFactory('growi:GlobalNotification');
 
 const GlobalNotification = (props) => {
-
   const { adminNotificationContainer } = props;
   const { t } = useTranslation('admin');
 
-  const onClickSubmit = useCallback(async() => {
+  const onClickSubmit = useCallback(async () => {
     try {
       await adminNotificationContainer.updateGlobalNotificationForPages();
-      toastSuccess(t('toaster.update_successed', { target: t('external_notification.external_notification'), ns: 'commons' }));
-    }
-    catch (err) {
+      toastSuccess(
+        t('toaster.update_successed', {
+          target: t('external_notification.external_notification'),
+          ns: 'commons',
+        }),
+      );
+    } catch (err) {
       toastError(err);
       logger.error(err);
     }
@@ -34,11 +35,17 @@ const GlobalNotification = (props) => {
   const { globalNotifications } = adminNotificationContainer.state;
   return (
     <>
-      <h2 className="border-bottom my-4">{t('notification_settings.valid_page')}</h2>
+      <h2 className="border-bottom my-4">
+        {t('notification_settings.valid_page')}
+      </h2>
 
       <p className="card custom-card bg-body-tertiary">
         {/* eslint-disable-next-line react/no-danger */}
-        <span dangerouslySetInnerHTML={{ __html: t('notification_settings.link_notification_help') }} />
+        <span
+          dangerouslySetInnerHTML={{
+            __html: t('notification_settings.link_notification_help'),
+          }}
+        />
       </p>
       <div className="row mb-4">
         <div className="col-md-8 offset-md-2">
@@ -47,12 +54,24 @@ const GlobalNotification = (props) => {
               id="isNotificationForOwnerPageEnabled"
               className="form-check-input"
               type="checkbox"
-              checked={adminNotificationContainer.state.isNotificationForOwnerPageEnabled || false}
-              onChange={() => { adminNotificationContainer.switchIsNotificationForOwnerPageEnabled() }}
+              checked={
+                adminNotificationContainer.state
+                  .isNotificationForOwnerPageEnabled || false
+              }
+              onChange={() => {
+                adminNotificationContainer.switchIsNotificationForOwnerPageEnabled();
+              }}
             />
-            <label className="form-label form-check-label" htmlFor="isNotificationForOwnerPageEnabled">
+            <label
+              className="form-label form-check-label"
+              htmlFor="isNotificationForOwnerPageEnabled"
+            >
               {/* eslint-disable-next-line react/no-danger */}
-              <span dangerouslySetInnerHTML={{ __html: t('notification_settings.just_me_notification_help') }} />
+              <span
+                dangerouslySetInnerHTML={{
+                  __html: t('notification_settings.just_me_notification_help'),
+                }}
+              />
             </label>
           </div>
         </div>
@@ -65,12 +84,24 @@ const GlobalNotification = (props) => {
               id="isNotificationForGroupPageEnabled"
               className="form-check-input"
               type="checkbox"
-              checked={adminNotificationContainer.state.isNotificationForGroupPageEnabled || false}
-              onChange={() => { adminNotificationContainer.switchIsNotificationForGroupPageEnabled() }}
+              checked={
+                adminNotificationContainer.state
+                  .isNotificationForGroupPageEnabled || false
+              }
+              onChange={() => {
+                adminNotificationContainer.switchIsNotificationForGroupPageEnabled();
+              }}
             />
-            <label className="form-label form-check-label" htmlFor="isNotificationForGroupPageEnabled">
+            <label
+              className="form-label form-check-label"
+              htmlFor="isNotificationForGroupPageEnabled"
+            >
               {/* eslint-disable-next-line react/no-danger */}
-              <span dangerouslySetInnerHTML={{ __html: t('notification_settings.group_notification_help') }} />
+              <span
+                dangerouslySetInnerHTML={{
+                  __html: t('notification_settings.group_notification_help'),
+                }}
+              />
             </label>
           </div>
         </div>
@@ -82,7 +113,8 @@ const GlobalNotification = (props) => {
             className="btn btn-primary"
             onClick={onClickSubmit}
             disabled={adminNotificationContainer.state.retrieveError}
-          >{t('Update')}
+          >
+            {t('Update')}
           </button>
         </div>
       </div>
@@ -94,14 +126,22 @@ const GlobalNotification = (props) => {
         className="btn btn-outline-secondary mb-3"
         type="button"
         onClick={() => router.push('/admin/global-notification/new')}
-      >{t('notification_settings.add_notification')}
+      >
+        {t('notification_settings.add_notification')}
       </button>
       <table className="table table-bordered">
         <thead>
           <tr>
             <th>ON/OFF</th>
             {/* eslint-disable-next-line react/no-danger */}
-            <th>{t('notification_settings.trigger_path')} <span dangerouslySetInnerHTML={{ __html: t('notification_settings.trigger_path_help') }} /></th>
+            <th>
+              {t('notification_settings.trigger_path')}{' '}
+              <span
+                dangerouslySetInnerHTML={{
+                  __html: t('notification_settings.trigger_path_help'),
+                }}
+              />
+            </th>
             <th>{t('notification_settings.trigger_events')}</th>
             <th>{t('notification_settings.notify_to')}</th>
             <th></th>
@@ -118,9 +158,12 @@ const GlobalNotification = (props) => {
 };
 
 GlobalNotification.propTypes = {
-  adminNotificationContainer: PropTypes.instanceOf(AdminNotificationContainer).isRequired,
+  adminNotificationContainer: PropTypes.instanceOf(AdminNotificationContainer)
+    .isRequired,
 };
 
-const GlobalNotificationWrapper = withUnstatedContainers(GlobalNotification, [AdminNotificationContainer]);
+const GlobalNotificationWrapper = withUnstatedContainers(GlobalNotification, [
+  AdminNotificationContainer,
+]);
 
 export default GlobalNotificationWrapper;

+ 91 - 40
apps/app/src/client/components/Admin/Notification/GlobalNotificationList.jsx

@@ -1,24 +1,20 @@
 import React from 'react';
-
 import { useTranslation } from 'next-i18next';
 import PropTypes from 'prop-types';
 import urljoin from 'url-join';
 
 import AdminNotificationContainer from '~/client/services/AdminNotificationContainer';
 import { apiv3Put } from '~/client/util/apiv3-client';
-import { toastSuccess, toastError } from '~/client/util/toastr';
+import { toastError, toastSuccess } from '~/client/util/toastr';
 import loggerFactory from '~/utils/logger';
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
-
 import NotificationDeleteModal from './NotificationDeleteModal';
 import { NotificationTypeIcon } from './NotificationTypeIcon';
 
-
 const logger = loggerFactory('growi:GolobalNotificationList');
 
 class GlobalNotificationList extends React.Component {
-
   constructor(props) {
     super(props);
 
@@ -36,34 +32,52 @@ class GlobalNotificationList extends React.Component {
     const { t } = this.props;
     const isEnabled = !notification.isEnabled;
     try {
-      await apiv3Put(`/notification-setting/global-notification/${notification._id}/enabled`, {
-        isEnabled,
-      });
-      toastSuccess(t('notification_settings.toggle_notification', { path: notification.triggerPath }));
+      await apiv3Put(
+        `/notification-setting/global-notification/${notification._id}/enabled`,
+        {
+          isEnabled,
+        },
+      );
+      toastSuccess(
+        t('notification_settings.toggle_notification', {
+          path: notification.triggerPath,
+        }),
+      );
       await this.props.adminNotificationContainer.retrieveNotificationData();
-    }
-    catch (err) {
+    } catch (err) {
       toastError(err);
       logger.error(err);
     }
   }
 
   openConfirmationModal(notification) {
-    this.setState({ isConfirmationModalOpen: true, notificationForConfiguration: notification });
+    this.setState({
+      isConfirmationModalOpen: true,
+      notificationForConfiguration: notification,
+    });
   }
 
   closeConfirmationModal() {
-    this.setState({ isConfirmationModalOpen: false, notificationForConfiguration: null });
+    this.setState({
+      isConfirmationModalOpen: false,
+      notificationForConfiguration: null,
+    });
   }
 
   async onClickSubmit() {
     const { t, adminNotificationContainer } = this.props;
 
     try {
-      const deletedNotificaton = await adminNotificationContainer.deleteGlobalNotificationPattern(this.state.notificationForConfiguration._id);
-      toastSuccess(t('notification_settings.delete_notification_pattern', { path: deletedNotificaton.triggerPath }));
-    }
-    catch (err) {
+      const deletedNotificaton =
+        await adminNotificationContainer.deleteGlobalNotificationPattern(
+          this.state.notificationForConfiguration._id,
+        );
+      toastSuccess(
+        t('notification_settings.delete_notification_pattern', {
+          path: deletedNotificaton.triggerPath,
+        }),
+      );
+    } catch (err) {
       toastError(err);
       logger.error(err);
     }
@@ -88,50 +102,65 @@ class GlobalNotificationList extends React.Component {
                     defaultChecked={notification.isEnabled}
                     onClick={() => this.toggleIsEnabled(notification)}
                   />
-                  <label className="form-label form-check-label" htmlFor={notification._id} />
+                  <label
+                    className="form-label form-check-label"
+                    htmlFor={notification._id}
+                  />
                 </div>
               </td>
-              <td>
-                {notification.triggerPath}
-              </td>
+              <td>{notification.triggerPath}</td>
               <td>
                 <ul className="list-inline mb-0">
                   {notification.triggerEvents.includes('pageCreate') && (
                     <li className="list-inline-item badge rounded-pill bg-success">
-                      <span className=" material-symbols-outlined">description</span> CREATE
+                      <span className=" material-symbols-outlined">
+                        description
+                      </span>{' '}
+                      CREATE
                     </li>
                   )}
                   {notification.triggerEvents.includes('pageEdit') && (
                     <li className="list-inline-item badge rounded-pill bg-warning text-dark">
-                      <span className="material-symbols-outlined">edit</span> EDIT
+                      <span className="material-symbols-outlined">edit</span>{' '}
+                      EDIT
                     </li>
                   )}
                   {notification.triggerEvents.includes('pageMove') && (
                     <li className="list-inline-item badge rounded-pill bg-pink">
-                      <span className="material-symbols-outlined">redo</span> MOVE
+                      <span className="material-symbols-outlined">redo</span>{' '}
+                      MOVE
                     </li>
                   )}
                   {notification.triggerEvents.includes('pageDelete') && (
                     <li className="list-inline-item badge rounded-pill bg-danger">
-                      <span className="material-symbols-outlined">delete_forever</span>DELETE
+                      <span className="material-symbols-outlined">
+                        delete_forever
+                      </span>
+                      DELETE
                     </li>
                   )}
                   {notification.triggerEvents.includes('pageLike') && (
                     <li className="list-inline-item badge rounded-pill bg-info">
-                      <span className="material-symbols-outlined">favorite</span> LIKE
+                      <span className="material-symbols-outlined">
+                        favorite
+                      </span>{' '}
+                      LIKE
                     </li>
                   )}
                   {notification.triggerEvents.includes('comment') && (
                     <li className="list-inline-item badge rounded-pill bg-primary">
-                      <span className="material-symbols-outlined">bubble_chart</span> POST
+                      <span className="material-symbols-outlined">
+                        bubble_chart
+                      </span>{' '}
+                      POST
                     </li>
                   )}
                 </ul>
               </td>
               <td>
                 <NotificationTypeIcon notification={notification} />
-                { notification.__t === 'mail' && notification.toEmail }
-                { notification.__t === 'slack' && notification.slackChannels }
+                {notification.__t === 'mail' && notification.toEmail}
+                {notification.__t === 'slack' && notification.slackChannels}
               </td>
               <td className="td-abs-center">
                 <div className="dropdown">
@@ -143,14 +172,32 @@ class GlobalNotificationList extends React.Component {
                     aria-haspopup="true"
                     aria-expanded="false"
                   >
-                    <span className="material-symbols-outlined">settings</span> <span className="caret"></span>
+                    <span className="material-symbols-outlined">settings</span>{' '}
+                    <span className="caret"></span>
                   </button>
-                  <div className="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownMenuButton">
-                    <a className="dropdown-item" href={urljoin('/admin/global-notification/', notification._id)}>
-                      <span className="material-symbols-outlined">note</span> {t('Edit')}
+                  <div
+                    className="dropdown-menu dropdown-menu-right"
+                    aria-labelledby="dropdownMenuButton"
+                  >
+                    <a
+                      className="dropdown-item"
+                      href={urljoin(
+                        '/admin/global-notification/',
+                        notification._id,
+                      )}
+                    >
+                      <span className="material-symbols-outlined">note</span>{' '}
+                      {t('Edit')}
                     </a>
-                    <button className="dropdown-item" type="button" onClick={() => this.openConfirmationModal(notification)}>
-                      <span className="material-symbols-outlined text-danger">delete_forever</span> {t('Delete')}
+                    <button
+                      className="dropdown-item"
+                      type="button"
+                      onClick={() => this.openConfirmationModal(notification)}
+                    >
+                      <span className="material-symbols-outlined text-danger">
+                        delete_forever
+                      </span>{' '}
+                      {t('Delete')}
                     </button>
                   </div>
                 </div>
@@ -163,19 +210,20 @@ class GlobalNotificationList extends React.Component {
             isOpen={this.state.isConfirmationModalOpen}
             onClose={this.closeConfirmationModal}
             onClickSubmit={this.onClickSubmit}
-            notificationForConfiguration={this.state.notificationForConfiguration}
+            notificationForConfiguration={
+              this.state.notificationForConfiguration
+            }
           />
         )}
       </React.Fragment>
     );
   }
-
 }
 
 GlobalNotificationList.propTypes = {
   t: PropTypes.func.isRequired, // i18next
-  adminNotificationContainer: PropTypes.instanceOf(AdminNotificationContainer).isRequired,
-
+  adminNotificationContainer: PropTypes.instanceOf(AdminNotificationContainer)
+    .isRequired,
 };
 
 const GlobalNotificationListWrapperFC = (props) => {
@@ -184,6 +232,9 @@ const GlobalNotificationListWrapperFC = (props) => {
   return <GlobalNotificationList t={t} {...props} />;
 };
 
-const GlobalNotificationListWrapper = withUnstatedContainers(GlobalNotificationListWrapperFC, [AdminNotificationContainer]);
+const GlobalNotificationListWrapper = withUnstatedContainers(
+  GlobalNotificationListWrapperFC,
+  [AdminNotificationContainer],
+);
 
 export default GlobalNotificationListWrapper;

+ 156 - 101
apps/app/src/client/components/Admin/Notification/ManageGlobalNotification.tsx

@@ -1,45 +1,49 @@
 import React, {
-  useCallback, useMemo, useEffect, useState, type JSX,
+  type JSX,
+  useCallback,
+  useEffect,
+  useMemo,
+  useState,
 } from 'react';
-
-import { useAtomValue } from 'jotai';
-import { useTranslation } from 'next-i18next';
 import Link from 'next/link';
 import { useRouter } from 'next/router';
+import { useAtomValue } from 'jotai';
+import { useTranslation } from 'next-i18next';
 
-import { NotifyType, TriggerEventType } from '~/client/interfaces/global-notification';
+import {
+  NotifyType,
+  TriggerEventType,
+} from '~/client/interfaces/global-notification';
 import { apiv3Post } from '~/client/util/apiv3-client';
 import { toastError } from '~/client/util/toastr';
 import { isMailerSetupAtom } from '~/states/server-configurations';
 import { useSWRxGlobalNotification } from '~/stores/global-notification';
 import loggerFactory from '~/utils/logger';
 
-
 import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
-
 import TriggerEventCheckBox from './TriggerEventCheckBox';
 
-
 const logger = loggerFactory('growi:manageGlobalNotification');
 
-
 type Props = {
-  globalNotificationId?: string,
-}
+  globalNotificationId?: string;
+};
 
 const ManageGlobalNotification = (props: Props): JSX.Element => {
-
   const [triggerPath, setTriggerPath] = useState('');
   const [notifyType, setNotifyType] = useState<NotifyType>(NotifyType.Email);
   const [emailToSend, setEmailToSend] = useState('');
   const [slackChannelToSend, setSlackChannelToSend] = useState('');
   const [triggerEvents, setTriggerEvents] = useState(new Set());
-  const { data: globalNotificationData, update: updateGlobalNotification } = useSWRxGlobalNotification(props.globalNotificationId || '');
-  const globalNotification = useMemo(() => globalNotificationData?.globalNotification, [globalNotificationData?.globalNotification]);
+  const { data: globalNotificationData, update: updateGlobalNotification } =
+    useSWRxGlobalNotification(props.globalNotificationId || '');
+  const globalNotification = useMemo(
+    () => globalNotificationData?.globalNotification,
+    [globalNotificationData?.globalNotification],
+  );
 
   const router = useRouter();
 
-
   useEffect(() => {
     if (globalNotification != null) {
       const notifyType = globalNotification.__t;
@@ -50,15 +54,15 @@ const ManageGlobalNotification = (props: Props): JSX.Element => {
 
       if (notifyType === NotifyType.Email) {
         setEmailToSend(globalNotification.toEmail);
-      }
-      else {
+      } else {
         setSlackChannelToSend(globalNotification.slackChannels);
       }
     }
   }, [globalNotification]);
 
   const isLoading = globalNotificationData === undefined;
-  const notExistsGlobalNotification = !isLoading && globalNotificationData == null;
+  const notExistsGlobalNotification =
+    !isLoading && globalNotificationData == null;
 
   useEffect(() => {
     if (notExistsGlobalNotification) {
@@ -66,22 +70,24 @@ const ManageGlobalNotification = (props: Props): JSX.Element => {
     }
   }, [notExistsGlobalNotification, router]);
 
+  const onChangeTriggerEvents = useCallback(
+    (triggerEvent) => {
+      let newTriggerEvents;
+
+      if (triggerEvents.has(triggerEvent)) {
+        newTriggerEvents = [...triggerEvents].filter(
+          (item) => item !== triggerEvent,
+        );
+        setTriggerEvents(new Set(newTriggerEvents));
+      } else {
+        newTriggerEvents = [...triggerEvents, triggerEvent];
+        setTriggerEvents(new Set(newTriggerEvents));
+      }
+    },
+    [triggerEvents],
+  );
 
-  const onChangeTriggerEvents = useCallback((triggerEvent) => {
-    let newTriggerEvents;
-
-    if (triggerEvents.has(triggerEvent)) {
-      newTriggerEvents = ([...triggerEvents].filter(item => item !== triggerEvent));
-      setTriggerEvents(new Set(newTriggerEvents));
-    }
-    else {
-      newTriggerEvents = [...triggerEvents, triggerEvent];
-      setTriggerEvents(new Set(newTriggerEvents));
-    }
-  }, [triggerEvents]);
-
-
-  const updateButtonClickedHandler = useCallback(async() => {
+  const updateButtonClickedHandler = useCallback(async () => {
     const requestParams = {
       triggerPath,
       notifyType,
@@ -94,18 +100,27 @@ const ManageGlobalNotification = (props: Props): JSX.Element => {
       if (props.globalNotificationId != null) {
         await updateGlobalNotification(requestParams);
         router.push('/admin/notification');
-      }
-      else {
-        await apiv3Post('/notification-setting/global-notification', requestParams);
+      } else {
+        await apiv3Post(
+          '/notification-setting/global-notification',
+          requestParams,
+        );
         router.push('/admin/notification');
       }
-    }
-    catch (err) {
+    } catch (err) {
       toastError(err);
       logger.error(err);
     }
-  }, [emailToSend, notifyType, props.globalNotificationId, router, slackChannelToSend, triggerEvents, triggerPath, updateGlobalNotification]);
-
+  }, [
+    emailToSend,
+    notifyType,
+    props.globalNotificationId,
+    router,
+    slackChannelToSend,
+    triggerEvents,
+    triggerPath,
+    updateGlobalNotification,
+  ]);
 
   // Mailer setup status (unused yet but kept for potential conditional logic)
   const isMailerSetup = useAtomValue(isMailerSetupAtom);
@@ -115,22 +130,33 @@ const ManageGlobalNotification = (props: Props): JSX.Element => {
     <>
       <div className="my-3">
         <Link href="/admin/notification" className="btn btn-outline-secondary">
-          <span className="material-symbols-outlined" aria-hidden="true">arrow_left_alt</span>
+          <span className="material-symbols-outlined" aria-hidden="true">
+            arrow_left_alt
+          </span>
           {t('notification_settings.back_to_list')}
         </Link>
       </div>
 
-
       <div className="row">
         <div className="form-box col-md-12">
-          <h2 className="border-bottom mb-5">{t('notification_settings.notification_detail')}</h2>
+          <h2 className="border-bottom mb-5">
+            {t('notification_settings.notification_detail')}
+          </h2>
         </div>
 
         <div className="col-sm-4">
           <h3>
-            <label htmlFor="triggerPath" className="form-label">{t('notification_settings.trigger_path')}
+            <label htmlFor="triggerPath" className="form-label">
+              {t('notification_settings.trigger_path')}
               {/* eslint-disable-next-line react/no-danger */}
-              <small dangerouslySetInnerHTML={{ __html: t('notification_settings.trigger_path_help', '<code>*</code>') }} />
+              <small
+                dangerouslySetInnerHTML={{
+                  __html: t(
+                    'notification_settings.trigger_path_help',
+                    '<code>*</code>',
+                  ),
+                }}
+              />
             </label>
           </h3>
           <div>
@@ -139,7 +165,9 @@ const ManageGlobalNotification = (props: Props): JSX.Element => {
               type="text"
               name="triggerPath"
               value={triggerPath}
-              onChange={(e) => { setTriggerPath(e.target.value) }}
+              onChange={(e) => {
+                setTriggerPath(e.target.value);
+              }}
               required
             />
           </div>
@@ -154,7 +182,9 @@ const ManageGlobalNotification = (props: Props): JSX.Element => {
                 name="notifyType"
                 value="mail"
                 checked={notifyType === NotifyType.Email}
-                onChange={() => { setNotifyType(NotifyType.Email) }}
+                onChange={() => {
+                  setNotifyType(NotifyType.Email);
+                }}
               />
               <label className="form-label form-check-label" htmlFor="mail">
                 <p className="fw-bold">Email</p>
@@ -168,7 +198,9 @@ const ManageGlobalNotification = (props: Props): JSX.Element => {
                 name="notifyType"
                 value="slack"
                 checked={notifyType === NotifyType.SLACK}
-                onChange={() => { setNotifyType(NotifyType.SLACK) }}
+                onChange={() => {
+                  setNotifyType(NotifyType.SLACK);
+                }}
               />
               <label className="form-label form-check-label" htmlFor="slack">
                 <p className="fw-bold">Slack</p>
@@ -176,57 +208,75 @@ const ManageGlobalNotification = (props: Props): JSX.Element => {
             </div>
           </div>
 
-          {notifyType === NotifyType.Email
-            ? (
-              <>
-                <div className="input-group notify-to-option" id="mail-input">
-                  <div>
-                    <span className="input-group-text" id="mail-addon"></span><span className="material-symbols-outlined">mail</span>
-                  </div>
-                  <input
-                    className="form-control"
-                    type="text"
-                    aria-describedby="mail-addon"
-                    name="toEmail"
-                    placeholder="Email"
-                    value={emailToSend}
-                    onChange={(e) => { setEmailToSend(e.target.value) }}
-                  />
-
+          {notifyType === NotifyType.Email ? (
+            <>
+              <div className="input-group notify-to-option" id="mail-input">
+                <div>
+                  <span className="input-group-text" id="mail-addon"></span>
+                  <span className="material-symbols-outlined">mail</span>
                 </div>
-
-                <p className="p-2">
-                  {/* eslint-disable-next-line react/no-danger */}
-                  {!isMailerSetup && <span className="form-text text-muted" dangerouslySetInnerHTML={{ __html: t('admin:mailer_setup_required') }} />}
-                  <b>Hint: </b>
-                  <a href="https://ifttt.com/create" target="blank">{t('notification_settings.email.ifttt_link')}
-                    <span className="material-symbols-outlined">share</span>
-                  </a>
-                </p>
-              </>
-            )
-            : (
-              <>
-                <div className="input-group notify-to-option" id="slack-input">
-                  <div>
-                    <span className="input-group-text" id="slack-channel-addon"></span><span className="material-symbols-outlined">tag</span>
-                  </div>
-                  <input
-                    className="form-control"
-                    type="text"
-                    aria-describedby="slack-channel-addon"
-                    name="notificationGlobal[slackChannels]"
-                    placeholder="Slack Channel"
-                    value={slackChannelToSend}
-                    onChange={(e) => { setSlackChannelToSend(e.target.value) }}
+                <input
+                  className="form-control"
+                  type="text"
+                  aria-describedby="mail-addon"
+                  name="toEmail"
+                  placeholder="Email"
+                  value={emailToSend}
+                  onChange={(e) => {
+                    setEmailToSend(e.target.value);
+                  }}
+                />
+              </div>
+
+              <p className="p-2">
+                {/* eslint-disable-next-line react/no-danger */}
+                {!isMailerSetup && (
+                  <span
+                    className="form-text text-muted"
+                    dangerouslySetInnerHTML={{
+                      __html: t('admin:mailer_setup_required'),
+                    }}
                   />
+                )}
+                <b>Hint: </b>
+                <a href="https://ifttt.com/create" target="blank">
+                  {t('notification_settings.email.ifttt_link')}
+                  <span className="material-symbols-outlined">share</span>
+                </a>
+              </p>
+            </>
+          ) : (
+            <>
+              <div className="input-group notify-to-option" id="slack-input">
+                <div>
+                  <span
+                    className="input-group-text"
+                    id="slack-channel-addon"
+                  ></span>
+                  <span className="material-symbols-outlined">tag</span>
                 </div>
-                <p className="p-2">
-                  {/* eslint-disable-next-line react/no-danger */}
-                  <span dangerouslySetInnerHTML={{ __html: t('notification_settings.channel_desc') }} />
-                </p>
-              </>
-            )}
+                <input
+                  className="form-control"
+                  type="text"
+                  aria-describedby="slack-channel-addon"
+                  name="notificationGlobal[slackChannels]"
+                  placeholder="Slack Channel"
+                  value={slackChannelToSend}
+                  onChange={(e) => {
+                    setSlackChannelToSend(e.target.value);
+                  }}
+                />
+              </div>
+              <p className="p-2">
+                {/* eslint-disable-next-line react/no-danger */}
+                <span
+                  dangerouslySetInnerHTML={{
+                    __html: t('notification_settings.channel_desc'),
+                  }}
+                />
+              </p>
+            </>
+          )}
         </div>
 
         <div className="offset-1 col-sm-5">
@@ -240,7 +290,8 @@ const ManageGlobalNotification = (props: Props): JSX.Element => {
                 onChange={() => onChangeTriggerEvents(TriggerEventType.CREATE)}
               >
                 <span className="badge rounded-pill bg-success">
-                  <span className="material-symbols-outlined">edit_note</span> CREATE
+                  <span className="material-symbols-outlined">edit_note</span>{' '}
+                  CREATE
                 </span>
               </TriggerEventCheckBox>
             </div>
@@ -276,7 +327,10 @@ const ManageGlobalNotification = (props: Props): JSX.Element => {
                 onChange={() => onChangeTriggerEvents(TriggerEventType.DELETE)}
               >
                 <span className="badge rounded-pill bg-danger">
-                  <span className="material-symbols-outlined">delete_forever</span>DELETE
+                  <span className="material-symbols-outlined">
+                    delete_forever
+                  </span>
+                  DELETE
                 </span>
               </TriggerEventCheckBox>
             </div>
@@ -288,7 +342,8 @@ const ManageGlobalNotification = (props: Props): JSX.Element => {
                 onChange={() => onChangeTriggerEvents(TriggerEventType.LIKE)}
               >
                 <span className="badge rounded-pill bg-info">
-                  <span className="material-symbols-outlined">favorite</span>LIKE
+                  <span className="material-symbols-outlined">favorite</span>
+                  LIKE
                 </span>
               </TriggerEventCheckBox>
             </div>
@@ -300,11 +355,11 @@ const ManageGlobalNotification = (props: Props): JSX.Element => {
                 onChange={() => onChangeTriggerEvents(TriggerEventType.POST)}
               >
                 <span className="badge rounded-pill bg-primary">
-                  <span className="material-symbols-outlined">language</span>POST
+                  <span className="material-symbols-outlined">language</span>
+                  POST
                 </span>
               </TriggerEventCheckBox>
             </div>
-
           </div>
         </div>
       </div>

+ 18 - 11
apps/app/src/client/components/Admin/Notification/NotificationDeleteModal.jsx

@@ -1,37 +1,44 @@
 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';
 
 class NotificationDeleteModal extends React.PureComponent {
-
   render() {
     const { t, notificationForConfiguration } = this.props;
     return (
       <Modal isOpen={this.props.isOpen} toggle={this.props.onClose}>
-        <ModalHeader tag="h4" toggle={this.props.onClose} className="text-danger">
-          <span className="material-symbols-outlined">delete_forever</span>Delete Global Notification Setting
+        <ModalHeader
+          tag="h4"
+          toggle={this.props.onClose}
+          className="text-danger"
+        >
+          <span className="material-symbols-outlined">delete_forever</span>
+          Delete Global Notification Setting
         </ModalHeader>
         <ModalBody>
           <p>
-            {t('notification_settings.delete_notification_pattern_desc1', { path: notificationForConfiguration.triggerPath })}
+            {t('notification_settings.delete_notification_pattern_desc1', {
+              path: notificationForConfiguration.triggerPath,
+            })}
           </p>
           <p className="text-danger">
             {t('notification_settings.delete_notification_pattern_desc2')}
           </p>
         </ModalBody>
         <ModalFooter>
-          <button type="button" className="btn btn-sm btn-danger" onClick={this.props.onClickSubmit}>
-            <span className="material-symbols-outlined">delete_forever</span> {t('Delete')}
+          <button
+            type="button"
+            className="btn btn-sm btn-danger"
+            onClick={this.props.onClickSubmit}
+          >
+            <span className="material-symbols-outlined">delete_forever</span>{' '}
+            {t('Delete')}
           </button>
         </ModalFooter>
       </Modal>
     );
   }
-
 }
 
 NotificationDeleteModal.propTypes = {

+ 77 - 42
apps/app/src/client/components/Admin/Notification/NotificationSetting.jsx

@@ -1,13 +1,8 @@
-import React, {
-  useCallback, useEffect, useMemo, useState,
-} from 'react';
-
+import React, { useCallback, useEffect, useMemo, useState } from 'react';
 import { SlackbotType } from '@growi/slack';
 import { useTranslation } from 'next-i18next';
 import PropTypes from 'prop-types';
-import {
-  TabContent, TabPane,
-} from 'reactstrap';
+import { TabContent, TabPane } from 'reactstrap';
 
 import AdminNotificationContainer from '~/client/services/AdminNotificationContainer';
 import { toastError } from '~/client/util/toastr';
@@ -16,8 +11,6 @@ import loggerFactory from '~/utils/logger';
 
 import CustomNav from '../../CustomNavigation/CustomNav';
 import { withUnstatedContainers } from '../../UnstatedUtils';
-
-
 import GlobalNotification from './GlobalNotification';
 import UserTriggerNotification from './UserTriggerNotification';
 
@@ -25,14 +18,19 @@ const logger = loggerFactory('growi:NotificationSetting');
 
 let retrieveErrors = null;
 
-
 // eslint-disable-next-line react/prop-types
 const Badge = ({ isEnabled }) => {
   const { t } = useTranslation('admin');
 
-  return isEnabled
-    ? <span className="badge text-bg-success">{t('external_notification.enabled')}</span>
-    : <span className="badge text-bg-primary">{t('external_notification.disabled')}</span>;
+  return isEnabled ? (
+    <span className="badge text-bg-success">
+      {t('external_notification.enabled')}
+    </span>
+  ) : (
+    <span className="badge text-bg-primary">
+      {t('external_notification.disabled')}
+    </span>
+  );
 };
 
 const SkeletonListItem = () => (
@@ -48,20 +46,31 @@ const SkeletonListItem = () => (
 const SlackIntegrationListItem = ({ isEnabled, currentBotType }) => {
   const { t } = useTranslation('admin');
 
-  const isCautionVisible = currentBotType === SlackbotType.OFFICIAL || currentBotType === SlackbotType.CUSTOM_WITH_PROXY;
+  const isCautionVisible =
+    currentBotType === SlackbotType.OFFICIAL ||
+    currentBotType === SlackbotType.CUSTOM_WITH_PROXY;
 
   return (
-    <li data-testid="slack-integration-list-item" className="list-group-item bg-body-tertiary">
+    <li
+      data-testid="slack-integration-list-item"
+      className="list-group-item bg-body-tertiary"
+    >
       <h4>
         <Badge isEnabled={isEnabled} />
-        <a href="/admin/slack-integration" className="ms-2">{t('slack_integration.slack_integration')}</a>
+        <a href="/admin/slack-integration" className="ms-2">
+          {t('slack_integration.slack_integration')}
+        </a>
       </h4>
-      { isCautionVisible && (
+      {isCautionVisible && (
         <ul className="mt-2 ps-4">
           {/* eslint-disable-next-line react/no-danger */}
-          <li dangerouslySetInnerHTML={{ __html: t('external_notification.caution_enabled') }} />
+          <li
+            dangerouslySetInnerHTML={{
+              __html: t('external_notification.caution_enabled'),
+            }}
+          />
         </ul>
-      ) }
+      )}
     </li>
   );
 };
@@ -74,16 +83,23 @@ const LegacySlackIntegrationListItem = ({ isEnabled }) => {
     <li className="list-group-item">
       <h4>
         <Badge isEnabled={isEnabled} />
-        <a href="/admin/slack-integration-legacy" className="ms-2">{t('slack_integration_legacy.slack_integration_legacy')}</a>
+        <a href="/admin/slack-integration-legacy" className="ms-2">
+          {t('slack_integration_legacy.slack_integration_legacy')}
+        </a>
       </h4>
-      { isEnabled && (
+      {isEnabled && (
         <ul className="mt-2 ps-4">
           <li>
             {/* eslint-disable-next-line react/no-danger */}
-            <span className="text-danger" dangerouslySetInnerHTML={{ __html: t('slack_integration_legacy.alert_deplicated') }}></span>
+            <span
+              className="text-danger"
+              dangerouslySetInnerHTML={{
+                __html: t('slack_integration_legacy.alert_deplicated'),
+              }}
+            ></span>
           </li>
         </ul>
-      ) }
+      )}
     </li>
   );
 };
@@ -95,24 +111,24 @@ function NotificationSetting(props) {
 
   const [isMounted, setMounted] = useState(false);
   const [activeTab, setActiveTab] = useState('user_trigger_notification');
-  const [activeComponents, setActiveComponents] = useState(new Set(['user_trigger_notification']));
+  const [activeComponents, setActiveComponents] = useState(
+    new Set(['user_trigger_notification']),
+  );
 
   const switchActiveTab = (selectedTab) => {
     setActiveTab(selectedTab);
     setActiveComponents(activeComponents.add(selectedTab));
   };
 
-  const fetchData = useCallback(async() => {
+  const fetchData = useCallback(async () => {
     try {
       await adminNotificationContainer.retrieveNotificationData();
-    }
-    catch (err) {
+    } catch (err) {
       const errs = toArrayIfNot(err);
       toastError(errs);
       logger.error(errs);
       retrieveErrors = errs;
-    }
-    finally {
+    } finally {
       setMounted(true);
     }
   }, [adminNotificationContainer]);
@@ -134,26 +150,37 @@ function NotificationSetting(props) {
     };
   }, []);
 
-  const { isSlackbotConfigured, isSlackLegacyConfigured, currentBotType } = adminNotificationContainer.state;
+  const { isSlackbotConfigured, isSlackLegacyConfigured, currentBotType } =
+    adminNotificationContainer.state;
   const isSlackEnabled = isSlackbotConfigured;
   const isSlackLegacyEnabled = !isSlackbotConfigured && isSlackLegacyConfigured;
 
   return (
     <div data-testid="admin-notification">
-      <h2 className="admin-setting-header">{t('external_notification.header_status')}</h2>
+      <h2 className="admin-setting-header">
+        {t('external_notification.header_status')}
+      </h2>
       <ul className="list-group">
-        { !isMounted && <SkeletonListItem />}
-        { isMounted && (
+        {!isMounted && <SkeletonListItem />}
+        {isMounted && (
           <>
-            <SlackIntegrationListItem isEnabled={isSlackEnabled} currentBotType={currentBotType} />
+            <SlackIntegrationListItem
+              isEnabled={isSlackEnabled}
+              currentBotType={currentBotType}
+            />
             {/* Legacy Slack Integration become visible only when new Slack Integration is disabled */}
-            { !isSlackEnabled && <LegacySlackIntegrationListItem isEnabled={isSlackLegacyEnabled} /> }
+            {!isSlackEnabled && (
+              <LegacySlackIntegrationListItem
+                isEnabled={isSlackLegacyEnabled}
+              />
+            )}
           </>
-        ) }
+        )}
       </ul>
 
-
-      <h2 className="admin-setting-header mt-5">{t('notification_settings.notification_settings')}</h2>
+      <h2 className="admin-setting-header mt-5">
+        {t('notification_settings.notification_settings')}
+      </h2>
 
       <CustomNav
         activeTab={activeTab}
@@ -165,20 +192,28 @@ function NotificationSetting(props) {
 
       <TabContent activeTab={activeTab} className="p-5">
         <TabPane tabId="user_trigger_notification">
-          {activeComponents.has('user_trigger_notification') && <UserTriggerNotification />}
+          {activeComponents.has('user_trigger_notification') && (
+            <UserTriggerNotification />
+          )}
         </TabPane>
         <TabPane tabId="global_notification">
-          {activeComponents.has('global_notification') && <GlobalNotification />}
+          {activeComponents.has('global_notification') && (
+            <GlobalNotification />
+          )}
         </TabPane>
       </TabContent>
     </div>
   );
 }
 
-const NotificationSettingWithUnstatedContainer = withUnstatedContainers(NotificationSetting, [AdminNotificationContainer]);
+const NotificationSettingWithUnstatedContainer = withUnstatedContainers(
+  NotificationSetting,
+  [AdminNotificationContainer],
+);
 
 NotificationSetting.propTypes = {
-  adminNotificationContainer: PropTypes.instanceOf(AdminNotificationContainer).isRequired,
+  adminNotificationContainer: PropTypes.instanceOf(AdminNotificationContainer)
+    .isRequired,
 };
 
 export default NotificationSettingWithUnstatedContainer;

+ 8 - 6
apps/app/src/client/components/Admin/Notification/NotificationTypeIcon.tsx

@@ -1,18 +1,18 @@
 import React, { type JSX } from 'react';
-
 import { UncontrolledTooltip } from 'reactstrap';
 
 import type { INotificationType } from '~/client/interfaces/notification';
 
-
 type NotificationTypeIconProps = {
   // supports 2 types:
   //   User trigger notification -> has 'provider: slack'
   //   Global notification -> has '__t: slack|mail'
-  notification: INotificationType
-}
+  notification: INotificationType;
+};
 
-export const NotificationTypeIcon = (props: NotificationTypeIconProps): JSX.Element => {
+export const NotificationTypeIcon = (
+  props: NotificationTypeIconProps,
+): JSX.Element => {
   const { __t, _id, provider } = props.notification;
 
   const type = __t != null && __t === 'mail' ? 'mail' : 'slack';
@@ -28,7 +28,9 @@ export const NotificationTypeIcon = (props: NotificationTypeIconProps): JSX.Elem
 
   return (
     <>
-      <span id={elemId} className="material-symbols-outlined me-1">{iconName}</span>
+      <span id={elemId} className="material-symbols-outlined me-1">
+        {iconName}
+      </span>
       <UncontrolledTooltip target={elemId}>{toolChip}</UncontrolledTooltip>
     </>
   );

+ 5 - 5
apps/app/src/client/components/Admin/Notification/TriggerEventCheckBox.jsx

@@ -1,5 +1,4 @@
 import React from 'react';
-
 import { useTranslation } from 'next-i18next';
 import PropTypes from 'prop-types';
 
@@ -15,15 +14,16 @@ const TriggerEventCheckBox = (props) => {
         checked={props.checked}
         onChange={props.onChange}
       />
-      <label className="form-label form-check-label" htmlFor={`trigger-event-${props.event}`}>
-        {props.children}{' '}
-        {t(`notification_settings.event_${props.event}`)}
+      <label
+        className="form-label form-check-label"
+        htmlFor={`trigger-event-${props.event}`}
+      >
+        {props.children} {t(`notification_settings.event_${props.event}`)}
       </label>
     </div>
   );
 };
 
-
 TriggerEventCheckBox.propTypes = {
   t: PropTypes.func.isRequired, // i18next
 

+ 18 - 13
apps/app/src/client/components/Admin/Notification/UserNotificationRow.jsx

@@ -1,16 +1,13 @@
 import React from 'react';
-
 import { useTranslation } from 'next-i18next';
 import PropTypes from 'prop-types';
 
 import AdminNotificationContainer from '~/client/services/AdminNotificationContainer';
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
-
 import { NotificationTypeIcon } from './NotificationTypeIcon';
 
 class UserNotificationRow extends React.PureComponent {
-
   render() {
     const { t, notification } = this.props;
     const id = `user-notification-${notification._id}`;
@@ -18,26 +15,32 @@ class UserNotificationRow extends React.PureComponent {
     return (
       <React.Fragment>
         <tr className="admin-notif-row" key={id}>
+          <td className="px-4">{notification.pathPattern}</td>
           <td className="px-4">
-            {notification.pathPattern}
-          </td>
-          <td className="px-4">
-            <NotificationTypeIcon notification={notification} />{notification.channel}
+            <NotificationTypeIcon notification={notification} />
+            {notification.channel}
           </td>
           <td>
-            <button type="submit" className="btn btn-outline-danger" onClick={() => { this.props.onClickDeleteBtn(notification._id) }}>{t('Delete')}</button>
+            <button
+              type="submit"
+              className="btn btn-outline-danger"
+              onClick={() => {
+                this.props.onClickDeleteBtn(notification._id);
+              }}
+            >
+              {t('Delete')}
+            </button>
           </td>
         </tr>
       </React.Fragment>
     );
-
   }
-
 }
 
 UserNotificationRow.propTypes = {
   t: PropTypes.func.isRequired, // i18next
-  adminNotificationContainer: PropTypes.instanceOf(AdminNotificationContainer).isRequired,
+  adminNotificationContainer: PropTypes.instanceOf(AdminNotificationContainer)
+    .isRequired,
 
   notification: PropTypes.object.isRequired,
   onClickDeleteBtn: PropTypes.func.isRequired,
@@ -50,7 +53,9 @@ const UserNotificationRowWrapperWrapperFC = (props) => {
   return <UserNotificationRow t={t} {...props} />;
 };
 
-const UserNotificationRowWrapper = withUnstatedContainers(UserNotificationRowWrapperWrapperFC, [AdminNotificationContainer]);
-
+const UserNotificationRowWrapper = withUnstatedContainers(
+  UserNotificationRowWrapperWrapperFC,
+  [AdminNotificationContainer],
+);
 
 export default UserNotificationRowWrapper;

+ 64 - 29
apps/app/src/client/components/Admin/Notification/UserTriggerNotification.jsx

@@ -1,20 +1,17 @@
 import React from 'react';
-
 import { useTranslation } from 'next-i18next';
 import PropTypes from 'prop-types';
 
 import AdminNotificationContainer from '~/client/services/AdminNotificationContainer';
-import { toastSuccess, toastError } from '~/client/util/toastr';
+import { toastError, toastSuccess } from '~/client/util/toastr';
 import loggerFactory from '~/utils/logger';
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
-
 import UserNotificationRow from './UserNotificationRow';
 
 const logger = loggerFactory('growi:slackAppConfiguration');
 
 class UserTriggerNotification extends React.Component {
-
   constructor(props) {
     super(props);
 
@@ -28,7 +25,6 @@ class UserTriggerNotification extends React.Component {
     this.validateForm = this.validateForm.bind(this);
     this.onClickSubmit = this.onClickSubmit.bind(this);
     this.onClickDeleteBtn = this.onClickDeleteBtn.bind(this);
-
   }
 
   /**
@@ -53,11 +49,13 @@ class UserTriggerNotification extends React.Component {
     const { t, adminNotificationContainer } = this.props;
 
     try {
-      await adminNotificationContainer.addNotificationPattern(this.state.pathPattern, this.state.channel);
+      await adminNotificationContainer.addNotificationPattern(
+        this.state.pathPattern,
+        this.state.channel,
+      );
       toastSuccess(t('notification_settings.add_notification_pattern'));
       this.setState({ pathPattern: '', channel: '' });
-    }
-    catch (err) {
+    } catch (err) {
       toastError(err);
       logger.error(err);
     }
@@ -67,10 +65,16 @@ class UserTriggerNotification extends React.Component {
     const { t, adminNotificationContainer } = this.props;
 
     try {
-      const deletedNotificaton = await adminNotificationContainer.deleteUserTriggerNotificationPattern(notificationIdForDelete);
-      toastSuccess(t('notification_settings.delete_notification_pattern', { path: deletedNotificaton.pathPattern }));
-    }
-    catch (err) {
+      const deletedNotificaton =
+        await adminNotificationContainer.deleteUserTriggerNotificationPattern(
+          notificationIdForDelete,
+        );
+      toastSuccess(
+        t('notification_settings.delete_notification_pattern', {
+          path: deletedNotificaton.pathPattern,
+        }),
+      );
+    } catch (err) {
       toastError(err);
       logger.error(err);
     }
@@ -78,11 +82,14 @@ class UserTriggerNotification extends React.Component {
 
   render() {
     const { t, adminNotificationContainer } = this.props;
-    const userNotifications = adminNotificationContainer.state.userNotifications || [];
+    const userNotifications =
+      adminNotificationContainer.state.userNotifications || [];
 
     return (
       <React.Fragment>
-        <h2 className="border-bottom my-4">{t('notification_settings.user_trigger_notification_header')}</h2>
+        <h2 className="border-bottom my-4">
+          {t('notification_settings.user_trigger_notification_header')}
+        </h2>
 
         <table className="table table-bordered">
           <thead>
@@ -101,18 +108,26 @@ class UserTriggerNotification extends React.Component {
                   name="pathPattern"
                   value={this.state.pathPattern}
                   placeholder="e.g. /projects/xxx/MTG/*"
-                  onChange={(e) => { this.changePathPattern(e.target.value) }}
+                  onChange={(e) => {
+                    this.changePathPattern(e.target.value);
+                  }}
                 />
                 <p className="p-2 mb-0">
                   {/* eslint-disable-next-line react/no-danger */}
-                  <span dangerouslySetInnerHTML={{ __html: t('notification_settings.pattern_desc') }} />
+                  <span
+                    dangerouslySetInnerHTML={{
+                      __html: t('notification_settings.pattern_desc'),
+                    }}
+                  />
                 </p>
               </td>
 
               <td>
                 <div className="input-group notify-to-option" id="slack-input">
                   <div>
-                    <span className="input-group-text"><span className="material-symbols-outlined">tag</span></span>
+                    <span className="input-group-text">
+                      <span className="material-symbols-outlined">tag</span>
+                    </span>
                   </div>
                   <input
                     className="form-control"
@@ -120,35 +135,52 @@ class UserTriggerNotification extends React.Component {
                     name="channel"
                     value={this.state.channel}
                     placeholder="e.g. project-xxx"
-                    onChange={(e) => { this.changeChannel(e.target.value) }}
+                    onChange={(e) => {
+                      this.changeChannel(e.target.value);
+                    }}
                   />
                 </div>
                 <p className="p-2 mb-0">
                   {/* eslint-disable-next-line react/no-danger */}
-                  <span dangerouslySetInnerHTML={{ __html: t('notification_settings.channel_desc') }} />
+                  <span
+                    dangerouslySetInnerHTML={{
+                      __html: t('notification_settings.channel_desc'),
+                    }}
+                  />
                 </p>
               </td>
               <td>
-                <button type="button" className="btn btn-primary" disabled={!this.validateForm()} onClick={this.onClickSubmit}>{t('commons:Add')}</button>
+                <button
+                  type="button"
+                  className="btn btn-primary"
+                  disabled={!this.validateForm()}
+                  onClick={this.onClickSubmit}
+                >
+                  {t('commons:Add')}
+                </button>
               </td>
             </tr>
-            {userNotifications.length > 0 && userNotifications.map((notification) => {
-              return <UserNotificationRow notification={notification} onClickDeleteBtn={this.onClickDeleteBtn} key={notification._id} />;
-            })}
+            {userNotifications.length > 0 &&
+              userNotifications.map((notification) => {
+                return (
+                  <UserNotificationRow
+                    notification={notification}
+                    onClickDeleteBtn={this.onClickDeleteBtn}
+                    key={notification._id}
+                  />
+                );
+              })}
           </tbody>
         </table>
       </React.Fragment>
     );
   }
-
-
 }
 
-
 UserTriggerNotification.propTypes = {
   t: PropTypes.func.isRequired, // i18next
-  adminNotificationContainer: PropTypes.instanceOf(AdminNotificationContainer).isRequired,
-
+  adminNotificationContainer: PropTypes.instanceOf(AdminNotificationContainer)
+    .isRequired,
 };
 
 const UserTriggerNotificationWrapperFC = (props) => {
@@ -157,6 +189,9 @@ const UserTriggerNotificationWrapperFC = (props) => {
   return <UserTriggerNotification t={t} {...props} />;
 };
 
-const UserTriggerNotificationWrapper = withUnstatedContainers(UserTriggerNotificationWrapperFC, [AdminNotificationContainer]);
+const UserTriggerNotificationWrapper = withUnstatedContainers(
+  UserTriggerNotificationWrapperFC,
+  [AdminNotificationContainer],
+);
 
 export default UserTriggerNotificationWrapper;

+ 7 - 10
apps/app/src/client/components/Admin/Security/DeleteAllShareLinksModal.jsx

@@ -1,10 +1,7 @@
 import React, { useCallback } from 'react';
-
 import { useTranslation } from 'next-i18next';
 import PropTypes from 'prop-types';
-import {
-  Button, Modal, ModalHeader, ModalBody, ModalFooter,
-} from 'reactstrap';
+import { Button, Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap';
 
 const DeleteAllShareLinksModal = React.memo((props) => {
   const { t, onClickDeleteButton, onClose } = props;
@@ -19,16 +16,18 @@ const DeleteAllShareLinksModal = React.memo((props) => {
   }, [onClose]);
 
   return (
-    <Modal isOpen={props.isOpen} toggle={closeButtonHandler} className="page-comment-delete-modal">
+    <Modal
+      isOpen={props.isOpen}
+      toggle={closeButtonHandler}
+      className="page-comment-delete-modal"
+    >
       <ModalHeader tag="h4" toggle={closeButtonHandler} className="text-danger">
         <span>
           <span className="material-symbols-outlined">delete_forever</span>
           {t('security_settings.delete_all_share_links')}
         </span>
       </ModalHeader>
-      <ModalBody>
-        { t('security_settings.share_link_notice')}
-      </ModalBody>
+      <ModalBody>{t('security_settings.share_link_notice')}</ModalBody>
       <ModalFooter>
         <Button onClick={closeButtonHandler}>{t('Cancel')}</Button>
         <Button color="danger" onClick={deleteAllLinkHandler}>
@@ -38,11 +37,9 @@ const DeleteAllShareLinksModal = React.memo((props) => {
       </ModalFooter>
     </Modal>
   );
-
 });
 DeleteAllShareLinksModal.displayName = 'DeleteAllShareLinksModal';
 
-
 DeleteAllShareLinksModal.propTypes = {
   t: PropTypes.func.isRequired, // i18next
 

+ 10 - 12
apps/app/src/client/components/Admin/Security/GitHubSecuritySetting.jsx

@@ -1,5 +1,4 @@
-import React, { useEffect, useCallback } from 'react';
-
+import React, { useCallback, useEffect } from 'react';
 import PropTypes from 'prop-types';
 
 import AdminGitHubSecurityContainer from '~/client/services/AdminGitHubSecurityContainer';
@@ -7,18 +6,15 @@ import { toastError } from '~/client/util/toastr';
 import { toArrayIfNot } from '~/utils/array-utils';
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
-
-
 import GitHubSecuritySettingContents from './GitHubSecuritySettingContents';
 
 const GitHubSecurityManagement = (props) => {
   const { adminGitHubSecurityContainer } = props;
 
-  const fetchGitHubSecuritySettingsData = useCallback(async() => {
+  const fetchGitHubSecuritySettingsData = useCallback(async () => {
     try {
       await adminGitHubSecurityContainer.retrieveSecurityData();
-    }
-    catch (err) {
+    } catch (err) {
       const errs = toArrayIfNot(err);
       toastError(errs);
     }
@@ -31,13 +27,15 @@ const GitHubSecurityManagement = (props) => {
   return <GitHubSecuritySettingContents />;
 };
 
-
 GitHubSecurityManagement.propTypes = {
-  adminGitHubSecurityContainer: PropTypes.instanceOf(AdminGitHubSecurityContainer).isRequired,
+  adminGitHubSecurityContainer: PropTypes.instanceOf(
+    AdminGitHubSecurityContainer,
+  ).isRequired,
 };
 
-const GitHubSecurityManagementWithUnstatedContainer = withUnstatedContainers(GitHubSecurityManagement, [
-  AdminGitHubSecurityContainer,
-]);
+const GitHubSecurityManagementWithUnstatedContainer = withUnstatedContainers(
+  GitHubSecurityManagement,
+  [AdminGitHubSecurityContainer],
+);
 
 export default GitHubSecurityManagementWithUnstatedContainer;

+ 146 - 57
apps/app/src/client/components/Admin/Security/GitHubSecuritySettingContents.tsx

@@ -1,35 +1,35 @@
 /* eslint-disable react/no-danger */
 import React, { useCallback, useEffect } from 'react';
-
 import { pathUtils } from '@growi/core/dist/utils';
 import { useTranslation } from 'next-i18next';
 import { useForm } from 'react-hook-form';
 import urljoin from 'url-join';
 
-
 import AdminGeneralSecurityContainer from '~/client/services/AdminGeneralSecurityContainer';
 import AdminGitHubSecurityContainer from '~/client/services/AdminGitHubSecurityContainer';
-import { toastSuccess, toastError } from '~/client/util/toastr';
+import { toastError, toastSuccess } from '~/client/util/toastr';
 import { useSiteUrlWithEmptyValueWarn } from '~/states/global';
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 
 type Props = {
-  adminGeneralSecurityContainer: AdminGeneralSecurityContainer
-  adminGitHubSecurityContainer: AdminGitHubSecurityContainer
+  adminGeneralSecurityContainer: AdminGeneralSecurityContainer;
+  adminGitHubSecurityContainer: AdminGitHubSecurityContainer;
 };
 
 const GitHubSecurityManagementContents = (props: Props) => {
-  const {
-    adminGeneralSecurityContainer, adminGitHubSecurityContainer,
-  } = props;
+  const { adminGeneralSecurityContainer, adminGitHubSecurityContainer } = props;
 
   const { t } = useTranslation('admin');
   const siteUrl = useSiteUrlWithEmptyValueWarn();
 
   const { isGitHubEnabled } = adminGeneralSecurityContainer.state;
-  const { githubClientId, githubClientSecret, retrieveError } = adminGitHubSecurityContainer.state;
-  const gitHubCallbackUrl = urljoin(pathUtils.removeTrailingSlash(siteUrl), '/passport/github/callback');
+  const { githubClientId, githubClientSecret, retrieveError } =
+    adminGitHubSecurityContainer.state;
+  const gitHubCallbackUrl = urljoin(
+    pathUtils.removeTrailingSlash(siteUrl),
+    '/passport/github/callback',
+  );
 
   const { register, handleSubmit, reset } = useForm();
 
@@ -41,32 +41,37 @@ const GitHubSecurityManagementContents = (props: Props) => {
     });
   }, [reset, githubClientId, githubClientSecret]);
 
-  const onClickSubmit = useCallback(async(data) => {
-    try {
-      await adminGitHubSecurityContainer.updateGitHubSetting({
-        githubClientId: data.githubClientId ?? '',
-        githubClientSecret: data.githubClientSecret ?? '',
-        isSameUsernameTreatedAsIdenticalUser: adminGitHubSecurityContainer.state.isSameUsernameTreatedAsIdenticalUser,
-      });
-      await adminGeneralSecurityContainer.retrieveSetupStratedies();
-      toastSuccess(t('security_settings.OAuth.GitHub.updated_github'));
-    }
-    catch (err) {
-      toastError(err);
-    }
-  }, [adminGitHubSecurityContainer, adminGeneralSecurityContainer, t]);
+  const onClickSubmit = useCallback(
+    async (data) => {
+      try {
+        await adminGitHubSecurityContainer.updateGitHubSetting({
+          githubClientId: data.githubClientId ?? '',
+          githubClientSecret: data.githubClientSecret ?? '',
+          isSameUsernameTreatedAsIdenticalUser:
+            adminGitHubSecurityContainer.state
+              .isSameUsernameTreatedAsIdenticalUser,
+        });
+        await adminGeneralSecurityContainer.retrieveSetupStratedies();
+        toastSuccess(t('security_settings.OAuth.GitHub.updated_github'));
+      } catch (err) {
+        toastError(err);
+      }
+    },
+    [adminGitHubSecurityContainer, adminGeneralSecurityContainer, t],
+  );
 
   return (
     <form onSubmit={handleSubmit(onClickSubmit)}>
       <React.Fragment>
-
         <h2 className="alert-anchor border-bottom">
           {t('security_settings.OAuth.GitHub.name')}
         </h2>
 
         {retrieveError != null && (
           <div className="alert alert-danger">
-            <p>{t('Error occurred')} : {retrieveError}</p>
+            <p>
+              {t('Error occurred')} : {retrieveError}
+            </p>
           </div>
         )}
 
@@ -77,20 +82,35 @@ const GitHubSecurityManagementContents = (props: Props) => {
                 id="isGitHubEnabled"
                 className="form-check-input"
                 type="checkbox"
-                checked={adminGeneralSecurityContainer.state.isGitHubEnabled || false}
-                onChange={() => { adminGeneralSecurityContainer.switchIsGitHubOAuthEnabled() }}
+                checked={
+                  adminGeneralSecurityContainer.state.isGitHubEnabled || false
+                }
+                onChange={() => {
+                  adminGeneralSecurityContainer.switchIsGitHubOAuthEnabled();
+                }}
               />
-              <label className="form-label form-check-label" htmlFor="isGitHubEnabled">
+              <label
+                className="form-label form-check-label"
+                htmlFor="isGitHubEnabled"
+              >
                 {t('security_settings.OAuth.GitHub.enable_github')}
               </label>
             </div>
-            {(!adminGeneralSecurityContainer.state.setupStrategies.includes('github') && isGitHubEnabled)
-              && <div className="badge text-bg-warning">{t('security_settings.setup_is_not_yet_complete')}</div>}
+            {!adminGeneralSecurityContainer.state.setupStrategies.includes(
+              'github',
+            ) &&
+              isGitHubEnabled && (
+                <div className="badge text-bg-warning">
+                  {t('security_settings.setup_is_not_yet_complete')}
+                </div>
+              )}
           </div>
         </div>
 
         <div className="row mb-4">
-          <label className="form-label col-12 col-md-3 text-start text-md-end py-2">{t('security_settings.callback_URL')}</label>
+          <label className="form-label col-12 col-md-3 text-start text-md-end py-2">
+            {t('security_settings.callback_URL')}
+          </label>
           <div className="col-12 col-md-6">
             <input
               className="form-control"
@@ -98,26 +118,40 @@ const GitHubSecurityManagementContents = (props: Props) => {
               value={gitHubCallbackUrl}
               readOnly
             />
-            <p className="form-text text-muted small">{t('security_settings.desc_of_callback_URL', { AuthName: 'OAuth' })}</p>
+            <p className="form-text text-muted small">
+              {t('security_settings.desc_of_callback_URL', {
+                AuthName: 'OAuth',
+              })}
+            </p>
             {(siteUrl == null || siteUrl === '') && (
               <div className="alert alert-danger">
                 <span className="material-symbols-outlined">error</span>
                 <span // eslint-disable-next-line max-len
-                  dangerouslySetInnerHTML={{ __html: t('alert.siteUrl_is_not_set', { link: `<a href="/admin/app">${t('headers.app_settings', { ns: 'commons' })}<span class="material-symbols-outlined">login</span></a>`, ns: 'commons' }) }}
+                  dangerouslySetInnerHTML={{
+                    __html: t('alert.siteUrl_is_not_set', {
+                      link: `<a href="/admin/app">${t('headers.app_settings', { ns: 'commons' })}<span class="material-symbols-outlined">login</span></a>`,
+                      ns: 'commons',
+                    }),
+                  }}
                 />
               </div>
             )}
           </div>
         </div>
 
-
         {isGitHubEnabled && (
           <React.Fragment>
-
-            <h3 className="border-bottom mb-4">{t('security_settings.configuration')}</h3>
+            <h3 className="border-bottom mb-4">
+              {t('security_settings.configuration')}
+            </h3>
 
             <div className="row mb-4">
-              <label htmlFor="githubClientId" className="col-3 text-end py-2 form-label">{t('security_settings.clientID')}</label>
+              <label
+                htmlFor="githubClientId"
+                className="col-3 text-end py-2 form-label"
+              >
+                {t('security_settings.clientID')}
+              </label>
               <div className="col-6">
                 <input
                   className="form-control"
@@ -125,13 +159,24 @@ const GitHubSecurityManagementContents = (props: Props) => {
                   {...register('githubClientId')}
                 />
                 <p className="form-text text-muted">
-                  <small dangerouslySetInnerHTML={{ __html: t('security_settings.Use env var if empty', { env: 'OAUTH_GITHUB_CLIENT_ID' }) }} />
+                  <small
+                    dangerouslySetInnerHTML={{
+                      __html: t('security_settings.Use env var if empty', {
+                        env: 'OAUTH_GITHUB_CLIENT_ID',
+                      }),
+                    }}
+                  />
                 </p>
               </div>
             </div>
 
             <div className="row mb-3">
-              <label htmlFor="githubClientSecret" className="col-3 text-end py-2 form-label">{t('security_settings.client_secret')}</label>
+              <label
+                htmlFor="githubClientSecret"
+                className="col-3 text-end py-2 form-label"
+              >
+                {t('security_settings.client_secret')}
+              </label>
               <div className="col-6">
                 <input
                   className="form-control"
@@ -139,7 +184,13 @@ const GitHubSecurityManagementContents = (props: Props) => {
                   {...register('githubClientSecret')}
                 />
                 <p className="form-text text-muted">
-                  <small dangerouslySetInnerHTML={{ __html: t('security_settings.Use env var if empty', { env: 'OAUTH_GITHUB_CLIENT_SECRET' }) }} />
+                  <small
+                    dangerouslySetInnerHTML={{
+                      __html: t('security_settings.Use env var if empty', {
+                        env: 'OAUTH_GITHUB_CLIENT_SECRET',
+                      }),
+                    }}
+                  />
                 </p>
               </div>
             </div>
@@ -151,29 +202,47 @@ const GitHubSecurityManagementContents = (props: Props) => {
                     id="bindByUserNameGitHub"
                     className="form-check-input"
                     type="checkbox"
-                    checked={adminGitHubSecurityContainer.state.isSameUsernameTreatedAsIdenticalUser || false}
-                    onChange={() => { adminGitHubSecurityContainer.switchIsSameUsernameTreatedAsIdenticalUser() }}
+                    checked={
+                      adminGitHubSecurityContainer.state
+                        .isSameUsernameTreatedAsIdenticalUser || false
+                    }
+                    onChange={() => {
+                      adminGitHubSecurityContainer.switchIsSameUsernameTreatedAsIdenticalUser();
+                    }}
                   />
                   <label
                     className="form-check-label"
                     htmlFor="bindByUserNameGitHub"
-                    dangerouslySetInnerHTML={{ __html: t('security_settings.Treat email matching as identical') }}
+                    dangerouslySetInnerHTML={{
+                      __html: t(
+                        'security_settings.Treat email matching as identical',
+                      ),
+                    }}
                   />
                 </div>
                 <p className="form-text text-muted">
-                  <small dangerouslySetInnerHTML={{ __html: t('security_settings.Treat email matching as identical_warn') }} />
+                  <small
+                    dangerouslySetInnerHTML={{
+                      __html: t(
+                        'security_settings.Treat email matching as identical_warn',
+                      ),
+                    }}
+                  />
                 </p>
               </div>
             </div>
 
             <div className="row mb-4">
               <div className="offset-3 col-5">
-                <button type="submit" className="btn btn-primary" disabled={retrieveError != null}>
+                <button
+                  type="submit"
+                  className="btn btn-primary"
+                  disabled={retrieveError != null}
+                >
                   {t('Update')}
                 </button>
               </div>
             </div>
-
           </React.Fragment>
         )}
 
@@ -181,19 +250,39 @@ const GitHubSecurityManagementContents = (props: Props) => {
 
         <div style={{ minHeight: '300px' }}>
           <h4>
-            <span className="material-symbols-outlined" aria-hidden="true">help</span>
-            <a href="#collapseHelpForGitHubOauth" data-bs-toggle="collapse"> {t('security_settings.OAuth.how_to.github')}</a>
+            <span className="material-symbols-outlined" aria-hidden="true">
+              help
+            </span>
+            <a href="#collapseHelpForGitHubOauth" data-bs-toggle="collapse">
+              {' '}
+              {t('security_settings.OAuth.how_to.github')}
+            </a>
           </h4>
           <div className="card custom-card bg-body-tertiary">
             <ol id="collapseHelpForGitHubOauth" className="collapse mb-0">
               {/* eslint-disable-next-line max-len */}
-              <li dangerouslySetInnerHTML={{ __html: t('security_settings.OAuth.GitHub.register_1', { link: '<a href="https://github.com/settings/developers" target=_blank>GitHub Developer Settings</a>' }) }} />
-              <li dangerouslySetInnerHTML={{ __html: t('security_settings.OAuth.GitHub.register_2', { url: gitHubCallbackUrl }) }} />
-              <li dangerouslySetInnerHTML={{ __html: t('security_settings.OAuth.GitHub.register_3') }} />
+              <li
+                dangerouslySetInnerHTML={{
+                  __html: t('security_settings.OAuth.GitHub.register_1', {
+                    link: '<a href="https://github.com/settings/developers" target=_blank>GitHub Developer Settings</a>',
+                  }),
+                }}
+              />
+              <li
+                dangerouslySetInnerHTML={{
+                  __html: t('security_settings.OAuth.GitHub.register_2', {
+                    url: gitHubCallbackUrl,
+                  }),
+                }}
+              />
+              <li
+                dangerouslySetInnerHTML={{
+                  __html: t('security_settings.OAuth.GitHub.register_3'),
+                }}
+              />
             </ol>
           </div>
         </div>
-
       </React.Fragment>
     </form>
   );
@@ -202,9 +291,9 @@ const GitHubSecurityManagementContents = (props: Props) => {
 /**
  * Wrapper component for using unstated
  */
-const GitHubSecurityManagementContentsWrapper = withUnstatedContainers(GitHubSecurityManagementContents, [
-  AdminGeneralSecurityContainer,
-  AdminGitHubSecurityContainer,
-]);
+const GitHubSecurityManagementContentsWrapper = withUnstatedContainers(
+  GitHubSecurityManagementContents,
+  [AdminGeneralSecurityContainer, AdminGitHubSecurityContainer],
+);
 
 export default GitHubSecurityManagementContentsWrapper;

+ 10 - 12
apps/app/src/client/components/Admin/Security/GoogleSecuritySetting.jsx

@@ -1,5 +1,4 @@
-import React, { useEffect, useCallback } from 'react';
-
+import React, { useCallback, useEffect } from 'react';
 import PropTypes from 'prop-types';
 
 import AdminGoogleSecurityContainer from '~/client/services/AdminGoogleSecurityContainer';
@@ -7,23 +6,20 @@ import { toastError } from '~/client/util/toastr';
 import { toArrayIfNot } from '~/utils/array-utils';
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
-
 import GoogleSecurityManagementContents from './GoogleSecuritySettingContents';
 
 const GoogleSecurityManagement = (props) => {
   const { adminGoogleSecurityContainer } = props;
 
-  const fetchGoogleSecuritySettingsData = useCallback(async() => {
+  const fetchGoogleSecuritySettingsData = useCallback(async () => {
     try {
       await adminGoogleSecurityContainer.retrieveSecurityData();
-    }
-    catch (err) {
+    } catch (err) {
       const errs = toArrayIfNot(err);
       toastError(errs);
     }
   }, [adminGoogleSecurityContainer]);
 
-
   useEffect(() => {
     fetchGoogleSecuritySettingsData();
   }, [adminGoogleSecurityContainer, fetchGoogleSecuritySettingsData]);
@@ -31,13 +27,15 @@ const GoogleSecurityManagement = (props) => {
   return <GoogleSecurityManagementContents />;
 };
 
-
 GoogleSecurityManagement.propTypes = {
-  adminGoogleSecurityContainer: PropTypes.instanceOf(AdminGoogleSecurityContainer).isRequired,
+  adminGoogleSecurityContainer: PropTypes.instanceOf(
+    AdminGoogleSecurityContainer,
+  ).isRequired,
 };
 
-const GoogleSecurityManagementWithUnstatedContainer = withUnstatedContainers(GoogleSecurityManagement, [
-  AdminGoogleSecurityContainer,
-]);
+const GoogleSecurityManagementWithUnstatedContainer = withUnstatedContainers(
+  GoogleSecurityManagement,
+  [AdminGoogleSecurityContainer],
+);
 
 export default GoogleSecurityManagementWithUnstatedContainer;

+ 157 - 59
apps/app/src/client/components/Admin/Security/GoogleSecuritySettingContents.tsx

@@ -1,6 +1,5 @@
 /* eslint-disable react/no-danger */
 import React, { useCallback, useEffect } from 'react';
-
 import { pathUtils } from '@growi/core/dist/utils';
 import { useTranslation } from 'next-i18next';
 import { useForm } from 'react-hook-form';
@@ -8,27 +7,29 @@ import urljoin from 'url-join';
 
 import AdminGeneralSecurityContainer from '~/client/services/AdminGeneralSecurityContainer';
 import AdminGoogleSecurityContainer from '~/client/services/AdminGoogleSecurityContainer';
-import { toastSuccess, toastError } from '~/client/util/toastr';
+import { toastError, toastSuccess } from '~/client/util/toastr';
 import { useSiteUrlWithEmptyValueWarn } from '~/states/global';
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 
 type Props = {
-  adminGeneralSecurityContainer: AdminGeneralSecurityContainer
-  adminGoogleSecurityContainer: AdminGoogleSecurityContainer
+  adminGeneralSecurityContainer: AdminGeneralSecurityContainer;
+  adminGoogleSecurityContainer: AdminGoogleSecurityContainer;
 };
 
 const GoogleSecurityManagementContents = (props: Props) => {
-  const {
-    adminGeneralSecurityContainer, adminGoogleSecurityContainer,
-  } = props;
+  const { adminGeneralSecurityContainer, adminGoogleSecurityContainer } = props;
 
   const { t } = useTranslation('admin');
   const siteUrl = useSiteUrlWithEmptyValueWarn();
 
   const { isGoogleEnabled } = adminGeneralSecurityContainer.state;
-  const { googleClientId, googleClientSecret, retrieveError } = adminGoogleSecurityContainer.state;
-  const googleCallbackUrl = urljoin(pathUtils.removeTrailingSlash(siteUrl), '/passport/google/callback');
+  const { googleClientId, googleClientSecret, retrieveError } =
+    adminGoogleSecurityContainer.state;
+  const googleCallbackUrl = urljoin(
+    pathUtils.removeTrailingSlash(siteUrl),
+    '/passport/google/callback',
+  );
 
   const { register, handleSubmit, reset } = useForm();
 
@@ -40,32 +41,37 @@ const GoogleSecurityManagementContents = (props: Props) => {
     });
   }, [reset, googleClientId, googleClientSecret]);
 
-  const onClickSubmit = useCallback(async(data) => {
-    try {
-      await adminGoogleSecurityContainer.updateGoogleSetting({
-        googleClientId: data.googleClientId ?? '',
-        googleClientSecret: data.googleClientSecret ?? '',
-        isSameEmailTreatedAsIdenticalUser: adminGoogleSecurityContainer.state.isSameEmailTreatedAsIdenticalUser,
-      });
-      await adminGeneralSecurityContainer.retrieveSetupStratedies();
-      toastSuccess(t('security_settings.OAuth.Google.updated_google'));
-    }
-    catch (err) {
-      toastError(err);
-    }
-  }, [adminGoogleSecurityContainer, adminGeneralSecurityContainer, t]);
+  const onClickSubmit = useCallback(
+    async (data) => {
+      try {
+        await adminGoogleSecurityContainer.updateGoogleSetting({
+          googleClientId: data.googleClientId ?? '',
+          googleClientSecret: data.googleClientSecret ?? '',
+          isSameEmailTreatedAsIdenticalUser:
+            adminGoogleSecurityContainer.state
+              .isSameEmailTreatedAsIdenticalUser,
+        });
+        await adminGeneralSecurityContainer.retrieveSetupStratedies();
+        toastSuccess(t('security_settings.OAuth.Google.updated_google'));
+      } catch (err) {
+        toastError(err);
+      }
+    },
+    [adminGoogleSecurityContainer, adminGeneralSecurityContainer, t],
+  );
 
   return (
     <form onSubmit={handleSubmit(onClickSubmit)}>
       <React.Fragment>
-
         <h2 className="alert-anchor border-bottom">
           {t('security_settings.OAuth.Google.name')}
         </h2>
 
         {retrieveError != null && (
           <div className="alert alert-danger">
-            <p>{t('Error occurred')} : {retrieveError}</p>
+            <p>
+              {t('Error occurred')} : {retrieveError}
+            </p>
           </div>
         )}
 
@@ -76,20 +82,35 @@ const GoogleSecurityManagementContents = (props: Props) => {
                 id="isGoogleEnabled"
                 className="form-check-input"
                 type="checkbox"
-                checked={adminGeneralSecurityContainer.state.isGoogleEnabled || false}
-                onChange={() => { adminGeneralSecurityContainer.switchIsGoogleOAuthEnabled() }}
+                checked={
+                  adminGeneralSecurityContainer.state.isGoogleEnabled || false
+                }
+                onChange={() => {
+                  adminGeneralSecurityContainer.switchIsGoogleOAuthEnabled();
+                }}
               />
-              <label className="form-label form-check-label" htmlFor="isGoogleEnabled">
+              <label
+                className="form-label form-check-label"
+                htmlFor="isGoogleEnabled"
+              >
                 {t('security_settings.OAuth.Google.enable_google')}
               </label>
             </div>
-            {(!adminGeneralSecurityContainer.state.setupStrategies.includes('google') && isGoogleEnabled)
-              && <div className="badge text-bg-warning">{t('security_settings.setup_is_not_yet_complete')}</div>}
+            {!adminGeneralSecurityContainer.state.setupStrategies.includes(
+              'google',
+            ) &&
+              isGoogleEnabled && (
+                <div className="badge text-bg-warning">
+                  {t('security_settings.setup_is_not_yet_complete')}
+                </div>
+              )}
           </div>
         </div>
 
         <div className="row mb-5">
-          <label className="form-label col-12 col-md-3 text-start text-md-end py-2">{t('security_settings.callback_URL')}</label>
+          <label className="form-label col-12 col-md-3 text-start text-md-end py-2">
+            {t('security_settings.callback_URL')}
+          </label>
           <div className="col-12 col-md-6">
             <input
               className="form-control"
@@ -97,27 +118,41 @@ const GoogleSecurityManagementContents = (props: Props) => {
               value={googleCallbackUrl}
               readOnly
             />
-            <p className="form-text text-muted small">{t('security_settings.desc_of_callback_URL', { AuthName: 'OAuth' })}</p>
+            <p className="form-text text-muted small">
+              {t('security_settings.desc_of_callback_URL', {
+                AuthName: 'OAuth',
+              })}
+            </p>
             {(siteUrl == null || siteUrl === '') && (
               <div className="alert alert-danger">
                 <span className="material-symbols-outlined">error</span>
                 <span
-                // eslint-disable-next-line max-len
-                  dangerouslySetInnerHTML={{ __html: t('alert.siteUrl_is_not_set', { link: `<a href="/admin/app">${t('headers.app_settings', { ns: 'commons' })}<span class="material-symbols-outlined">login</span></a>`, ns: 'commons' }) }}
+                  // eslint-disable-next-line max-len
+                  dangerouslySetInnerHTML={{
+                    __html: t('alert.siteUrl_is_not_set', {
+                      link: `<a href="/admin/app">${t('headers.app_settings', { ns: 'commons' })}<span class="material-symbols-outlined">login</span></a>`,
+                      ns: 'commons',
+                    }),
+                  }}
                 />
               </div>
             )}
           </div>
         </div>
 
-
         {isGoogleEnabled && (
           <React.Fragment>
-
-            <h3 className="border-bottom mb-4">{t('security_settings.configuration')}</h3>
+            <h3 className="border-bottom mb-4">
+              {t('security_settings.configuration')}
+            </h3>
 
             <div className="row mb-4">
-              <label htmlFor="googleClientId" className="col-3 text-end py-2 form-label">{t('security_settings.clientID')}</label>
+              <label
+                htmlFor="googleClientId"
+                className="col-3 text-end py-2 form-label"
+              >
+                {t('security_settings.clientID')}
+              </label>
               <div className="col-6">
                 <input
                   className="form-control"
@@ -125,13 +160,24 @@ const GoogleSecurityManagementContents = (props: Props) => {
                   {...register('googleClientId')}
                 />
                 <p className="form-text text-muted">
-                  <small dangerouslySetInnerHTML={{ __html: t('security_settings.Use env var if empty', { env: 'OAUTH_GOOGLE_CLIENT_ID' }) }} />
+                  <small
+                    dangerouslySetInnerHTML={{
+                      __html: t('security_settings.Use env var if empty', {
+                        env: 'OAUTH_GOOGLE_CLIENT_ID',
+                      }),
+                    }}
+                  />
                 </p>
               </div>
             </div>
 
             <div className="row mb-4">
-              <label htmlFor="googleClientSecret" className="col-3 text-end py-2 form-label">{t('security_settings.client_secret')}</label>
+              <label
+                htmlFor="googleClientSecret"
+                className="col-3 text-end py-2 form-label"
+              >
+                {t('security_settings.client_secret')}
+              </label>
               <div className="col-6">
                 <input
                   className="form-control"
@@ -139,7 +185,13 @@ const GoogleSecurityManagementContents = (props: Props) => {
                   {...register('googleClientSecret')}
                 />
                 <p className="form-text text-muted">
-                  <small dangerouslySetInnerHTML={{ __html: t('security_settings.Use env var if empty', { env: 'OAUTH_GOOGLE_CLIENT_SECRET' }) }} />
+                  <small
+                    dangerouslySetInnerHTML={{
+                      __html: t('security_settings.Use env var if empty', {
+                        env: 'OAUTH_GOOGLE_CLIENT_SECRET',
+                      }),
+                    }}
+                  />
                 </p>
               </div>
             </div>
@@ -151,29 +203,47 @@ const GoogleSecurityManagementContents = (props: Props) => {
                     id="bindByUserNameGoogle"
                     className="form-check-input"
                     type="checkbox"
-                    checked={adminGoogleSecurityContainer.state.isSameEmailTreatedAsIdenticalUser || false}
-                    onChange={() => { adminGoogleSecurityContainer.switchIsSameEmailTreatedAsIdenticalUser() }}
+                    checked={
+                      adminGoogleSecurityContainer.state
+                        .isSameEmailTreatedAsIdenticalUser || false
+                    }
+                    onChange={() => {
+                      adminGoogleSecurityContainer.switchIsSameEmailTreatedAsIdenticalUser();
+                    }}
                   />
                   <label
                     className="form-check-label"
                     htmlFor="bindByUserNameGoogle"
-                    dangerouslySetInnerHTML={{ __html: t('security_settings.Treat email matching as identical') }}
+                    dangerouslySetInnerHTML={{
+                      __html: t(
+                        'security_settings.Treat email matching as identical',
+                      ),
+                    }}
                   />
                 </div>
                 <p className="form-text text-muted">
-                  <small dangerouslySetInnerHTML={{ __html: t('security_settings.Treat email matching as identical_warn') }} />
+                  <small
+                    dangerouslySetInnerHTML={{
+                      __html: t(
+                        'security_settings.Treat email matching as identical_warn',
+                      ),
+                    }}
+                  />
                 </p>
               </div>
             </div>
 
             <div className="row mb-4">
               <div className="offset-3 col-5">
-                <button type="submit" className="btn btn-primary" disabled={retrieveError != null}>
+                <button
+                  type="submit"
+                  className="btn btn-primary"
+                  disabled={retrieveError != null}
+                >
                   {t('Update')}
                 </button>
               </div>
             </div>
-
           </React.Fragment>
         )}
 
@@ -181,29 +251,57 @@ const GoogleSecurityManagementContents = (props: Props) => {
 
         <div style={{ minHeight: '300px' }}>
           <h4>
-            <span className="material-symbols-outlined" aria-hidden="true">help</span>
-            <a href="#collapseHelpForGoogleOauth" data-bs-toggle="collapse"> {t('security_settings.OAuth.how_to.google')}</a>
+            <span className="material-symbols-outlined" aria-hidden="true">
+              help
+            </span>
+            <a href="#collapseHelpForGoogleOauth" data-bs-toggle="collapse">
+              {' '}
+              {t('security_settings.OAuth.how_to.google')}
+            </a>
           </h4>
           <div className="card custom-card bg-body-tertiary">
             <ol id="collapseHelpForGoogleOauth" className="collapse mb-0">
               {/* eslint-disable-next-line max-len */}
-              <li dangerouslySetInnerHTML={{ __html: t('security_settings.OAuth.Google.register_1', { link: '<a href="https://console.cloud.google.com/apis/credentials" target=_blank>Google Cloud Platform API Manager</a>' }) }} />
-              <li dangerouslySetInnerHTML={{ __html: t('security_settings.OAuth.Google.register_2') }} />
-              <li dangerouslySetInnerHTML={{ __html: t('security_settings.OAuth.Google.register_3') }} />
-              <li dangerouslySetInnerHTML={{ __html: t('security_settings.OAuth.Google.register_4', { url: googleCallbackUrl }) }} />
-              <li dangerouslySetInnerHTML={{ __html: t('security_settings.OAuth.Google.register_5') }} />
+              <li
+                dangerouslySetInnerHTML={{
+                  __html: t('security_settings.OAuth.Google.register_1', {
+                    link: '<a href="https://console.cloud.google.com/apis/credentials" target=_blank>Google Cloud Platform API Manager</a>',
+                  }),
+                }}
+              />
+              <li
+                dangerouslySetInnerHTML={{
+                  __html: t('security_settings.OAuth.Google.register_2'),
+                }}
+              />
+              <li
+                dangerouslySetInnerHTML={{
+                  __html: t('security_settings.OAuth.Google.register_3'),
+                }}
+              />
+              <li
+                dangerouslySetInnerHTML={{
+                  __html: t('security_settings.OAuth.Google.register_4', {
+                    url: googleCallbackUrl,
+                  }),
+                }}
+              />
+              <li
+                dangerouslySetInnerHTML={{
+                  __html: t('security_settings.OAuth.Google.register_5'),
+                }}
+              />
             </ol>
           </div>
         </div>
-
       </React.Fragment>
     </form>
   );
 };
 
-const GoogleSecurityManagementContentsWrapper = withUnstatedContainers(GoogleSecurityManagementContents, [
-  AdminGeneralSecurityContainer,
-  AdminGoogleSecurityContainer,
-]);
+const GoogleSecurityManagementContentsWrapper = withUnstatedContainers(
+  GoogleSecurityManagementContents,
+  [AdminGeneralSecurityContainer, AdminGoogleSecurityContainer],
+);
 
 export default GoogleSecurityManagementContentsWrapper;

+ 53 - 31
apps/app/src/client/components/Admin/Security/LdapAuthTest.tsx

@@ -1,25 +1,22 @@
-import React, { useState, type JSX } from 'react';
-
+import React, { type JSX, useState } from 'react';
 import { useTranslation } from 'next-i18next';
 
 import { apiPost } from '~/client/util/apiv1-client';
-import { toastSuccess, toastError } from '~/client/util/toastr';
+import { toastError, toastSuccess } from '~/client/util/toastr';
 import type { IResTestLdap } from '~/interfaces/ldap';
 import loggerFactory from '~/utils/logger';
 
 const logger = loggerFactory('growi:security:AdminLdapSecurityContainer');
 
 type LdapAuthTestProps = {
-  username: string,
-  password: string,
-  onChangeUsername: (username: string) => void,
-  onChangePassword: (password: string) => void,
-}
+  username: string;
+  password: string;
+  onChangeUsername: (username: string) => void;
+  onChangePassword: (password: string) => void;
+};
 
 export const LdapAuthTest = (props: LdapAuthTestProps): JSX.Element => {
-  const {
-    username, password, onChangeUsername, onChangePassword,
-  } = props;
+  const { username, password, onChangeUsername, onChangePassword } = props;
   const { t } = useTranslation();
   const [logs, setLogs] = useState('');
   const [errorMessage, setErrorMessage] = useState('');
@@ -36,7 +33,7 @@ export const LdapAuthTest = (props: LdapAuthTestProps): JSX.Element => {
   /**
    * Test ldap auth
    */
-  const testLdapCredentials = async() => {
+  const testLdapCredentials = async () => {
     try {
       const response = await apiPost<IResTestLdap>('/login/testLdap', {
         loginForm: {
@@ -45,9 +42,8 @@ export const LdapAuthTest = (props: LdapAuthTestProps): JSX.Element => {
         },
       });
 
-      const {
-        err, message, status, ldapConfiguration, ldapAccountInfo,
-      } = response;
+      const { err, message, status, ldapConfiguration, ldapAccountInfo } =
+        response;
 
       // add logs
       if (err) {
@@ -68,62 +64,88 @@ export const LdapAuthTest = (props: LdapAuthTestProps): JSX.Element => {
       }
 
       if (ldapConfiguration) {
-        const prettified = JSON.stringify(ldapConfiguration.server, undefined, 4);
+        const prettified = JSON.stringify(
+          ldapConfiguration.server,
+          undefined,
+          4,
+        );
         addLogs(`LDAP Configuration : ${prettified}`);
       }
       if (ldapAccountInfo) {
         const prettified = JSON.stringify(ldapAccountInfo, undefined, 4);
         addLogs(`Retrieved LDAP Account : ${prettified}`);
       }
-
-    }
-    // Catch server communication error
-    catch (err) {
+    } catch (err) {
+      // Catch server communication error
       toastError(err);
       logger.error(err);
     }
   };
 
-
   return (
     <React.Fragment>
-      {successMessage !== '' && <div className="alert alert-success">{successMessage}</div>}
-      {errorMessage !== '' && <div className="alert alert-warning">{errorMessage}</div>}
+      {successMessage !== '' && (
+        <div className="alert alert-success">{successMessage}</div>
+      )}
+      {errorMessage !== '' && (
+        <div className="alert alert-warning">{errorMessage}</div>
+      )}
       <div className="row mt-3">
-        <label htmlFor="username" className="col-3 col-form-label text-end">{t('username')}</label>
+        <label htmlFor="username" className="col-3 col-form-label text-end">
+          {t('username')}
+        </label>
         <div className="col-6">
           <input
             className="form-control"
             name="username"
             value={username}
-            onChange={(e) => { onChangeUsername(e.target.value) }}
+            onChange={(e) => {
+              onChangeUsername(e.target.value);
+            }}
             autoComplete="off"
           />
         </div>
       </div>
       <div className="row mt-3">
-        <label htmlFor="password" className="col-3 col-form-label text-end">{t('Password')}</label>
+        <label htmlFor="password" className="col-3 col-form-label text-end">
+          {t('Password')}
+        </label>
         <div className="col-6">
           <input
             className="form-control"
             type="password"
             name="password"
             value={password}
-            onChange={(e) => { onChangePassword(e.target.value) }}
+            onChange={(e) => {
+              onChangePassword(e.target.value);
+            }}
             autoComplete="off"
           />
         </div>
       </div>
 
       <div className="mt-4">
-        <label className="form-label"><h5>Logs</h5></label>
-        <textarea id="taLogs" className="col form-control" rows={4} value={logs} readOnly />
+        <label className="form-label">
+          <h5>Logs</h5>
+        </label>
+        <textarea
+          id="taLogs"
+          className="col form-control"
+          rows={4}
+          value={logs}
+          readOnly
+        />
       </div>
 
       <div className="mt-4">
-        <button type="button" className="btn btn-outline-secondary offset-5 col-2" onClick={testLdapCredentials}>Test</button>
+        <button
+          type="button"
+          className="btn btn-outline-secondary offset-5 col-2"
+          onClick={testLdapCredentials}
+        >
+          Test
+        </button>
       </div>
     </React.Fragment>
-
   );
 };

+ 1 - 12
apps/app/src/client/components/Admin/Security/LdapAuthTestModal.jsx

@@ -1,19 +1,11 @@
 import React from 'react';
-
 import PropTypes from 'prop-types';
-import {
-  Modal,
-  ModalHeader,
-  ModalBody,
-} from 'reactstrap';
+import { Modal, ModalBody, ModalHeader } from 'reactstrap';
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
-
 import { LdapAuthTest } from './LdapAuthTest';
 
-
 class LdapAuthTestModal extends React.Component {
-
   constructor(props) {
     super(props);
 
@@ -41,7 +33,6 @@ class LdapAuthTestModal extends React.Component {
   }
 
   render() {
-
     return (
       <Modal isOpen={this.props.isOpen} toggle={this.props.onClose}>
         <ModalHeader tag="h4" toggle={this.props.onClose} className="text-info">
@@ -58,10 +49,8 @@ class LdapAuthTestModal extends React.Component {
       </Modal>
     );
   }
-
 }
 
-
 LdapAuthTestModal.propTypes = {
   isOpen: PropTypes.bool.isRequired,
   onClose: PropTypes.func.isRequired,

+ 9 - 10
apps/app/src/client/components/Admin/Security/LdapSecuritySetting.jsx

@@ -1,5 +1,4 @@
-import React, { useEffect, useCallback } from 'react';
-
+import React, { useCallback, useEffect } from 'react';
 import PropTypes from 'prop-types';
 
 import AdminLdapSecurityContainer from '~/client/services/AdminLdapSecurityContainer';
@@ -7,17 +6,15 @@ import { toastError } from '~/client/util/toastr';
 import { toArrayIfNot } from '~/utils/array-utils';
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
-
 import LdapSecuritySettingContents from './LdapSecuritySettingContents';
 
 const LdapSecuritySetting = (props) => {
   const { adminLdapSecurityContainer } = props;
 
-  const fetchLdapSecuritySettingsData = useCallback(async() => {
+  const fetchLdapSecuritySettingsData = useCallback(async () => {
     try {
       await adminLdapSecurityContainer.retrieveSecurityData();
-    }
-    catch (err) {
+    } catch (err) {
       const errs = toArrayIfNot(err);
       toastError(errs);
     }
@@ -31,11 +28,13 @@ const LdapSecuritySetting = (props) => {
 };
 
 LdapSecuritySetting.propTypes = {
-  adminLdapSecurityContainer: PropTypes.instanceOf(AdminLdapSecurityContainer).isRequired,
+  adminLdapSecurityContainer: PropTypes.instanceOf(AdminLdapSecurityContainer)
+    .isRequired,
 };
 
-const LdapSecuritySettingWithUnstatedContainer = withUnstatedContainers(LdapSecuritySetting, [
-  AdminLdapSecurityContainer,
-]);
+const LdapSecuritySettingWithUnstatedContainer = withUnstatedContainers(
+  LdapSecuritySetting,
+  [AdminLdapSecurityContainer],
+);
 
 export default LdapSecuritySettingWithUnstatedContainer;

+ 296 - 130
apps/app/src/client/components/Admin/Security/LdapSecuritySettingContents.tsx

@@ -1,19 +1,14 @@
-import React, {
-  useState, useEffect, useCallback,
-} from 'react';
-
+import React, { useCallback, useEffect, useState } from 'react';
 import { useTranslation } from 'next-i18next';
 import { useForm } from 'react-hook-form';
 
 import AdminGeneralSecurityContainer from '~/client/services/AdminGeneralSecurityContainer';
 import AdminLdapSecurityContainer from '~/client/services/AdminLdapSecurityContainer';
-import { toastSuccess, toastError } from '~/client/util/toastr';
+import { toastError, toastSuccess } from '~/client/util/toastr';
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
-
 import LdapAuthTestModal from './LdapAuthTestModal';
 
-
 type Props = {
   adminGeneralSecurityContainer: AdminGeneralSecurityContainer;
   adminLdapSecurityContainer: AdminLdapSecurityContainer;
@@ -26,12 +21,20 @@ const LdapSecuritySettingContents = (props: Props) => {
 
   const { isLdapEnabled } = adminGeneralSecurityContainer.state;
   const {
-    serverUrl, ldapBindDN, ldapBindDNPassword, ldapSearchFilter,
-    ldapAttrMapUsername, ldapAttrMapMail, ldapAttrMapName,
-    ldapGroupSearchBase, ldapGroupSearchFilter, ldapGroupDnProperty,
+    serverUrl,
+    ldapBindDN,
+    ldapBindDNPassword,
+    ldapSearchFilter,
+    ldapAttrMapUsername,
+    ldapAttrMapMail,
+    ldapAttrMapName,
+    ldapGroupSearchBase,
+    ldapGroupSearchFilter,
+    ldapGroupDnProperty,
   } = adminLdapSecurityContainer.state;
 
-  const [isLdapAuthTestModalShown, setIsLdapAuthTestModalShown] = useState(false);
+  const [isLdapAuthTestModalShown, setIsLdapAuthTestModalShown] =
+    useState(false);
 
   const { register, handleSubmit, reset } = useForm();
 
@@ -49,34 +52,46 @@ const LdapSecuritySettingContents = (props: Props) => {
       ldapGroupDnProperty,
     });
   }, [
-    reset, serverUrl, ldapBindDN, ldapBindDNPassword, ldapSearchFilter,
-    ldapAttrMapUsername, ldapAttrMapMail, ldapAttrMapName,
-    ldapGroupSearchBase, ldapGroupSearchFilter, ldapGroupDnProperty,
+    reset,
+    serverUrl,
+    ldapBindDN,
+    ldapBindDNPassword,
+    ldapSearchFilter,
+    ldapAttrMapUsername,
+    ldapAttrMapMail,
+    ldapAttrMapName,
+    ldapGroupSearchBase,
+    ldapGroupSearchFilter,
+    ldapGroupDnProperty,
   ]);
 
-  const onSubmit = useCallback(async(data) => {
-    try {
-      await adminLdapSecurityContainer.updateLdapSetting({
-        serverUrl: data.serverUrl,
-        isUserBind: adminLdapSecurityContainer.state.isUserBind,
-        ldapBindDN: data.ldapBindDN,
-        ldapBindDNPassword: data.ldapBindDNPassword,
-        ldapSearchFilter: data.ldapSearchFilter,
-        ldapAttrMapUsername: data.ldapAttrMapUsername,
-        isSameUsernameTreatedAsIdenticalUser: adminLdapSecurityContainer.state.isSameUsernameTreatedAsIdenticalUser,
-        ldapAttrMapMail: data.ldapAttrMapMail,
-        ldapAttrMapName: data.ldapAttrMapName,
-        ldapGroupSearchBase: data.ldapGroupSearchBase,
-        ldapGroupSearchFilter: data.ldapGroupSearchFilter,
-        ldapGroupDnProperty: data.ldapGroupDnProperty,
-      });
-      await adminGeneralSecurityContainer.retrieveSetupStratedies();
-      toastSuccess(t('security_settings.ldap.updated_ldap'));
-    }
-    catch (err) {
-      toastError(err);
-    }
-  }, [t, adminLdapSecurityContainer, adminGeneralSecurityContainer]);
+  const onSubmit = useCallback(
+    async (data) => {
+      try {
+        await adminLdapSecurityContainer.updateLdapSetting({
+          serverUrl: data.serverUrl,
+          isUserBind: adminLdapSecurityContainer.state.isUserBind,
+          ldapBindDN: data.ldapBindDN,
+          ldapBindDNPassword: data.ldapBindDNPassword,
+          ldapSearchFilter: data.ldapSearchFilter,
+          ldapAttrMapUsername: data.ldapAttrMapUsername,
+          isSameUsernameTreatedAsIdenticalUser:
+            adminLdapSecurityContainer.state
+              .isSameUsernameTreatedAsIdenticalUser,
+          ldapAttrMapMail: data.ldapAttrMapMail,
+          ldapAttrMapName: data.ldapAttrMapName,
+          ldapGroupSearchBase: data.ldapGroupSearchBase,
+          ldapGroupSearchFilter: data.ldapGroupSearchFilter,
+          ldapGroupDnProperty: data.ldapGroupDnProperty,
+        });
+        await adminGeneralSecurityContainer.retrieveSetupStratedies();
+        toastSuccess(t('security_settings.ldap.updated_ldap'));
+      } catch (err) {
+        toastError(err);
+      }
+    },
+    [t, adminLdapSecurityContainer, adminGeneralSecurityContainer],
+  );
 
   const openLdapAuthTestModal = useCallback(() => {
     setIsLdapAuthTestModalShown(true);
@@ -88,10 +103,7 @@ const LdapSecuritySettingContents = (props: Props) => {
 
   return (
     <React.Fragment>
-
-      <h2 className="alert-anchor border-bottom mb-4">
-        LDAP
-      </h2>
+      <h2 className="alert-anchor border-bottom mb-4">LDAP</h2>
 
       <div className="row my-4">
         <div className="col-6 offset-3">
@@ -101,25 +113,39 @@ const LdapSecuritySettingContents = (props: Props) => {
               className="form-check-input"
               type="checkbox"
               checked={isLdapEnabled}
-              onChange={() => { adminGeneralSecurityContainer.switchIsLdapEnabled() }}
+              onChange={() => {
+                adminGeneralSecurityContainer.switchIsLdapEnabled();
+              }}
             />
-            <label className="form-label form-check-label" htmlFor="isLdapEnabled">
+            <label
+              className="form-label form-check-label"
+              htmlFor="isLdapEnabled"
+            >
               {t('security_settings.ldap.enable_ldap')}
             </label>
           </div>
-          {(!adminGeneralSecurityContainer.state.setupStrategies.includes('ldap') && isLdapEnabled)
-              && <div className="badge text-bg-warning">{t('security_settings.setup_is_not_yet_complete')}</div>}
+          {!adminGeneralSecurityContainer.state.setupStrategies.includes(
+            'ldap',
+          ) &&
+            isLdapEnabled && (
+              <div className="badge text-bg-warning">
+                {t('security_settings.setup_is_not_yet_complete')}
+              </div>
+            )}
         </div>
       </div>
 
-
       {isLdapEnabled && (
         <form onSubmit={handleSubmit(onSubmit)}>
-
-          <h3 className="border-bottom mb-4">{t('security_settings.configuration')}</h3>
+          <h3 className="border-bottom mb-4">
+            {t('security_settings.configuration')}
+          </h3>
 
           <div className="row my-3">
-            <label htmlFor="serverUrl" className="text-start text-md-end col-md-3 col-form-label">
+            <label
+              htmlFor="serverUrl"
+              className="text-start text-md-end col-md-3 col-form-label"
+            >
               Server URL
             </label>
             <div className="col-md-9">
@@ -132,9 +158,14 @@ const LdapSecuritySettingContents = (props: Props) => {
                 <p
                   className="form-text text-muted"
                   // eslint-disable-next-line react/no-danger
-                  dangerouslySetInnerHTML={{ __html: t('security_settings.ldap.server_url_detail') }}
+                  dangerouslySetInnerHTML={{
+                    __html: t('security_settings.ldap.server_url_detail'),
+                  }}
                 />
-                {t('security_settings.example')}: <code>ldaps://ldap.company.com/ou=people,dc=company,dc=com</code>
+                {t('security_settings.example')}:{' '}
+                <code>
+                  ldaps://ldap.company.com/ou=people,dc=company,dc=com
+                </code>
               </small>
             </div>
           </div>
@@ -153,15 +184,36 @@ const LdapSecuritySettingContents = (props: Props) => {
                   aria-haspopup="true"
                   aria-expanded="true"
                 >
-                  {adminLdapSecurityContainer.state.isUserBind
-                    ? <span className="pull-left">{t('security_settings.ldap.bind_user')}</span>
-                    : <span className="pull-left">{t('security_settings.ldap.bind_manager')}</span>}
+                  {adminLdapSecurityContainer.state.isUserBind ? (
+                    <span className="pull-left">
+                      {t('security_settings.ldap.bind_user')}
+                    </span>
+                  ) : (
+                    <span className="pull-left">
+                      {t('security_settings.ldap.bind_manager')}
+                    </span>
+                  )}
                 </button>
-                <div className="dropdown-menu" aria-labelledby="dropdownMenuButton">
-                  <button className="dropdown-item" type="button" onClick={() => { adminLdapSecurityContainer.changeLdapBindMode(true) }}>
+                <div
+                  className="dropdown-menu"
+                  aria-labelledby="dropdownMenuButton"
+                >
+                  <button
+                    className="dropdown-item"
+                    type="button"
+                    onClick={() => {
+                      adminLdapSecurityContainer.changeLdapBindMode(true);
+                    }}
+                  >
                     {t('security_settings.ldap.bind_user')}
                   </button>
-                  <button className="dropdown-item" type="button" onClick={() => { adminLdapSecurityContainer.changeLdapBindMode(false) }}>
+                  <button
+                    className="dropdown-item"
+                    type="button"
+                    onClick={() => {
+                      adminLdapSecurityContainer.changeLdapBindMode(false);
+                    }}
+                  >
                     {t('security_settings.ldap.bind_manager')}
                   </button>
                 </div>
@@ -179,53 +231,73 @@ const LdapSecuritySettingContents = (props: Props) => {
                 type="text"
                 {...register('ldapBindDN')}
               />
-              {(adminLdapSecurityContainer.state.isUserBind === true) ? (
+              {adminLdapSecurityContainer.state.isUserBind === true ? (
                 <p className="form-text text-muted">
                   <small>
-                    {t('security_settings.ldap.bind_DN_user_detail1')}<br />
+                    {t('security_settings.ldap.bind_DN_user_detail1')}
+                    <br />
                     {/* eslint-disable-next-line react/no-danger */}
-                    <span dangerouslySetInnerHTML={{ __html: t('security_settings.ldap.bind_DN_user_detail2') }} /><br />
-                    {t('security_settings.example')}1: <code>uid={'{{ username }}'},dc=domain,dc=com</code><br />
-                    {t('security_settings.example')}2: <code>{'{{ username }}'}@domain.com</code>
+                    <span
+                      dangerouslySetInnerHTML={{
+                        __html: t(
+                          'security_settings.ldap.bind_DN_user_detail2',
+                        ),
+                      }}
+                    />
+                    <br />
+                    {t('security_settings.example')}1:{' '}
+                    <code>uid={'{{ username }}'},dc=domain,dc=com</code>
+                    <br />
+                    {t('security_settings.example')}2:{' '}
+                    <code>{'{{ username }}'}@domain.com</code>
                   </small>
                 </p>
-              )
-                : (
-                  <p className="form-text text-muted">
-                    <small>
-                      {t('security_settings.ldap.bind_DN_manager_detail')}<br />
-                      {t('security_settings.example')}1: <code>uid=admin,dc=domain,dc=com</code><br />
-                      {t('security_settings.example')}2: <code>admin@domain.com</code>
-                    </small>
-                  </p>
-                )}
+              ) : (
+                <p className="form-text text-muted">
+                  <small>
+                    {t('security_settings.ldap.bind_DN_manager_detail')}
+                    <br />
+                    {t('security_settings.example')}1:{' '}
+                    <code>uid=admin,dc=domain,dc=com</code>
+                    <br />
+                    {t('security_settings.example')}2:{' '}
+                    <code>admin@domain.com</code>
+                  </small>
+                </p>
+              )}
             </div>
           </div>
 
           <div className="row my-3">
-            <label className="text-start text-md-end col-md-3 col-form-label" htmlFor="bindDNPassword">
+            <label
+              className="text-start text-md-end col-md-3 col-form-label"
+              htmlFor="bindDNPassword"
+            >
               <strong>{t('security_settings.ldap.bind_DN_password')}</strong>
             </label>
             <div className="col-md-9">
-              {(adminLdapSecurityContainer.state.isUserBind) ? (
+              {adminLdapSecurityContainer.state.isUserBind ? (
                 <p className="card custom-card">
                   <small>
                     {t('security_settings.ldap.bind_DN_password_user_detail')}
                   </small>
                 </p>
-              )
-                : (
-                  <>
-                    <input
-                      className="form-control"
-                      type="password"
-                      {...register('ldapBindDNPassword')}
-                    />
-                    <p className="form-text text-muted">
-                      <small>{t('security_settings.ldap.bind_DN_password_manager_detail')}</small>
-                    </p>
-                  </>
-                )}
+              ) : (
+                <>
+                  <input
+                    className="form-control"
+                    type="password"
+                    {...register('ldapBindDNPassword')}
+                  />
+                  <p className="form-text text-muted">
+                    <small>
+                      {t(
+                        'security_settings.ldap.bind_DN_password_manager_detail',
+                      )}
+                    </small>
+                  </p>
+                </>
+              )}
             </div>
           </div>
 
@@ -241,18 +313,33 @@ const LdapSecuritySettingContents = (props: Props) => {
               />
               <p className="form-text text-muted">
                 <small>
-                  {t('security_settings.ldap.search_filter_detail1')}<br />
+                  {t('security_settings.ldap.search_filter_detail1')}
+                  <br />
                   {/* eslint-disable-next-line react/no-danger */}
-                  <span dangerouslySetInnerHTML={{ __html: t('security_settings.ldap.search_filter_detail2') }} /><br />
+                  <span
+                    dangerouslySetInnerHTML={{
+                      __html: t('security_settings.ldap.search_filter_detail2'),
+                    }}
+                  />
+                  <br />
                   {/* eslint-disable-next-line react/no-danger */}
-                  <span dangerouslySetInnerHTML={{ __html: t('security_settings.ldap.search_filter_detail3') }} />
+                  <span
+                    dangerouslySetInnerHTML={{
+                      __html: t('security_settings.ldap.search_filter_detail3'),
+                    }}
+                  />
                 </small>
               </p>
               <p className="form-text text-muted">
                 <small>
-                  {t('security_settings.example')}1 - {t('security_settings.ldap.search_filter_example1')}:
-                  <code>(|(uid={'{{username}}'})(mail={'{{username}}'}))</code><br />
-                  {t('security_settings.example')}2 - {t('security_settings.ldap.search_filter_example2')}:
+                  {t('security_settings.example')}1 -{' '}
+                  {t('security_settings.ldap.search_filter_example1')}:
+                  <code>
+                    (|(uid={'{{username}}'})(mail={'{{username}}'}))
+                  </code>
+                  <br />
+                  {t('security_settings.example')}2 -{' '}
+                  {t('security_settings.ldap.search_filter_example2')}:
                   <code>(sAMAccountName={'{{username}}'})</code>
                 </small>
               </p>
@@ -264,7 +351,10 @@ const LdapSecuritySettingContents = (props: Props) => {
           </h3>
 
           <div className="row my-3">
-            <label className="form-label text-start text-md-end col-md-3 col-form-label" htmlFor="attrMapUsername">
+            <label
+              className="form-label text-start text-md-end col-md-3 col-form-label"
+              htmlFor="attrMapUsername"
+            >
               <strong>{t('username')}</strong>
             </label>
             <div className="col-md-9">
@@ -276,7 +366,11 @@ const LdapSecuritySettingContents = (props: Props) => {
               />
               <p className="form-text text-muted">
                 {/* eslint-disable-next-line react/no-danger */}
-                <small dangerouslySetInnerHTML={{ __html: t('security_settings.ldap.username_detail') }} />
+                <small
+                  dangerouslySetInnerHTML={{
+                    __html: t('security_settings.ldap.username_detail'),
+                  }}
+                />
               </p>
             </div>
           </div>
@@ -288,25 +382,43 @@ const LdapSecuritySettingContents = (props: Props) => {
                   type="checkbox"
                   className="form-check-input"
                   id="isSameUsernameTreatedAsIdenticalUser"
-                  checked={adminLdapSecurityContainer.state.isSameUsernameTreatedAsIdenticalUser}
-                  onChange={() => { adminLdapSecurityContainer.switchIsSameUsernameTreatedAsIdenticalUser() }}
+                  checked={
+                    adminLdapSecurityContainer.state
+                      .isSameUsernameTreatedAsIdenticalUser
+                  }
+                  onChange={() => {
+                    adminLdapSecurityContainer.switchIsSameUsernameTreatedAsIdenticalUser();
+                  }}
                 />
                 <label
                   className="form-check-label"
                   htmlFor="isSameUsernameTreatedAsIdenticalUser"
                   // eslint-disable-next-line react/no-danger
-                  dangerouslySetInnerHTML={{ __html: t('security_settings.Treat username matching as identical') }}
+                  dangerouslySetInnerHTML={{
+                    __html: t(
+                      'security_settings.Treat username matching as identical',
+                    ),
+                  }}
                 />
               </div>
               <p className="form-text text-muted">
                 {/* eslint-disable-next-line react/no-danger */}
-                <small dangerouslySetInnerHTML={{ __html: t('security_settings.Treat username matching as identical_warn') }} />
+                <small
+                  dangerouslySetInnerHTML={{
+                    __html: t(
+                      'security_settings.Treat username matching as identical_warn',
+                    ),
+                  }}
+                />
               </p>
             </div>
           </div>
 
           <div className="row my-3">
-            <label className="form-label text-start text-md-end col-md-3 col-form-label" htmlFor="attrMapMail">
+            <label
+              className="form-label text-start text-md-end col-md-3 col-form-label"
+              htmlFor="attrMapMail"
+            >
               <strong>{t('Email')}</strong>
             </label>
             <div className="col-md-9">
@@ -317,15 +429,16 @@ const LdapSecuritySettingContents = (props: Props) => {
                 {...register('ldapAttrMapMail')}
               />
               <p className="form-text text-muted">
-                <small>
-                  {t('security_settings.ldap.mail_detail')}
-                </small>
+                <small>{t('security_settings.ldap.mail_detail')}</small>
               </p>
             </div>
           </div>
 
           <div className="row my-3">
-            <label className="form-label text-start text-md-end col-md-3 col-form-label" htmlFor="attrMapName">
+            <label
+              className="form-label text-start text-md-end col-md-3 col-form-label"
+              htmlFor="attrMapName"
+            >
               <strong>{t('Name')}</strong>
             </label>
             <div className="col-md-9">
@@ -335,21 +448,23 @@ const LdapSecuritySettingContents = (props: Props) => {
                 {...register('ldapAttrMapName')}
               />
               <p className="form-text text-muted">
-                <small>
-                  {t('security_settings.ldap.name_detail')}
-                </small>
+                <small>{t('security_settings.ldap.name_detail')}</small>
               </p>
             </div>
           </div>
 
-
           <h3 className="alert-anchor border-bottom mb-4">
             {t('security_settings.ldap.group_search_filter')} ({t('optional')})
           </h3>
 
           <div className="row my-3">
-            <label className="form-label text-start text-md-end col-md-3 col-form-label" htmlFor="groupSearchBase">
-              <strong>{t('security_settings.ldap.group_search_base_DN')}</strong>
+            <label
+              className="form-label text-start text-md-end col-md-3 col-form-label"
+              htmlFor="groupSearchBase"
+            >
+              <strong>
+                {t('security_settings.ldap.group_search_base_DN')}
+              </strong>
             </label>
             <div className="col-md-9">
               <input
@@ -360,15 +475,26 @@ const LdapSecuritySettingContents = (props: Props) => {
               <p className="form-text text-muted">
                 <small>
                   {/* eslint-disable-next-line react/no-danger */}
-                  <span dangerouslySetInnerHTML={{ __html: t('security_settings.ldap.group_search_base_DN_detail') }} /><br />
-                  {t('security_settings.example')}: <code>ou=groups,dc=domain,dc=com</code>
+                  <span
+                    dangerouslySetInnerHTML={{
+                      __html: t(
+                        'security_settings.ldap.group_search_base_DN_detail',
+                      ),
+                    }}
+                  />
+                  <br />
+                  {t('security_settings.example')}:{' '}
+                  <code>ou=groups,dc=domain,dc=com</code>
                 </small>
               </p>
             </div>
           </div>
 
           <div className="row my-3">
-            <label className="form-label text-start text-md-end col-md-3 col-form-label" htmlFor="groupSearchFilter">
+            <label
+              className="form-label text-start text-md-end col-md-3 col-form-label"
+              htmlFor="groupSearchFilter"
+            >
               <strong>{t('security_settings.ldap.group_search_filter')}</strong>
             </label>
             <div className="col-md-9">
@@ -380,9 +506,29 @@ const LdapSecuritySettingContents = (props: Props) => {
               <p className="form-text text-muted">
                 <small>
                   {/* eslint-disable react/no-danger */}
-                  <span dangerouslySetInnerHTML={{ __html: t('security_settings.ldap.group_search_filter_detail1') }} /><br />
-                  <span dangerouslySetInnerHTML={{ __html: t('security_settings.ldap.group_search_filter_detail2') }} /><br />
-                  <span dangerouslySetInnerHTML={{ __html: t('security_settings.ldap.group_search_filter_detail3') }} />
+                  <span
+                    dangerouslySetInnerHTML={{
+                      __html: t(
+                        'security_settings.ldap.group_search_filter_detail1',
+                      ),
+                    }}
+                  />
+                  <br />
+                  <span
+                    dangerouslySetInnerHTML={{
+                      __html: t(
+                        'security_settings.ldap.group_search_filter_detail2',
+                      ),
+                    }}
+                  />
+                  <br />
+                  <span
+                    dangerouslySetInnerHTML={{
+                      __html: t(
+                        'security_settings.ldap.group_search_filter_detail3',
+                      ),
+                    }}
+                  />
                   {/* eslint-enable react/no-danger */}
                 </small>
               </p>
@@ -390,15 +536,26 @@ const LdapSecuritySettingContents = (props: Props) => {
                 <small>
                   {t('security_settings.example')}:
                   {/* eslint-disable-next-line react/no-danger */}
-                  <span dangerouslySetInnerHTML={{ __html: t('security_settings.ldap.group_search_filter_detail4') }} />
+                  <span
+                    dangerouslySetInnerHTML={{
+                      __html: t(
+                        'security_settings.ldap.group_search_filter_detail4',
+                      ),
+                    }}
+                  />
                 </small>
               </p>
             </div>
           </div>
 
           <div className="row my-3">
-            <label className="form-label text-start text-md-end col-md-3 col-form-label" htmlFor="groupDnProperty">
-              <strong>{t('security_settings.ldap.group_search_user_DN_property')}</strong>
+            <label
+              className="form-label text-start text-md-end col-md-3 col-form-label"
+              htmlFor="groupDnProperty"
+            >
+              <strong>
+                {t('security_settings.ldap.group_search_user_DN_property')}
+              </strong>
             </label>
             <div className="col-md-9">
               <input
@@ -409,7 +566,13 @@ const LdapSecuritySettingContents = (props: Props) => {
               />
               <p className="form-text text-muted">
                 {/* eslint-disable-next-line react/no-danger */}
-                <small dangerouslySetInnerHTML={{ __html: t('security_settings.ldap.group_search_user_DN_property_detail') }} />
+                <small
+                  dangerouslySetInnerHTML={{
+                    __html: t(
+                      'security_settings.ldap.group_search_user_DN_property_detail',
+                    ),
+                  }}
+                />
               </p>
             </div>
           </div>
@@ -418,7 +581,9 @@ const LdapSecuritySettingContents = (props: Props) => {
               <button
                 type="submit"
                 className="btn btn-primary"
-                disabled={adminLdapSecurityContainer.state.retrieveError != null}
+                disabled={
+                  adminLdapSecurityContainer.state.retrieveError != null
+                }
               >
                 {t('Update')}
               </button>
@@ -426,24 +591,25 @@ const LdapSecuritySettingContents = (props: Props) => {
                 type="button"
                 className="btn btn-outline-secondary ms-2"
                 onClick={openLdapAuthTestModal}
-              >{t('security_settings.ldap.test_config')}
+              >
+                {t('security_settings.ldap.test_config')}
               </button>
             </div>
           </div>
-
         </form>
       )}
 
-
-      <LdapAuthTestModal isOpen={isLdapAuthTestModalShown} onClose={closeLdapAuthTestModal} />
-
+      <LdapAuthTestModal
+        isOpen={isLdapAuthTestModalShown}
+        onClose={closeLdapAuthTestModal}
+      />
     </React.Fragment>
   );
 };
 
-const LdapSecuritySettingContentsWrapper = withUnstatedContainers(LdapSecuritySettingContents, [
-  AdminGeneralSecurityContainer,
-  AdminLdapSecurityContainer,
-]);
+const LdapSecuritySettingContentsWrapper = withUnstatedContainers(
+  LdapSecuritySettingContents,
+  [AdminGeneralSecurityContainer, AdminLdapSecurityContainer],
+);
 
 export default LdapSecuritySettingContentsWrapper;

+ 9 - 11
apps/app/src/client/components/Admin/Security/LocalSecuritySetting.jsx

@@ -1,5 +1,4 @@
-import React, { useEffect, useCallback } from 'react';
-
+import React, { useCallback, useEffect } from 'react';
 import PropTypes from 'prop-types';
 
 import AdminLocalSecurityContainer from '~/client/services/AdminLocalSecurityContainer';
@@ -7,23 +6,20 @@ import { toastError } from '~/client/util/toastr';
 import { toArrayIfNot } from '~/utils/array-utils';
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
-
 import LocalSecuritySettingContents from './LocalSecuritySettingContents';
 
 const LocalSecuritySetting = (props) => {
   const { adminLocalSecurityContainer } = props;
 
-  const fetchLocalSecuritySettingsData = useCallback(async() => {
+  const fetchLocalSecuritySettingsData = useCallback(async () => {
     try {
       await adminLocalSecurityContainer.retrieveSecurityData();
-    }
-    catch (err) {
+    } catch (err) {
       const errs = toArrayIfNot(err);
       toastError(errs);
     }
   }, [adminLocalSecurityContainer]);
 
-
   useEffect(() => {
     fetchLocalSecuritySettingsData();
   }, [adminLocalSecurityContainer, fetchLocalSecuritySettingsData]);
@@ -32,11 +28,13 @@ const LocalSecuritySetting = (props) => {
 };
 
 LocalSecuritySetting.propTypes = {
-  adminLocalSecurityContainer: PropTypes.instanceOf(AdminLocalSecurityContainer).isRequired,
+  adminLocalSecurityContainer: PropTypes.instanceOf(AdminLocalSecurityContainer)
+    .isRequired,
 };
 
-const LocalSecuritySettingWithUnstatedContainer = withUnstatedContainers(LocalSecuritySetting, [
-  AdminLocalSecurityContainer,
-]);
+const LocalSecuritySettingWithUnstatedContainer = withUnstatedContainers(
+  LocalSecuritySetting,
+  [AdminLocalSecurityContainer],
+);
 
 export default LocalSecuritySettingWithUnstatedContainer;

+ 118 - 55
apps/app/src/client/components/Admin/Security/LocalSecuritySettingContents.tsx

@@ -1,12 +1,12 @@
 import React, { useCallback, useEffect } from 'react';
 import Link from 'next/link';
-import { useTranslation } from 'next-i18next';
 import { useAtomValue } from 'jotai';
+import { useTranslation } from 'next-i18next';
 import { useForm } from 'react-hook-form';
 
 import AdminGeneralSecurityContainer from '~/client/services/AdminGeneralSecurityContainer';
 import AdminLocalSecurityContainer from '~/client/services/AdminLocalSecurityContainer';
-import { toastSuccess, toastError } from '~/client/util/toastr';
+import { toastError, toastSuccess } from '~/client/util/toastr';
 import { isMailerSetupAtom } from '~/states/server-configurations';
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
@@ -17,58 +17,69 @@ type Props = {
 };
 
 const LocalSecuritySettingContents = (props: Props): JSX.Element => {
-  const {
-    adminGeneralSecurityContainer,
-    adminLocalSecurityContainer,
-  } = props;
+  const { adminGeneralSecurityContainer, adminLocalSecurityContainer } = props;
 
   const { t } = useTranslation('admin');
   const isMailerSetup = useAtomValue(isMailerSetupAtom);
 
   const { register, handleSubmit, reset } = useForm();
 
-  const { registrationMode, isPasswordResetEnabled, isEmailAuthenticationEnabled } = adminLocalSecurityContainer.state;
+  const {
+    registrationMode,
+    isPasswordResetEnabled,
+    isEmailAuthenticationEnabled,
+  } = adminLocalSecurityContainer.state;
   const { isLocalEnabled } = adminGeneralSecurityContainer.state;
 
   useEffect(() => {
     reset({
-      registrationWhitelist: adminLocalSecurityContainer.state.registrationWhitelist.join('\n'),
+      registrationWhitelist:
+        adminLocalSecurityContainer.state.registrationWhitelist.join('\n'),
     });
   }, [reset, adminLocalSecurityContainer.state.registrationWhitelist]);
 
-  const onSubmit = useCallback(async(data) => {
-    try {
-      await adminLocalSecurityContainer.updateLocalSecuritySetting({
-        registrationMode: adminLocalSecurityContainer.state.registrationMode,
-        registrationWhitelist: data.registrationWhitelist.split('\n'),
-        isPasswordResetEnabled: adminLocalSecurityContainer.state.isPasswordResetEnabled,
-        isEmailAuthenticationEnabled: adminLocalSecurityContainer.state.isEmailAuthenticationEnabled,
-      });
-      await adminGeneralSecurityContainer.retrieveSetupStratedies();
-      toastSuccess(t('security_settings.updated_general_security_setting'));
-    }
-    catch (err) {
-      toastError(err);
-    }
-  }, [t, adminGeneralSecurityContainer, adminLocalSecurityContainer]);
+  const onSubmit = useCallback(
+    async (data) => {
+      try {
+        await adminLocalSecurityContainer.updateLocalSecuritySetting({
+          registrationMode: adminLocalSecurityContainer.state.registrationMode,
+          registrationWhitelist: data.registrationWhitelist.split('\n'),
+          isPasswordResetEnabled:
+            adminLocalSecurityContainer.state.isPasswordResetEnabled,
+          isEmailAuthenticationEnabled:
+            adminLocalSecurityContainer.state.isEmailAuthenticationEnabled,
+        });
+        await adminGeneralSecurityContainer.retrieveSetupStratedies();
+        toastSuccess(t('security_settings.updated_general_security_setting'));
+      } catch (err) {
+        toastError(err);
+      }
+    },
+    [t, adminGeneralSecurityContainer, adminLocalSecurityContainer],
+  );
 
   return (
     <>
       {adminLocalSecurityContainer.state.retrieveError != null && (
         <div className="alert alert-danger">
           <p>
-            {t('Error occurred')} : {adminLocalSecurityContainer.state.retrieveError}
+            {t('Error occurred')} :{' '}
+            {adminLocalSecurityContainer.state.retrieveError}
           </p>
         </div>
       )}
-      <h2 className="alert-anchor border-bottom">{t('security_settings.Local.name')}</h2>
+      <h2 className="alert-anchor border-bottom">
+        {t('security_settings.Local.name')}
+      </h2>
 
       {adminLocalSecurityContainer.state.useOnlyEnvVars && (
         <p
           className="alert alert-info"
           // eslint-disable-next-line max-len
           dangerouslySetInnerHTML={{
-            __html: t('security_settings.Local.note for the only env option', { env: 'LOCAL_STRATEGY_USES_ONLY_ENV_VARS_FOR_SOME_OPTIONS' }),
+            __html: t('security_settings.Local.note for the only env option', {
+              env: 'LOCAL_STRATEGY_USES_ONLY_ENV_VARS_FOR_SOME_OPTIONS',
+            }),
           }}
         />
       )}
@@ -81,22 +92,34 @@ const LocalSecuritySettingContents = (props: Props): JSX.Element => {
               className="form-check-input"
               id="isLocalEnabled"
               checked={isLocalEnabled}
-              onChange={() => adminGeneralSecurityContainer.switchIsLocalEnabled()}
+              onChange={() =>
+                adminGeneralSecurityContainer.switchIsLocalEnabled()
+              }
               disabled={adminLocalSecurityContainer.state.useOnlyEnvVars}
             />
-            <label className="form-label form-check-label" htmlFor="isLocalEnabled">
+            <label
+              className="form-label form-check-label"
+              htmlFor="isLocalEnabled"
+            >
               {t('security_settings.Local.enable_local')}
             </label>
           </div>
-          {!adminGeneralSecurityContainer.state.setupStrategies.includes('local') && isLocalEnabled && (
-            <div className="badge bg-warning text-dark">{t('security_settings.setup_is_not_yet_complete')}</div>
-          )}
+          {!adminGeneralSecurityContainer.state.setupStrategies.includes(
+            'local',
+          ) &&
+            isLocalEnabled && (
+              <div className="badge bg-warning text-dark">
+                {t('security_settings.setup_is_not_yet_complete')}
+              </div>
+            )}
         </div>
       </div>
 
       {isLocalEnabled && (
         <form onSubmit={handleSubmit(onSubmit)}>
-          <h3 className="border-bottom">{t('security_settings.configuration')}</h3>
+          <h3 className="border-bottom">
+            {t('security_settings.configuration')}
+          </h3>
 
           <div className="row">
             <div className="col-12 col-md-4 text-start text-md-end py-2">
@@ -112,16 +135,24 @@ const LocalSecuritySettingContents = (props: Props): JSX.Element => {
                   aria-haspopup="true"
                   aria-expanded="true"
                 >
-                  {registrationMode === 'Open' && t('security_settings.registration_mode.open')}
-                  {registrationMode === 'Restricted' && t('security_settings.registration_mode.restricted')}
-                  {registrationMode === 'Closed' && t('security_settings.registration_mode.closed')}
+                  {registrationMode === 'Open' &&
+                    t('security_settings.registration_mode.open')}
+                  {registrationMode === 'Restricted' &&
+                    t('security_settings.registration_mode.restricted')}
+                  {registrationMode === 'Closed' &&
+                    t('security_settings.registration_mode.closed')}
                 </button>
-                <div className="dropdown-menu" aria-labelledby="dropdownMenuButton">
+                <div
+                  className="dropdown-menu"
+                  aria-labelledby="dropdownMenuButton"
+                >
                   <button
                     className="dropdown-item"
                     type="button"
                     onClick={() => {
-                      adminLocalSecurityContainer.changeRegistrationMode('Open');
+                      adminLocalSecurityContainer.changeRegistrationMode(
+                        'Open',
+                      );
                     }}
                   >
                     {t('security_settings.registration_mode.open')}
@@ -130,7 +161,9 @@ const LocalSecuritySettingContents = (props: Props): JSX.Element => {
                     className="dropdown-item"
                     type="button"
                     onClick={() => {
-                      adminLocalSecurityContainer.changeRegistrationMode('Restricted');
+                      adminLocalSecurityContainer.changeRegistrationMode(
+                        'Restricted',
+                      );
                     }}
                   >
                     {t('security_settings.registration_mode.restricted')}
@@ -139,19 +172,29 @@ const LocalSecuritySettingContents = (props: Props): JSX.Element => {
                     className="dropdown-item"
                     type="button"
                     onClick={() => {
-                      adminLocalSecurityContainer.changeRegistrationMode('Closed');
+                      adminLocalSecurityContainer.changeRegistrationMode(
+                        'Closed',
+                      );
                     }}
                   >
                     {t('security_settings.registration_mode.closed')}
                   </button>
                 </div>
               </div>
-              <p className="form-text text-muted small">{t('security_settings.register_limitation_desc')}</p>
+              <p className="form-text text-muted small">
+                {t('security_settings.register_limitation_desc')}
+              </p>
             </div>
           </div>
           <div className="row">
             <div className="col-12 col-md-4 text-start text-md-end">
-              <strong dangerouslySetInnerHTML={{ __html: t('security_settings.The whitelist of registration permission E-mail address') }} />
+              <strong
+                dangerouslySetInnerHTML={{
+                  __html: t(
+                    'security_settings.The whitelist of registration permission E-mail address',
+                  ),
+                }}
+              />
             </div>
             <div className="col-12 col-md-8">
               <textarea
@@ -171,7 +214,9 @@ const LocalSecuritySettingContents = (props: Props): JSX.Element => {
           </div>
 
           <div className="row">
-            <label className="col-12 col-md-4 text-start text-md-end  col-form-label">{t('security_settings.Local.password_reset_by_users')}</label>
+            <label className="col-12 col-md-4 text-start text-md-end  col-form-label">
+              {t('security_settings.Local.password_reset_by_users')}
+            </label>
             <div className="col-12 col-md-8">
               <div className="form-check form-switch form-check-success">
                 <input
@@ -179,17 +224,25 @@ const LocalSecuritySettingContents = (props: Props): JSX.Element => {
                   className="form-check-input"
                   id="isPasswordResetEnabled"
                   checked={isPasswordResetEnabled}
-                  onChange={() => adminLocalSecurityContainer.switchIsPasswordResetEnabled()}
+                  onChange={() =>
+                    adminLocalSecurityContainer.switchIsPasswordResetEnabled()
+                  }
                 />
-                <label className="form-label form-check-label" htmlFor="isPasswordResetEnabled">
+                <label
+                  className="form-label form-check-label"
+                  htmlFor="isPasswordResetEnabled"
+                >
                   {t('security_settings.Local.enable_password_reset_by_users')}
                 </label>
               </div>
               {!isMailerSetup && (
                 <div className="alert alert-warning p-2 my-1 small d-inline-block">
-                  <span>{t('commons:alert.password_reset_please_enable_mailer')}</span>
+                  <span>
+                    {t('commons:alert.password_reset_please_enable_mailer')}
+                  </span>
                   <Link href="/admin/app#mail-settings">
-                    <span className="material-symbols-outlined">link</span> {t('app_setting.mail_settings')}
+                    <span className="material-symbols-outlined">link</span>{' '}
+                    {t('app_setting.mail_settings')}
                   </Link>
                 </div>
               )}
@@ -200,7 +253,9 @@ const LocalSecuritySettingContents = (props: Props): JSX.Element => {
           </div>
 
           <div className="row">
-            <label className="col-12 col-md-4 text-start text-md-end  col-form-label">{t('security_settings.Local.email_authentication')}</label>
+            <label className="col-12 col-md-4 text-start text-md-end  col-form-label">
+              {t('security_settings.Local.email_authentication')}
+            </label>
             <div className="col-12 col-md-8">
               <div className="form-check form-switch form-check-success">
                 <input
@@ -208,9 +263,14 @@ const LocalSecuritySettingContents = (props: Props): JSX.Element => {
                   className="form-check-input"
                   id="isEmailAuthenticationEnabled"
                   checked={isEmailAuthenticationEnabled}
-                  onChange={() => adminLocalSecurityContainer.switchIsEmailAuthenticationEnabled()}
+                  onChange={() =>
+                    adminLocalSecurityContainer.switchIsEmailAuthenticationEnabled()
+                  }
                 />
-                <label className="form-label form-check-label" htmlFor="isEmailAuthenticationEnabled">
+                <label
+                  className="form-label form-check-label"
+                  htmlFor="isEmailAuthenticationEnabled"
+                >
                   {t('security_settings.Local.enable_email_authentication')}
                 </label>
               </div>
@@ -218,7 +278,8 @@ const LocalSecuritySettingContents = (props: Props): JSX.Element => {
                 <div className="alert alert-warning p-2 my-1 small d-inline-block">
                   <span>{t('commons:alert.please_enable_mailer')}</span>
                   <Link href="/admin/app#mail-settings">
-                    <span className="material-symbols-outlined">link</span> {t('app_setting.mail_settings')}
+                    <span className="material-symbols-outlined">link</span>{' '}
+                    {t('app_setting.mail_settings')}
                   </Link>
                 </div>
               )}
@@ -233,7 +294,9 @@ const LocalSecuritySettingContents = (props: Props): JSX.Element => {
               <button
                 type="submit"
                 className="btn btn-primary"
-                disabled={adminLocalSecurityContainer.state.retrieveError != null}
+                disabled={
+                  adminLocalSecurityContainer.state.retrieveError != null
+                }
               >
                 {t('Update')}
               </button>
@@ -245,9 +308,9 @@ const LocalSecuritySettingContents = (props: Props): JSX.Element => {
   );
 };
 
-const LocalSecuritySettingContentsWrapper = withUnstatedContainers(LocalSecuritySettingContents, [
-  AdminGeneralSecurityContainer,
-  AdminLocalSecurityContainer,
-]);
+const LocalSecuritySettingContentsWrapper = withUnstatedContainers(
+  LocalSecuritySettingContents,
+  [AdminGeneralSecurityContainer, AdminLocalSecurityContainer],
+);
 
 export default LocalSecuritySettingContentsWrapper;

+ 9 - 10
apps/app/src/client/components/Admin/Security/OidcSecuritySetting.jsx

@@ -1,5 +1,4 @@
-import React, { useEffect, useCallback } from 'react';
-
+import React, { useCallback, useEffect } from 'react';
 import PropTypes from 'prop-types';
 
 import AdminOidcSecurityContainer from '~/client/services/AdminOidcSecurityContainer';
@@ -7,17 +6,15 @@ import { toastError } from '~/client/util/toastr';
 import { toArrayIfNot } from '~/utils/array-utils';
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
-
 import OidcSecurityManagementContents from './OidcSecuritySettingContents';
 
 const OidcSecurityManagement = (props) => {
   const { adminOidcSecurityContainer } = props;
 
-  const fetchOidcSecuritySettingsData = useCallback(async() => {
+  const fetchOidcSecuritySettingsData = useCallback(async () => {
     try {
       await adminOidcSecurityContainer.retrieveSecurityData();
-    }
-    catch (err) {
+    } catch (err) {
       const errs = toArrayIfNot(err);
       toastError(errs);
     }
@@ -31,11 +28,13 @@ const OidcSecurityManagement = (props) => {
 };
 
 OidcSecurityManagement.propTypes = {
-  adminOidcSecurityContainer: PropTypes.instanceOf(AdminOidcSecurityContainer).isRequired,
+  adminOidcSecurityContainer: PropTypes.instanceOf(AdminOidcSecurityContainer)
+    .isRequired,
 };
 
-const OidcSecurityManagementWithUnstatedContainer = withUnstatedContainers(OidcSecurityManagement, [
-  AdminOidcSecurityContainer,
-]);
+const OidcSecurityManagementWithUnstatedContainer = withUnstatedContainers(
+  OidcSecurityManagement,
+  [AdminOidcSecurityContainer],
+);
 
 export default OidcSecurityManagementWithUnstatedContainer;

+ 360 - 106
apps/app/src/client/components/Admin/Security/OidcSecuritySettingContents.tsx

@@ -1,14 +1,12 @@
-import React, { useEffect, useCallback } from 'react';
-
+import React, { useCallback, useEffect } from 'react';
 import { pathUtils } from '@growi/core/dist/utils';
 import { useTranslation } from 'next-i18next';
 import { useForm } from 'react-hook-form';
 import urljoin from 'url-join';
 
-
 import AdminGeneralSecurityContainer from '~/client/services/AdminGeneralSecurityContainer';
 import AdminOidcSecurityContainer from '~/client/services/AdminOidcSecurityContainer';
-import { toastSuccess, toastError } from '~/client/util/toastr';
+import { toastError, toastSuccess } from '~/client/util/toastr';
 import { useSiteUrlWithEmptyValueWarn } from '~/states/global';
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
@@ -22,18 +20,31 @@ const OidcSecurityManagementContents = (props: Props) => {
   const { t } = useTranslation('admin');
   const siteUrl = useSiteUrlWithEmptyValueWarn();
 
-  const {
-    adminGeneralSecurityContainer, adminOidcSecurityContainer,
-  } = props;
+  const { adminGeneralSecurityContainer, adminOidcSecurityContainer } = props;
   const { isOidcEnabled } = adminGeneralSecurityContainer.state;
   const {
-    oidcProviderName, oidcIssuerHost, oidcClientId, oidcClientSecret,
-    oidcAuthorizationEndpoint, oidcTokenEndpoint, oidcRevocationEndpoint, oidcIntrospectionEndpoint,
-    oidcUserInfoEndpoint, oidcEndSessionEndpoint, oidcRegistrationEndpoint, oidcJWKSUri,
-    oidcAttrMapId, oidcAttrMapUserName, oidcAttrMapName, oidcAttrMapEmail,
+    oidcProviderName,
+    oidcIssuerHost,
+    oidcClientId,
+    oidcClientSecret,
+    oidcAuthorizationEndpoint,
+    oidcTokenEndpoint,
+    oidcRevocationEndpoint,
+    oidcIntrospectionEndpoint,
+    oidcUserInfoEndpoint,
+    oidcEndSessionEndpoint,
+    oidcRegistrationEndpoint,
+    oidcJWKSUri,
+    oidcAttrMapId,
+    oidcAttrMapUserName,
+    oidcAttrMapName,
+    oidcAttrMapEmail,
   } = adminOidcSecurityContainer.state;
 
-  const oidcCallbackUrl = urljoin(pathUtils.removeTrailingSlash(siteUrl), '/passport/oidc/callback');
+  const oidcCallbackUrl = urljoin(
+    pathUtils.removeTrailingSlash(siteUrl),
+    '/passport/oidc/callback',
+  );
 
   const { register, handleSubmit, reset } = useForm();
 
@@ -57,41 +68,59 @@ const OidcSecurityManagementContents = (props: Props) => {
       oidcAttrMapEmail,
     });
   }, [
-    reset, oidcProviderName, oidcIssuerHost, oidcClientId, oidcClientSecret,
-    oidcAuthorizationEndpoint, oidcTokenEndpoint, oidcRevocationEndpoint, oidcIntrospectionEndpoint,
-    oidcUserInfoEndpoint, oidcEndSessionEndpoint, oidcRegistrationEndpoint, oidcJWKSUri,
-    oidcAttrMapId, oidcAttrMapUserName, oidcAttrMapName, oidcAttrMapEmail,
+    reset,
+    oidcProviderName,
+    oidcIssuerHost,
+    oidcClientId,
+    oidcClientSecret,
+    oidcAuthorizationEndpoint,
+    oidcTokenEndpoint,
+    oidcRevocationEndpoint,
+    oidcIntrospectionEndpoint,
+    oidcUserInfoEndpoint,
+    oidcEndSessionEndpoint,
+    oidcRegistrationEndpoint,
+    oidcJWKSUri,
+    oidcAttrMapId,
+    oidcAttrMapUserName,
+    oidcAttrMapName,
+    oidcAttrMapEmail,
   ]);
 
-  const onSubmit = useCallback(async(data) => {
-    try {
-      await adminOidcSecurityContainer.updateOidcSetting({
-        oidcProviderName: data.oidcProviderName,
-        oidcIssuerHost: data.oidcIssuerHost,
-        oidcClientId: data.oidcClientId,
-        oidcClientSecret: data.oidcClientSecret,
-        oidcAuthorizationEndpoint: data.oidcAuthorizationEndpoint,
-        oidcTokenEndpoint: data.oidcTokenEndpoint,
-        oidcRevocationEndpoint: data.oidcRevocationEndpoint,
-        oidcIntrospectionEndpoint: data.oidcIntrospectionEndpoint,
-        oidcUserInfoEndpoint: data.oidcUserInfoEndpoint,
-        oidcEndSessionEndpoint: data.oidcEndSessionEndpoint,
-        oidcRegistrationEndpoint: data.oidcRegistrationEndpoint,
-        oidcJWKSUri: data.oidcJWKSUri,
-        oidcAttrMapId: data.oidcAttrMapId,
-        oidcAttrMapUserName: data.oidcAttrMapUserName,
-        oidcAttrMapName: data.oidcAttrMapName,
-        oidcAttrMapEmail: data.oidcAttrMapEmail,
-        isSameUsernameTreatedAsIdenticalUser: adminOidcSecurityContainer.state.isSameUsernameTreatedAsIdenticalUser,
-        isSameEmailTreatedAsIdenticalUser: adminOidcSecurityContainer.state.isSameEmailTreatedAsIdenticalUser,
-      });
-      await adminGeneralSecurityContainer.retrieveSetupStratedies();
-      toastSuccess(t('security_settings.OAuth.OIDC.updated_oidc'));
-    }
-    catch (err) {
-      toastError(err);
-    }
-  }, [t, adminOidcSecurityContainer, adminGeneralSecurityContainer]);
+  const onSubmit = useCallback(
+    async (data) => {
+      try {
+        await adminOidcSecurityContainer.updateOidcSetting({
+          oidcProviderName: data.oidcProviderName,
+          oidcIssuerHost: data.oidcIssuerHost,
+          oidcClientId: data.oidcClientId,
+          oidcClientSecret: data.oidcClientSecret,
+          oidcAuthorizationEndpoint: data.oidcAuthorizationEndpoint,
+          oidcTokenEndpoint: data.oidcTokenEndpoint,
+          oidcRevocationEndpoint: data.oidcRevocationEndpoint,
+          oidcIntrospectionEndpoint: data.oidcIntrospectionEndpoint,
+          oidcUserInfoEndpoint: data.oidcUserInfoEndpoint,
+          oidcEndSessionEndpoint: data.oidcEndSessionEndpoint,
+          oidcRegistrationEndpoint: data.oidcRegistrationEndpoint,
+          oidcJWKSUri: data.oidcJWKSUri,
+          oidcAttrMapId: data.oidcAttrMapId,
+          oidcAttrMapUserName: data.oidcAttrMapUserName,
+          oidcAttrMapName: data.oidcAttrMapName,
+          oidcAttrMapEmail: data.oidcAttrMapEmail,
+          isSameUsernameTreatedAsIdenticalUser:
+            adminOidcSecurityContainer.state
+              .isSameUsernameTreatedAsIdenticalUser,
+          isSameEmailTreatedAsIdenticalUser:
+            adminOidcSecurityContainer.state.isSameEmailTreatedAsIdenticalUser,
+        });
+        await adminGeneralSecurityContainer.retrieveSetupStratedies();
+        toastSuccess(t('security_settings.OAuth.OIDC.updated_oidc'));
+      } catch (err) {
+        toastError(err);
+      }
+    },
+    [t, adminOidcSecurityContainer, adminGeneralSecurityContainer],
+  );
 
   return (
     <>
@@ -107,19 +136,32 @@ const OidcSecurityManagementContents = (props: Props) => {
               className="form-check-input"
               type="checkbox"
               checked={adminGeneralSecurityContainer.state.isOidcEnabled}
-              onChange={() => { adminGeneralSecurityContainer.switchIsOidcEnabled() }}
+              onChange={() => {
+                adminGeneralSecurityContainer.switchIsOidcEnabled();
+              }}
             />
-            <label className="form-label form-check-label" htmlFor="isOidcEnabled">
+            <label
+              className="form-label form-check-label"
+              htmlFor="isOidcEnabled"
+            >
               {t('security_settings.OAuth.enable_oidc')}
             </label>
           </div>
-          {(!adminGeneralSecurityContainer.state.setupStrategies.includes('oidc') && isOidcEnabled)
-              && <div className="badge text-bg-warning">{t('security_settings.setup_is_not_yet_complete')}</div>}
+          {!adminGeneralSecurityContainer.state.setupStrategies.includes(
+            'oidc',
+          ) &&
+            isOidcEnabled && (
+              <div className="badge text-bg-warning">
+                {t('security_settings.setup_is_not_yet_complete')}
+              </div>
+            )}
         </div>
       </div>
 
       <div className="row mb-5">
-        <label className="text-start text-md-end col-md-3 col-form-label">{t('security_settings.callback_URL')}</label>
+        <label className="text-start text-md-end col-md-3 col-form-label">
+          {t('security_settings.callback_URL')}
+        </label>
         <div className="col-md-6">
           <input
             className="form-control"
@@ -127,13 +169,20 @@ const OidcSecurityManagementContents = (props: Props) => {
             value={oidcCallbackUrl}
             readOnly
           />
-          <p className="form-text text-muted small">{t('security_settings.desc_of_callback_URL', { AuthName: 'OAuth' })}</p>
+          <p className="form-text text-muted small">
+            {t('security_settings.desc_of_callback_URL', { AuthName: 'OAuth' })}
+          </p>
           {(siteUrl == null || siteUrl === '') && (
             <div className="alert alert-danger">
               <span className="material-symbols-outlined">error</span>
               <span
                 // eslint-disable-next-line max-len
-                dangerouslySetInnerHTML={{ __html: t('alert.siteUrl_is_not_set', { link: `<a href="/admin/app">${t('headers.app_settings', { ns: 'commons' })}<span class="material-symbols-outlined">login</span></a>`, ns: 'commons' }) }}
+                dangerouslySetInnerHTML={{
+                  __html: t('alert.siteUrl_is_not_set', {
+                    link: `<a href="/admin/app">${t('headers.app_settings', { ns: 'commons' })}<span class="material-symbols-outlined">login</span></a>`,
+                    ns: 'commons',
+                  }),
+                }}
               />
             </div>
           )}
@@ -142,11 +191,17 @@ const OidcSecurityManagementContents = (props: Props) => {
 
       {isOidcEnabled && (
         <form onSubmit={handleSubmit(onSubmit)}>
-
-          <h3 className="border-bottom mb-4">{t('security_settings.configuration')}</h3>
+          <h3 className="border-bottom mb-4">
+            {t('security_settings.configuration')}
+          </h3>
 
           <div className="row mb-4">
-            <label htmlFor="oidcProviderName" className="text-start text-md-end col-md-3 col-form-label">{t('security_settings.providerName')}</label>
+            <label
+              htmlFor="oidcProviderName"
+              className="text-start text-md-end col-md-3 col-form-label"
+            >
+              {t('security_settings.providerName')}
+            </label>
             <div className="col-md-6">
               <input
                 className="form-control"
@@ -157,7 +212,12 @@ const OidcSecurityManagementContents = (props: Props) => {
           </div>
 
           <div className="row mb-4">
-            <label htmlFor="oidcIssuerHost" className="text-start text-md-end col-md-3 col-form-label">{t('security_settings.issuerHost')}</label>
+            <label
+              htmlFor="oidcIssuerHost"
+              className="text-start text-md-end col-md-3 col-form-label"
+            >
+              {t('security_settings.issuerHost')}
+            </label>
             <div className="col-md-6">
               <input
                 className="form-control"
@@ -165,13 +225,24 @@ const OidcSecurityManagementContents = (props: Props) => {
                 {...register('oidcIssuerHost')}
               />
               <p className="form-text text-muted">
-                <small dangerouslySetInnerHTML={{ __html: t('security_settings.Use env var if empty', { env: 'OAUTH_OIDC_ISSUER_HOST' }) }} />
+                <small
+                  dangerouslySetInnerHTML={{
+                    __html: t('security_settings.Use env var if empty', {
+                      env: 'OAUTH_OIDC_ISSUER_HOST',
+                    }),
+                  }}
+                />
               </p>
             </div>
           </div>
 
           <div className="row mb-4">
-            <label htmlFor="oidcClientId" className="text-start text-md-end col-md-3 col-form-label">{t('security_settings.clientID')}</label>
+            <label
+              htmlFor="oidcClientId"
+              className="text-start text-md-end col-md-3 col-form-label"
+            >
+              {t('security_settings.clientID')}
+            </label>
             <div className="col-md-6">
               <input
                 className="form-control"
@@ -179,13 +250,24 @@ const OidcSecurityManagementContents = (props: Props) => {
                 {...register('oidcClientId')}
               />
               <p className="form-text text-muted">
-                <small dangerouslySetInnerHTML={{ __html: t('security_settings.Use env var if empty', { env: 'OAUTH_OIDC_CLIENT_ID' }) }} />
+                <small
+                  dangerouslySetInnerHTML={{
+                    __html: t('security_settings.Use env var if empty', {
+                      env: 'OAUTH_OIDC_CLIENT_ID',
+                    }),
+                  }}
+                />
               </p>
             </div>
           </div>
 
           <div className="row mb-4">
-            <label htmlFor="oidcClientSecret" className="text-start text-md-end col-md-3 col-form-label">{t('security_settings.client_secret')}</label>
+            <label
+              htmlFor="oidcClientSecret"
+              className="text-start text-md-end col-md-3 col-form-label"
+            >
+              {t('security_settings.client_secret')}
+            </label>
             <div className="col-md-6">
               <input
                 className="form-control"
@@ -193,13 +275,22 @@ const OidcSecurityManagementContents = (props: Props) => {
                 {...register('oidcClientSecret')}
               />
               <p className="form-text text-muted">
-                <small dangerouslySetInnerHTML={{ __html: t('security_settings.Use env var if empty', { env: 'OAUTH_OIDC_CLIENT_SECRET' }) }} />
+                <small
+                  dangerouslySetInnerHTML={{
+                    __html: t('security_settings.Use env var if empty', {
+                      env: 'OAUTH_OIDC_CLIENT_SECRET',
+                    }),
+                  }}
+                />
               </p>
             </div>
           </div>
 
           <div className="row mb-4">
-            <label htmlFor="oidcAuthorizationEndpoint" className="text-start text-md-end col-md-3 col-form-label">
+            <label
+              htmlFor="oidcAuthorizationEndpoint"
+              className="text-start text-md-end col-md-3 col-form-label"
+            >
               {t('security_settings.authorization_endpoint')}
             </label>
             <div className="col-md-6">
@@ -209,13 +300,24 @@ const OidcSecurityManagementContents = (props: Props) => {
                 {...register('oidcAuthorizationEndpoint')}
               />
               <p className="form-text text-muted">
-                <small dangerouslySetInnerHTML={{ __html: t('security_settings.OAuth.OIDC.Use discovered URL if empty') }} />
+                <small
+                  dangerouslySetInnerHTML={{
+                    __html: t(
+                      'security_settings.OAuth.OIDC.Use discovered URL if empty',
+                    ),
+                  }}
+                />
               </p>
             </div>
           </div>
 
           <div className="row mb-4">
-            <label htmlFor="oidcTokenEndpoint" className="text-start text-md-end col-md-3 col-form-label">{t('security_settings.token_endpoint')}</label>
+            <label
+              htmlFor="oidcTokenEndpoint"
+              className="text-start text-md-end col-md-3 col-form-label"
+            >
+              {t('security_settings.token_endpoint')}
+            </label>
             <div className="col-md-6">
               <input
                 className="form-control"
@@ -223,13 +325,22 @@ const OidcSecurityManagementContents = (props: Props) => {
                 {...register('oidcTokenEndpoint')}
               />
               <p className="form-text text-muted">
-                <small dangerouslySetInnerHTML={{ __html: t('security_settings.OAuth.OIDC.Use discovered URL if empty') }} />
+                <small
+                  dangerouslySetInnerHTML={{
+                    __html: t(
+                      'security_settings.OAuth.OIDC.Use discovered URL if empty',
+                    ),
+                  }}
+                />
               </p>
             </div>
           </div>
 
           <div className="row mb-4">
-            <label htmlFor="oidcRevocationEndpoint" className="text-start text-md-end col-md-3 col-form-label">
+            <label
+              htmlFor="oidcRevocationEndpoint"
+              className="text-start text-md-end col-md-3 col-form-label"
+            >
               {t('security_settings.revocation_endpoint')}
             </label>
             <div className="col-md-6">
@@ -239,13 +350,22 @@ const OidcSecurityManagementContents = (props: Props) => {
                 {...register('oidcRevocationEndpoint')}
               />
               <p className="form-text text-muted">
-                <small dangerouslySetInnerHTML={{ __html: t('security_settings.OAuth.OIDC.Use discovered URL if empty') }} />
+                <small
+                  dangerouslySetInnerHTML={{
+                    __html: t(
+                      'security_settings.OAuth.OIDC.Use discovered URL if empty',
+                    ),
+                  }}
+                />
               </p>
             </div>
           </div>
 
           <div className="row mb-4">
-            <label htmlFor="oidcIntrospectionEndpoint" className="text-start text-md-end col-md-3 col-form-label">
+            <label
+              htmlFor="oidcIntrospectionEndpoint"
+              className="text-start text-md-end col-md-3 col-form-label"
+            >
               {t('security_settings.introspection_endpoint')}
             </label>
             <div className="col-md-6">
@@ -255,13 +375,22 @@ const OidcSecurityManagementContents = (props: Props) => {
                 {...register('oidcIntrospectionEndpoint')}
               />
               <p className="form-text text-muted">
-                <small dangerouslySetInnerHTML={{ __html: t('security_settings.OAuth.OIDC.Use discovered URL if empty') }} />
+                <small
+                  dangerouslySetInnerHTML={{
+                    __html: t(
+                      'security_settings.OAuth.OIDC.Use discovered URL if empty',
+                    ),
+                  }}
+                />
               </p>
             </div>
           </div>
 
           <div className="row mb-4">
-            <label htmlFor="oidcUserInfoEndpoint" className="text-start text-md-end col-md-3 col-form-label">
+            <label
+              htmlFor="oidcUserInfoEndpoint"
+              className="text-start text-md-end col-md-3 col-form-label"
+            >
               {t('security_settings.userinfo_endpoint')}
             </label>
             <div className="col-md-6">
@@ -271,13 +400,22 @@ const OidcSecurityManagementContents = (props: Props) => {
                 {...register('oidcUserInfoEndpoint')}
               />
               <p className="form-text text-muted">
-                <small dangerouslySetInnerHTML={{ __html: t('security_settings.OAuth.OIDC.Use discovered URL if empty') }} />
+                <small
+                  dangerouslySetInnerHTML={{
+                    __html: t(
+                      'security_settings.OAuth.OIDC.Use discovered URL if empty',
+                    ),
+                  }}
+                />
               </p>
             </div>
           </div>
 
           <div className="row mb-4">
-            <label htmlFor="oidcEndSessionEndpoint" className="text-start text-md-end col-md-3 col-form-label">
+            <label
+              htmlFor="oidcEndSessionEndpoint"
+              className="text-start text-md-end col-md-3 col-form-label"
+            >
               {t('security_settings.end_session_endpoint')}
             </label>
             <div className="col-md-6">
@@ -287,13 +425,22 @@ const OidcSecurityManagementContents = (props: Props) => {
                 {...register('oidcEndSessionEndpoint')}
               />
               <p className="form-text text-muted">
-                <small dangerouslySetInnerHTML={{ __html: t('security_settings.OAuth.OIDC.Use discovered URL if empty') }} />
+                <small
+                  dangerouslySetInnerHTML={{
+                    __html: t(
+                      'security_settings.OAuth.OIDC.Use discovered URL if empty',
+                    ),
+                  }}
+                />
               </p>
             </div>
           </div>
 
           <div className="row mb-4">
-            <label htmlFor="oidcRegistrationEndpoint" className="text-start text-md-end col-md-3 col-form-label">
+            <label
+              htmlFor="oidcRegistrationEndpoint"
+              className="text-start text-md-end col-md-3 col-form-label"
+            >
               {t('security_settings.registration_endpoint')}
             </label>
             <div className="col-md-6">
@@ -303,13 +450,24 @@ const OidcSecurityManagementContents = (props: Props) => {
                 {...register('oidcRegistrationEndpoint')}
               />
               <p className="form-text text-muted">
-                <small dangerouslySetInnerHTML={{ __html: t('security_settings.OAuth.OIDC.Use discovered URL if empty') }} />
+                <small
+                  dangerouslySetInnerHTML={{
+                    __html: t(
+                      'security_settings.OAuth.OIDC.Use discovered URL if empty',
+                    ),
+                  }}
+                />
               </p>
             </div>
           </div>
 
           <div className="row mb-4">
-            <label htmlFor="oidcJWKSUri" className="text-start text-md-end col-md-3 col-form-label">{t('security_settings.jwks_uri')}</label>
+            <label
+              htmlFor="oidcJWKSUri"
+              className="text-start text-md-end col-md-3 col-form-label"
+            >
+              {t('security_settings.jwks_uri')}
+            </label>
             <div className="col-md-6">
               <input
                 className="form-control"
@@ -317,7 +475,13 @@ const OidcSecurityManagementContents = (props: Props) => {
                 {...register('oidcJWKSUri')}
               />
               <p className="form-text text-muted">
-                <small dangerouslySetInnerHTML={{ __html: t('security_settings.OAuth.OIDC.Use discovered URL if empty') }} />
+                <small
+                  dangerouslySetInnerHTML={{
+                    __html: t(
+                      'security_settings.OAuth.OIDC.Use discovered URL if empty',
+                    ),
+                  }}
+                />
               </p>
             </div>
           </div>
@@ -327,7 +491,12 @@ const OidcSecurityManagementContents = (props: Props) => {
           </h3>
 
           <div className="row mb-4">
-            <label htmlFor="oidcAttrMapId" className="text-start text-md-end col-md-3 col-form-label">Identifier</label>
+            <label
+              htmlFor="oidcAttrMapId"
+              className="text-start text-md-end col-md-3 col-form-label"
+            >
+              Identifier
+            </label>
             <div className="col-md-6">
               <input
                 className="form-control"
@@ -335,13 +504,22 @@ const OidcSecurityManagementContents = (props: Props) => {
                 {...register('oidcAttrMapId')}
               />
               <p className="form-text text-muted">
-                <small dangerouslySetInnerHTML={{ __html: t('security_settings.OAuth.OIDC.id_detail') }} />
+                <small
+                  dangerouslySetInnerHTML={{
+                    __html: t('security_settings.OAuth.OIDC.id_detail'),
+                  }}
+                />
               </p>
             </div>
           </div>
 
           <div className="row mb-4">
-            <label htmlFor="oidcAttrMapUserName" className="text-start text-md-end col-md-3 col-form-label">{t('username')}</label>
+            <label
+              htmlFor="oidcAttrMapUserName"
+              className="text-start text-md-end col-md-3 col-form-label"
+            >
+              {t('username')}
+            </label>
             <div className="col-md-6">
               <input
                 className="form-control"
@@ -349,13 +527,22 @@ const OidcSecurityManagementContents = (props: Props) => {
                 {...register('oidcAttrMapUserName')}
               />
               <p className="form-text text-muted">
-                <small dangerouslySetInnerHTML={{ __html: t('security_settings.OAuth.OIDC.username_detail') }} />
+                <small
+                  dangerouslySetInnerHTML={{
+                    __html: t('security_settings.OAuth.OIDC.username_detail'),
+                  }}
+                />
               </p>
             </div>
           </div>
 
           <div className="row mb-4">
-            <label htmlFor="oidcAttrMapName" className="text-start text-md-end col-md-3 col-form-label">{t('Name')}</label>
+            <label
+              htmlFor="oidcAttrMapName"
+              className="text-start text-md-end col-md-3 col-form-label"
+            >
+              {t('Name')}
+            </label>
             <div className="col-md-6">
               <input
                 className="form-control"
@@ -363,13 +550,22 @@ const OidcSecurityManagementContents = (props: Props) => {
                 {...register('oidcAttrMapName')}
               />
               <p className="form-text text-muted">
-                <small dangerouslySetInnerHTML={{ __html: t('security_settings.OAuth.OIDC.name_detail') }} />
+                <small
+                  dangerouslySetInnerHTML={{
+                    __html: t('security_settings.OAuth.OIDC.name_detail'),
+                  }}
+                />
               </p>
             </div>
           </div>
 
           <div className="row mb-4">
-            <label htmlFor="oidcAttrMapEmail" className="text-start text-md-end col-md-3 col-form-label">{t('Email')}</label>
+            <label
+              htmlFor="oidcAttrMapEmail"
+              className="text-start text-md-end col-md-3 col-form-label"
+            >
+              {t('Email')}
+            </label>
             <div className="col-md-6">
               <input
                 className="form-control"
@@ -377,13 +573,21 @@ const OidcSecurityManagementContents = (props: Props) => {
                 {...register('oidcAttrMapEmail')}
               />
               <p className="form-text text-muted">
-                <small dangerouslySetInnerHTML={{ __html: t('security_settings.OAuth.OIDC.mapping_detail', { target: t('Email') }) }} />
+                <small
+                  dangerouslySetInnerHTML={{
+                    __html: t('security_settings.OAuth.OIDC.mapping_detail', {
+                      target: t('Email'),
+                    }),
+                  }}
+                />
               </p>
             </div>
           </div>
 
           <div className="row mb-4">
-            <label className="form-label text-start text-md-end col-md-3 col-form-label">{t('security_settings.callback_URL')}</label>
+            <label className="form-label text-start text-md-end col-md-3 col-form-label">
+              {t('security_settings.callback_URL')}
+            </label>
             <div className="col-md-6">
               <input
                 className="form-control"
@@ -391,13 +595,22 @@ const OidcSecurityManagementContents = (props: Props) => {
                 defaultValue={oidcCallbackUrl}
                 readOnly
               />
-              <p className="form-text text-muted small">{t('security_settings.desc_of_callback_URL', { AuthName: 'OAuth' })}</p>
+              <p className="form-text text-muted small">
+                {t('security_settings.desc_of_callback_URL', {
+                  AuthName: 'OAuth',
+                })}
+              </p>
               {(siteUrl == null || siteUrl === '') && (
                 <div className="alert alert-danger">
                   <span className="material-symbols-outlined">error</span>
                   <span
                     // eslint-disable-next-line max-len
-                    dangerouslySetInnerHTML={{ __html: t('alert.siteUrl_is_not_set', { link: `<a href="/admin/app">${t('headers.app_settings', { ns: 'commons' })}<span class="material-symbols-outlined">login</span></a>`, ns: 'commons' }) }}
+                    dangerouslySetInnerHTML={{
+                      __html: t('alert.siteUrl_is_not_set', {
+                        link: `<a href="/admin/app">${t('headers.app_settings', { ns: 'commons' })}<span class="material-symbols-outlined">login</span></a>`,
+                        ns: 'commons',
+                      }),
+                    }}
                   />
                 </div>
               )}
@@ -411,17 +624,32 @@ const OidcSecurityManagementContents = (props: Props) => {
                   id="bindByUserName-oidc"
                   className="form-check-input"
                   type="checkbox"
-                  checked={adminOidcSecurityContainer.state.isSameUsernameTreatedAsIdenticalUser}
-                  onChange={() => { adminOidcSecurityContainer.switchIsSameUsernameTreatedAsIdenticalUser() }}
+                  checked={
+                    adminOidcSecurityContainer.state
+                      .isSameUsernameTreatedAsIdenticalUser
+                  }
+                  onChange={() => {
+                    adminOidcSecurityContainer.switchIsSameUsernameTreatedAsIdenticalUser();
+                  }}
                 />
                 <label
                   className="form-label form-check-label"
                   htmlFor="bindByUserName-oidc"
-                  dangerouslySetInnerHTML={{ __html: t('security_settings.Treat username matching as identical') }}
+                  dangerouslySetInnerHTML={{
+                    __html: t(
+                      'security_settings.Treat username matching as identical',
+                    ),
+                  }}
                 />
               </div>
               <p className="form-text text-muted">
-                <small dangerouslySetInnerHTML={{ __html: t('security_settings.Treat username matching as identical_warn') }} />
+                <small
+                  dangerouslySetInnerHTML={{
+                    __html: t(
+                      'security_settings.Treat username matching as identical_warn',
+                    ),
+                  }}
+                />
               </p>
             </div>
           </div>
@@ -433,17 +661,32 @@ const OidcSecurityManagementContents = (props: Props) => {
                   id="bindByEmail-oidc"
                   className="form-check-input"
                   type="checkbox"
-                  checked={adminOidcSecurityContainer.state.isSameEmailTreatedAsIdenticalUser || false}
-                  onChange={() => { adminOidcSecurityContainer.switchIsSameEmailTreatedAsIdenticalUser() }}
+                  checked={
+                    adminOidcSecurityContainer.state
+                      .isSameEmailTreatedAsIdenticalUser || false
+                  }
+                  onChange={() => {
+                    adminOidcSecurityContainer.switchIsSameEmailTreatedAsIdenticalUser();
+                  }}
                 />
                 <label
                   className="form-label form-check-label"
                   htmlFor="bindByEmail-oidc"
-                  dangerouslySetInnerHTML={{ __html: t('security_settings.Treat email matching as identical') }}
+                  dangerouslySetInnerHTML={{
+                    __html: t(
+                      'security_settings.Treat email matching as identical',
+                    ),
+                  }}
                 />
               </div>
               <p className="form-text text-muted">
-                <small dangerouslySetInnerHTML={{ __html: t('security_settings.Treat email matching as identical_warn') }} />
+                <small
+                  dangerouslySetInnerHTML={{
+                    __html: t(
+                      'security_settings.Treat email matching as identical_warn',
+                    ),
+                  }}
+                />
               </p>
             </div>
           </div>
@@ -453,7 +696,9 @@ const OidcSecurityManagementContents = (props: Props) => {
               <button
                 type="submit"
                 className="btn btn-primary"
-                disabled={adminOidcSecurityContainer.state.retrieveError != null}
+                disabled={
+                  adminOidcSecurityContainer.state.retrieveError != null
+                }
               >
                 {t('Update')}
               </button>
@@ -462,30 +707,39 @@ const OidcSecurityManagementContents = (props: Props) => {
         </form>
       )}
 
-
       <hr />
 
       <div style={{ minHeight: '300px' }}>
         <h4>
-          <span className="material-symbols-outlined" aria-hidden="true">help</span>
-          <a href="#collapseHelpForOidcOauth" data-bs-toggle="collapse"> {t('security_settings.OAuth.how_to.oidc')}</a>
+          <span className="material-symbols-outlined" aria-hidden="true">
+            help
+          </span>
+          <a href="#collapseHelpForOidcOauth" data-bs-toggle="collapse">
+            {' '}
+            {t('security_settings.OAuth.how_to.oidc')}
+          </a>
         </h4>
         <div className=" card custom-card bg-body-tertiary">
           <ol id="collapseHelpForOidcOauth" className="collapse mb-0">
             <li>{t('security_settings.OAuth.OIDC.register_1')}</li>
-            <li dangerouslySetInnerHTML={{ __html: t('security_settings.OAuth.OIDC.register_2', { url: oidcCallbackUrl }) }} />
+            <li
+              dangerouslySetInnerHTML={{
+                __html: t('security_settings.OAuth.OIDC.register_2', {
+                  url: oidcCallbackUrl,
+                }),
+              }}
+            />
             <li>{t('security_settings.OAuth.OIDC.register_3')}</li>
           </ol>
         </div>
       </div>
-
     </>
   );
 };
 
-const OidcSecurityManagementContentsWrapper = withUnstatedContainers(OidcSecurityManagementContents, [
-  AdminGeneralSecurityContainer,
-  AdminOidcSecurityContainer,
-]);
+const OidcSecurityManagementContentsWrapper = withUnstatedContainers(
+  OidcSecurityManagementContents,
+  [AdminGeneralSecurityContainer, AdminOidcSecurityContainer],
+);
 
 export default OidcSecurityManagementContentsWrapper;

+ 9 - 10
apps/app/src/client/components/Admin/Security/SamlSecuritySetting.jsx

@@ -1,5 +1,4 @@
-import React, { useEffect, useCallback } from 'react';
-
+import React, { useCallback, useEffect } from 'react';
 import PropTypes from 'prop-types';
 
 import AdminSamlSecurityContainer from '~/client/services/AdminSamlSecurityContainer';
@@ -7,17 +6,15 @@ import { toastError } from '~/client/util/toastr';
 import { toArrayIfNot } from '~/utils/array-utils';
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
-
 import SamlSecuritySettingContents from './SamlSecuritySettingContents';
 
 const SamlSecurityManagement = (props) => {
   const { adminSamlSecurityContainer } = props;
 
-  const fetchSamlSecuritySettingsData = useCallback(async() => {
+  const fetchSamlSecuritySettingsData = useCallback(async () => {
     try {
       await adminSamlSecurityContainer.retrieveSecurityData();
-    }
-    catch (err) {
+    } catch (err) {
       const errs = toArrayIfNot(err);
       toastError(errs);
     }
@@ -31,11 +28,13 @@ const SamlSecurityManagement = (props) => {
 };
 
 SamlSecurityManagement.propTypes = {
-  adminSamlSecurityContainer: PropTypes.instanceOf(AdminSamlSecurityContainer).isRequired,
+  adminSamlSecurityContainer: PropTypes.instanceOf(AdminSamlSecurityContainer)
+    .isRequired,
 };
 
-const SamlSecurityManagementWithUnstatedContainer = withUnstatedContainers(SamlSecurityManagement, [
-  AdminSamlSecurityContainer,
-]);
+const SamlSecurityManagementWithUnstatedContainer = withUnstatedContainers(
+  SamlSecurityManagement,
+  [AdminSamlSecurityContainer],
+);
 
 export default SamlSecurityManagementWithUnstatedContainer;

+ 332 - 114
apps/app/src/client/components/Admin/Security/SamlSecuritySettingContents.tsx

@@ -1,16 +1,14 @@
 /* eslint-disable react/no-danger */
-import React, { useState, useEffect, useCallback } from 'react';
-
+import React, { useCallback, useEffect, useState } from 'react';
 import { pathUtils } from '@growi/core/dist/utils';
 import { useTranslation } from 'next-i18next';
 import { useForm } from 'react-hook-form';
 import { Collapse } from 'reactstrap';
 import urljoin from 'url-join';
 
-
 import AdminGeneralSecurityContainer from '~/client/services/AdminGeneralSecurityContainer';
 import AdminSamlSecurityContainer from '~/client/services/AdminSamlSecurityContainer';
-import { toastSuccess, toastError } from '~/client/util/toastr';
+import { toastError, toastSuccess } from '~/client/util/toastr';
 import { useSiteUrlWithEmptyValueWarn } from '~/states/global';
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
@@ -21,9 +19,7 @@ type Props = {
 };
 
 const SamlSecurityManagementContents = (props: Props) => {
-  const {
-    adminGeneralSecurityContainer, adminSamlSecurityContainer,
-  } = props;
+  const { adminGeneralSecurityContainer, adminSamlSecurityContainer } = props;
 
   const { t } = useTranslation('admin');
   const siteUrl = useSiteUrlWithEmptyValueWarn();
@@ -37,51 +33,60 @@ const SamlSecurityManagementContents = (props: Props) => {
       samlIssuer: adminSamlSecurityContainer.state.samlIssuer || '',
       samlCert: adminSamlSecurityContainer.state.samlCert || '',
       samlAttrMapId: adminSamlSecurityContainer.state.samlAttrMapId || '',
-      samlAttrMapUsername: adminSamlSecurityContainer.state.samlAttrMapUsername || '',
+      samlAttrMapUsername:
+        adminSamlSecurityContainer.state.samlAttrMapUsername || '',
       samlAttrMapMail: adminSamlSecurityContainer.state.samlAttrMapMail || '',
-      samlAttrMapFirstName: adminSamlSecurityContainer.state.samlAttrMapFirstName || '',
-      samlAttrMapLastName: adminSamlSecurityContainer.state.samlAttrMapLastName || '',
+      samlAttrMapFirstName:
+        adminSamlSecurityContainer.state.samlAttrMapFirstName || '',
+      samlAttrMapLastName:
+        adminSamlSecurityContainer.state.samlAttrMapLastName || '',
       samlABLCRule: adminSamlSecurityContainer.state.samlABLCRule || '',
     });
   }, [adminSamlSecurityContainer.state, reset]);
 
-  const onSubmit = useCallback(async(data) => {
-    try {
-      await adminSamlSecurityContainer.updateSamlSetting({
-        samlEntryPoint: data.samlEntryPoint,
-        samlIssuer: data.samlIssuer,
-        samlCert: data.samlCert,
-        samlAttrMapId: data.samlAttrMapId,
-        samlAttrMapUsername: data.samlAttrMapUsername,
-        samlAttrMapMail: data.samlAttrMapMail,
-        samlAttrMapFirstName: data.samlAttrMapFirstName,
-        samlAttrMapLastName: data.samlAttrMapLastName,
-        isSameUsernameTreatedAsIdenticalUser: adminSamlSecurityContainer.state.isSameUsernameTreatedAsIdenticalUser,
-        isSameEmailTreatedAsIdenticalUser: adminSamlSecurityContainer.state.isSameEmailTreatedAsIdenticalUser,
-        samlABLCRule: data.samlABLCRule,
-      });
-      toastSuccess(t('security_settings.SAML.updated_saml'));
-    }
-    catch (err) {
-      toastError(err);
-    }
-
-    try {
-      await adminGeneralSecurityContainer.retrieveSetupStratedies();
-    }
-    catch (err) {
-      toastError(err);
-    }
-  }, [adminSamlSecurityContainer, adminGeneralSecurityContainer, t]);
+  const onSubmit = useCallback(
+    async (data) => {
+      try {
+        await adminSamlSecurityContainer.updateSamlSetting({
+          samlEntryPoint: data.samlEntryPoint,
+          samlIssuer: data.samlIssuer,
+          samlCert: data.samlCert,
+          samlAttrMapId: data.samlAttrMapId,
+          samlAttrMapUsername: data.samlAttrMapUsername,
+          samlAttrMapMail: data.samlAttrMapMail,
+          samlAttrMapFirstName: data.samlAttrMapFirstName,
+          samlAttrMapLastName: data.samlAttrMapLastName,
+          isSameUsernameTreatedAsIdenticalUser:
+            adminSamlSecurityContainer.state
+              .isSameUsernameTreatedAsIdenticalUser,
+          isSameEmailTreatedAsIdenticalUser:
+            adminSamlSecurityContainer.state.isSameEmailTreatedAsIdenticalUser,
+          samlABLCRule: data.samlABLCRule,
+        });
+        toastSuccess(t('security_settings.SAML.updated_saml'));
+      } catch (err) {
+        toastError(err);
+      }
+
+      try {
+        await adminGeneralSecurityContainer.retrieveSetupStratedies();
+      } catch (err) {
+        toastError(err);
+      }
+    },
+    [adminSamlSecurityContainer, adminGeneralSecurityContainer, t],
+  );
 
   const { useOnlyEnvVars } = adminSamlSecurityContainer.state;
   const { isSamlEnabled } = adminGeneralSecurityContainer.state;
 
-  const samlCallbackUrl = urljoin(pathUtils.removeTrailingSlash(siteUrl), '/passport/saml/callback');
+  const samlCallbackUrl = urljoin(
+    pathUtils.removeTrailingSlash(siteUrl),
+    '/passport/saml/callback',
+  );
 
   return (
     <React.Fragment>
-
       <h2 className="alert-anchor border-bottom">
         {t('security_settings.SAML.name')}
       </h2>
@@ -89,7 +94,11 @@ const SamlSecurityManagementContents = (props: Props) => {
       {useOnlyEnvVars && (
         <p
           className="alert alert-info"
-          dangerouslySetInnerHTML={{ __html: t('security_settings.SAML.note for the only env option', { env: 'SAML_USES_ONLY_ENV_VARS_FOR_SOME_OPTIONS' }) }}
+          dangerouslySetInnerHTML={{
+            __html: t('security_settings.SAML.note for the only env option', {
+              env: 'SAML_USES_ONLY_ENV_VARS_FOR_SOME_OPTIONS',
+            }),
+          }}
         />
       )}
 
@@ -101,20 +110,33 @@ const SamlSecurityManagementContents = (props: Props) => {
               className="form-check-input"
               type="checkbox"
               checked={adminGeneralSecurityContainer.state.isSamlEnabled}
-              onChange={() => { adminGeneralSecurityContainer.switchIsSamlEnabled() }}
+              onChange={() => {
+                adminGeneralSecurityContainer.switchIsSamlEnabled();
+              }}
               disabled={adminSamlSecurityContainer.state.useOnlyEnvVars}
             />
-            <label className="form-label form-check-label" htmlFor="isSamlEnabled">
+            <label
+              className="form-label form-check-label"
+              htmlFor="isSamlEnabled"
+            >
               {t('security_settings.SAML.enable_saml')}
             </label>
           </div>
-          {(!adminGeneralSecurityContainer.state.setupStrategies.includes('saml') && isSamlEnabled)
-              && <div className="badge text-bg-warning">{t('security_settings.setup_is_not_yet_complete')}</div>}
+          {!adminGeneralSecurityContainer.state.setupStrategies.includes(
+            'saml',
+          ) &&
+            isSamlEnabled && (
+              <div className="badge text-bg-warning">
+                {t('security_settings.setup_is_not_yet_complete')}
+              </div>
+            )}
         </div>
       </div>
 
       <div className="row mb-5">
-        <label className="text-start text-md-end col-md-3 col-form-label">{t('security_settings.callback_URL')}</label>
+        <label className="text-start text-md-end col-md-3 col-form-label">
+          {t('security_settings.callback_URL')}
+        </label>
         <div className="col-md-6">
           <input
             className="form-control"
@@ -122,13 +144,22 @@ const SamlSecurityManagementContents = (props: Props) => {
             defaultValue={samlCallbackUrl}
             readOnly
           />
-          <p className="form-text text-muted small">{t('security_settings.desc_of_callback_URL', { AuthName: 'SAML Identity' })}</p>
+          <p className="form-text text-muted small">
+            {t('security_settings.desc_of_callback_URL', {
+              AuthName: 'SAML Identity',
+            })}
+          </p>
           {(siteUrl == null || siteUrl === '') && (
             <div className="alert alert-danger">
               <span className="material-symbols-outlined">error</span>
               <span
                 // eslint-disable-next-line max-len
-                dangerouslySetInnerHTML={{ __html: t('alert.siteUrl_is_not_set', { link: `<a href="/admin/app">${t('headers.app_settings', { ns: 'commons' })}<span class="material-symbols-outlined">login</span></a>`, ns: 'commons' }) }}
+                dangerouslySetInnerHTML={{
+                  __html: t('alert.siteUrl_is_not_set', {
+                    link: `<a href="/admin/app">${t('headers.app_settings', { ns: 'commons' })}<span class="material-symbols-outlined">login</span></a>`,
+                    ns: 'commons',
+                  }),
+                }}
               />
             </div>
           )}
@@ -137,32 +168,44 @@ const SamlSecurityManagementContents = (props: Props) => {
 
       {isSamlEnabled && (
         <form onSubmit={handleSubmit(onSubmit)}>
-
-          {(adminSamlSecurityContainer.state.missingMandatoryConfigKeys.length !== 0) && (
+          {adminSamlSecurityContainer.state.missingMandatoryConfigKeys
+            .length !== 0 && (
             <div className="alert alert-danger">
               {t('security_settings.missing mandatory configs')}
               <ul className="mb-0">
-                {adminSamlSecurityContainer.state.missingMandatoryConfigKeys.map((configKey) => {
-                  const key = configKey.replace('security:passport-saml:', '');
-                  return <li key={configKey}>{t(`security_settings.form_item_name.${key}`)}</li>;
-                })}
+                {adminSamlSecurityContainer.state.missingMandatoryConfigKeys.map(
+                  (configKey) => {
+                    const key = configKey.replace(
+                      'security:passport-saml:',
+                      '',
+                    );
+                    return (
+                      <li key={configKey}>
+                        {t(`security_settings.form_item_name.${key}`)}
+                      </li>
+                    );
+                  },
+                )}
               </ul>
             </div>
           )}
 
+          <h3 className="alert-anchor border-bottom mb-3">Basic Settings</h3>
 
-          <h3 className="alert-anchor border-bottom mb-3">
-            Basic Settings
-          </h3>
-
-          <table className={`table settings-table ${useOnlyEnvVars && 'use-only-env-vars'}`}>
+          <table
+            className={`table settings-table ${useOnlyEnvVars && 'use-only-env-vars'}`}
+          >
             <colgroup>
               <col className="item-name" />
               <col className="from-db" />
               <col className="from-env-vars" />
             </colgroup>
             <thead>
-              <tr><th></th><th>Database</th><th>Environment variables</th></tr>
+              <tr>
+                <th></th>
+                <th>Database</th>
+                <th>Environment variables</th>
+              </tr>
             </thead>
             <tbody>
               <tr>
@@ -183,7 +226,14 @@ const SamlSecurityManagementContents = (props: Props) => {
                     readOnly
                   />
                   <p className="form-text text-muted">
-                    <small dangerouslySetInnerHTML={{ __html: t('security_settings.SAML.Use env var if empty', { env: 'SAML_ENTRY_POINT' }) }} />
+                    <small
+                      dangerouslySetInnerHTML={{
+                        __html: t(
+                          'security_settings.SAML.Use env var if empty',
+                          { env: 'SAML_ENTRY_POINT' },
+                        ),
+                      }}
+                    />
                   </p>
                 </td>
               </tr>
@@ -205,7 +255,14 @@ const SamlSecurityManagementContents = (props: Props) => {
                     readOnly
                   />
                   <p className="form-text text-muted">
-                    <small dangerouslySetInnerHTML={{ __html: t('security_settings.SAML.Use env var if empty', { env: 'SAML_ISSUER' }) }} />
+                    <small
+                      dangerouslySetInnerHTML={{
+                        __html: t(
+                          'security_settings.SAML.Use env var if empty',
+                          { env: 'SAML_ISSUER' },
+                        ),
+                      }}
+                    />
                   </p>
                 </td>
               </tr>
@@ -219,14 +276,13 @@ const SamlSecurityManagementContents = (props: Props) => {
                     {...register('samlCert')}
                   />
                   <p>
-                    <small>
-                      {t('security_settings.SAML.cert_detail')}
-                    </small>
+                    <small>{t('security_settings.SAML.cert_detail')}</small>
                   </p>
                   <div>
                     <small>
                       e.g.
-                      <pre className="card custom-card">{`-----BEGIN CERTIFICATE-----
+                      <pre className="card custom-card">
+                        {`-----BEGIN CERTIFICATE-----
 MIICBzCCAXACCQD4US7+0A/b/zANBgkqhkiG9w0BAQsFADBIMQswCQYDVQQGEwJK
 UDEOMAwGA1UECAwFVG9reW8xFTATBgNVBAoMDFdFU0VFSywgSW5jLjESMBAGA1UE
 ...
@@ -246,7 +302,14 @@ pWVdnzS1VCO8fKsJ7YYIr+JmHvseph3kFUOI5RqkCcMZlKUv83aUThsTHw==
                     value={adminSamlSecurityContainer.state.envCert || ''}
                   />
                   <p className="form-text text-muted">
-                    <small dangerouslySetInnerHTML={{ __html: t('security_settings.SAML.Use env var if empty', { env: 'SAML_CERT' }) }} />
+                    <small
+                      dangerouslySetInnerHTML={{
+                        __html: t(
+                          'security_settings.SAML.Use env var if empty',
+                          { env: 'SAML_CERT' },
+                        ),
+                      }}
+                    />
                   </p>
                 </td>
               </tr>
@@ -264,7 +327,11 @@ pWVdnzS1VCO8fKsJ7YYIr+JmHvseph3kFUOI5RqkCcMZlKUv83aUThsTHw==
               <col className="from-env-vars" />
             </colgroup>
             <thead>
-              <tr><th></th><th>Database</th><th>Environment variables</th></tr>
+              <tr>
+                <th></th>
+                <th>Database</th>
+                <th>Environment variables</th>
+              </tr>
             </thead>
             <tbody>
               <tr>
@@ -276,9 +343,7 @@ pWVdnzS1VCO8fKsJ7YYIr+JmHvseph3kFUOI5RqkCcMZlKUv83aUThsTHw==
                     {...register('samlAttrMapId')}
                   />
                   <p className="form-text text-muted">
-                    <small>
-                      {t('security_settings.SAML.id_detail')}
-                    </small>
+                    <small>{t('security_settings.SAML.id_detail')}</small>
                   </p>
                 </td>
                 <td>
@@ -289,7 +354,14 @@ pWVdnzS1VCO8fKsJ7YYIr+JmHvseph3kFUOI5RqkCcMZlKUv83aUThsTHw==
                     readOnly
                   />
                   <p className="form-text text-muted">
-                    <small dangerouslySetInnerHTML={{ __html: t('security_settings.SAML.Use env var if empty', { env: 'SAML_ATTR_MAPPING_ID' }) }} />
+                    <small
+                      dangerouslySetInnerHTML={{
+                        __html: t(
+                          'security_settings.SAML.Use env var if empty',
+                          { env: 'SAML_ATTR_MAPPING_ID' },
+                        ),
+                      }}
+                    />
                   </p>
                 </td>
               </tr>
@@ -302,18 +374,31 @@ pWVdnzS1VCO8fKsJ7YYIr+JmHvseph3kFUOI5RqkCcMZlKUv83aUThsTHw==
                     {...register('samlAttrMapUsername')}
                   />
                   <p className="form-text text-muted">
-                    <small dangerouslySetInnerHTML={{ __html: t('security_settings.SAML.username_detail') }} />
+                    <small
+                      dangerouslySetInnerHTML={{
+                        __html: t('security_settings.SAML.username_detail'),
+                      }}
+                    />
                   </p>
                 </td>
                 <td>
                   <input
                     className="form-control"
                     type="text"
-                    value={adminSamlSecurityContainer.state.envAttrMapUsername || ''}
+                    value={
+                      adminSamlSecurityContainer.state.envAttrMapUsername || ''
+                    }
                     readOnly
                   />
                   <p className="form-text text-muted">
-                    <small dangerouslySetInnerHTML={{ __html: t('security_settings.SAML.Use env var if empty', { env: 'SAML_ATTR_MAPPING_USERNAME' }) }} />
+                    <small
+                      dangerouslySetInnerHTML={{
+                        __html: t(
+                          'security_settings.SAML.Use env var if empty',
+                          { env: 'SAML_ATTR_MAPPING_USERNAME' },
+                        ),
+                      }}
+                    />
                   </p>
                 </td>
               </tr>
@@ -326,23 +411,40 @@ pWVdnzS1VCO8fKsJ7YYIr+JmHvseph3kFUOI5RqkCcMZlKUv83aUThsTHw==
                     {...register('samlAttrMapMail')}
                   />
                   <p className="form-text text-muted">
-                    <small dangerouslySetInnerHTML={{ __html: t('security_settings.SAML.mapping_detail', { target: 'Email' }) }} />
+                    <small
+                      dangerouslySetInnerHTML={{
+                        __html: t('security_settings.SAML.mapping_detail', {
+                          target: 'Email',
+                        }),
+                      }}
+                    />
                   </p>
                 </td>
                 <td>
                   <input
                     className="form-control"
                     type="text"
-                    value={adminSamlSecurityContainer.state.envAttrMapMail || ''}
+                    value={
+                      adminSamlSecurityContainer.state.envAttrMapMail || ''
+                    }
                     readOnly
                   />
                   <p className="form-text text-muted">
-                    <small dangerouslySetInnerHTML={{ __html: t('security_settings.SAML.Use env var if empty', { env: 'SAML_ATTR_MAPPING_MAIL' }) }} />
+                    <small
+                      dangerouslySetInnerHTML={{
+                        __html: t(
+                          'security_settings.SAML.Use env var if empty',
+                          { env: 'SAML_ATTR_MAPPING_MAIL' },
+                        ),
+                      }}
+                    />
                   </p>
                 </td>
               </tr>
               <tr>
-                <th>{t('security_settings.form_item_name.attrMapFirstName')}</th>
+                <th>
+                  {t('security_settings.form_item_name.attrMapFirstName')}
+                </th>
                 <td>
                   <input
                     className="form-control"
@@ -351,21 +453,45 @@ pWVdnzS1VCO8fKsJ7YYIr+JmHvseph3kFUOI5RqkCcMZlKUv83aUThsTHw==
                   />
                   <p className="form-text text-muted">
                     {/* eslint-disable-next-line max-len */}
-                    <small dangerouslySetInnerHTML={{ __html: t('security_settings.SAML.mapping_detail', { target: t('security_settings.form_item_name.attrMapFirstName') }) }} />
+                    <small
+                      dangerouslySetInnerHTML={{
+                        __html: t('security_settings.SAML.mapping_detail', {
+                          target: t(
+                            'security_settings.form_item_name.attrMapFirstName',
+                          ),
+                        }),
+                      }}
+                    />
                   </p>
                 </td>
                 <td>
                   <input
                     className="form-control"
                     type="text"
-                    value={adminSamlSecurityContainer.state.envAttrMapFirstName || ''}
+                    value={
+                      adminSamlSecurityContainer.state.envAttrMapFirstName || ''
+                    }
                     readOnly
                   />
                   <p className="form-text text-muted">
                     <small>
-                      <span dangerouslySetInnerHTML={{ __html: t('security_settings.SAML.Use env var if empty', { env: 'SAML_ATTR_MAPPING_FIRST_NAME' }) }} />
+                      <span
+                        dangerouslySetInnerHTML={{
+                          __html: t(
+                            'security_settings.SAML.Use env var if empty',
+                            { env: 'SAML_ATTR_MAPPING_FIRST_NAME' },
+                          ),
+                        }}
+                      />
                       <br />
-                      <span dangerouslySetInnerHTML={{ __html: t('security_settings.Use default if both are empty', { target: 'firstName' }) }} />
+                      <span
+                        dangerouslySetInnerHTML={{
+                          __html: t(
+                            'security_settings.Use default if both are empty',
+                            { target: 'firstName' },
+                          ),
+                        }}
+                      />
                     </small>
                   </p>
                 </td>
@@ -380,21 +506,45 @@ pWVdnzS1VCO8fKsJ7YYIr+JmHvseph3kFUOI5RqkCcMZlKUv83aUThsTHw==
                   />
                   <p className="form-text text-muted">
                     {/* eslint-disable-next-line max-len */}
-                    <small dangerouslySetInnerHTML={{ __html: t('security_settings.SAML.mapping_detail', { target: t('security_settings.form_item_name.attrMapLastName') }) }} />
+                    <small
+                      dangerouslySetInnerHTML={{
+                        __html: t('security_settings.SAML.mapping_detail', {
+                          target: t(
+                            'security_settings.form_item_name.attrMapLastName',
+                          ),
+                        }),
+                      }}
+                    />
                   </p>
                 </td>
                 <td>
                   <input
                     className="form-control"
                     type="text"
-                    value={adminSamlSecurityContainer.state.envAttrMapLastName || ''}
+                    value={
+                      adminSamlSecurityContainer.state.envAttrMapLastName || ''
+                    }
                     readOnly
                   />
                   <p className="form-text text-muted">
                     <small>
-                      <span dangerouslySetInnerHTML={{ __html: t('security_settings.SAML.Use env var if empty', { env: 'SAML_ATTR_MAPPING_LAST_NAME' }) }} />
+                      <span
+                        dangerouslySetInnerHTML={{
+                          __html: t(
+                            'security_settings.SAML.Use env var if empty',
+                            { env: 'SAML_ATTR_MAPPING_LAST_NAME' },
+                          ),
+                        }}
+                      />
                       <br />
-                      <span dangerouslySetInnerHTML={{ __html: t('security_settings.Use default if both are empty', { target: 'lastName' }) }} />
+                      <span
+                        dangerouslySetInnerHTML={{
+                          __html: t(
+                            'security_settings.Use default if both are empty',
+                            { target: 'lastName' },
+                          ),
+                        }}
+                      />
                     </small>
                   </p>
                 </td>
@@ -412,17 +562,32 @@ pWVdnzS1VCO8fKsJ7YYIr+JmHvseph3kFUOI5RqkCcMZlKUv83aUThsTHw==
                 id="bindByUserName-SAML"
                 className="form-check-input"
                 type="checkbox"
-                checked={adminSamlSecurityContainer.state.isSameUsernameTreatedAsIdenticalUser || false}
-                onChange={() => { adminSamlSecurityContainer.switchIsSameUsernameTreatedAsIdenticalUser() }}
+                checked={
+                  adminSamlSecurityContainer.state
+                    .isSameUsernameTreatedAsIdenticalUser || false
+                }
+                onChange={() => {
+                  adminSamlSecurityContainer.switchIsSameUsernameTreatedAsIdenticalUser();
+                }}
               />
               <label
                 className="form-label form-check-label"
                 htmlFor="bindByUserName-SAML"
-                dangerouslySetInnerHTML={{ __html: t('security_settings.Treat username matching as identical') }}
+                dangerouslySetInnerHTML={{
+                  __html: t(
+                    'security_settings.Treat username matching as identical',
+                  ),
+                }}
               />
             </div>
             <p className="form-text text-muted">
-              <small dangerouslySetInnerHTML={{ __html: t('security_settings.Treat username matching as identical_warn') }} />
+              <small
+                dangerouslySetInnerHTML={{
+                  __html: t(
+                    'security_settings.Treat username matching as identical_warn',
+                  ),
+                }}
+              />
             </p>
           </div>
 
@@ -432,17 +597,32 @@ pWVdnzS1VCO8fKsJ7YYIr+JmHvseph3kFUOI5RqkCcMZlKUv83aUThsTHw==
                 id="bindByEmail-SAML"
                 className="form-check-input"
                 type="checkbox"
-                checked={adminSamlSecurityContainer.state.isSameEmailTreatedAsIdenticalUser || false}
-                onChange={() => { adminSamlSecurityContainer.switchIsSameEmailTreatedAsIdenticalUser() }}
+                checked={
+                  adminSamlSecurityContainer.state
+                    .isSameEmailTreatedAsIdenticalUser || false
+                }
+                onChange={() => {
+                  adminSamlSecurityContainer.switchIsSameEmailTreatedAsIdenticalUser();
+                }}
               />
               <label
                 className="form-label form-check-label"
                 htmlFor="bindByEmail-SAML"
-                dangerouslySetInnerHTML={{ __html: t('security_settings.Treat email matching as identical') }}
+                dangerouslySetInnerHTML={{
+                  __html: t(
+                    'security_settings.Treat email matching as identical',
+                  ),
+                }}
               />
             </div>
             <p className="form-text text-muted">
-              <small dangerouslySetInnerHTML={{ __html: t('security_settings.Treat email matching as identical_warn') }} />
+              <small
+                dangerouslySetInnerHTML={{
+                  __html: t(
+                    'security_settings.Treat email matching as identical_warn',
+                  ),
+                }}
+              />
             </p>
           </div>
 
@@ -451,7 +631,13 @@ pWVdnzS1VCO8fKsJ7YYIr+JmHvseph3kFUOI5RqkCcMZlKUv83aUThsTHw==
           </h3>
 
           <p className="form-text text-muted">
-            <small dangerouslySetInnerHTML={{ __html: t('security_settings.SAML.attr_based_login_control_detail') }} />
+            <small
+              dangerouslySetInnerHTML={{
+                __html: t(
+                  'security_settings.SAML.attr_based_login_control_detail',
+                ),
+              }}
+            />
           </p>
 
           <table className="table settings-table">
@@ -461,13 +647,15 @@ pWVdnzS1VCO8fKsJ7YYIr+JmHvseph3kFUOI5RqkCcMZlKUv83aUThsTHw==
               <col className="from-env-vars" />
             </colgroup>
             <thead>
-              <tr><th></th><th>Database</th><th>Environment variables</th></tr>
+              <tr>
+                <th></th>
+                <th>Database</th>
+                <th>Environment variables</th>
+              </tr>
             </thead>
             <tbody>
               <tr>
-                <th>
-                  { t('security_settings.form_item_name.ABLCRule') }
-                </th>
+                <th>{t('security_settings.form_item_name.ABLCRule')}</th>
                 <td>
                   <textarea
                     className="form-control"
@@ -481,8 +669,12 @@ pWVdnzS1VCO8fKsJ7YYIr+JmHvseph3kFUOI5RqkCcMZlKUv83aUThsTHw==
                         target="_blank"
                         rel="noreferer noreferrer"
                       >
-                        Apache Lucene - Query Parser Syntax <span className="growi-custom-icons">external_link</span>
-                      </a>.
+                        Apache Lucene - Query Parser Syntax{' '}
+                        <span className="growi-custom-icons">
+                          external_link
+                        </span>
+                      </a>
+                      .
                     </p>
                     <div className="accordion" id="accordionId">
                       <div className="accordion-item p-1">
@@ -496,14 +688,33 @@ pWVdnzS1VCO8fKsJ7YYIr+JmHvseph3kFUOI5RqkCcMZlKUv83aUThsTHw==
                           >
                             <span className="material-symbols-outlined me-1">
                               {isHelpOpened ? 'expand_more' : 'chevron_right'}
-                            </span> Show more...
+                            </span>{' '}
+                            Show more...
                           </button>
                         </h2>
                         <Collapse isOpen={isHelpOpened}>
                           <div className="accordion-body">
-                            <p dangerouslySetInnerHTML={{ __html: t('security_settings.SAML.attr_based_login_control_rule_help') }} />
-                            <p dangerouslySetInnerHTML={{ __html: t('security_settings.SAML.attr_based_login_control_rule_example1') }} />
-                            <p dangerouslySetInnerHTML={{ __html: t('security_settings.SAML.attr_based_login_control_rule_example2') }} />
+                            <p
+                              dangerouslySetInnerHTML={{
+                                __html: t(
+                                  'security_settings.SAML.attr_based_login_control_rule_help',
+                                ),
+                              }}
+                            />
+                            <p
+                              dangerouslySetInnerHTML={{
+                                __html: t(
+                                  'security_settings.SAML.attr_based_login_control_rule_example1',
+                                ),
+                              }}
+                            />
+                            <p
+                              dangerouslySetInnerHTML={{
+                                __html: t(
+                                  'security_settings.SAML.attr_based_login_control_rule_example2',
+                                ),
+                              }}
+                            />
                           </div>
                         </Collapse>
                       </div>
@@ -517,7 +728,14 @@ pWVdnzS1VCO8fKsJ7YYIr+JmHvseph3kFUOI5RqkCcMZlKUv83aUThsTHw==
                     readOnly
                   />
                   <p className="form-text text-muted">
-                    <small dangerouslySetInnerHTML={{ __html: t('security_settings.SAML.Use env var if empty', { env: 'SAML_ABLC_RULE' }) }} />
+                    <small
+                      dangerouslySetInnerHTML={{
+                        __html: t(
+                          'security_settings.SAML.Use env var if empty',
+                          { env: 'SAML_ABLC_RULE' },
+                        ),
+                      }}
+                    />
                   </p>
                 </td>
               </tr>
@@ -529,23 +747,23 @@ pWVdnzS1VCO8fKsJ7YYIr+JmHvseph3kFUOI5RqkCcMZlKUv83aUThsTHw==
               <button
                 type="submit"
                 className="btn btn-primary"
-                disabled={adminSamlSecurityContainer.state.retrieveError != null}
+                disabled={
+                  adminSamlSecurityContainer.state.retrieveError != null
+                }
               >
                 {t('Update')}
               </button>
             </div>
           </div>
-
         </form>
       )}
-
     </React.Fragment>
   );
 };
 
-const SamlSecurityManagementContentsWrapper = withUnstatedContainers(SamlSecurityManagementContents, [
-  AdminGeneralSecurityContainer,
-  AdminSamlSecurityContainer,
-]);
+const SamlSecurityManagementContentsWrapper = withUnstatedContainers(
+  SamlSecurityManagementContents,
+  [AdminGeneralSecurityContainer, AdminSamlSecurityContainer],
+);
 
 export default SamlSecurityManagementContentsWrapper;

+ 9 - 8
apps/app/src/client/components/Admin/Security/SecurityManagement.tsx

@@ -1,25 +1,23 @@
-import React, { useEffect, useCallback } from 'react';
+import React, { useCallback, useEffect } from 'react';
 
 import AdminGeneralSecurityContainer from '~/client/services/AdminGeneralSecurityContainer';
 import { toastError } from '~/client/util/toastr';
 import { toArrayIfNot } from '~/utils/array-utils';
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
-
 import SecurityManagementContents from './SecurityManagementContents';
 
 type Props = {
-  adminGeneralSecurityContainer: AdminGeneralSecurityContainer
-}
+  adminGeneralSecurityContainer: AdminGeneralSecurityContainer;
+};
 
 const SecurityManagement = (props: Props) => {
   const { adminGeneralSecurityContainer } = props;
 
-  const fetchGeneralSecuritySettingsData = useCallback(async() => {
+  const fetchGeneralSecuritySettingsData = useCallback(async () => {
     try {
       await adminGeneralSecurityContainer.retrieveSecurityData();
-    }
-    catch (err) {
+    } catch (err) {
       const errs = toArrayIfNot(err);
       toastError(errs);
     }
@@ -32,6 +30,9 @@ const SecurityManagement = (props: Props) => {
   return <SecurityManagementContents />;
 };
 
-const SecurityManagementWithUnstatedContainer = withUnstatedContainers(SecurityManagement, [AdminGeneralSecurityContainer]);
+const SecurityManagementWithUnstatedContainer = withUnstatedContainers(
+  SecurityManagement,
+  [AdminGeneralSecurityContainer],
+);
 
 export default SecurityManagementWithUnstatedContainer;

+ 27 - 15
apps/app/src/client/components/Admin/Security/SecurityManagementContents.jsx

@@ -1,11 +1,9 @@
 import React, { useMemo, useState } from 'react';
-
-import { useTranslation } from 'next-i18next';
 import Link from 'next/link';
+import { useTranslation } from 'next-i18next';
 import { TabContent, TabPane } from 'reactstrap';
 
 import CustomNav from '../../CustomNavigation/CustomNav';
-
 import GitHubSecuritySetting from './GitHubSecuritySetting';
 import GoogleSecuritySetting from './GoogleSecuritySetting';
 import LdapSecuritySetting from './LdapSecuritySetting';
@@ -19,7 +17,9 @@ const SecurityManagementContents = () => {
   const { t } = useTranslation('admin');
 
   const [activeTab, setActiveTab] = useState('passport_local');
-  const [activeComponents, setActiveComponents] = useState(new Set(['passport_local']));
+  const [activeComponents, setActiveComponents] = useState(
+    new Set(['passport_local']),
+  );
 
   const switchActiveTab = (selectedTab) => {
     setActiveTab(selectedTab);
@@ -33,7 +33,9 @@ const SecurityManagementContents = () => {
         i18n: 'ID/Pass',
       },
       passport_ldap: {
-        Icon: () => <span className="material-symbols-outlined">network_node</span>,
+        Icon: () => (
+          <span className="material-symbols-outlined">network_node</span>
+        ),
         i18n: 'LDAP',
       },
       passport_saml: {
@@ -45,17 +47,20 @@ const SecurityManagementContents = () => {
         i18n: 'OIDC',
       },
       passport_google: {
-        Icon: () => <span className="growi-custom-icons align-bottom">google</span>,
+        Icon: () => (
+          <span className="growi-custom-icons align-bottom">google</span>
+        ),
         i18n: 'Google',
       },
       passport_github: {
-        Icon: () => <span className="growi-custom-icons align-bottom">github</span>,
+        Icon: () => (
+          <span className="growi-custom-icons align-bottom">github</span>
+        ),
         i18n: 'GitHub',
       },
     };
   }, []);
 
-
   return (
     <div data-testid="admin-security">
       <div className="mb-5">
@@ -67,22 +72,26 @@ const SecurityManagementContents = () => {
         <ShareLinkSetting />
       </div>
 
-
       {/* XSS configuration link */}
       <div className="mb-5">
-        <h2 className="border-bottom pb-2">{t('security_settings.xss_prevent_setting')}</h2>
+        <h2 className="border-bottom pb-2">
+          {t('security_settings.xss_prevent_setting')}
+        </h2>
         <div className="mt-4">
           <Link
             href="/admin/markdown/#preventXSS"
             style={{ fontSize: 'large' }}
           >
-            <span className="material-symbols-outlined me-1">login</span> {t('security_settings.xss_prevent_setting_link')}
+            <span className="material-symbols-outlined me-1">login</span>{' '}
+            {t('security_settings.xss_prevent_setting_link')}
           </Link>
         </div>
       </div>
 
       <div className="auth-mechanism-configurations">
-        <h2 className="border-bottom pb-2">{t('security_settings.Authentication mechanism settings')}</h2>
+        <h2 className="border-bottom pb-2">
+          {t('security_settings.Authentication mechanism settings')}
+        </h2>
         <CustomNav
           activeTab={activeTab}
           navTabMapping={navTabMapping}
@@ -104,16 +113,19 @@ const SecurityManagementContents = () => {
             {activeComponents.has('passport_oidc') && <OidcSecuritySetting />}
           </TabPane>
           <TabPane tabId="passport_google">
-            {activeComponents.has('passport_google') && <GoogleSecuritySetting />}
+            {activeComponents.has('passport_google') && (
+              <GoogleSecuritySetting />
+            )}
           </TabPane>
           <TabPane tabId="passport_github">
-            {activeComponents.has('passport_github') && <GitHubSecuritySetting />}
+            {activeComponents.has('passport_github') && (
+              <GitHubSecuritySetting />
+            )}
           </TabPane>
         </TabContent>
       </div>
     </div>
   );
-
 };
 
 export default SecurityManagementContents;

+ 19 - 6
apps/app/src/client/components/Admin/Security/SecuritySetting/CommentManageRightsSettings.tsx

@@ -1,4 +1,4 @@
-import React from 'react';
+import type React from 'react';
 
 import type AdminGeneralSecurityContainer from '~/client/services/AdminGeneralSecurityContainer';
 
@@ -7,7 +7,10 @@ type Props = {
   t: (key: string) => string;
 };
 
-export const CommentManageRightsSettings: React.FC<Props> = ({ adminGeneralSecurityContainer, t }) => {
+export const CommentManageRightsSettings: React.FC<Props> = ({
+  adminGeneralSecurityContainer,
+  t,
+}) => {
   const { isRomUserAllowedToComment } = adminGeneralSecurityContainer.state;
 
   return (
@@ -30,22 +33,32 @@ export const CommentManageRightsSettings: React.FC<Props> = ({ adminGeneralSecur
               aria-expanded="true"
             >
               <span className="float-start">
-                {isRomUserAllowedToComment === true && t('security_settings.read_only_users_comment.accept')}
-                {isRomUserAllowedToComment === false && t('security_settings.read_only_users_comment.deny')}
+                {isRomUserAllowedToComment === true &&
+                  t('security_settings.read_only_users_comment.accept')}
+                {isRomUserAllowedToComment === false &&
+                  t('security_settings.read_only_users_comment.deny')}
               </span>
             </button>
             <div className="dropdown-menu" aria-labelledby="dropdownMenuButton">
               <button
                 className="dropdown-item"
                 type="button"
-                onClick={() => { adminGeneralSecurityContainer.switchIsRomUserAllowedToComment(false) }}
+                onClick={() => {
+                  adminGeneralSecurityContainer.switchIsRomUserAllowedToComment(
+                    false,
+                  );
+                }}
               >
                 {t('security_settings.read_only_users_comment.deny')}
               </button>
               <button
                 className="dropdown-item"
                 type="button"
-                onClick={() => { adminGeneralSecurityContainer.switchIsRomUserAllowedToComment(true) }}
+                onClick={() => {
+                  adminGeneralSecurityContainer.switchIsRomUserAllowedToComment(
+                    true,
+                  );
+                }}
               >
                 {t('security_settings.read_only_users_comment.accept')}
               </button>

+ 17 - 6
apps/app/src/client/components/Admin/Security/SecuritySetting/PageAccessRightsSettings.tsx

@@ -1,5 +1,5 @@
 /* eslint-disable react/no-danger */
-import React from 'react';
+import type React from 'react';
 
 import type AdminGeneralSecurityContainer from '~/client/services/AdminGeneralSecurityContainer';
 
@@ -8,7 +8,10 @@ type Props = {
   t: (key: string, options?: Record<string, unknown>) => string;
 };
 
-export const PageAccessRightsSettings: React.FC<Props> = ({ adminGeneralSecurityContainer, t }) => {
+export const PageAccessRightsSettings: React.FC<Props> = ({
+  adminGeneralSecurityContainer,
+  t,
+}) => {
   const { currentRestrictGuestMode } = adminGeneralSecurityContainer.state;
 
   return (
@@ -31,22 +34,30 @@ export const PageAccessRightsSettings: React.FC<Props> = ({ adminGeneralSecurity
               aria-expanded="true"
             >
               <span className="float-start">
-                {currentRestrictGuestMode === 'Deny' && t('security_settings.guest_mode.deny')}
-                {currentRestrictGuestMode === 'Readonly' && t('security_settings.guest_mode.readonly')}
+                {currentRestrictGuestMode === 'Deny' &&
+                  t('security_settings.guest_mode.deny')}
+                {currentRestrictGuestMode === 'Readonly' &&
+                  t('security_settings.guest_mode.readonly')}
               </span>
             </button>
             <div className="dropdown-menu" aria-labelledby="dropdownMenuButton">
               <button
                 className="dropdown-item"
                 type="button"
-                onClick={() => { adminGeneralSecurityContainer.changeRestrictGuestMode('Deny') }}
+                onClick={() => {
+                  adminGeneralSecurityContainer.changeRestrictGuestMode('Deny');
+                }}
               >
                 {t('security_settings.guest_mode.deny')}
               </button>
               <button
                 className="dropdown-item"
                 type="button"
-                onClick={() => { adminGeneralSecurityContainer.changeRestrictGuestMode('Readonly') }}
+                onClick={() => {
+                  adminGeneralSecurityContainer.changeRestrictGuestMode(
+                    'Readonly',
+                  );
+                }}
               >
                 {t('security_settings.guest_mode.readonly')}
               </button>

+ 320 - 170
apps/app/src/client/components/Admin/Security/SecuritySetting/PageDeleteRightsSettings.tsx

@@ -1,20 +1,23 @@
-import React, { useCallback } from 'react';
-
+import type React from 'react';
+import { useCallback } from 'react';
 import { Collapse } from 'reactstrap';
 
 import type AdminGeneralSecurityContainer from '~/client/services/AdminGeneralSecurityContainer';
 import {
-  PageDeleteConfigValue,
   type IPageDeleteConfigValue,
   type IPageDeleteConfigValueToProcessValidation,
+  PageDeleteConfigValue,
 } from '~/interfaces/page-delete-config';
-import { validateDeleteConfigs, prepareDeleteConfigValuesForCalc } from '~/utils/page-delete-config';
+import {
+  prepareDeleteConfigValuesForCalc,
+  validateDeleteConfigs,
+} from '~/utils/page-delete-config';
 
 import {
   DeletionType,
   type DeletionTypeValue,
-  getDeletionTypeForT,
   getDeleteConfigValueForT,
+  getDeletionTypeForT,
   isRecursiveDeletion,
   isTypeDeletion,
 } from './types';
@@ -24,7 +27,10 @@ type Props = {
   t: (key: string) => string;
 };
 
-export const PageDeleteRightsSettings: React.FC<Props> = ({ adminGeneralSecurityContainer, t }) => {
+export const PageDeleteRightsSettings: React.FC<Props> = ({
+  adminGeneralSecurityContainer,
+  t,
+}) => {
   const {
     currentPageDeletionAuthority,
     currentPageCompleteDeletionAuthority,
@@ -32,189 +38,291 @@ export const PageDeleteRightsSettings: React.FC<Props> = ({ adminGeneralSecurity
     currentPageRecursiveCompleteDeletionAuthority,
   } = adminGeneralSecurityContainer.state;
 
-  const getRecursiveDeletionConfigState = useCallback((deletionType: DeletionTypeValue) => {
-    if (isTypeDeletion(deletionType)) {
+  const getRecursiveDeletionConfigState = useCallback(
+    (deletionType: DeletionTypeValue) => {
+      if (isTypeDeletion(deletionType)) {
+        return [
+          adminGeneralSecurityContainer.state
+            .currentPageRecursiveDeletionAuthority,
+          adminGeneralSecurityContainer.changePageRecursiveDeletionAuthority,
+        ] as const;
+      }
+
       return [
-        adminGeneralSecurityContainer.state.currentPageRecursiveDeletionAuthority,
-        adminGeneralSecurityContainer.changePageRecursiveDeletionAuthority,
+        adminGeneralSecurityContainer.state
+          .currentPageRecursiveCompleteDeletionAuthority,
+        adminGeneralSecurityContainer.changePageRecursiveCompleteDeletionAuthority,
       ] as const;
-    }
-
-    return [
-      adminGeneralSecurityContainer.state.currentPageRecursiveCompleteDeletionAuthority,
-      adminGeneralSecurityContainer.changePageRecursiveCompleteDeletionAuthority,
-    ] as const;
-  }, [adminGeneralSecurityContainer]);
+    },
+    [adminGeneralSecurityContainer],
+  );
 
-  const previousPageRecursiveAuthorityState = useCallback((deletionType: DeletionTypeValue) => {
-    return isTypeDeletion(deletionType)
-      ? adminGeneralSecurityContainer.state.previousPageRecursiveDeletionAuthority
-      : adminGeneralSecurityContainer.state.previousPageRecursiveCompleteDeletionAuthority;
-  }, [adminGeneralSecurityContainer]);
+  const previousPageRecursiveAuthorityState = useCallback(
+    (deletionType: DeletionTypeValue) => {
+      return isTypeDeletion(deletionType)
+        ? adminGeneralSecurityContainer.state
+            .previousPageRecursiveDeletionAuthority
+        : adminGeneralSecurityContainer.state
+            .previousPageRecursiveCompleteDeletionAuthority;
+    },
+    [adminGeneralSecurityContainer],
+  );
 
-  const setPagePreviousRecursiveAuthorityState = useCallback((deletionType: DeletionTypeValue, previousState: IPageDeleteConfigValue | null) => {
-    if (isTypeDeletion(deletionType)) {
-      adminGeneralSecurityContainer.changePreviousPageRecursiveDeletionAuthority(previousState);
-      return;
-    }
+  const setPagePreviousRecursiveAuthorityState = useCallback(
+    (
+      deletionType: DeletionTypeValue,
+      previousState: IPageDeleteConfigValue | null,
+    ) => {
+      if (isTypeDeletion(deletionType)) {
+        adminGeneralSecurityContainer.changePreviousPageRecursiveDeletionAuthority(
+          previousState,
+        );
+        return;
+      }
 
-    adminGeneralSecurityContainer.changePreviousPageRecursiveCompleteDeletionAuthority(previousState);
-  }, [adminGeneralSecurityContainer]);
+      adminGeneralSecurityContainer.changePreviousPageRecursiveCompleteDeletionAuthority(
+        previousState,
+      );
+    },
+    [adminGeneralSecurityContainer],
+  );
 
-  const expandDeleteOptionsState = useCallback((deletionType: DeletionTypeValue) => {
-    return isTypeDeletion(deletionType)
-      ? adminGeneralSecurityContainer.state.expandOtherOptionsForDeletion
-      : adminGeneralSecurityContainer.state.expandOtherOptionsForCompleteDeletion;
-  }, [adminGeneralSecurityContainer]);
+  const expandDeleteOptionsState = useCallback(
+    (deletionType: DeletionTypeValue) => {
+      return isTypeDeletion(deletionType)
+        ? adminGeneralSecurityContainer.state.expandOtherOptionsForDeletion
+        : adminGeneralSecurityContainer.state
+            .expandOtherOptionsForCompleteDeletion;
+    },
+    [adminGeneralSecurityContainer],
+  );
 
-  const setExpandOtherDeleteOptionsState = useCallback((deletionType: DeletionTypeValue, bool: boolean) => {
-    if (isTypeDeletion(deletionType)) {
-      adminGeneralSecurityContainer.switchExpandOtherOptionsForDeletion(bool);
-      return;
-    }
-    adminGeneralSecurityContainer.switchExpandOtherOptionsForCompleteDeletion(bool);
-  }, [adminGeneralSecurityContainer]);
+  const setExpandOtherDeleteOptionsState = useCallback(
+    (deletionType: DeletionTypeValue, bool: boolean) => {
+      if (isTypeDeletion(deletionType)) {
+        adminGeneralSecurityContainer.switchExpandOtherOptionsForDeletion(bool);
+        return;
+      }
+      adminGeneralSecurityContainer.switchExpandOtherOptionsForCompleteDeletion(
+        bool,
+      );
+    },
+    [adminGeneralSecurityContainer],
+  );
 
-  const setDeletionConfigState = useCallback((
+  const setDeletionConfigState = useCallback(
+    (
       newState: IPageDeleteConfigValue,
       setState: (value: IPageDeleteConfigValue) => void,
       deletionType: DeletionTypeValue,
-  ) => {
-    setState(newState);
+    ) => {
+      setState(newState);
 
-    if (previousPageRecursiveAuthorityState(deletionType) !== null) {
-      setPagePreviousRecursiveAuthorityState(deletionType, null);
-    }
+      if (previousPageRecursiveAuthorityState(deletionType) !== null) {
+        setPagePreviousRecursiveAuthorityState(deletionType, null);
+      }
 
-    if (isRecursiveDeletion(deletionType)) {
-      return;
-    }
+      if (isRecursiveDeletion(deletionType)) {
+        return;
+      }
 
-    const [recursiveState, setRecursiveState] = getRecursiveDeletionConfigState(deletionType);
+      const [recursiveState, setRecursiveState] =
+        getRecursiveDeletionConfigState(deletionType);
 
-    const calculableValue = prepareDeleteConfigValuesForCalc(
-      newState as IPageDeleteConfigValueToProcessValidation,
-      recursiveState as IPageDeleteConfigValueToProcessValidation,
-    );
-    const shouldForceUpdate = !validateDeleteConfigs(calculableValue[0], calculableValue[1]);
-    if (shouldForceUpdate) {
-      setRecursiveState(newState);
-      setPagePreviousRecursiveAuthorityState(deletionType, recursiveState);
-      setExpandOtherDeleteOptionsState(deletionType, true);
-    }
-  }, [
-    getRecursiveDeletionConfigState,
-    previousPageRecursiveAuthorityState,
-    setPagePreviousRecursiveAuthorityState,
-    setExpandOtherDeleteOptionsState,
-  ]);
+      const calculableValue = prepareDeleteConfigValuesForCalc(
+        newState as IPageDeleteConfigValueToProcessValidation,
+        recursiveState as IPageDeleteConfigValueToProcessValidation,
+      );
+      const shouldForceUpdate = !validateDeleteConfigs(
+        calculableValue[0],
+        calculableValue[1],
+      );
+      if (shouldForceUpdate) {
+        setRecursiveState(newState);
+        setPagePreviousRecursiveAuthorityState(deletionType, recursiveState);
+        setExpandOtherDeleteOptionsState(deletionType, true);
+      }
+    },
+    [
+      getRecursiveDeletionConfigState,
+      previousPageRecursiveAuthorityState,
+      setPagePreviousRecursiveAuthorityState,
+      setExpandOtherDeleteOptionsState,
+    ],
+  );
 
-  const renderPageDeletePermissionDropdown = useCallback((
+  const renderPageDeletePermissionDropdown = useCallback(
+    (
       currentState: IPageDeleteConfigValue,
       setState: (value: IPageDeleteConfigValue) => void,
       deletionType: DeletionTypeValue,
       isButtonDisabled: boolean,
-  ) => {
-    return (
-      <div className="dropdown">
-        <button
-          className="btn btn-outline-secondary dropdown-toggle text-end"
-          type="button"
-          id="dropdownMenuButton"
-          data-bs-toggle="dropdown"
-          aria-haspopup="true"
-          aria-expanded="true"
-        >
-          <span className="float-start">{t(getDeleteConfigValueForT(currentState))}</span>
-        </button>
-        <div className="dropdown-menu" aria-labelledby="dropdownMenuButton">
-          {isRecursiveDeletion(deletionType)
-            ? (
+    ) => {
+      return (
+        <div className="dropdown">
+          <button
+            className="btn btn-outline-secondary dropdown-toggle text-end"
+            type="button"
+            id="dropdownMenuButton"
+            data-bs-toggle="dropdown"
+            aria-haspopup="true"
+            aria-expanded="true"
+          >
+            <span className="float-start">
+              {t(getDeleteConfigValueForT(currentState))}
+            </span>
+          </button>
+          <div className="dropdown-menu" aria-labelledby="dropdownMenuButton">
+            {isRecursiveDeletion(deletionType) ? (
               <button
                 className="dropdown-item"
                 type="button"
-                onClick={() => { setDeletionConfigState(PageDeleteConfigValue.Inherit, setState, deletionType) }}
+                onClick={() => {
+                  setDeletionConfigState(
+                    PageDeleteConfigValue.Inherit,
+                    setState,
+                    deletionType,
+                  );
+                }}
               >
                 {t('security_settings.inherit')}
               </button>
-            )
-            : (
+            ) : (
               <button
                 className="dropdown-item"
                 type="button"
-                onClick={() => { setDeletionConfigState(PageDeleteConfigValue.Anyone, setState, deletionType) }}
+                onClick={() => {
+                  setDeletionConfigState(
+                    PageDeleteConfigValue.Anyone,
+                    setState,
+                    deletionType,
+                  );
+                }}
               >
                 {t('security_settings.anyone')}
               </button>
             )}
-          <button
-            className={`dropdown-item ${isButtonDisabled ? 'disabled' : ''}`}
-            type="button"
-            onClick={() => { setDeletionConfigState(PageDeleteConfigValue.AdminAndAuthor, setState, deletionType) }}
-          >
-            {t('security_settings.admin_and_author')}
-          </button>
-          <button
-            className="dropdown-item"
-            type="button"
-            onClick={() => { setDeletionConfigState(PageDeleteConfigValue.AdminOnly, setState, deletionType) }}
-          >
-            {t('security_settings.admin_only')}
-          </button>
+            <button
+              className={`dropdown-item ${isButtonDisabled ? 'disabled' : ''}`}
+              type="button"
+              onClick={() => {
+                setDeletionConfigState(
+                  PageDeleteConfigValue.AdminAndAuthor,
+                  setState,
+                  deletionType,
+                );
+              }}
+            >
+              {t('security_settings.admin_and_author')}
+            </button>
+            <button
+              className="dropdown-item"
+              type="button"
+              onClick={() => {
+                setDeletionConfigState(
+                  PageDeleteConfigValue.AdminOnly,
+                  setState,
+                  deletionType,
+                );
+              }}
+            >
+              {t('security_settings.admin_only')}
+            </button>
+          </div>
+          <p className="form-text text-muted small">
+            {t(
+              `security_settings.${getDeletionTypeForT(deletionType)}_explanation`,
+            )}
+          </p>
         </div>
-        <p className="form-text text-muted small">{t(`security_settings.${getDeletionTypeForT(deletionType)}_explanation`)}</p>
-      </div>
-    );
-  }, [t, setDeletionConfigState]);
+      );
+    },
+    [t, setDeletionConfigState],
+  );
 
-  const renderPageDeletePermission = useCallback((
+  const renderPageDeletePermission = useCallback(
+    (
       currentState: IPageDeleteConfigValue,
       setState: (value: IPageDeleteConfigValue) => void,
       deletionType: DeletionTypeValue,
       isButtonDisabled: boolean,
-  ) => {
-    const expandDeleteOptions = expandDeleteOptionsState(deletionType);
+    ) => {
+      const expandDeleteOptions = expandDeleteOptionsState(deletionType);
 
-    return (
-      <div key={`page-delete-permission-dropdown-${deletionType}`} className="row">
-        <div className="col-md-4 text-md-end">
-          {!isRecursiveDeletion(deletionType) && isTypeDeletion(deletionType) && <strong>{t('security_settings.page_delete')}</strong>}
-          {!isRecursiveDeletion(deletionType) && !isTypeDeletion(deletionType) && <strong>{t('security_settings.page_delete_completely')}</strong>}
-        </div>
+      return (
+        <div
+          key={`page-delete-permission-dropdown-${deletionType}`}
+          className="row"
+        >
+          <div className="col-md-4 text-md-end">
+            {!isRecursiveDeletion(deletionType) &&
+              isTypeDeletion(deletionType) && (
+                <strong>{t('security_settings.page_delete')}</strong>
+              )}
+            {!isRecursiveDeletion(deletionType) &&
+              !isTypeDeletion(deletionType) && (
+                <strong>{t('security_settings.page_delete_completely')}</strong>
+              )}
+          </div>
 
-        <div className="col-md-8">
-          {!isRecursiveDeletion(deletionType)
-            ? (
+          <div className="col-md-8">
+            {!isRecursiveDeletion(deletionType) ? (
               <>
-                {renderPageDeletePermissionDropdown(currentState, setState, deletionType, isButtonDisabled)}
-                {currentState === PageDeleteConfigValue.Anyone && deletionType === DeletionType.CompleteDeletion && (
-                  <>
-                    <input
-                      id="isAllGroupMembershipRequiredForPageCompleteDeletionCheckbox"
-                      className="form-check-input"
-                      type="checkbox"
-                      checked={adminGeneralSecurityContainer.state.isAllGroupMembershipRequiredForPageCompleteDeletion}
-                      onChange={() => { adminGeneralSecurityContainer.switchIsAllGroupMembershipRequiredForPageCompleteDeletion() }}
-                    />
-                    <label className="form-check-label" htmlFor="isAllGroupMembershipRequiredForPageCompleteDeletionCheckbox">
-                      {t('security_settings.is_all_group_membership_required_for_page_complete_deletion')}
-                    </label>
-                    <p className="form-text text-muted small mt-2">
-                      {t('security_settings.is_all_group_membership_required_for_page_complete_deletion_explanation')}
-                    </p>
-                  </>
+                {renderPageDeletePermissionDropdown(
+                  currentState,
+                  setState,
+                  deletionType,
+                  isButtonDisabled,
                 )}
+                {currentState === PageDeleteConfigValue.Anyone &&
+                  deletionType === DeletionType.CompleteDeletion && (
+                    <>
+                      <input
+                        id="isAllGroupMembershipRequiredForPageCompleteDeletionCheckbox"
+                        className="form-check-input"
+                        type="checkbox"
+                        checked={
+                          adminGeneralSecurityContainer.state
+                            .isAllGroupMembershipRequiredForPageCompleteDeletion
+                        }
+                        onChange={() => {
+                          adminGeneralSecurityContainer.switchIsAllGroupMembershipRequiredForPageCompleteDeletion();
+                        }}
+                      />
+                      <label
+                        className="form-check-label"
+                        htmlFor="isAllGroupMembershipRequiredForPageCompleteDeletionCheckbox"
+                      >
+                        {t(
+                          'security_settings.is_all_group_membership_required_for_page_complete_deletion',
+                        )}
+                      </label>
+                      <p className="form-text text-muted small mt-2">
+                        {t(
+                          'security_settings.is_all_group_membership_required_for_page_complete_deletion_explanation',
+                        )}
+                      </p>
+                    </>
+                  )}
               </>
-            )
-            : (
+            ) : (
               <>
                 <button
                   type="button"
                   className="btn btn-link p-0 mb-4"
                   aria-expanded="false"
-                  onClick={() => setExpandOtherDeleteOptionsState(deletionType, !expandDeleteOptions)}
+                  onClick={() =>
+                    setExpandOtherDeleteOptionsState(
+                      deletionType,
+                      !expandDeleteOptions,
+                    )
+                  }
                 >
-                  <span className={`material-symbols-outlined me-1 ${expandDeleteOptions ? 'rotate-90' : ''}`}>navigate_next</span>
+                  <span
+                    className={`material-symbols-outlined me-1 ${expandDeleteOptions ? 'rotate-90' : ''}`}
+                  >
+                    navigate_next
+                  </span>
                   {t('security_settings.other_options')}
                 </button>
                 <Collapse isOpen={expandDeleteOptions}>
@@ -223,67 +331,109 @@ export const PageDeleteRightsSettings: React.FC<Props> = ({ adminGeneralSecurity
                       <span className="text-warning">
                         <span className="material-symbols-outlined">info</span>
                         {/* eslint-disable-next-line react/no-danger */}
-                        <span dangerouslySetInnerHTML={{ __html: t('security_settings.page_delete_rights_caution') }} />
+                        <span
+                          dangerouslySetInnerHTML={{
+                            __html: t(
+                              'security_settings.page_delete_rights_caution',
+                            ),
+                          }}
+                        />
                       </span>
                     </p>
-                    {previousPageRecursiveAuthorityState(deletionType) !== null && (
+                    {previousPageRecursiveAuthorityState(deletionType) !==
+                      null && (
                       <div className="mb-3">
-                        <strong>{t('security_settings.forced_update_desc')}</strong>
-                        <code>{t(getDeleteConfigValueForT(previousPageRecursiveAuthorityState(deletionType)))}</code>
+                        <strong>
+                          {t('security_settings.forced_update_desc')}
+                        </strong>
+                        <code>
+                          {t(
+                            getDeleteConfigValueForT(
+                              previousPageRecursiveAuthorityState(deletionType),
+                            ),
+                          )}
+                        </code>
                       </div>
                     )}
-                    {renderPageDeletePermissionDropdown(currentState, setState, deletionType, isButtonDisabled)}
+                    {renderPageDeletePermissionDropdown(
+                      currentState,
+                      setState,
+                      deletionType,
+                      isButtonDisabled,
+                    )}
                   </div>
                 </Collapse>
               </>
             )}
+          </div>
         </div>
-      </div>
-    );
-  }, [
-    adminGeneralSecurityContainer,
-    expandDeleteOptionsState,
-    previousPageRecursiveAuthorityState,
-    renderPageDeletePermissionDropdown,
-    setExpandOtherDeleteOptionsState,
-    t,
-  ]);
+      );
+    },
+    [
+      adminGeneralSecurityContainer,
+      expandDeleteOptionsState,
+      previousPageRecursiveAuthorityState,
+      renderPageDeletePermissionDropdown,
+      setExpandOtherDeleteOptionsState,
+      t,
+    ],
+  );
 
-  const isButtonDisabledForDeletion = !validateDeleteConfigs(currentPageDeletionAuthority, PageDeleteConfigValue.AdminAndAuthor);
+  const isButtonDisabledForDeletion = !validateDeleteConfigs(
+    currentPageDeletionAuthority,
+    PageDeleteConfigValue.AdminAndAuthor,
+  );
 
-  const isButtonDisabledForCompleteDeletion = !validateDeleteConfigs(currentPageCompleteDeletionAuthority, PageDeleteConfigValue.AdminAndAuthor);
+  const isButtonDisabledForCompleteDeletion = !validateDeleteConfigs(
+    currentPageCompleteDeletionAuthority,
+    PageDeleteConfigValue.AdminAndAuthor,
+  );
 
   return (
     <>
       <h4 className="mb-3">{t('security_settings.page_delete_rights')}</h4>
       {[
-        [currentPageDeletionAuthority, adminGeneralSecurityContainer.changePageDeletionAuthority, DeletionType.Deletion, false],
+        [
+          currentPageDeletionAuthority,
+          adminGeneralSecurityContainer.changePageDeletionAuthority,
+          DeletionType.Deletion,
+          false,
+        ],
         [
           currentPageRecursiveDeletionAuthority,
           adminGeneralSecurityContainer.changePageRecursiveDeletionAuthority,
           DeletionType.RecursiveDeletion,
           isButtonDisabledForDeletion,
         ],
-      ].map(arr => renderPageDeletePermission(
-        arr[0] as IPageDeleteConfigValue,
-        arr[1] as (value: IPageDeleteConfigValue) => void,
-        arr[2] as DeletionTypeValue,
-        arr[3] as boolean,
-      ))}
+      ].map((arr) =>
+        renderPageDeletePermission(
+          arr[0] as IPageDeleteConfigValue,
+          arr[1] as (value: IPageDeleteConfigValue) => void,
+          arr[2] as DeletionTypeValue,
+          arr[3] as boolean,
+        ),
+      )}
       {[
-        [currentPageCompleteDeletionAuthority, adminGeneralSecurityContainer.changePageCompleteDeletionAuthority, DeletionType.CompleteDeletion, false],
+        [
+          currentPageCompleteDeletionAuthority,
+          adminGeneralSecurityContainer.changePageCompleteDeletionAuthority,
+          DeletionType.CompleteDeletion,
+          false,
+        ],
         [
           currentPageRecursiveCompleteDeletionAuthority,
           adminGeneralSecurityContainer.changePageRecursiveCompleteDeletionAuthority,
           DeletionType.RecursiveCompleteDeletion,
           isButtonDisabledForCompleteDeletion,
         ],
-      ].map(arr => renderPageDeletePermission(
-        arr[0] as IPageDeleteConfigValue,
-        arr[1] as (value: IPageDeleteConfigValue) => void,
-        arr[2] as DeletionTypeValue,
-        arr[3] as boolean,
-      ))}
+      ].map((arr) =>
+        renderPageDeletePermission(
+          arr[0] as IPageDeleteConfigValue,
+          arr[1] as (value: IPageDeleteConfigValue) => void,
+          arr[2] as DeletionTypeValue,
+          arr[3] as boolean,
+        ),
+      )}
     </>
   );
 };

+ 45 - 16
apps/app/src/client/components/Admin/Security/SecuritySetting/PageListDisplaySettings.tsx

@@ -1,4 +1,4 @@
-import React from 'react';
+import type React from 'react';
 
 import type AdminGeneralSecurityContainer from '~/client/services/AdminGeneralSecurityContainer';
 
@@ -7,7 +7,10 @@ type Props = {
   t: (key: string) => string;
 };
 
-export const PageListDisplaySettings: React.FC<Props> = ({ adminGeneralSecurityContainer, t }) => {
+export const PageListDisplaySettings: React.FC<Props> = ({
+  adminGeneralSecurityContainer,
+  t,
+}) => {
   return (
     <>
       <h4 className="alert-anchor">
@@ -47,24 +50,37 @@ export const PageListDisplaySettings: React.FC<Props> = ({ adminGeneralSecurityC
                     aria-expanded="true"
                   >
                     <span className="float-start">
-                      {adminGeneralSecurityContainer.state.currentOwnerRestrictionDisplayMode === 'Displayed'
-                        && t('security_settings.always_displayed')}
-                      {adminGeneralSecurityContainer.state.currentOwnerRestrictionDisplayMode === 'Hidden'
-                        && t('security_settings.always_hidden')}
+                      {adminGeneralSecurityContainer.state
+                        .currentOwnerRestrictionDisplayMode === 'Displayed' &&
+                        t('security_settings.always_displayed')}
+                      {adminGeneralSecurityContainer.state
+                        .currentOwnerRestrictionDisplayMode === 'Hidden' &&
+                        t('security_settings.always_hidden')}
                     </span>
                   </button>
-                  <div className="dropdown-menu" aria-labelledby="isShowRestrictedByOwner">
+                  <div
+                    className="dropdown-menu"
+                    aria-labelledby="isShowRestrictedByOwner"
+                  >
                     <button
                       className="dropdown-item"
                       type="button"
-                      onClick={() => { adminGeneralSecurityContainer.changeOwnerRestrictionDisplayMode('Displayed') }}
+                      onClick={() => {
+                        adminGeneralSecurityContainer.changeOwnerRestrictionDisplayMode(
+                          'Displayed',
+                        );
+                      }}
                     >
                       {t('security_settings.always_displayed')}
                     </button>
                     <button
                       className="dropdown-item"
                       type="button"
-                      onClick={() => { adminGeneralSecurityContainer.changeOwnerRestrictionDisplayMode('Hidden') }}
+                      onClick={() => {
+                        adminGeneralSecurityContainer.changeOwnerRestrictionDisplayMode(
+                          'Hidden',
+                        );
+                      }}
                     >
                       {t('security_settings.always_hidden')}
                     </button>
@@ -84,24 +100,37 @@ export const PageListDisplaySettings: React.FC<Props> = ({ adminGeneralSecurityC
                     aria-expanded="true"
                   >
                     <span className="float-start">
-                      {adminGeneralSecurityContainer.state.currentGroupRestrictionDisplayMode === 'Displayed'
-                        && t('security_settings.always_displayed')}
-                      {adminGeneralSecurityContainer.state.currentGroupRestrictionDisplayMode === 'Hidden'
-                        && t('security_settings.always_hidden')}
+                      {adminGeneralSecurityContainer.state
+                        .currentGroupRestrictionDisplayMode === 'Displayed' &&
+                        t('security_settings.always_displayed')}
+                      {adminGeneralSecurityContainer.state
+                        .currentGroupRestrictionDisplayMode === 'Hidden' &&
+                        t('security_settings.always_hidden')}
                     </span>
                   </button>
-                  <div className="dropdown-menu" aria-labelledby="isShowRestrictedByGroup">
+                  <div
+                    className="dropdown-menu"
+                    aria-labelledby="isShowRestrictedByGroup"
+                  >
                     <button
                       className="dropdown-item"
                       type="button"
-                      onClick={() => { adminGeneralSecurityContainer.changeGroupRestrictionDisplayMode('Displayed') }}
+                      onClick={() => {
+                        adminGeneralSecurityContainer.changeGroupRestrictionDisplayMode(
+                          'Displayed',
+                        );
+                      }}
                     >
                       {t('security_settings.always_displayed')}
                     </button>
                     <button
                       className="dropdown-item"
                       type="button"
-                      onClick={() => { adminGeneralSecurityContainer.changeGroupRestrictionDisplayMode('Hidden') }}
+                      onClick={() => {
+                        adminGeneralSecurityContainer.changeGroupRestrictionDisplayMode(
+                          'Hidden',
+                        );
+                      }}
                     >
                       {t('security_settings.always_hidden')}
                     </button>

+ 9 - 4
apps/app/src/client/components/Admin/Security/SecuritySetting/SessionMaxAgeSettings.tsx

@@ -1,5 +1,4 @@
-import React from 'react';
-
+import type React from 'react';
 import type { UseFormRegister } from 'react-hook-form';
 
 type Props = {
@@ -23,10 +22,16 @@ export const SessionMaxAgeSettings: React.FC<Props> = ({ register, t }) => {
             placeholder="2592000000"
           />
           {/* eslint-disable-next-line react/no-danger */}
-          <p className="form-text text-muted" dangerouslySetInnerHTML={{ __html: t('security_settings.max_age_desc') }} />
+          <p
+            className="form-text text-muted"
+            dangerouslySetInnerHTML={{
+              __html: t('security_settings.max_age_desc'),
+            }}
+          />
           <p className="card custom-card bg-warning-subtle">
             <span className="text-warning">
-              <span className="material-symbols-outlined">info</span> {t('security_settings.max_age_caution')}
+              <span className="material-symbols-outlined">info</span>{' '}
+              {t('security_settings.max_age_caution')}
             </span>
           </p>
         </div>

+ 43 - 13
apps/app/src/client/components/Admin/Security/SecuritySetting/UserHomepageDeletionSettings.tsx

@@ -1,5 +1,5 @@
 /* eslint-disable react/no-danger */
-import React from 'react';
+import type React from 'react';
 
 import type AdminGeneralSecurityContainer from '~/client/services/AdminGeneralSecurityContainer';
 
@@ -8,10 +8,15 @@ type Props = {
   t: (key: string) => string;
 };
 
-export const UserHomepageDeletionSettings: React.FC<Props> = ({ adminGeneralSecurityContainer, t }) => {
+export const UserHomepageDeletionSettings: React.FC<Props> = ({
+  adminGeneralSecurityContainer,
+  t,
+}) => {
   return (
     <>
-      <h4 className="mb-3">{t('security_settings.user_homepage_deletion.user_homepage_deletion')}</h4>
+      <h4 className="mb-3">
+        {t('security_settings.user_homepage_deletion.user_homepage_deletion')}
+      </h4>
       <div className="row mb-4">
         <div className="col-md-10 offset-md-2">
           <div className="form-check form-switch form-check-success">
@@ -19,11 +24,21 @@ export const UserHomepageDeletionSettings: React.FC<Props> = ({ adminGeneralSecu
               type="checkbox"
               className="form-check-input"
               id="is-user-page-deletion-enabled"
-              checked={adminGeneralSecurityContainer.state.isUsersHomepageDeletionEnabled}
-              onChange={() => { adminGeneralSecurityContainer.switchIsUsersHomepageDeletionEnabled() }}
+              checked={
+                adminGeneralSecurityContainer.state
+                  .isUsersHomepageDeletionEnabled
+              }
+              onChange={() => {
+                adminGeneralSecurityContainer.switchIsUsersHomepageDeletionEnabled();
+              }}
             />
-            <label className="form-label form-check-label" htmlFor="is-user-page-deletion-enabled">
-              {t('security_settings.user_homepage_deletion.enable_user_homepage_deletion')}
+            <label
+              className="form-label form-check-label"
+              htmlFor="is-user-page-deletion-enabled"
+            >
+              {t(
+                'security_settings.user_homepage_deletion.enable_user_homepage_deletion',
+              )}
             </label>
           </div>
           <div className="custom-control custom-switch custom-checkbox-success mt-2">
@@ -31,17 +46,32 @@ export const UserHomepageDeletionSettings: React.FC<Props> = ({ adminGeneralSecu
               type="checkbox"
               className="form-check-input"
               id="is-force-delete-user-homepage-on-user-deletion"
-              checked={adminGeneralSecurityContainer.state.isForceDeleteUserHomepageOnUserDeletion}
-              onChange={() => { adminGeneralSecurityContainer.switchIsForceDeleteUserHomepageOnUserDeletion() }}
-              disabled={!adminGeneralSecurityContainer.state.isUsersHomepageDeletionEnabled}
+              checked={
+                adminGeneralSecurityContainer.state
+                  .isForceDeleteUserHomepageOnUserDeletion
+              }
+              onChange={() => {
+                adminGeneralSecurityContainer.switchIsForceDeleteUserHomepageOnUserDeletion();
+              }}
+              disabled={
+                !adminGeneralSecurityContainer.state
+                  .isUsersHomepageDeletionEnabled
+              }
             />
-            <label className="form-check-label" htmlFor="is-force-delete-user-homepage-on-user-deletion">
-              {t('security_settings.user_homepage_deletion.enable_force_delete_user_homepage_on_user_deletion')}
+            <label
+              className="form-check-label"
+              htmlFor="is-force-delete-user-homepage-on-user-deletion"
+            >
+              {t(
+                'security_settings.user_homepage_deletion.enable_force_delete_user_homepage_on_user_deletion',
+              )}
             </label>
           </div>
           <p
             className="form-text text-muted small mt-2"
-            dangerouslySetInnerHTML={{ __html: t('security_settings.user_homepage_deletion.desc') }}
+            dangerouslySetInnerHTML={{
+              __html: t('security_settings.user_homepage_deletion.desc'),
+            }}
           />
         </div>
       </div>

+ 81 - 37
apps/app/src/client/components/Admin/Security/SecuritySetting/index.tsx

@@ -1,13 +1,12 @@
-import React, { useCallback, useEffect } from 'react';
-
+import type React from 'react';
+import { useCallback, useEffect } from 'react';
 import { useTranslation } from 'next-i18next';
 import { useForm } from 'react-hook-form';
 
 import AdminGeneralSecurityContainer from '~/client/services/AdminGeneralSecurityContainer';
-import { toastSuccess, toastError } from '~/client/util/toastr';
+import { toastError, toastSuccess } from '~/client/util/toastr';
 
 import { withUnstatedContainers } from '../../../UnstatedUtils';
-
 import { CommentManageRightsSettings } from './CommentManageRightsSettings';
 import { PageAccessRightsSettings } from './PageAccessRightsSettings';
 import { PageDeleteRightsSettings } from './PageDeleteRightsSettings';
@@ -23,7 +22,9 @@ type Props = {
   adminGeneralSecurityContainer: AdminGeneralSecurityContainer;
 };
 
-const SecuritySettingComponent: React.FC<Props> = ({ adminGeneralSecurityContainer }) => {
+const SecuritySettingComponent: React.FC<Props> = ({
+  adminGeneralSecurityContainer,
+}) => {
   const { t } = useTranslation('admin');
   const { register, handleSubmit, reset } = useForm<FormData>();
 
@@ -34,35 +35,56 @@ const SecuritySettingComponent: React.FC<Props> = ({ adminGeneralSecurityContain
     });
   }, [reset, adminGeneralSecurityContainer.state.sessionMaxAge]);
 
-  const onSubmit = useCallback(async(data: FormData) => {
-    try {
-      // Save all security settings with form data
-      await adminGeneralSecurityContainer.updateGeneralSecuritySetting({
-        sessionMaxAge: data.sessionMaxAge,
-        restrictGuestMode: adminGeneralSecurityContainer.state.currentRestrictGuestMode,
-        pageDeletionAuthority: adminGeneralSecurityContainer.state.currentPageDeletionAuthority,
-        pageCompleteDeletionAuthority: adminGeneralSecurityContainer.state.currentPageCompleteDeletionAuthority,
-        pageRecursiveDeletionAuthority: adminGeneralSecurityContainer.state.currentPageRecursiveDeletionAuthority,
-        pageRecursiveCompleteDeletionAuthority: adminGeneralSecurityContainer.state.currentPageRecursiveCompleteDeletionAuthority,
-        isAllGroupMembershipRequiredForPageCompleteDeletion: adminGeneralSecurityContainer.state.isAllGroupMembershipRequiredForPageCompleteDeletion,
-        hideRestrictedByGroup: adminGeneralSecurityContainer.state.currentGroupRestrictionDisplayMode === 'Hidden',
-        hideRestrictedByOwner: adminGeneralSecurityContainer.state.currentOwnerRestrictionDisplayMode === 'Hidden',
-        isUsersHomepageDeletionEnabled: adminGeneralSecurityContainer.state.isUsersHomepageDeletionEnabled,
-        isForceDeleteUserHomepageOnUserDeletion: adminGeneralSecurityContainer.state.isForceDeleteUserHomepageOnUserDeletion,
-        isRomUserAllowedToComment: adminGeneralSecurityContainer.state.isRomUserAllowedToComment,
-      });
-      toastSuccess(t('security_settings.updated_general_security_setting'));
-    }
-    catch (err) {
-      toastError(err);
-    }
-  }, [adminGeneralSecurityContainer, t]);
+  const onSubmit = useCallback(
+    async (data: FormData) => {
+      try {
+        // Save all security settings with form data
+        await adminGeneralSecurityContainer.updateGeneralSecuritySetting({
+          sessionMaxAge: data.sessionMaxAge,
+          restrictGuestMode:
+            adminGeneralSecurityContainer.state.currentRestrictGuestMode,
+          pageDeletionAuthority:
+            adminGeneralSecurityContainer.state.currentPageDeletionAuthority,
+          pageCompleteDeletionAuthority:
+            adminGeneralSecurityContainer.state
+              .currentPageCompleteDeletionAuthority,
+          pageRecursiveDeletionAuthority:
+            adminGeneralSecurityContainer.state
+              .currentPageRecursiveDeletionAuthority,
+          pageRecursiveCompleteDeletionAuthority:
+            adminGeneralSecurityContainer.state
+              .currentPageRecursiveCompleteDeletionAuthority,
+          isAllGroupMembershipRequiredForPageCompleteDeletion:
+            adminGeneralSecurityContainer.state
+              .isAllGroupMembershipRequiredForPageCompleteDeletion,
+          hideRestrictedByGroup:
+            adminGeneralSecurityContainer.state
+              .currentGroupRestrictionDisplayMode === 'Hidden',
+          hideRestrictedByOwner:
+            adminGeneralSecurityContainer.state
+              .currentOwnerRestrictionDisplayMode === 'Hidden',
+          isUsersHomepageDeletionEnabled:
+            adminGeneralSecurityContainer.state.isUsersHomepageDeletionEnabled,
+          isForceDeleteUserHomepageOnUserDeletion:
+            adminGeneralSecurityContainer.state
+              .isForceDeleteUserHomepageOnUserDeletion,
+          isRomUserAllowedToComment:
+            adminGeneralSecurityContainer.state.isRomUserAllowedToComment,
+        });
+        toastSuccess(t('security_settings.updated_general_security_setting'));
+      } catch (err) {
+        toastError(err);
+      }
+    },
+    [adminGeneralSecurityContainer, t],
+  );
 
   if (adminGeneralSecurityContainer.state.retrieveError != null) {
     return (
       <div>
         <p>
-          {t('Error occurred')} : {adminGeneralSecurityContainer.state.retrieveError}
+          {t('Error occurred')} :{' '}
+          {adminGeneralSecurityContainer.state.retrieveError}
         </p>
       </div>
     );
@@ -70,22 +92,41 @@ const SecuritySettingComponent: React.FC<Props> = ({ adminGeneralSecurityContain
 
   return (
     <div data-testid="admin-security-setting">
-      <h2 className="border-bottom mb-5">{t('security_settings.security_settings')}</h2>
+      <h2 className="border-bottom mb-5">
+        {t('security_settings.security_settings')}
+      </h2>
 
       <form onSubmit={handleSubmit(onSubmit)}>
         <div className="vstack gap-3">
-          <PageListDisplaySettings adminGeneralSecurityContainer={adminGeneralSecurityContainer} t={t} />
-          <PageAccessRightsSettings adminGeneralSecurityContainer={adminGeneralSecurityContainer} t={t} />
-          <PageDeleteRightsSettings adminGeneralSecurityContainer={adminGeneralSecurityContainer} t={t} />
-          <UserHomepageDeletionSettings adminGeneralSecurityContainer={adminGeneralSecurityContainer} t={t} />
-          <CommentManageRightsSettings adminGeneralSecurityContainer={adminGeneralSecurityContainer} t={t} />
+          <PageListDisplaySettings
+            adminGeneralSecurityContainer={adminGeneralSecurityContainer}
+            t={t}
+          />
+          <PageAccessRightsSettings
+            adminGeneralSecurityContainer={adminGeneralSecurityContainer}
+            t={t}
+          />
+          <PageDeleteRightsSettings
+            adminGeneralSecurityContainer={adminGeneralSecurityContainer}
+            t={t}
+          />
+          <UserHomepageDeletionSettings
+            adminGeneralSecurityContainer={adminGeneralSecurityContainer}
+            t={t}
+          />
+          <CommentManageRightsSettings
+            adminGeneralSecurityContainer={adminGeneralSecurityContainer}
+            t={t}
+          />
           <SessionMaxAgeSettings register={register} t={t} />
 
           <div className="text-center text-md-start offset-md-3 col-md-5">
             <button
               type="submit"
               className="btn btn-primary"
-              disabled={adminGeneralSecurityContainer.state.retrieveError != null}
+              disabled={
+                adminGeneralSecurityContainer.state.retrieveError != null
+              }
             >
               {t('Update')}
             </button>
@@ -96,4 +137,7 @@ const SecuritySettingComponent: React.FC<Props> = ({ adminGeneralSecurityContain
   );
 };
 
-export const SecuritySetting = withUnstatedContainers(SecuritySettingComponent, [AdminGeneralSecurityContainer]);
+export const SecuritySetting = withUnstatedContainers(
+  SecuritySettingComponent,
+  [AdminGeneralSecurityContainer],
+);

+ 22 - 7
apps/app/src/client/components/Admin/Security/SecuritySetting/types.ts

@@ -1,4 +1,7 @@
-import { PageDeleteConfigValue, type IPageDeleteConfigValue } from '~/interfaces/page-delete-config';
+import {
+  type IPageDeleteConfigValue,
+  PageDeleteConfigValue,
+} from '~/interfaces/page-delete-config';
 
 export const DeletionTypeForT = Object.freeze({
   Deletion: 'deletion',
@@ -15,9 +18,11 @@ export const DeletionType = Object.freeze({
 } as const);
 
 export type DeletionTypeKey = keyof typeof DeletionType;
-export type DeletionTypeValue = typeof DeletionType[DeletionTypeKey];
+export type DeletionTypeValue = (typeof DeletionType)[DeletionTypeKey];
 
-export const getDeletionTypeForT = (deletionType: DeletionTypeValue): string => {
+export const getDeletionTypeForT = (
+  deletionType: DeletionTypeValue,
+): string => {
   switch (deletionType) {
     case DeletionType.Deletion:
       return DeletionTypeForT.Deletion;
@@ -30,7 +35,9 @@ export const getDeletionTypeForT = (deletionType: DeletionTypeValue): string =>
   }
 };
 
-export const getDeleteConfigValueForT = (deleteConfigValue: IPageDeleteConfigValue | null): string => {
+export const getDeleteConfigValueForT = (
+  deleteConfigValue: IPageDeleteConfigValue | null,
+): string => {
   switch (deleteConfigValue) {
     case PageDeleteConfigValue.Anyone:
     case null:
@@ -51,8 +58,13 @@ export const getDeleteConfigValueForT = (deleteConfigValue: IPageDeleteConfigVal
  * @param deletionType Deletion type
  * @returns boolean
  */
-export const isRecursiveDeletion = (deletionType: DeletionTypeValue): boolean => {
-  return deletionType === DeletionType.RecursiveDeletion || deletionType === DeletionType.RecursiveCompleteDeletion;
+export const isRecursiveDeletion = (
+  deletionType: DeletionTypeValue,
+): boolean => {
+  return (
+    deletionType === DeletionType.RecursiveDeletion ||
+    deletionType === DeletionType.RecursiveCompleteDeletion
+  );
 };
 
 /**
@@ -61,5 +73,8 @@ export const isRecursiveDeletion = (deletionType: DeletionTypeValue): boolean =>
  * @returns boolean
  */
 export const isTypeDeletion = (deletionType: DeletionTypeValue): boolean => {
-  return deletionType === DeletionType.Deletion || deletionType === DeletionType.RecursiveDeletion;
+  return (
+    deletionType === DeletionType.Deletion ||
+    deletionType === DeletionType.RecursiveDeletion
+  );
 };

+ 71 - 57
apps/app/src/client/components/Admin/Security/ShareLinkSetting.tsx

@@ -1,34 +1,28 @@
-import React, {
-  useCallback, useEffect, useState,
-} from 'react';
-
+import React, { useCallback, useEffect, useState } from 'react';
 import { useTranslation } from 'next-i18next';
 
 import AdminGeneralSecurityContainer from '~/client/services/AdminGeneralSecurityContainer';
 import { apiv3Delete } from '~/client/util/apiv3-client';
-import { toastSuccess, toastError } from '~/client/util/toastr';
+import { toastError, toastSuccess } from '~/client/util/toastr';
 
 import ShareLinkList from '../../PageAccessoriesModal/ShareLink/ShareLinkList';
 import PaginationWrapper from '../../PaginationWrapper';
 import { withUnstatedContainers } from '../../UnstatedUtils';
-
 import DeleteAllShareLinksModal from './DeleteAllShareLinksModal';
 
 type PagerProps = {
-  activePage: number,
-  pagingHandler: (page: number) => Promise<void>,
-  totalLinks: number,
-  limit: number,
-}
+  activePage: number;
+  pagingHandler: (page: number) => Promise<void>;
+  totalLinks: number;
+  limit: number;
+};
 
 type ShareLinkSettingProps = {
-  adminGeneralSecurityContainer: AdminGeneralSecurityContainer,
-}
+  adminGeneralSecurityContainer: AdminGeneralSecurityContainer;
+};
 
 const Pager = (props: PagerProps) => {
-  const {
-    activePage, pagingHandler, totalLinks, limit,
-  } = props;
+  const { activePage, pagingHandler, totalLinks, limit } = props;
 
   return (
     <PaginationWrapper
@@ -43,66 +37,81 @@ const Pager = (props: PagerProps) => {
 };
 
 const ShareLinkSetting = (props: ShareLinkSettingProps) => {
-
   const { t } = useTranslation('admin');
   const { adminGeneralSecurityContainer } = props;
   const {
-    shareLinks, shareLinksActivePage, totalshareLinks, shareLinksPagingLimit,
-    disableLinkSharing, setupStrategies,
+    shareLinks,
+    shareLinksActivePage,
+    totalshareLinks,
+    shareLinksPagingLimit,
+    disableLinkSharing,
+    setupStrategies,
   } = adminGeneralSecurityContainer.state;
-  const [isDeleteConfirmModalShown, setIsDeleteConfirmModalShown] = useState<boolean>();
-
-  const getShareLinkList = useCallback(async(page: number) => {
-    try {
-      await adminGeneralSecurityContainer.retrieveShareLinksByPagingNum(page);
-    }
-    catch (err) {
-      toastError(err);
-    }
-  }, [adminGeneralSecurityContainer]);
+  const [isDeleteConfirmModalShown, setIsDeleteConfirmModalShown] =
+    useState<boolean>();
+
+  const getShareLinkList = useCallback(
+    async (page: number) => {
+      try {
+        await adminGeneralSecurityContainer.retrieveShareLinksByPagingNum(page);
+      } catch (err) {
+        toastError(err);
+      }
+    },
+    [adminGeneralSecurityContainer],
+  );
 
   // for Next routing
   useEffect(() => {
     getShareLinkList(1);
   }, [getShareLinkList]);
 
-  const deleteAllLinksButtonHandler = useCallback(async() => {
+  const deleteAllLinksButtonHandler = useCallback(async () => {
     try {
       const res = await apiv3Delete('/share-links/all');
       const { deletedCount } = res.data;
-      toastSuccess(t('toaster.remove_share_link', { count: deletedCount, ns: 'commons' }));
-    }
-    catch (err) {
+      toastSuccess(
+        t('toaster.remove_share_link', { count: deletedCount, ns: 'commons' }),
+      );
+    } catch (err) {
       toastError(err);
     }
     getShareLinkList(1);
   }, [getShareLinkList, t]);
 
-  const deleteLinkById = useCallback(async(shareLinkId: string) => {
-    try {
-      const res = await apiv3Delete(`/share-links/${shareLinkId}`);
-      const { deletedShareLink } = res.data;
-      toastSuccess(t('toaster.remove_share_link_success', { shareLinkId: deletedShareLink._id, ns: 'commons' }));
-    }
-    catch (err) {
-      toastError(err);
-    }
-    getShareLinkList(shareLinksActivePage);
-  }, [shareLinksActivePage, getShareLinkList, t]);
+  const deleteLinkById = useCallback(
+    async (shareLinkId: string) => {
+      try {
+        const res = await apiv3Delete(`/share-links/${shareLinkId}`);
+        const { deletedShareLink } = res.data;
+        toastSuccess(
+          t('toaster.remove_share_link_success', {
+            shareLinkId: deletedShareLink._id,
+            ns: 'commons',
+          }),
+        );
+      } catch (err) {
+        toastError(err);
+      }
+      getShareLinkList(shareLinksActivePage);
+    },
+    [shareLinksActivePage, getShareLinkList, t],
+  );
 
-  const switchDisableLinkSharing = useCallback(async() => {
+  const switchDisableLinkSharing = useCallback(async () => {
     try {
       await adminGeneralSecurityContainer.switchDisableLinkSharing();
       toastSuccess(t('toaster.switch_disable_link_sharing_success'));
-    }
-    catch (err) {
+    } catch (err) {
       toastError(err);
     }
   }, [adminGeneralSecurityContainer, t]);
 
   return (
     <>
-      <h2 className="alert-anchor border-bottom mb-4">{t('security_settings.share_link_management')}</h2>
+      <h2 className="alert-anchor border-bottom mb-4">
+        {t('security_settings.share_link_management')}
+      </h2>
       <h4>{t('security_settings.share_link_rights')}</h4>
       <div className="row mt-4 mb-5">
         <div className="col-6 offset-3">
@@ -114,18 +123,23 @@ const ShareLinkSetting = (props: ShareLinkSettingProps) => {
               checked={!disableLinkSharing}
               onChange={() => switchDisableLinkSharing()}
             />
-            <label className="form-label form-check-label" htmlFor="disableLinkSharing">
+            <label
+              className="form-label form-check-label"
+              htmlFor="disableLinkSharing"
+            >
               {t('security_settings.enable_link_sharing')}
             </label>
           </div>
           {!setupStrategies.includes('local') && disableLinkSharing && (
-            <div className="badge bg-warning text-dark">{t('security_settings.setup_is_not_yet_complete')}</div>
+            <div className="badge bg-warning text-dark">
+              {t('security_settings.setup_is_not_yet_complete')}
+            </div>
           )}
         </div>
       </div>
       <h4>{t('security_settings.all_share_links')}</h4>
 
-      {(shareLinks.length !== 0) ? (
+      {shareLinks.length !== 0 ? (
         <>
           <Pager
             activePage={shareLinksActivePage}
@@ -139,10 +153,9 @@ const ShareLinkSetting = (props: ShareLinkSettingProps) => {
             isAdmin
           />
         </>
-      )
-        : (<p className="text-center">{t('security_settings.No_share_links')}</p>
-        )
-      }
+      ) : (
+        <p className="text-center">{t('security_settings.No_share_links')}</p>
+      )}
 
       <button
         className="pull-right btn btn-danger mt-2"
@@ -158,7 +171,6 @@ const ShareLinkSetting = (props: ShareLinkSettingProps) => {
         onClose={() => setIsDeleteConfirmModalShown(false)}
         onClickDeleteButton={deleteAllLinksButtonHandler}
       />
-
     </>
   );
 };
@@ -166,6 +178,8 @@ const ShareLinkSetting = (props: ShareLinkSettingProps) => {
 /**
  * Wrapper component for using unstated
  */
-const ShareLinkSettingWrapper = withUnstatedContainers(ShareLinkSetting, [AdminGeneralSecurityContainer]);
+const ShareLinkSettingWrapper = withUnstatedContainers(ShareLinkSetting, [
+  AdminGeneralSecurityContainer,
+]);
 
 export default ShareLinkSettingWrapper;

+ 0 - 4
biome.json

@@ -32,16 +32,12 @@
       "!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/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/Bookmarks",
       "!apps/app/src/client/components/DescendantsPageListModal",
       "!apps/app/src/client/components/InAppNotification",