From b77e1133cbacc865a9014431f04ba97d787fbc90 Mon Sep 17 00:00:00 2001
From: Xubin Ren <52506698+Re-bin@users.noreply.github.com>
Date: Fri, 7 Aug 2026 11:21:09 +0800
Subject: [PATCH] fix(webui): preserve preset deletion workflow
---
.../src/components/settings/SettingsView.tsx | 26 ++-----
webui/src/tests/settings-view.test.tsx | 70 +------------------
2 files changed, 6 insertions(+), 90 deletions(-)
diff --git a/webui/src/components/settings/SettingsView.tsx b/webui/src/components/settings/SettingsView.tsx
index b23efd66f..648222712 100644
--- a/webui/src/components/settings/SettingsView.tsx
+++ b/webui/src/components/settings/SettingsView.tsx
@@ -1467,21 +1467,10 @@ export function SettingsView({
) {
return;
}
- const presetName = modelPresetPendingDelete.name;
- const nextOrder = modelCallOrder.includes(presetName)
- ? modelCallOrder.filter((name) => name !== presetName)
- : null;
- if (nextOrder?.length === 0) return;
setSaving(true);
try {
- if (nextOrder) {
- const orderedPayload = await updateModelCallOrder(token, nextOrder);
- applyPayload(orderedPayload);
- onModelNameChange(orderedPayload.agent.model || null);
- }
- const payload = await deleteModelConfiguration(token, presetName);
+ const payload = await deleteModelConfiguration(token, modelPresetPendingDelete.name);
applyPayload(payload);
- onModelNameChange(payload.agent.model || null);
setModelPresetPendingDelete(null);
setError(null);
} catch (err) {
@@ -3384,11 +3373,6 @@ function ModelsSettings({
const selectedPresetReferenced = Boolean(
selectedPreset && callOrder.includes(selectedPreset.name),
);
- const selectedPresetOnlyActive = Boolean(
- selectedPresetReferenced &&
- selectedPreset &&
- !callOrder.some((name) => name !== selectedPreset.name),
- );
const callOrderBusy = orderSaving || saving;
const selectPreset = (preset: SettingsPayload["model_presets"][number]) => {
const toggleCurrentPreset = !creating && selectedPreset?.name === preset.name;
@@ -3453,7 +3437,7 @@ function ModelsSettings({
key={key}
id="model-preset-editor"
data-testid="model-preset-editor"
- className="divide-y divide-border/45 bg-muted/10 motion-reduce:animate-none animate-in fade-in-0 slide-in-from-top-1 duration-200"
+ className="divide-y divide-border/45 bg-muted/30 motion-reduce:animate-none animate-in fade-in-0 slide-in-from-top-1 duration-200"
>
{creating ? (
@@ -3582,16 +3566,16 @@ function ModelsSettings({
size="sm"
variant="ghost"
className="rounded-full text-muted-foreground hover:text-destructive"
- disabled={selectedPresetOnlyActive || saving || orderSaving}
+ disabled={selectedPresetReferenced || saving || orderSaving}
aria-describedby={
- selectedPresetOnlyActive ? "model-preset-delete-hint" : undefined
+ selectedPresetReferenced ? "model-preset-delete-hint" : undefined
}
onClick={() => onDeleteConfiguration(selectedPreset)}
>
{tx("settings.actions.delete", "Delete")}
- {selectedPresetOnlyActive ? (
+ {selectedPresetReferenced ? (
{
expect(trigger).toHaveAttribute("aria-expanded", "true");
expect(trigger).toHaveAttribute("aria-controls", "model-preset-editor");
expect(row.nextElementSibling).toBe(editor);
- expect(editor).toHaveClass("slide-in-from-top-1", "bg-muted/10");
+ expect(editor).toHaveClass("slide-in-from-top-1", "bg-muted/30");
expect(within(editor).getByDisplayValue("Primary")).toBeInTheDocument();
const deleteButton = within(editor).getByRole("button", { name: "Delete" });
expect(deleteButton).toBeDisabled();
@@ -2182,74 +2182,6 @@ describe("SettingsView Apps catalog", () => {
expect(screen.queryByTestId("model-preset-editor")).not.toBeInTheDocument();
});
- it("removes an active preset from the call order before deleting it", async () => {
- const { payload, backupPreset } = settingsPayloadWithBackup();
- const orderedPayload: SettingsPayload = {
- ...payload,
- agent: {
- ...payload.agent,
- model: backupPreset.model,
- provider: backupPreset.provider,
- resolved_provider: backupPreset.resolved_provider,
- model_preset: backupPreset.name,
- },
- model_presets: payload.model_presets.map((preset) => ({
- ...preset,
- active: preset.name === backupPreset.name,
- })),
- model_call_order: [backupPreset.name],
- };
- const deletedPayload: SettingsPayload = {
- ...orderedPayload,
- model_presets: orderedPayload.model_presets.filter(
- (preset) => preset.name !== "primary",
- ),
- };
- const fetchMock = vi.fn(async (input: RequestInfo | URL) => {
- const url = String(input);
- if (url === "/api/settings") return jsonResponse(payload);
- if (url === "/api/settings/cli-apps") {
- return jsonResponse({ apps: [], installed_count: 0 });
- }
- if (url === "/api/settings/mcp-presets") {
- return jsonResponse({ presets: [], installed_count: 0 });
- }
- if (url.startsWith("/api/settings/model-call-order/update?")) {
- return jsonResponse(orderedPayload);
- }
- if (url.startsWith("/api/settings/model-configurations/delete?")) {
- return jsonResponse(deletedPayload);
- }
- return { ok: false, status: 404, json: async () => ({}) } as Response;
- });
- vi.stubGlobal("fetch", fetchMock);
-
- renderSettingsView({ initialSection: "models", initialSettings: payload });
-
- await togglePresetEditor();
- const deleteButton = screen.getByRole("button", { name: "Delete" });
- expect(deleteButton).toBeEnabled();
- fireEvent.click(deleteButton);
- const dialog = await screen.findByRole("dialog", { name: "Delete model preset?" });
- fireEvent.click(within(dialog).getByRole("button", { name: "Delete" }));
-
- await waitFor(() => {
- const urls = fetchMock.mock.calls.map(([input]) => String(input));
- const orderCallIndex = urls.findIndex((url) =>
- url.startsWith("/api/settings/model-call-order/update?"),
- );
- const deleteCallIndex = urls.findIndex((url) =>
- url.startsWith("/api/settings/model-configurations/delete?"),
- );
- expect(orderCallIndex).toBeGreaterThanOrEqual(0);
- expect(deleteCallIndex).toBeGreaterThan(orderCallIndex);
- const orderUrl = new URL(urls[orderCallIndex], "http://nanobot.test");
- expect(JSON.parse(orderUrl.searchParams.get("order") ?? "[]")).toEqual(["backup"]);
- });
- expect(screen.queryByTestId("model-call-order-row-primary")).not.toBeInTheDocument();
- expect(screen.getByTestId("model-call-order-row-backup")).toBeInTheDocument();
- });
-
it("drags model presets to reorder and saves the model call order immediately", async () => {
const { payload, backupPreset } = settingsPayloadWithBackup();
const updatedPayload: SettingsPayload = {