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
98 lines
2.6 KiB
TypeScript
98 lines
2.6 KiB
TypeScript
import { useTranslation } from "react-i18next";
|
||
import EndpointSpeedTest from "./EndpointSpeedTest";
|
||
import { ApiKeySection, EndpointField } from "./shared";
|
||
import type { ProviderCategory } from "@/types";
|
||
|
||
interface EndpointCandidate {
|
||
url: string;
|
||
}
|
||
|
||
interface CodexFormFieldsProps {
|
||
providerId?: string;
|
||
// API Key
|
||
codexApiKey: string;
|
||
onApiKeyChange: (key: string) => void;
|
||
category?: ProviderCategory;
|
||
shouldShowApiKeyLink: boolean;
|
||
websiteUrl: string;
|
||
|
||
// Base URL
|
||
shouldShowSpeedTest: boolean;
|
||
codexBaseUrl: string;
|
||
onBaseUrlChange: (url: string) => void;
|
||
isEndpointModalOpen: boolean;
|
||
onEndpointModalToggle: (open: boolean) => void;
|
||
onCustomEndpointsChange: (endpoints: string[]) => void;
|
||
|
||
// Speed Test Endpoints
|
||
speedTestEndpoints: EndpointCandidate[];
|
||
}
|
||
|
||
export function CodexFormFields({
|
||
providerId,
|
||
codexApiKey,
|
||
onApiKeyChange,
|
||
category,
|
||
shouldShowApiKeyLink,
|
||
websiteUrl,
|
||
shouldShowSpeedTest,
|
||
codexBaseUrl,
|
||
onBaseUrlChange,
|
||
isEndpointModalOpen,
|
||
onEndpointModalToggle,
|
||
onCustomEndpointsChange,
|
||
speedTestEndpoints,
|
||
}: CodexFormFieldsProps) {
|
||
const { t } = useTranslation();
|
||
|
||
return (
|
||
<>
|
||
{/* Codex API Key 输入框 */}
|
||
<ApiKeySection
|
||
id="codexApiKey"
|
||
label="API Key"
|
||
value={codexApiKey}
|
||
onChange={onApiKeyChange}
|
||
category={category}
|
||
shouldShowLink={shouldShowApiKeyLink}
|
||
websiteUrl={websiteUrl}
|
||
placeholder={{
|
||
official: t("providerForm.codexOfficialNoApiKey", {
|
||
defaultValue: "官方供应商无需 API Key",
|
||
}),
|
||
thirdParty: t("providerForm.codexApiKeyAutoFill", {
|
||
defaultValue: "输入 API Key,将自动填充到配置",
|
||
}),
|
||
}}
|
||
/>
|
||
|
||
{/* Codex Base URL 输入框 */}
|
||
{shouldShowSpeedTest && (
|
||
<EndpointField
|
||
id="codexBaseUrl"
|
||
label={t("codexConfig.apiUrlLabel")}
|
||
value={codexBaseUrl}
|
||
onChange={onBaseUrlChange}
|
||
placeholder={t("providerForm.codexApiEndpointPlaceholder")}
|
||
hint={t("providerForm.codexApiHint")}
|
||
onManageClick={() => onEndpointModalToggle(true)}
|
||
/>
|
||
)}
|
||
|
||
{/* 端点测速弹窗 - Codex */}
|
||
{shouldShowSpeedTest && isEndpointModalOpen && (
|
||
<EndpointSpeedTest
|
||
appId="codex"
|
||
providerId={providerId}
|
||
value={codexBaseUrl}
|
||
onChange={onBaseUrlChange}
|
||
initialEndpoints={speedTestEndpoints}
|
||
visible={isEndpointModalOpen}
|
||
onClose={() => onEndpointModalToggle(false)}
|
||
onCustomEndpointsChange={onCustomEndpointsChange}
|
||
/>
|
||
)}
|
||
</>
|
||
);
|
||
}
|