mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-08-07 18:08:34 +00:00
fix(webui): preserve preset deletion workflow
This commit is contained in:
parent
1b12fbae39
commit
b77e1133cb
@ -1467,21 +1467,10 @@ export function SettingsView({
|
|||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const presetName = modelPresetPendingDelete.name;
|
|
||||||
const nextOrder = modelCallOrder.includes(presetName)
|
|
||||||
? modelCallOrder.filter((name) => name !== presetName)
|
|
||||||
: null;
|
|
||||||
if (nextOrder?.length === 0) return;
|
|
||||||
setSaving(true);
|
setSaving(true);
|
||||||
try {
|
try {
|
||||||
if (nextOrder) {
|
const payload = await deleteModelConfiguration(token, modelPresetPendingDelete.name);
|
||||||
const orderedPayload = await updateModelCallOrder(token, nextOrder);
|
|
||||||
applyPayload(orderedPayload);
|
|
||||||
onModelNameChange(orderedPayload.agent.model || null);
|
|
||||||
}
|
|
||||||
const payload = await deleteModelConfiguration(token, presetName);
|
|
||||||
applyPayload(payload);
|
applyPayload(payload);
|
||||||
onModelNameChange(payload.agent.model || null);
|
|
||||||
setModelPresetPendingDelete(null);
|
setModelPresetPendingDelete(null);
|
||||||
setError(null);
|
setError(null);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -3384,11 +3373,6 @@ function ModelsSettings({
|
|||||||
const selectedPresetReferenced = Boolean(
|
const selectedPresetReferenced = Boolean(
|
||||||
selectedPreset && callOrder.includes(selectedPreset.name),
|
selectedPreset && callOrder.includes(selectedPreset.name),
|
||||||
);
|
);
|
||||||
const selectedPresetOnlyActive = Boolean(
|
|
||||||
selectedPresetReferenced &&
|
|
||||||
selectedPreset &&
|
|
||||||
!callOrder.some((name) => name !== selectedPreset.name),
|
|
||||||
);
|
|
||||||
const callOrderBusy = orderSaving || saving;
|
const callOrderBusy = orderSaving || saving;
|
||||||
const selectPreset = (preset: SettingsPayload["model_presets"][number]) => {
|
const selectPreset = (preset: SettingsPayload["model_presets"][number]) => {
|
||||||
const toggleCurrentPreset = !creating && selectedPreset?.name === preset.name;
|
const toggleCurrentPreset = !creating && selectedPreset?.name === preset.name;
|
||||||
@ -3453,7 +3437,7 @@ function ModelsSettings({
|
|||||||
key={key}
|
key={key}
|
||||||
id="model-preset-editor"
|
id="model-preset-editor"
|
||||||
data-testid="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 ? (
|
{creating ? (
|
||||||
<div className="flex min-h-[52px] items-center px-4 py-3 sm:px-5">
|
<div className="flex min-h-[52px] items-center px-4 py-3 sm:px-5">
|
||||||
@ -3582,16 +3566,16 @@ function ModelsSettings({
|
|||||||
size="sm"
|
size="sm"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
className="rounded-full text-muted-foreground hover:text-destructive"
|
className="rounded-full text-muted-foreground hover:text-destructive"
|
||||||
disabled={selectedPresetOnlyActive || saving || orderSaving}
|
disabled={selectedPresetReferenced || saving || orderSaving}
|
||||||
aria-describedby={
|
aria-describedby={
|
||||||
selectedPresetOnlyActive ? "model-preset-delete-hint" : undefined
|
selectedPresetReferenced ? "model-preset-delete-hint" : undefined
|
||||||
}
|
}
|
||||||
onClick={() => onDeleteConfiguration(selectedPreset)}
|
onClick={() => onDeleteConfiguration(selectedPreset)}
|
||||||
>
|
>
|
||||||
<Trash2 className="mr-1.5 h-3.5 w-3.5" aria-hidden />
|
<Trash2 className="mr-1.5 h-3.5 w-3.5" aria-hidden />
|
||||||
{tx("settings.actions.delete", "Delete")}
|
{tx("settings.actions.delete", "Delete")}
|
||||||
</Button>
|
</Button>
|
||||||
{selectedPresetOnlyActive ? (
|
{selectedPresetReferenced ? (
|
||||||
<span
|
<span
|
||||||
id="model-preset-delete-hint"
|
id="model-preset-delete-hint"
|
||||||
className="text-[11px] leading-4 text-muted-foreground"
|
className="text-[11px] leading-4 text-muted-foreground"
|
||||||
|
|||||||
@ -2167,7 +2167,7 @@ describe("SettingsView Apps catalog", () => {
|
|||||||
expect(trigger).toHaveAttribute("aria-expanded", "true");
|
expect(trigger).toHaveAttribute("aria-expanded", "true");
|
||||||
expect(trigger).toHaveAttribute("aria-controls", "model-preset-editor");
|
expect(trigger).toHaveAttribute("aria-controls", "model-preset-editor");
|
||||||
expect(row.nextElementSibling).toBe(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();
|
expect(within(editor).getByDisplayValue("Primary")).toBeInTheDocument();
|
||||||
const deleteButton = within(editor).getByRole("button", { name: "Delete" });
|
const deleteButton = within(editor).getByRole("button", { name: "Delete" });
|
||||||
expect(deleteButton).toBeDisabled();
|
expect(deleteButton).toBeDisabled();
|
||||||
@ -2182,74 +2182,6 @@ describe("SettingsView Apps catalog", () => {
|
|||||||
expect(screen.queryByTestId("model-preset-editor")).not.toBeInTheDocument();
|
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 () => {
|
it("drags model presets to reorder and saves the model call order immediately", async () => {
|
||||||
const { payload, backupPreset } = settingsPayloadWithBackup();
|
const { payload, backupPreset } = settingsPayloadWithBackup();
|
||||||
const updatedPayload: SettingsPayload = {
|
const updatedPayload: SettingsPayload = {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user