From 42c0e7be06f58fc57d368a8d2c937e0a322c8258 Mon Sep 17 00:00:00 2001 From: LTbinglingfeng Date: Mon, 25 May 2026 23:52:50 +0800 Subject: [PATCH] fix(providers): confirm discarding unsaved edits on category switch Previously confirmDiscardIfDirty only fired for Sheet-driven close paths (Cancel button, backdrop, escape). Clicking a different provider in the left rail bypassed the prompt and silently dropped the form changes. ProviderSheet now exposes the existing guard via an imperative handle, which the workbench page calls before swapping the active brand. --- .../providers/ProvidersWorkbenchPage.tsx | 26 ++++++++++++------- .../providers/sheets/ProviderSheet.tsx | 10 ++++++- 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/src/features/providers/ProvidersWorkbenchPage.tsx b/src/features/providers/ProvidersWorkbenchPage.tsx index f8d704a..28feb8a 100644 --- a/src/features/providers/ProvidersWorkbenchPage.tsx +++ b/src/features/providers/ProvidersWorkbenchPage.tsx @@ -1,4 +1,4 @@ -import { useCallback, useMemo, useState } from 'react'; +import { useCallback, useMemo, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { usePageTransitionLayer } from '@/components/common/PageTransitionLayer'; import { useHeaderRefresh } from '@/hooks/useHeaderRefresh'; @@ -15,7 +15,7 @@ import type { OpenAISortBy, SortDir, } from './components/OpenAIBrandToolbar'; -import { ProviderSheet } from './sheets/ProviderSheet'; +import { ProviderSheet, type ProviderSheetHandle } from './sheets/ProviderSheet'; import { useProviderWorkbench } from './useProviderWorkbench'; import type { ProviderBrand, ProviderResource } from './types'; import styles from './ProvidersWorkbenchPage.module.scss'; @@ -81,6 +81,7 @@ export function ProvidersWorkbenchPage() { mode: 'detail', resource: null, }); + const sheetRef = useRef(null); const connected = connectionStatus === 'connected'; const { usageByProvider, refreshRecentRequests } = useProviderRecentRequests({ @@ -372,13 +373,19 @@ export function ProvidersWorkbenchPage() { groups={groups} activeBrand={activeGroup.id} onSelect={(brand) => { - setActiveBrand(brand); - setFilter(''); - setOpenaiSelectedModels(new Set()); - // 关闭 Sheet 以避免数据错位 - if (sheetState.open && sheetState.brand !== brand) { - closeSheet(); - } + const isSwitching = sheetState.open && sheetState.brand !== brand; + const proceed = isSwitching && sheetRef.current + ? sheetRef.current.confirmDiscardIfDirty() + : Promise.resolve(true); + void proceed.then((ok) => { + if (!ok) return; + setActiveBrand(brand); + setFilter(''); + setOpenaiSelectedModels(new Set()); + if (isSwitching) { + closeSheet(); + } + }); }} /> { diff --git a/src/features/providers/sheets/ProviderSheet.tsx b/src/features/providers/sheets/ProviderSheet.tsx index 4813c9d..50c6427 100644 --- a/src/features/providers/sheets/ProviderSheet.tsx +++ b/src/features/providers/sheets/ProviderSheet.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useId, useState } from 'react'; +import { useCallback, useEffect, useId, useImperativeHandle, useState, type Ref } from 'react'; import { useTranslation } from 'react-i18next'; import { Sheet } from '@/components/ui/Sheet'; import { IconLoader2, IconPencil } from '@/components/ui/icons'; @@ -24,6 +24,10 @@ export interface ProviderSheetState { resource: ProviderResource | null; } +export interface ProviderSheetHandle { + confirmDiscardIfDirty: () => Promise; +} + interface ProviderSheetProps { state: ProviderSheetState; onClose: () => void; @@ -31,6 +35,7 @@ interface ProviderSheetProps { workbench: UseProviderWorkbenchResult; onCreated: () => void; onUpdated: () => void; + ref?: Ref; } export function ProviderSheet({ @@ -40,6 +45,7 @@ export function ProviderSheet({ workbench, onCreated, onUpdated, + ref, }: ProviderSheetProps) { const { t } = useTranslation(); const { showConfirmation } = useNotificationStore(); @@ -79,6 +85,8 @@ export function ProviderSheet({ }); }, [isDirty, isEditingForm, showConfirmation, submitting, t]); + useImperativeHandle(ref, () => ({ confirmDiscardIfDirty }), [confirmDiscardIfDirty]); + const handleCancelClick = useCallback(() => { void confirmDiscardIfDirty().then((ok) => { if (ok) onClose();