From 6bca83f70a83a2cdaadcab4f006bf6b1fe5a588d Mon Sep 17 00:00:00 2001 From: LTbinglingfeng Date: Sat, 13 Jun 2026 05:20:30 +0800 Subject: [PATCH] refactor(plugins): enhance plugin configuration handling and improve error messages --- src/features/plugins/PluginsPage.tsx | 101 ++++++++++++++++----------- src/i18n/locales/en.json | 2 + src/i18n/locales/zh-CN.json | 2 + src/i18n/locales/zh-TW.json | 2 + src/services/api/plugins.ts | 13 +++- src/types/plugin.ts | 2 + 6 files changed, 79 insertions(+), 43 deletions(-) diff --git a/src/features/plugins/PluginsPage.tsx b/src/features/plugins/PluginsPage.tsx index 4429035..fdf7cd5 100644 --- a/src/features/plugins/PluginsPage.tsx +++ b/src/features/plugins/PluginsPage.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useMemo, useState, type ChangeEvent } from 'react'; +import { useCallback, useEffect, useMemo, useRef, useState, type ChangeEvent } from 'react'; import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; import { Button } from '@/components/ui/Button'; @@ -21,7 +21,12 @@ import { useHeaderRefresh } from '@/hooks/useHeaderRefresh'; import { pluginsApi } from '@/services/api'; import { useAuthStore, useConfigStore, useNotificationStore } from '@/stores'; import { getErrorMessage, isRecord } from '@/utils/helpers'; -import type { PluginConfigField, PluginListEntry, PluginListResponse } from '@/types'; +import type { + PluginConfigField, + PluginConfigObject, + PluginListEntry, + PluginListResponse, +} from '@/types'; import { getPluginTitle, resolvePluginAssetURL } from './pluginResources'; import styles from './PluginsPage.module.scss'; @@ -45,29 +50,11 @@ function PluginCardLogo({ src }: { src: string }) { ); } -const cloneRecord = (value: unknown): Record => - isRecord(value) ? { ...value } : {}; - const hasStatus = (error: unknown, status: number) => isRecord(error) && error.status === status; const normalizeFieldType = (field: PluginConfigField) => field.type.trim().toLowerCase(); -const getPluginsConfigMap = (rawConfig: Record): Record => { - const plugins = rawConfig.plugins; - if (!isRecord(plugins)) return {}; - const configs = plugins.configs; - return isRecord(configs) ? configs : {}; -}; - -const getPluginRawConfig = ( - rawConfig: Record, - pluginID: string -): Record => { - const configs = getPluginsConfigMap(rawConfig); - return cloneRecord(configs[pluginID]); -}; - const stringifyArrayItem = (value: unknown): string => { if (value === undefined || value === null) return ''; if (typeof value === 'string') return value; @@ -98,7 +85,7 @@ const getFieldDraftValue = (field: PluginConfigField, value: unknown): PluginDra const buildDraft = ( plugin: PluginListEntry, - currentConfig: Record + currentConfig: PluginConfigObject ): PluginConfigDraft => { const enabled = typeof currentConfig.enabled === 'boolean' ? currentConfig.enabled : plugin.enabled; const priority = @@ -146,11 +133,11 @@ const parseJSONField = ( const buildConfigPayload = ( draft: PluginConfigDraft, fields: PluginConfigField[], - currentConfig: Record, + currentConfig: PluginConfigObject, t: (key: string, options?: Record) => string ) => { const errors: Record = {}; - const nextConfig: Record = { ...currentConfig }; + const nextConfig: PluginConfigObject = { ...currentConfig }; const priorityText = draft.priority.trim(); nextConfig.enabled = draft.enabled; @@ -235,18 +222,19 @@ export function PluginsPage() { const navigate = useNavigate(); const connectionStatus = useAuthStore((state) => state.connectionStatus); const apiBase = useAuthStore((state) => state.apiBase); - const fetchConfig = useConfigStore((state) => state.fetchConfig); const clearConfigCache = useConfigStore((state) => state.clearCache); const showNotification = useNotificationStore((state) => state.showNotification); const [data, setData] = useState(null); - const [rawConfig, setRawConfig] = useState>({}); const [filter, setFilter] = useState(''); const [loading, setLoading] = useState(true); const [error, setError] = useState(''); const [editingPlugin, setEditingPlugin] = useState(null); + const [editingConfig, setEditingConfig] = useState({}); const [draft, setDraft] = useState(null); const [mutatingID, setMutatingID] = useState(''); + const [openingConfigID, setOpeningConfigID] = useState(''); + const configRequestSeq = useRef(0); const connected = connectionStatus === 'connected'; @@ -260,12 +248,8 @@ export function PluginsPage() { setLoading(true); setError(''); try { - const [plugins, config] = await Promise.all([ - pluginsApi.list(), - fetchConfig(undefined, true).catch(() => null), - ]); + const plugins = await pluginsApi.list(); setData(plugins); - setRawConfig(config?.raw ?? {}); } catch (err: unknown) { setError( hasStatus(err, 404) @@ -275,7 +259,7 @@ export function PluginsPage() { } finally { setLoading(false); } - }, [connected, fetchConfig, t]); + }, [connected, t]); useHeaderRefresh(loadPlugins, connected); @@ -320,15 +304,48 @@ export function PluginsPage() { [apiBase] ); - const openConfigSheet = (plugin: PluginListEntry) => { - const currentConfig = getPluginRawConfig(rawConfig, plugin.id); + const openConfigSheet = async (plugin: PluginListEntry) => { + if (openingConfigID || mutatingID) return; + + const requestSeq = configRequestSeq.current + 1; + configRequestSeq.current = requestSeq; + setOpeningConfigID(plugin.id); setEditingPlugin(plugin); - setDraft(buildDraft(plugin, currentConfig)); + setEditingConfig({}); + setDraft(null); + + try { + const currentConfig = await pluginsApi.getConfig(plugin.id); + if (configRequestSeq.current !== requestSeq) return; + + setEditingConfig(currentConfig); + setDraft(buildDraft(plugin, currentConfig)); + } catch (err: unknown) { + if (configRequestSeq.current !== requestSeq) return; + + setEditingPlugin(null); + setEditingConfig({}); + setDraft(null); + showNotification( + hasStatus(err, 404) + ? t('plugin_management.config_not_found') + : `${t('plugin_management.config_load_failed')}: ${getErrorMessage( + err, + t('plugin_management.config_load_failed') + )}`, + 'error' + ); + } finally { + if (configRequestSeq.current === requestSeq) { + setOpeningConfigID(''); + } + } }; const closeConfigSheet = () => { - if (mutatingID) return; + if (mutatingID || openingConfigID) return; setEditingPlugin(null); + setEditingConfig({}); setDraft(null); }; @@ -357,12 +374,11 @@ export function PluginsPage() { }; const handleSaveConfig = async () => { - if (!editingPlugin || !draft) return; - const currentConfig = getPluginRawConfig(rawConfig, editingPlugin.id); + if (!editingPlugin || !draft || openingConfigID || mutatingID) return; const { nextConfig, errors } = buildConfigPayload( draft, editingPlugin.configFields, - currentConfig, + editingConfig, t ); @@ -378,6 +394,7 @@ export function PluginsPage() { clearConfigCache(); await loadPlugins(); setEditingPlugin(null); + setEditingConfig({}); setDraft(null); showNotification(t('plugin_management.save_success'), 'success'); } catch (err: unknown) { @@ -706,7 +723,8 @@ export function PluginsPage() { {visiblePlugins.map((plugin) => { const logo = resolvePluginAsset(plugin.logo || plugin.metadata?.logo || ''); const github = plugin.metadata?.githubRepository.trim(); - const mutating = mutatingID === plugin.id; + const openingConfig = openingConfigID === plugin.id; + const actionBusy = Boolean(mutatingID || openingConfigID); const version = plugin.metadata?.version; const author = plugin.metadata?.author; @@ -780,14 +798,15 @@ export function PluginsPage() { handleTogglePlugin(plugin, enabled)} - disabled={!connected || mutating} + disabled={!connected || actionBusy} ariaLabel={t('plugin_management.enabled')} />