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

Change to using react hook form

arvid-e 2 месяцев назад
Родитель
Сommit
0a9a1b6ea3

+ 113 - 156
apps/app/src/client/components/Admin/MarkdownSetting/ContentDispositionSettings.tsx

@@ -1,236 +1,193 @@
-import React, {
-  useState, useCallback, useEffect, useMemo,
-} from 'react';
+import React, { useState, useCallback, useEffect } from 'react';
 
 import { useTranslation } from 'next-i18next';
+import { useForm } from 'react-hook-form';
 
 import { useContentDisposition, type ContentDispositionSettings } from '../../../services/AdminContentDispositionSettings';
 import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
 
+interface MimeTypeListProps {
+  title: string;
+  items: string[];
+  emptyText: string;
+  onRemove: (mimeType: string) => void;
+  removeLabel: string;
+  isUpdating: boolean;
+}
 
 const normalizeMimeType = (mimeType: string): string => mimeType.trim().toLowerCase();
 
-const removeMimeTypeFromArray = (array: string[], mimeType: string): string[] => (
-  array.filter(m => m !== mimeType)
+const MimeTypeList = ({
+  title, items, emptyText, onRemove, removeLabel, isUpdating,
+}: MimeTypeListProps) => (
+  <div className="col-md-6 col-sm-12 mb-4">
+    <div className="card shadow-sm rounded-3">
+      <div className="card-header bg-transparent fw-bold">{title}</div>
+      <div className="card-body">
+        <ul className="list-group list-group-flush">
+          {items.length === 0 && <li className="list-group-item text-muted small border-0">{emptyText}</li>}
+          {items.map((m: string) => (
+            <li key={m} className="list-group-item d-flex justify-content-between align-items-center border-0 px-0">
+              <code>{m}</code>
+              <button
+                type="button"
+                className="btn btn-sm btn-outline-danger rounded-3"
+                onClick={() => onRemove(m)}
+                disabled={isUpdating}
+              >
+                {removeLabel}
+              </button>
+            </li>
+          ))}
+        </ul>
+      </div>
+    </div>
+  </div>
 );
 
 const ContentDispositionSettings: React.FC = () => {
   const { t } = useTranslation('admin');
-
   const {
-    currentSettings,
-    isLoading,
-    isUpdating,
-    updateSettings,
+    currentSettings, isLoading, isUpdating, updateSettings,
   } = useContentDisposition();
 
-  const [pendingSettings, setPendingSettings] = useState<ContentDispositionSettings | null>(null);
   const [currentInput, setCurrentInput] = useState<string>('');
   const [error, setError] = useState<string | null>(null);
 
+  const {
+    handleSubmit,
+    setValue,
+    watch,
+    reset,
+    formState: { isDirty },
+  } = useForm<ContentDispositionSettings>({
+    defaultValues: {
+      inlineMimeTypes: [],
+      attachmentMimeTypes: [],
+    },
+  });
+
   useEffect(() => {
     if (currentSettings) {
-      setPendingSettings({
-        inlineMimeTypes: [...currentSettings.inlineMimeTypes],
-        attachmentMimeTypes: [...currentSettings.attachmentMimeTypes],
-      });
-      setError(null);
+      reset(currentSettings);
     }
-  }, [currentSettings]);
-
-  // Use the pending settings for display, falling back to an empty object if not loaded yet
-  const displaySettings = pendingSettings ?? { inlineMimeTypes: [], attachmentMimeTypes: [] };
-
-  // Calculate if there are differences between saved and pending state
-  const hasPendingChanges = useMemo(() => {
-    if (!currentSettings || !pendingSettings) return false;
-    // Check if the mime type lists have changed
-    return JSON.stringify(currentSettings.inlineMimeTypes.sort()) !== JSON.stringify(pendingSettings.inlineMimeTypes.sort())
-           || JSON.stringify(currentSettings.attachmentMimeTypes.sort()) !== JSON.stringify(pendingSettings.attachmentMimeTypes.sort());
-  }, [currentSettings, pendingSettings]);
+  }, [currentSettings, reset]);
 
+  const inlineMimeTypes = watch('inlineMimeTypes');
+  const attachmentMimeTypes = watch('attachmentMimeTypes');
 
   const handleSetMimeType = useCallback((disposition: 'inline' | 'attachment') => {
     const mimeType = normalizeMimeType(currentInput);
     if (!mimeType) return;
 
-    setError(null);
-    setPendingSettings((prev) => {
-      if (!prev) return null;
+    const otherDisposition = disposition === 'inline' ? 'attachment' : 'inline';
 
-      const newSettings = { ...prev };
-      const otherDisposition = disposition === 'inline' ? 'attachment' : 'inline';
+    const currentTargetList = watch(`${disposition}MimeTypes`);
+    const currentOtherList = watch(`${otherDisposition}MimeTypes`);
 
-      // Add to the target list (if not already present)
-      const targetKey = `${disposition}MimeTypes` as keyof ContentDispositionSettings;
-      if (!newSettings[targetKey].includes(mimeType)) {
-        newSettings[targetKey] = [...newSettings[targetKey], mimeType];
-      }
+    if (!currentTargetList.includes(mimeType)) {
+      setValue(`${disposition}MimeTypes`, [...currentTargetList, mimeType], { shouldDirty: true });
+    }
 
-      // Remove from the other list
-      const otherKey = `${otherDisposition}MimeTypes` as keyof ContentDispositionSettings;
-      newSettings[otherKey] = removeMimeTypeFromArray(newSettings[otherKey], mimeType);
+    setValue(
+      `${otherDisposition}MimeTypes`,
+      currentOtherList.filter(m => m !== mimeType),
+      { shouldDirty: true },
+    );
 
-      return newSettings;
-    });
     setCurrentInput('');
-  }, [currentInput]);
-
-  const handleSetInline = useCallback(() => handleSetMimeType('inline'), [handleSetMimeType]);
-  const handleSetAttachment = useCallback(() => handleSetMimeType('attachment'), [handleSetMimeType]);
-
-  // Handler for removing from pending state
-  const handleRemove = useCallback((mimeType: string, disposition: 'inline' | 'attachment') => {
     setError(null);
-    setPendingSettings((prev) => {
-      if (!prev) return null;
-      const key = `${disposition}MimeTypes` as keyof ContentDispositionSettings;
-      return {
-        ...prev,
-        [key]: removeMimeTypeFromArray(prev[key], mimeType),
-      };
-    });
-  }, []);
-
-  // Handler for updating pending change
-  const handleUpdate = useCallback(async(): Promise<void> => {
-    if (!pendingSettings || !hasPendingChanges || isUpdating) return;
+  }, [currentInput, setValue, watch]);
 
-    setError(null);
+  const handleRemove = useCallback((mimeType: string, disposition: 'inline' | 'attachment') => {
+    const currentList = watch(`${disposition}MimeTypes`);
+    setValue(
+      `${disposition}MimeTypes`,
+      currentList.filter(m => m !== mimeType),
+      { shouldDirty: true },
+    );
+  }, [setValue, watch]);
+
+  const onSubmit = async(data: ContentDispositionSettings) => {
     try {
-      await updateSettings(pendingSettings);
+      setError(null);
+      await updateSettings(data);
+      reset(data);
     }
     catch (err) {
-      const errorMessage = (err instanceof Error) ? err.message : 'An unknown error occurred during update.';
-      setError(`Failed to update settings: ${errorMessage}`);
+      setError((err as Error).message);
     }
-  }, [pendingSettings, hasPendingChanges, isUpdating, updateSettings]);
+  };
 
-  if (isLoading && !currentSettings) {
-    return <div>Loading content disposition settings...</div>;
-  }
-
-  const renderInlineMimeTypes = displaySettings.inlineMimeTypes;
-  const renderAttachmentMimeTypes = displaySettings.attachmentMimeTypes;
+  if (isLoading && !currentSettings) return <div>Loading...</div>;
 
   return (
     <div className="row">
       <div className="col-12">
-        <h2 className="pb-2">{t('markdown_settings.content-disposition_header')}</h2>
+        <h2 className="mb-4 border-0">{t('markdown_settings.content-disposition_header')}</h2>
 
-        {/* INPUT SECTION */}
-        <div className="card shadow-sm mb-4">
+        <div className="card shadow-sm mb-4 rounded-3 border-0">
           <div className="card-body">
             <div className="form-group">
-              <label className="form-label fw-bold">{t('markdown_settings.content-disposition_options.add_header')}</label>
-              <div className="d-flex align-items-center gap-2">
+              <label className="form-label fw-bold">
+                {t('markdown_settings.content-disposition_options.add_header')}
+              </label>
+              <div className="d-flex align-items-center gap-2 mb-3">
                 <input
                   type="text"
-                  className="form-control"
+                  className="form-control rounded-3 w-50"
                   value={currentInput}
                   onChange={e => setCurrentInput(e.target.value)}
                   placeholder="e.g. image/png"
                 />
                 <button
-                  className="btn btn-primary px-3 flex-shrink-0"
+                  className="btn btn-primary px-3 flex-shrink-0 rounded-3 fw-bold"
                   type="button"
-                  onClick={handleSetInline}
+                  onClick={() => handleSetMimeType('inline')}
                   disabled={!currentInput.trim() || isUpdating}
                 >
                   {t('markdown_settings.content-disposition_options.inline_button')}
                 </button>
                 <button
-                  className="btn btn-primary text-white px-3 flex-shrink-0"
+                  className="btn btn-primary text-white px-3 flex-shrink-0 rounded-3 fw-bold"
                   type="button"
-                  onClick={handleSetAttachment}
+                  onClick={() => handleSetMimeType('attachment')}
                   disabled={!currentInput.trim() || isUpdating}
                 >
                   {t('markdown_settings.content-disposition_options.attachment_button')}
                 </button>
               </div>
-              <small className="form-text text-muted mt-2 d-block">
+              <small className="form-text text-muted">
                 {t('markdown_settings.content-disposition_options.note')}
               </small>
             </div>
           </div>
         </div>
 
-        {error && (
-          <div className="alert alert-danger">{error}</div>
-        )}
+        {error && <div className="alert alert-danger rounded-3">{error}</div>}
 
         <div className="row">
-          {/* INLINE LIST COLUMN */}
-          <div className="col-md-6 col-sm-12 align-self-start">
-            <div className="card">
-              <div className="card-header">
-                <span className="fw-bold">
-                  {t('markdown_settings.content-disposition_options.inline_header')}
-                </span>
-              </div>
-              <div className="card-body">
-                <ul className="list-group list-group-flush">
-                  {renderInlineMimeTypes.length === 0 && (
-                    <li className="list-group-item text-muted">
-                      {t('markdown_settings.content-disposition_options.no_inline')}
-                    </li>
-                  )}
-                  {renderInlineMimeTypes.map((mimeType: string) => (
-                    <li key={mimeType} className="list-group-item d-flex justify-content-between align-items-center">
-                      <code>{mimeType}</code>
-                      <button
-                        type="button"
-                        className="btn btn-sm btn-outline-danger rounded-3"
-                        onClick={() => handleRemove(mimeType, 'inline')}
-                        disabled={isUpdating}
-                      >
-                        {t('markdown_settings.content-disposition_options.remove_button')}
-                      </button>
-                    </li>
-                  ))}
-                </ul>
-              </div>
-            </div>
-          </div>
-
-          {/* ATTACHMENT LIST COLUMN */}
-          <div className="col-md-6 col-sm-12 align-self-start">
-            <div className="card">
-              <div className="card-header">
-                <span className="fw-bold">
-                  {t('markdown_settings.content-disposition_options.attachment_header')}
-                </span>
-              </div>
-              <div className="card-body">
-                <ul className="list-group list-group-flush">
-                  {renderAttachmentMimeTypes.length === 0 && (
-                    <li className="list-group-item text-muted">
-                      {t('markdown_settings.content-disposition_options.no_attachment')}
-                    </li>
-                  )}
-                  {renderAttachmentMimeTypes.map((mimeType: string) => (
-                    <li key={mimeType} className="list-group-item d-flex justify-content-between align-items-center">
-                      <code>{mimeType}</code>
-                      <button
-                        type="button"
-                        className="btn btn-sm btn-outline-danger rounded-3"
-                        onClick={() => handleRemove(mimeType, 'attachment')}
-                        disabled={isUpdating}
-                      >
-                        {t('markdown_settings.content-disposition_options.remove_button')}
-                      </button>
-                    </li>
-                  ))}
-                </ul>
-              </div>
-            </div>
-          </div>
-
+          <MimeTypeList
+            title={t('markdown_settings.content-disposition_options.inline_header')}
+            items={inlineMimeTypes}
+            emptyText={t('markdown_settings.content-disposition_options.no_inline')}
+            onRemove={m => handleRemove(m, 'inline')}
+            removeLabel={t('markdown_settings.content-disposition_options.remove_button')}
+            isUpdating={isUpdating}
+          />
+          <MimeTypeList
+            title={t('markdown_settings.content-disposition_options.attachment_header')}
+            items={attachmentMimeTypes}
+            emptyText={t('markdown_settings.content-disposition_options.no_attachment')}
+            onRemove={m => handleRemove(m, 'attachment')}
+            removeLabel={t('markdown_settings.content-disposition_options.remove_button')}
+            isUpdating={isUpdating}
+          />
         </div>
 
         <AdminUpdateButtonRow
-          onClick={handleUpdate}
-          disabled={!hasPendingChanges || isUpdating}
+          onClick={handleSubmit(onSubmit)}
+          disabled={!isDirty || isUpdating}
         />
       </div>
     </div>