Browse Source

refactor Customize settings form

Yuki Takei 5 months ago
parent
commit
bdd319c9d9

+ 96 - 34
.serena/memories/admin-forms-migration-progress.md

@@ -38,50 +38,60 @@
 
 
 #### AdminCustomizeContainer 配下
 #### AdminCustomizeContainer 配下
 
 
-6. **CustomizeCssSetting.tsx** ✨ NEW
+6. **CustomizeCssSetting.tsx** ✨
    - パス: `apps/app/src/client/components/Admin/Customize/CustomizeCssSetting.tsx`
    - パス: `apps/app/src/client/components/Admin/Customize/CustomizeCssSetting.tsx`
    - 担当フィールド: カスタム CSS
    - 担当フィールド: カスタム CSS
    - 特記事項: textarea での大きなテキスト入力、空値更新が重要
    - 特記事項: textarea での大きなテキスト入力、空値更新が重要
    - テスト状況: ⏳ 未テスト(IME 入力、空値更新の確認が必要)
    - テスト状況: ⏳ 未テスト(IME 入力、空値更新の確認が必要)
 
 
-7. **CustomizeScriptSetting.tsx** ✨ NEW
+7. **CustomizeScriptSetting.tsx** ✨
    - パス: `apps/app/src/client/components/Admin/Customize/CustomizeScriptSetting.tsx`
    - パス: `apps/app/src/client/components/Admin/Customize/CustomizeScriptSetting.tsx`
    - 担当フィールド: カスタムスクリプト(JavaScript)
    - 担当フィールド: カスタムスクリプト(JavaScript)
    - 特記事項: Google Tag Manager の例を含む、空値更新が重要
    - 特記事項: Google Tag Manager の例を含む、空値更新が重要
    - テスト状況: ⏳ 未テスト(IME 入力、空値更新の確認が必要)
    - テスト状況: ⏳ 未テスト(IME 入力、空値更新の確認が必要)
 
 
-8. **CustomizeNoscriptSetting.tsx** ✨ NEW
+8. **CustomizeNoscriptSetting.tsx** ✨
    - パス: `apps/app/src/client/components/Admin/Customize/CustomizeNoscriptSetting.tsx`
    - パス: `apps/app/src/client/components/Admin/Customize/CustomizeNoscriptSetting.tsx`
    - 担当フィールド: カスタム noscript タグ(HTML)
    - 担当フィールド: カスタム noscript タグ(HTML)
    - 特記事項: Google Tag Manager の iframe 例を含む、空値更新が重要
    - 特記事項: Google Tag Manager の iframe 例を含む、空値更新が重要
    - テスト状況: ⏳ 未テスト(IME 入力、空値更新の確認が必要)
    - テスト状況: ⏳ 未テスト(IME 入力、空値更新の確認が必要)
 
 
+#### SWR Store ベース
+
+9. **CustomizeTitle.tsx** ✨ NEW
+   - パス: `apps/app/src/client/components/Admin/Customize/CustomizeTitle.tsx`
+   - 担当フィールド: カスタムタイトル(HTML title タグのテンプレート)
+   - 特記事項: Unstated Container ではなく SWR の `useCustomizeTitle` を使用
+   - テスト状況: ⏳ 未テスト(IME 入力の確認が必要)
+
 ### 🔄 移行対象候補(未着手)
 ### 🔄 移行対象候補(未着手)
 
 
 #### AdminCustomizeContainer 配下
 #### AdminCustomizeContainer 配下
 
 
-以下のコンポーネントは AdminCustomizeContainer を使用しているが、フォームの構造が異なる可能性があるため要調査:
+以下のコンポーネントは AdminCustomizeContainer を使用しているが、複雑な構造のため優先度低
 
 
-- `CustomizeFunctionSetting.tsx` - 機能設定(複数のチェックボックス/選択肢)
-- `CustomizePresentationSetting.tsx` - プレゼンテーション設定
-- その他の Customize 配下のコンポーネント
+- `CustomizeFunctionSetting.tsx` - 機能設定(複数のチェックボックス/選択肢、テキスト入力なし)
+- `CustomizePresentationSetting.tsx` - プレゼンテーション設定(チェックボックスのみ、テキスト入力なし)
 
 
 #### 他の Admin Container 配下
 #### 他の Admin Container 配下
 
 
+以下は複雑で大規模なため、後回し:
+
 - AdminSecurityContainer 配下のフォーム
 - AdminSecurityContainer 配下のフォーム
-  - `OidcSecuritySetting.jsx` とその Contents
-  - `SamlSecuritySetting.jsx` とその Contents
-  - `LdapSecuritySetting.jsx` とその Contents
-  - `GoogleSecuritySetting.jsx` とその Contents
-  - `GitHubSecuritySetting.jsx` とその Contents
-  - `LocalSecuritySetting.jsx` とその Contents
+  - `OidcSecuritySettingContents.jsx` - OIDC 設定(多数の input フィールド)
+  - `SamlSecuritySettingContents.jsx` - SAML 設定(textarea あり、複雑)
+  - `LdapSecuritySettingContents.jsx` - LDAP 設定(多数の input フィールド)
+  - `GoogleSecuritySettingContents.jsx`
+  - `GitHubSecuritySettingContents.jsx`
+  - `LocalSecuritySettingContents.jsx`
+
 - AdminMarkdownContainer 配下のフォーム
 - AdminMarkdownContainer 配下のフォーム
-  - `XssForm.jsx`
+  - `XssForm.jsx` - XSS 設定(クラスコンポーネント、複雑)
+  - `WhitelistInput.tsx` - ホワイトリスト入力(XssForm の子コンポーネント)
   - `LineBreakForm.jsx`
   - `LineBreakForm.jsx`
-  - その他の MarkdownSetting 配下のコンポーネント
-- AdminImportContainer 配下のフォーム
-- AdminExternalAccountsContainer 配下のフォーム
-- その他の Admin*Container 配下のフォーム
+
+- 画像アップロード関連(React Hook Form に不適)
+  - `CustomizeLogoSetting.tsx` - ロゴ画像のアップロードと切り抜き
 
 
 ### 📋 次のステップ
 ### 📋 次のステップ
 
 
@@ -89,18 +99,17 @@
    - CustomizeCssSetting の IME 入力テスト
    - CustomizeCssSetting の IME 入力テスト
    - CustomizeScriptSetting の IME 入力テスト
    - CustomizeScriptSetting の IME 入力テスト
    - CustomizeNoscriptSetting の IME 入力テスト
    - CustomizeNoscriptSetting の IME 入力テスト
+   - CustomizeTitle の IME 入力テスト
    - 空値更新のテスト(これらのフィールドは空にできることが重要)
    - 空値更新のテスト(これらのフィールドは空にできることが重要)
 
 
-2. **CustomizeFunctionSetting の調査と移行**
-   - より複雑なフォーム構造の可能性がある
-   - チェックボックスや選択肢の扱いを確認
+2. **他のシンプルなテキスト入力フォームを探す**
+   - Admin 配下で単純な input/textarea を持つコンポーネントを特定
+   - 優先順位: シンプル > デグレリスクが低い > 使用頻度が高い
 
 
-3. **Security 関連フォームの優先順位決定**
-   - よく使われる認証方式から優先的に移行
-   - LDAP, OIDC, SAML などは企業での利用が多い
-
-4. **Markdown 関連フォームの調査**
-   - XssForm.jsx と LineBreakForm.jsx を確認
+3. **複雑なフォームは後回し**
+   - Security 関連の大規模フォーム
+   - クラスコンポーネント
+   - 画像アップロード関連
 
 
 ## 発見した問題と解決策
 ## 発見した問題と解決策
 
 
@@ -122,9 +131,7 @@
 
 
 ## 移行パターンの確立
 ## 移行パターンの確立
 
 
-以下のパターンが確立されました:
-
-### 単一 textarea フィールドのパターン
+### パターン1: Container ベースの単一 textarea フィールド
 
 
 ```typescript
 ```typescript
 const {
 const {
@@ -158,10 +165,50 @@ return (
 );
 );
 ```
 ```
 
 
-このパターンは以下に適用可能:
-- CustomizeCssSetting (CSS)
-- CustomizeScriptSetting (JavaScript)
-- CustomizeNoscriptSetting (HTML/noscript)
+適用済み:
+- CustomizeCssSetting
+- CustomizeScriptSetting
+- CustomizeNoscriptSetting
+
+### パターン2: SWR Store ベースの単一 input フィールド
+
+```typescript
+const { data: storeData } = useStoreHook();
+
+const {
+  register,
+  handleSubmit,
+  reset,
+} = useForm();
+
+useEffect(() => {
+  reset({
+    fieldName: storeData ?? '',
+  });
+}, [storeData, reset]);
+
+const onSubmit = useCallback(async(data) => {
+  try {
+    await apiv3Put('/api/endpoint', {
+      fieldName: data.fieldName,
+    });
+    toastSuccess('...');
+  }
+  catch (err) {
+    toastError(err);
+  }
+}, []);
+
+return (
+  <form onSubmit={handleSubmit(onSubmit)}>
+    <input {...register('fieldName')} />
+    <AdminUpdateButtonRow type="submit" />
+  </form>
+);
+```
+
+適用済み:
+- CustomizeTitle
 
 
 ## 削除したファイル
 ## 削除したファイル
 
 
@@ -171,6 +218,21 @@ return (
 
 
 - ✅ `apps/app/src/client/components/Admin/Common/AdminUpdateButtonRow.tsx` - `type` prop を追加(submit/button/reset)
 - ✅ `apps/app/src/client/components/Admin/Common/AdminUpdateButtonRow.tsx` - `type` prop を追加(submit/button/reset)
 
 
+## 移行対象外(理由付き)
+
+### 複雑すぎるもの
+- **OidcSecuritySettingContents.jsx** - 10+ の input フィールド、条件付きレンダリング
+- **SamlSecuritySettingContents.jsx** - textarea + 多数の input、複雑なテーブルレイアウト
+- **LdapSecuritySettingContents.jsx** - 10+ の input フィールド、ドロップダウン、条件付きレンダリング
+- **XssForm.jsx** - クラスコンポーネント、ラジオボタン、子コンポーネント、条件付きレンダリング
+
+### React Hook Form に不適
+- **CustomizeLogoSetting.tsx** - 画像ファイルアップロード、画像切り抜き機能
+
+### テキスト入力がない
+- **CustomizeFunctionSetting.tsx** - チェックボックスとドロップダウンのみ
+- **CustomizePresentationSetting.tsx** - チェックボックスのみ
+
 ## ブランチ情報
 ## ブランチ情報
 
 
 - 作業ブランチ: `imprv/admin-form`
 - 作業ブランチ: `imprv/admin-form`

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

@@ -1,7 +1,8 @@
 import type { FC } from 'react';
 import type { FC } from 'react';
-import React, { useState } from 'react';
+import React, { useCallback, useEffect } from 'react';
 
 
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
+import { useForm } from 'react-hook-form';
 import { Card, CardBody } from 'reactstrap';
 import { Card, CardBody } from 'reactstrap';
 
 
 import { apiv3Put } from '~/client/util/apiv3-client';
 import { apiv3Put } from '~/client/util/apiv3-client';
@@ -16,19 +17,30 @@ export const CustomizeTitle: FC = () => {
 
 
   const { data: customizeTitle } = useCustomizeTitle();
   const { data: customizeTitle } = useCustomizeTitle();
 
 
-  const [currentCustomizeTitle, setCrrentCustomizeTitle] = useState(customizeTitle ?? '');
+  const {
+    register,
+    handleSubmit,
+    reset,
+  } = useForm();
 
 
-  const onClickSubmit = async() => {
+  // Sync form with store data
+  useEffect(() => {
+    reset({
+      customizeTitle: customizeTitle ?? '',
+    });
+  }, [customizeTitle, reset]);
+
+  const onSubmit = useCallback(async(data) => {
     try {
     try {
       await apiv3Put('/customize-setting/customize-title', {
       await apiv3Put('/customize-setting/customize-title', {
-        customizeTitle: currentCustomizeTitle,
+        customizeTitle: data.customizeTitle,
       });
       });
       toastSuccess(t('toaster.update_successed', { target: t('admin:customize_settings.custom_title'), ns: 'commons' }));
       toastSuccess(t('toaster.update_successed', { target: t('admin:customize_settings.custom_title'), ns: 'commons' }));
     }
     }
     catch (err) {
     catch (err) {
       toastError(err);
       toastError(err);
     }
     }
-  };
+  }, [t]);
 
 
   return (
   return (
     <React.Fragment>
     <React.Fragment>
@@ -64,16 +76,17 @@ export const CustomizeTitle: FC = () => {
           <br />
           <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>
         </div>
-        <div className="col-12">
-          <input
-            className="form-control"
-            value={currentCustomizeTitle}
-            onChange={(e) => { setCrrentCustomizeTitle(e.target.value) }}
-          />
-        </div>
-        <div className="col-12">
-          <AdminUpdateButtonRow onClick={onClickSubmit} disabled={false} />
-        </div>
+        <form onSubmit={handleSubmit(onSubmit)}>
+          <div className="col-12">
+            <input
+              className="form-control"
+              {...register('customizeTitle')}
+            />
+          </div>
+          <div className="col-12">
+            <AdminUpdateButtonRow type="submit" disabled={false} />
+          </div>
+        </form>
       </div>
       </div>
     </React.Fragment>
     </React.Fragment>
   );
   );