Files
cc-switch/src/components/providers/EditProviderDialog.tsx
Jason 0778347f84 refactor(endpoints): implement deferred submission and fix clear-all bug
Implement Solution A (complete deferred submission) for custom endpoint
management, replacing the dual-mode system with unified local staging.

Changes:
- Remove immediate backend saves from EndpointSpeedTest
  * handleAddEndpoint: local state update only
  * handleRemoveEndpoint: local state update only
  * handleSelect: remove lastUsed timestamp update
- Add explicit clear detection in ProviderForm
  * Distinguish "user cleared endpoints" from "user didn't modify"
  * Pass empty object {} as clear signal vs null for no-change
- Fix mergeProviderMeta to handle three distinct cases:
  * null/undefined: don't modify endpoints (no meta sent)
  * empty object {}: explicitly clear endpoints (send empty meta)
  * with data: add/update endpoints (overwrite)

Fixed Critical Bug:
When users deleted all custom endpoints, changes were not saved because:
- draftCustomEndpoints=[] resulted in customEndpointsToSave=null
- mergeProviderMeta(meta, null) returned undefined
- Backend interpreted missing meta as "don't modify", preserving old values

Solution:
Detect when user had endpoints and cleared them (hadEndpoints && length===0),
then pass empty object to mergeProviderMeta as explicit clear signal.

Architecture Improvements:
- Transaction atomicity: all fields submitted together on form save
- UX consistency: add/edit modes behave identically
- Cancel button: true rollback with no immediate saves
- Code simplification: removed ~40 lines of immediate save error handling

Testing:
- TypeScript type check: passed
- Rust backend tests: 10/10 passed
- Build: successful
2025-11-04 15:30:54 +08:00

155 lines
4.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useCallback, useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { Save } from "lucide-react";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import type { Provider } from "@/types";
import {
ProviderForm,
type ProviderFormValues,
} from "@/components/providers/forms/ProviderForm";
import { providersApi, vscodeApi, type AppId } from "@/lib/api";
interface EditProviderDialogProps {
open: boolean;
provider: Provider | null;
onOpenChange: (open: boolean) => void;
onSubmit: (provider: Provider) => Promise<void> | void;
appId: AppId;
}
export function EditProviderDialog({
open,
provider,
onOpenChange,
onSubmit,
appId,
}: EditProviderDialogProps) {
const { t } = useTranslation();
// 默认使用传入的 provider.settingsConfig若当前编辑对象是“当前生效供应商”则尝试读取实时配置替换初始值
const [liveSettings, setLiveSettings] = useState<Record<
string,
unknown
> | null>(null);
useEffect(() => {
let cancelled = false;
const load = async () => {
if (!open || !provider) {
setLiveSettings(null);
return;
}
try {
const currentId = await providersApi.getCurrent(appId);
if (currentId && provider.id === currentId) {
try {
const live = (await vscodeApi.getLiveProviderSettings(
appId,
)) as Record<string, unknown>;
if (!cancelled && live && typeof live === "object") {
setLiveSettings(live);
}
} catch {
// 读取实时配置失败则回退到 SSOT不打断编辑流程
if (!cancelled) setLiveSettings(null);
}
} else {
if (!cancelled) setLiveSettings(null);
}
} finally {
// no-op
}
};
void load();
return () => {
cancelled = true;
};
}, [open, provider, appId]);
const initialSettingsConfig = useMemo(() => {
return (liveSettings ?? provider?.settingsConfig ?? {}) as Record<
string,
unknown
>;
}, [liveSettings, provider]);
const handleSubmit = useCallback(
async (values: ProviderFormValues) => {
if (!provider) return;
const parsedConfig = JSON.parse(values.settingsConfig) as Record<
string,
unknown
>;
const updatedProvider: Provider = {
...provider,
name: values.name.trim(),
websiteUrl: values.websiteUrl?.trim() || undefined,
settingsConfig: parsedConfig,
...(values.presetCategory ? { category: values.presetCategory } : {}),
// 保留或更新 meta 字段
...(values.meta ? { meta: values.meta } : {}),
};
await onSubmit(updatedProvider);
onOpenChange(false);
},
[onSubmit, onOpenChange, provider],
);
if (!provider) {
return null;
}
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-3xl max-h-[85vh] min-h-[600px] flex flex-col">
<DialogHeader>
<DialogTitle>{t("provider.editProvider")}</DialogTitle>
<DialogDescription>
{t("provider.editProviderHint")}
</DialogDescription>
</DialogHeader>
<div className="flex-1 overflow-y-auto px-6 py-4">
<ProviderForm
appId={appId}
providerId={provider.id}
submitLabel={t("common.save")}
onSubmit={handleSubmit}
onCancel={() => onOpenChange(false)}
initialData={{
name: provider.name,
websiteUrl: provider.websiteUrl,
// 若读取到实时配置则优先使用
settingsConfig: initialSettingsConfig,
category: provider.category,
meta: provider.meta,
}}
showButtons={false}
/>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}>
{t("common.cancel")}
</Button>
<Button type="submit" form="provider-form">
<Save className="h-4 w-4" />
{t("common.save")}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}