fix(webui): stabilize repeated model preset rows
This commit is contained in:
@@ -3171,13 +3171,20 @@ function ModelsSettings({
|
||||
const namedPresets = settings.model_presets.filter((preset) => !preset.is_default);
|
||||
const namedPresetsByName = new Map(namedPresets.map((preset) => [preset.name, preset]));
|
||||
const unorderedPresets = namedPresets.filter((preset) => !callOrder.includes(preset.name));
|
||||
const callOrderOccurrences = new Map<string, number>();
|
||||
const presetRows = [
|
||||
...callOrder.map((name, orderIndex) => ({
|
||||
name,
|
||||
orderIndex,
|
||||
preset: namedPresetsByName.get(name),
|
||||
})),
|
||||
...callOrder.map((name, orderIndex) => {
|
||||
const occurrence = callOrderOccurrences.get(name) ?? 0;
|
||||
callOrderOccurrences.set(name, occurrence + 1);
|
||||
return {
|
||||
key: `ordered:${name}:${occurrence}`,
|
||||
name,
|
||||
orderIndex,
|
||||
preset: namedPresetsByName.get(name),
|
||||
};
|
||||
}),
|
||||
...unorderedPresets.map((preset) => ({
|
||||
key: `disabled:${preset.name}`,
|
||||
name: preset.name,
|
||||
orderIndex: -1,
|
||||
preset,
|
||||
@@ -3321,7 +3328,7 @@ function ModelsSettings({
|
||||
) : (
|
||||
<>
|
||||
<div role="list" className="divide-y divide-border/45">
|
||||
{presetRows.map(({ name, orderIndex, preset }) => {
|
||||
{presetRows.map(({ key, name, orderIndex, preset }) => {
|
||||
const ordered = orderIndex >= 0;
|
||||
const provider = preset
|
||||
? modelPresetProviderKey(preset, settings)
|
||||
@@ -3343,7 +3350,7 @@ function ModelsSettings({
|
||||
draggedCallOrderIndex < orderIndex;
|
||||
return (
|
||||
<div
|
||||
key={name}
|
||||
key={key}
|
||||
role="listitem"
|
||||
tabIndex={ordered ? 0 : -1}
|
||||
draggable={ordered && !callOrderBusy}
|
||||
|
||||
@@ -2130,6 +2130,77 @@ describe("SettingsView Apps catalog", () => {
|
||||
expect(screen.getByRole("button", { name: "Save preset" })).toBeEnabled();
|
||||
});
|
||||
|
||||
it("keeps repeated fallback preset rows stable when changing the primary preset", async () => {
|
||||
const { payload, backupPreset } = settingsPayloadWithBackup();
|
||||
const initialPayload: 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: ["backup", "primary", "backup"],
|
||||
};
|
||||
const updatedPayload: SettingsPayload = {
|
||||
...initialPayload,
|
||||
agent: {
|
||||
...payload.agent,
|
||||
model_preset: "primary",
|
||||
},
|
||||
model_presets: payload.model_presets.map((preset) => ({
|
||||
...preset,
|
||||
active: preset.name === "primary",
|
||||
})),
|
||||
model_call_order: ["primary", "backup", "backup"],
|
||||
};
|
||||
const fetchMock = vi.fn(async (input: RequestInfo | URL) => {
|
||||
const url = String(input);
|
||||
if (url === "/api/settings") return jsonResponse(initialPayload);
|
||||
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(updatedPayload);
|
||||
}
|
||||
return { ok: false, status: 404, json: async () => ({}) } as Response;
|
||||
});
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
|
||||
renderSettingsView({ initialSection: "models", initialSettings: initialPayload });
|
||||
|
||||
const primaryRow = screen.getByTestId("model-call-order-row-primary");
|
||||
const firstBackupRow = screen.getAllByTestId("model-call-order-row-backup")[0];
|
||||
const dataTransfer = {
|
||||
dropEffect: "move",
|
||||
effectAllowed: "move",
|
||||
setData: vi.fn(),
|
||||
};
|
||||
fireEvent.dragStart(primaryRow, { dataTransfer });
|
||||
fireEvent.dragEnter(firstBackupRow, { dataTransfer });
|
||||
fireEvent.drop(firstBackupRow, { dataTransfer });
|
||||
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
screen
|
||||
.getAllByTestId(/^model-call-order-row-/)
|
||||
.map((row) => row.getAttribute("data-testid")),
|
||||
).toEqual([
|
||||
"model-call-order-row-primary",
|
||||
"model-call-order-row-backup",
|
||||
"model-call-order-row-backup",
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it("restores the model call order when immediate persistence fails", async () => {
|
||||
const { payload } = settingsPayloadWithBackup();
|
||||
const fetchMock = vi.fn(async (input: RequestInfo | URL) => {
|
||||
|
||||
Reference in New Issue
Block a user