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
This commit is contained in:
Jason
2025-11-04 15:30:54 +08:00
parent 49c2855b10
commit 0778347f84
7 changed files with 115 additions and 99 deletions

View File

@@ -2,9 +2,10 @@ import type { CustomEndpoint, ProviderMeta } from "@/types";
/**
* 合并供应商元数据中的自定义端点。
* - 当 customEndpoints 为空对象或 null 时,移除自定义端点但保留其它元数据。
* - 当 customEndpoints 为空对象时,明确删除自定义端点但保留其它元数据。
* - 当 customEndpoints 为 null/undefined 时,不修改端点(保留原有端点)。
* - 当 customEndpoints 存在时,覆盖原有自定义端点。
* - 若结果为空对象则返回 undefined避免写入空 meta。
* - 若结果为空对象且非明确清空场景则返回 undefined避免写入空 meta。
*/
export function mergeProviderMeta(
initialMeta: ProviderMeta | undefined,
@@ -13,6 +14,12 @@ export function mergeProviderMeta(
const hasCustomEndpoints =
!!customEndpoints && Object.keys(customEndpoints).length > 0;
// 明确清空:传入空对象(非 null/undefined表示用户想要删除所有端点
const isExplicitClear =
customEndpoints !== null &&
customEndpoints !== undefined &&
Object.keys(customEndpoints).length === 0;
if (hasCustomEndpoints) {
return {
...(initialMeta ? { ...initialMeta } : {}),
@@ -20,6 +27,25 @@ export function mergeProviderMeta(
};
}
// 明确清空端点
if (isExplicitClear) {
if (!initialMeta) {
// 新供应商且用户没有添加端点(理论上不会到这里)
return undefined;
}
if ("custom_endpoints" in initialMeta) {
const { custom_endpoints, ...rest } = initialMeta;
// 保留其他字段(如 usage_script
// 即使 rest 为空,也要返回空对象(让后端知道要清空 meta
return Object.keys(rest).length > 0 ? rest : {};
}
// initialMeta 中本来就没有 custom_endpoints
return { ...initialMeta };
}
// null/undefined用户没有修改端点保持不变
if (!initialMeta) {
return undefined;
}