From f963d58e6a61f6c7bea17d0748762f5a10ec5149 Mon Sep 17 00:00:00 2001 From: Jason Date: Fri, 17 Oct 2025 17:49:16 +0800 Subject: [PATCH] refactor: extract business logic to useProviderActions hook MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Major improvements: - Create `src/hooks/useProviderActions.ts` (147 lines) - Consolidate provider operations (add, update, delete, switch) - Extract Claude plugin sync logic - Extract usage script save logic - Simplify `App.tsx` (347 → 226 lines, -35%) - Remove 8 callback functions - Remove Claude plugin sync logic - Remove usage script save logic - Cleaner and more maintainable - Replace `onNotify` prop with `toast` in: - `UsageScriptModal.tsx` - `McpPanel.tsx` - `McpFormModal.tsx` - `McpWizardModal.tsx` - Unified notification system using sonner Benefits: - Reduced coupling and improved maintainability - Business logic isolated in hooks, easier to test - Consistent notification system across the app --- src/App.tsx | 211 ++++++-------------------- src/components/UsageScriptModal.tsx | 28 ++-- src/components/mcp/McpFormModal.tsx | 24 ++- src/components/mcp/McpPanel.tsx | 37 ++--- src/components/mcp/McpWizardModal.tsx | 13 +- src/hooks/useProviderActions.ts | 147 ++++++++++++++++++ 6 files changed, 234 insertions(+), 226 deletions(-) create mode 100644 src/hooks/useProviderActions.ts diff --git a/src/App.tsx b/src/App.tsx index ccf1367..0188a09 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,22 +1,11 @@ -import { useCallback, useEffect, useMemo, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { toast } from "sonner"; -import { useQueryClient } from "@tanstack/react-query"; import { Plus, Settings } from "lucide-react"; -import type { Provider, UsageScript } from "@/types"; -import { - useProvidersQuery, - useAddProviderMutation, - useUpdateProviderMutation, - useDeleteProviderMutation, - useSwitchProviderMutation, -} from "@/lib/query"; -import { - providersApi, - settingsApi, - type AppType, - type ProviderSwitchEvent, -} from "@/lib/api"; +import type { Provider } from "@/types"; +import { useProvidersQuery } from "@/lib/query"; +import { providersApi, settingsApi, type AppType, type ProviderSwitchEvent } from "@/lib/api"; +import { useProviderActions } from "@/hooks/useProviderActions"; import { extractErrorMessage } from "@/utils/errorUtils"; import { AppSwitcher } from "@/components/AppSwitcher"; import { ModeToggle } from "@/components/mode-toggle"; @@ -32,7 +21,6 @@ import { Button } from "@/components/ui/button"; function App() { const { t } = useTranslation(); - const queryClient = useQueryClient(); const [activeApp, setActiveApp] = useState("claude"); const [isSettingsOpen, setIsSettingsOpen] = useState(false); @@ -46,11 +34,16 @@ function App() { const providers = useMemo(() => data?.providers ?? {}, [data]); const currentProviderId = data?.currentProviderId ?? ""; - const addProviderMutation = useAddProviderMutation(activeApp); - const updateProviderMutation = useUpdateProviderMutation(activeApp); - const deleteProviderMutation = useDeleteProviderMutation(activeApp); - const switchProviderMutation = useSwitchProviderMutation(activeApp); + // 🎯 使用 useProviderActions Hook 统一管理所有 Provider 操作 + const { + addProvider, + updateProvider, + switchProvider, + deleteProvider, + saveUsageScript, + } = useProviderActions(activeApp); + // 监听来自托盘菜单的切换事件 useEffect(() => { let unsubscribe: (() => void) | undefined; @@ -74,154 +67,42 @@ function App() { }; }, [activeApp, refetch]); - const handleNotify = useCallback( - (message: string, type: "success" | "error", duration?: number) => { - const options = duration ? { duration } : undefined; - if (type === "error") { - toast.error(message, options); - } else { - toast.success(message, options); - } - }, - [], - ); - - const handleOpenWebsite = useCallback( - async (url: string) => { - try { - await settingsApi.openExternal(url); - } catch (error) { - const detail = - extractErrorMessage(error) || - t("notifications.openLinkFailed", { - defaultValue: "链接打开失败", - }); - toast.error(detail); - } - }, - [t], - ); - - const handleAddProvider = useCallback( - async (provider: Omit) => { - await addProviderMutation.mutateAsync(provider); - }, - [addProviderMutation], - ); - - const handleEditProvider = useCallback( - async (provider: Provider) => { - try { - await updateProviderMutation.mutateAsync(provider); - await providersApi.updateTrayMenu(); - setEditingProvider(null); - } catch { - // 错误提示由 mutation 统一处理 - } - }, - [updateProviderMutation], - ); - - const handleSyncClaudePlugin = useCallback( - async (provider: Provider) => { - if (activeApp !== "claude") return; - - try { - const settings = await settingsApi.get(); - if (!settings?.enableClaudePluginIntegration) { - return; - } - - const isOfficial = provider.category === "official"; - await settingsApi.applyClaudePluginConfig({ official: isOfficial }); - - toast.success( - isOfficial - ? t("notifications.appliedToClaudePlugin", { - defaultValue: "已同步为官方配置", - }) - : t("notifications.removedFromClaudePlugin", { - defaultValue: "已移除 Claude 插件配置", - }), - { duration: 2200 }, - ); - } catch (error) { - const detail = - extractErrorMessage(error) || - t("notifications.syncClaudePluginFailed", { - defaultValue: "同步 Claude 插件失败", - }); - toast.error(detail, { duration: 4200 }); - } - }, - [activeApp, t], - ); - - const handleSwitchProvider = useCallback( - async (provider: Provider) => { - try { - await switchProviderMutation.mutateAsync(provider.id); - await handleSyncClaudePlugin(provider); - } catch { - // 错误提示由 mutation 与同步函数处理 - } - }, - [switchProviderMutation, handleSyncClaudePlugin], - ); - - const handleRequestDelete = useCallback((provider: Provider) => { - setConfirmDelete(provider); - }, []); - - const handleConfirmDelete = useCallback(async () => { - if (!confirmDelete) return; + // 打开网站链接 + const handleOpenWebsite = async (url: string) => { try { - await deleteProviderMutation.mutateAsync(confirmDelete.id); - } finally { - setConfirmDelete(null); + await settingsApi.openExternal(url); + } catch (error) { + const detail = + extractErrorMessage(error) || + t("notifications.openLinkFailed", { + defaultValue: "链接打开失败", + }); + toast.error(detail); } - }, [confirmDelete, deleteProviderMutation]); + }; - const handleImportSuccess = useCallback(async () => { + // 编辑供应商 + const handleEditProvider = async (provider: Provider) => { + await updateProvider(provider); + setEditingProvider(null); + }; + + // 确认删除供应商 + const handleConfirmDelete = async () => { + if (!confirmDelete) return; + await deleteProvider(confirmDelete.id); + setConfirmDelete(null); + }; + + // 导入配置成功后刷新 + const handleImportSuccess = async () => { await refetch(); try { await providersApi.updateTrayMenu(); } catch (error) { console.error("[App] Failed to refresh tray menu", error); } - }, [refetch]); - - const handleSaveUsageScript = useCallback( - async (provider: Provider, script: UsageScript) => { - try { - const updatedProvider: Provider = { - ...provider, - meta: { - ...provider.meta, - usage_script: script, - }, - }; - - await providersApi.update(updatedProvider, activeApp); - await queryClient.invalidateQueries({ - queryKey: ["providers", activeApp], - }); - toast.success( - t("provider.usageSaved", { - defaultValue: "用量查询配置已保存", - }), - ); - } catch (error) { - const detail = - extractErrorMessage(error) || - t("provider.usageSaveFailed", { - defaultValue: "用量查询配置保存失败", - }); - toast.error(detail); - } - }, - [activeApp, queryClient, t], - ); + }; return (
@@ -271,9 +152,9 @@ function App() { currentProviderId={currentProviderId} appType={activeApp} isLoading={isLoading} - onSwitch={handleSwitchProvider} + onSwitch={switchProvider} onEdit={setEditingProvider} - onDelete={handleRequestDelete} + onDelete={setConfirmDelete} onConfigureUsage={setUsageProvider} onOpenWebsite={handleOpenWebsite} onCreate={() => setIsAddOpen(true)} @@ -285,7 +166,7 @@ function App() { open={isAddOpen} onOpenChange={setIsAddOpen} appType={activeApp} - onSubmit={handleAddProvider} + onSubmit={addProvider} /> setUsageProvider(null)} onSave={(script) => { - void handleSaveUsageScript(usageProvider, script); + void saveUsageScript(usageProvider, script); }} - onNotify={handleNotify} /> )} @@ -338,7 +218,6 @@ function App() { open={isMcpOpen} onOpenChange={setIsMcpOpen} appType={activeApp} - onNotify={handleNotify} />
); diff --git a/src/components/UsageScriptModal.tsx b/src/components/UsageScriptModal.tsx index d6d2c02..e7ab09e 100644 --- a/src/components/UsageScriptModal.tsx +++ b/src/components/UsageScriptModal.tsx @@ -1,5 +1,6 @@ import React, { useState } from "react"; import { Play, Wand2 } from "lucide-react"; +import { toast } from "sonner"; import { Provider, UsageScript } from "../types"; import { usageApi, type AppType } from "@/lib/api"; import JsonEditor from "./JsonEditor"; @@ -21,11 +22,6 @@ interface UsageScriptModalProps { isOpen: boolean; onClose: () => void; onSave: (script: UsageScript) => void; - onNotify?: ( - message: string, - type: "success" | "error", - duration?: number, - ) => void; } // 预设模板(JS 对象字面量格式) @@ -91,7 +87,6 @@ const UsageScriptModal: React.FC = ({ isOpen, onClose, onSave, - onNotify, }) => { const [script, setScript] = useState(() => { return ( @@ -109,19 +104,18 @@ const UsageScriptModal: React.FC = ({ const handleSave = () => { // 验证脚本格式 if (script.enabled && !script.code.trim()) { - onNotify?.("脚本配置不能为空", "error"); + toast.error("脚本配置不能为空"); return; } // 基本的 JS 语法检查(检查是否包含 return 语句) if (script.enabled && !script.code.includes("return")) { - onNotify?.("脚本必须包含 return 语句", "error", 5000); + toast.error("脚本必须包含 return 语句", { duration: 5000 }); return; } onSave(script); onClose(); - onNotify?.("用量查询配置已保存", "success", 2000); }; const handleTest = async () => { @@ -136,12 +130,16 @@ const UsageScriptModal: React.FC = ({ return `${planInfo} 剩余: ${plan.remaining} ${plan.unit}`; }) .join(", "); - onNotify?.(`测试成功!${summary}`, "success", 3000); + toast.success(`测试成功!${summary}`, { duration: 3000 }); } else { - onNotify?.(`测试失败: ${result.error || "无数据返回"}`, "error", 5000); + toast.error(`测试失败: ${result.error || "无数据返回"}`, { + duration: 5000, + }); } } catch (error: any) { - onNotify?.(`测试失败: ${error?.message || "未知错误"}`, "error", 5000); + toast.error(`测试失败: ${error?.message || "未知错误"}`, { + duration: 5000, + }); } finally { setTesting(false); } @@ -158,9 +156,11 @@ const UsageScriptModal: React.FC = ({ printWidth: 80, }); setScript({ ...script, code: formatted.trim() }); - onNotify?.("格式化成功", "success", 1000); + toast.success("格式化成功", { duration: 1000 }); } catch (error: any) { - onNotify?.(`格式化失败: ${error?.message || "语法错误"}`, "error", 3000); + toast.error(`格式化失败: ${error?.message || "语法错误"}`, { + duration: 3000, + }); } }; diff --git a/src/components/mcp/McpFormModal.tsx b/src/components/mcp/McpFormModal.tsx index 6f8a8aa..e3a4143 100644 --- a/src/components/mcp/McpFormModal.tsx +++ b/src/components/mcp/McpFormModal.tsx @@ -1,5 +1,6 @@ import React, { useMemo, useState, useEffect } from "react"; import { useTranslation } from "react-i18next"; +import { toast } from "sonner"; import { Save, AlertCircle, @@ -36,11 +37,6 @@ interface McpFormModalProps { ) => Promise; onClose: () => void; existingIds?: string[]; - onNotify?: ( - message: string, - type: "success" | "error", - duration?: number, - ) => void; } /** @@ -55,7 +51,6 @@ const McpFormModal: React.FC = ({ onSave, onClose, existingIds = [], - onNotify, }) => { const { t } = useTranslation(); const { formatTomlError, validateTomlConfig, validateJsonConfig } = @@ -278,7 +273,7 @@ const McpFormModal: React.FC = ({ const handleSubmit = async () => { const trimmedId = formId.trim(); if (!trimmedId) { - onNotify?.(t("mcp.error.idRequired"), "error", 3000); + toast.error(t("mcp.error.idRequired"), { duration: 3000 }); return; } @@ -296,7 +291,7 @@ const McpFormModal: React.FC = ({ const tomlError = validateTomlConfig(formConfig); setConfigError(tomlError); if (tomlError) { - onNotify?.(t("mcp.error.tomlInvalid"), "error", 3000); + toast.error(t("mcp.error.tomlInvalid"), { duration: 3000 }); return; } @@ -313,7 +308,7 @@ const McpFormModal: React.FC = ({ } catch (e: any) { const msg = e?.message || String(e); setConfigError(formatTomlError(msg)); - onNotify?.(t("mcp.error.tomlInvalid"), "error", 4000); + toast.error(t("mcp.error.tomlInvalid"), { duration: 4000 }); return; } } @@ -322,7 +317,7 @@ const McpFormModal: React.FC = ({ const jsonError = validateJsonConfig(formConfig); setConfigError(jsonError); if (jsonError) { - onNotify?.(t("mcp.error.jsonInvalid"), "error", 3000); + toast.error(t("mcp.error.jsonInvalid"), { duration: 3000 }); return; } @@ -338,7 +333,7 @@ const McpFormModal: React.FC = ({ serverSpec = JSON.parse(formConfig) as McpServerSpec; } catch (e: any) { setConfigError(t("mcp.error.jsonInvalid")); - onNotify?.(t("mcp.error.jsonInvalid"), "error", 4000); + toast.error(t("mcp.error.jsonInvalid"), { duration: 4000 }); return; } } @@ -346,11 +341,11 @@ const McpFormModal: React.FC = ({ // 前置必填校验 if (serverSpec?.type === "stdio" && !serverSpec?.command?.trim()) { - onNotify?.(t("mcp.error.commandRequired"), "error", 3000); + toast.error(t("mcp.error.commandRequired"), { duration: 3000 }); return; } if (serverSpec?.type === "http" && !serverSpec?.url?.trim()) { - onNotify?.(t("mcp.wizard.urlRequired"), "error", 3000); + toast.error(t("mcp.wizard.urlRequired"), { duration: 3000 }); return; } @@ -408,7 +403,7 @@ const McpFormModal: React.FC = ({ const detail = extractErrorMessage(error); const mapped = translateMcpBackendError(detail, t); const msg = mapped || detail || t("mcp.error.saveFailed"); - onNotify?.(msg, "error", mapped || detail ? 6000 : 4000); + toast.error(msg, { duration: mapped || detail ? 6000 : 4000 }); } finally { setSaving(false); } @@ -678,7 +673,6 @@ const McpFormModal: React.FC = ({ isOpen={isWizardOpen} onClose={() => setIsWizardOpen(false)} onApply={handleWizardApply} - onNotify={onNotify} initialTitle={formId} initialServer={wizardInitialSpec} /> diff --git a/src/components/mcp/McpPanel.tsx b/src/components/mcp/McpPanel.tsx index 1aea617..cd4f3cb 100644 --- a/src/components/mcp/McpPanel.tsx +++ b/src/components/mcp/McpPanel.tsx @@ -1,5 +1,6 @@ import React, { useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; +import { toast } from "sonner"; import { Plus, Server, Check } from "lucide-react"; import { Button } from "@/components/ui/button"; import { @@ -13,16 +14,14 @@ import { McpServer } from "@/types"; import McpListItem from "./McpListItem"; import McpFormModal from "./McpFormModal"; import { ConfirmDialog } from "../ConfirmDialog"; -import { extractErrorMessage, translateMcpBackendError } from "@/utils/errorUtils"; +import { + extractErrorMessage, + translateMcpBackendError, +} from "@/utils/errorUtils"; interface McpPanelProps { open: boolean; onOpenChange: (open: boolean) => void; - onNotify?: ( - message: string, - type: "success" | "error", - duration?: number, - ) => void; appType: AppType; } @@ -33,7 +32,6 @@ interface McpPanelProps { const McpPanel: React.FC = ({ open, onOpenChange, - onNotify, appType, }) => { const { t } = useTranslation(); @@ -91,20 +89,18 @@ const McpPanel: React.FC = ({ try { // 后台调用 API await mcpApi.setEnabled(appType, id, enabled); - onNotify?.( + toast.success( enabled ? t("mcp.msg.enabled") : t("mcp.msg.disabled"), - "success", - 1500, + { duration: 1500 }, ); } catch (e: any) { // 失败时回滚 setServers(previousServers); const detail = extractErrorMessage(e); const mapped = translateMcpBackendError(detail, t); - onNotify?.( + toast.error( mapped || detail || t("mcp.error.saveFailed"), - "error", - mapped || detail ? 6000 : 5000, + { duration: mapped || detail ? 6000 : 5000 }, ); } }; @@ -129,14 +125,13 @@ const McpPanel: React.FC = ({ await mcpApi.deleteServerInConfig(appType, id); await reload(); setConfirmDialog(null); - onNotify?.(t("mcp.msg.deleted"), "success", 1500); + toast.success(t("mcp.msg.deleted"), { duration: 1500 }); } catch (e: any) { const detail = extractErrorMessage(e); const mapped = translateMcpBackendError(detail, t); - onNotify?.( + toast.error( mapped || detail || t("mcp.error.deleteFailed"), - "error", - mapped || detail ? 6000 : 5000, + { duration: mapped || detail ? 6000 : 5000 }, ); } }, @@ -156,14 +151,13 @@ const McpPanel: React.FC = ({ await reload(); setIsFormOpen(false); setEditingId(null); - onNotify?.(t("mcp.msg.saved"), "success", 1500); + toast.success(t("mcp.msg.saved"), { duration: 1500 }); } catch (e: any) { const detail = extractErrorMessage(e); const mapped = translateMcpBackendError(detail, t); - onNotify?.( + toast.error( mapped || detail || t("mcp.error.saveFailed"), - "error", - mapped || detail ? 6000 : 5000, + { duration: mapped || detail ? 6000 : 5000 }, ); // 继续抛出错误,让表单层可以给到直观反馈(避免被更高层遮挡) throw e; @@ -283,7 +277,6 @@ const McpPanel: React.FC = ({ existingIds={Object.keys(servers)} onSave={handleSave} onClose={handleCloseForm} - onNotify={onNotify} /> )} diff --git a/src/components/mcp/McpWizardModal.tsx b/src/components/mcp/McpWizardModal.tsx index 3c6a98d..43f8e75 100644 --- a/src/components/mcp/McpWizardModal.tsx +++ b/src/components/mcp/McpWizardModal.tsx @@ -1,5 +1,6 @@ import React, { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; +import { toast } from "sonner"; import { Save } from "lucide-react"; import { Button } from "@/components/ui/button"; import { @@ -15,11 +16,6 @@ interface McpWizardModalProps { isOpen: boolean; onClose: () => void; onApply: (title: string, json: string) => void; - onNotify?: ( - message: string, - type: "success" | "error", - duration?: number, - ) => void; initialTitle?: string; initialServer?: McpServerSpec; } @@ -80,7 +76,6 @@ const McpWizardModal: React.FC = ({ isOpen, onClose, onApply, - onNotify, initialTitle, initialServer, }) => { @@ -137,15 +132,15 @@ const McpWizardModal: React.FC = ({ const handleApply = () => { if (!wizardTitle.trim()) { - onNotify?.(t("mcp.error.idRequired"), "error", 3000); + toast.error(t("mcp.error.idRequired"), { duration: 3000 }); return; } if (wizardType === "stdio" && !wizardCommand.trim()) { - onNotify?.(t("mcp.error.commandRequired"), "error", 3000); + toast.error(t("mcp.error.commandRequired"), { duration: 3000 }); return; } if (wizardType === "http" && !wizardUrl.trim()) { - onNotify?.(t("mcp.wizard.urlRequired"), "error", 3000); + toast.error(t("mcp.wizard.urlRequired"), { duration: 3000 }); return; } diff --git a/src/hooks/useProviderActions.ts b/src/hooks/useProviderActions.ts new file mode 100644 index 0000000..e8856db --- /dev/null +++ b/src/hooks/useProviderActions.ts @@ -0,0 +1,147 @@ +import { useCallback } from "react"; +import { useQueryClient } from "@tanstack/react-query"; +import { toast } from "sonner"; +import { useTranslation } from "react-i18next"; +import { providersApi, settingsApi, type AppType } from "@/lib/api"; +import type { Provider, UsageScript } from "@/types"; +import { + useAddProviderMutation, + useUpdateProviderMutation, + useDeleteProviderMutation, + useSwitchProviderMutation, +} from "@/lib/query"; +import { extractErrorMessage } from "@/utils/errorUtils"; + +/** + * Hook for managing provider actions (add, update, delete, switch) + * Extracts business logic from App.tsx + */ +export function useProviderActions(activeApp: AppType) { + const { t } = useTranslation(); + const queryClient = useQueryClient(); + + const addProviderMutation = useAddProviderMutation(activeApp); + const updateProviderMutation = useUpdateProviderMutation(activeApp); + const deleteProviderMutation = useDeleteProviderMutation(activeApp); + const switchProviderMutation = useSwitchProviderMutation(activeApp); + + // Claude 插件同步逻辑 + const syncClaudePlugin = useCallback( + async (provider: Provider) => { + if (activeApp !== "claude") return; + + try { + const settings = await settingsApi.get(); + if (!settings?.enableClaudePluginIntegration) { + return; + } + + const isOfficial = provider.category === "official"; + await settingsApi.applyClaudePluginConfig({ official: isOfficial }); + + toast.success( + isOfficial + ? t("notifications.appliedToClaudePlugin", { + defaultValue: "已同步为官方配置", + }) + : t("notifications.removedFromClaudePlugin", { + defaultValue: "已移除 Claude 插件配置", + }), + { duration: 2200 }, + ); + } catch (error) { + const detail = + extractErrorMessage(error) || + t("notifications.syncClaudePluginFailed", { + defaultValue: "同步 Claude 插件失败", + }); + toast.error(detail, { duration: 4200 }); + } + }, + [activeApp, t], + ); + + // 添加供应商 + const addProvider = useCallback( + async (provider: Omit) => { + await addProviderMutation.mutateAsync(provider); + }, + [addProviderMutation], + ); + + // 更新供应商 + const updateProvider = useCallback( + async (provider: Provider) => { + await updateProviderMutation.mutateAsync(provider); + await providersApi.updateTrayMenu(); + }, + [updateProviderMutation], + ); + + // 切换供应商 + const switchProvider = useCallback( + async (provider: Provider) => { + try { + await switchProviderMutation.mutateAsync(provider.id); + await syncClaudePlugin(provider); + } catch { + // 错误提示由 mutation 与同步函数处理 + } + }, + [switchProviderMutation, syncClaudePlugin], + ); + + // 删除供应商 + const deleteProvider = useCallback( + async (id: string) => { + await deleteProviderMutation.mutateAsync(id); + }, + [deleteProviderMutation], + ); + + // 保存用量脚本 + const saveUsageScript = useCallback( + async (provider: Provider, script: UsageScript) => { + try { + const updatedProvider: Provider = { + ...provider, + meta: { + ...provider.meta, + usage_script: script, + }, + }; + + await providersApi.update(updatedProvider, activeApp); + await queryClient.invalidateQueries({ + queryKey: ["providers", activeApp], + }); + toast.success( + t("provider.usageSaved", { + defaultValue: "用量查询配置已保存", + }), + ); + } catch (error) { + const detail = + extractErrorMessage(error) || + t("provider.usageSaveFailed", { + defaultValue: "用量查询配置保存失败", + }); + toast.error(detail); + } + }, + [activeApp, queryClient, t], + ); + + return { + addProvider, + updateProvider, + switchProvider, + deleteProvider, + saveUsageScript, + isLoading: + addProviderMutation.isPending || + updateProviderMutation.isPending || + deleteProviderMutation.isPending || + switchProviderMutation.isPending, + }; +}