fix(webui): scope preset editor to one row

This commit is contained in:
Xubin Ren 2026-08-07 12:28:57 +08:00
parent 3fc69b2922
commit 3836c32874
2 changed files with 43 additions and 15 deletions

View File

@ -3312,6 +3312,7 @@ function ModelsSettings({
const tx = (key: string, fallback: string, values?: Record<string, unknown>) => const tx = (key: string, fallback: string, values?: Record<string, unknown>) =>
t(key, { defaultValue: fallback, ...(values ?? {}) }); t(key, { defaultValue: fallback, ...(values ?? {}) });
const [editorOpen, setEditorOpen] = useState(false); const [editorOpen, setEditorOpen] = useState(false);
const [editorRowKey, setEditorRowKey] = useState<string | null>(null);
const [advancedOpen, setAdvancedOpen] = useState(false); const [advancedOpen, setAdvancedOpen] = useState(false);
const [draggedCallOrderIndex, setDraggedCallOrderIndex] = useState<number | null>(null); const [draggedCallOrderIndex, setDraggedCallOrderIndex] = useState<number | null>(null);
const [dragOverCallOrderIndex, setDragOverCallOrderIndex] = useState<number | null>(null); const [dragOverCallOrderIndex, setDragOverCallOrderIndex] = useState<number | null>(null);
@ -3338,6 +3339,10 @@ function ModelsSettings({
})), })),
]; ];
const selectedPreset = namedPresetsByName.get(form.modelPreset) ?? null; const selectedPreset = namedPresetsByName.get(form.modelPreset) ?? null;
const activeEditorRowKey =
editorRowKey ??
presetRows.find((row) => row.name === selectedPreset?.name)?.key ??
null;
useEffect(() => { useEffect(() => {
setAdvancedOpen(false); setAdvancedOpen(false);
}, [editorOpen, selectedPreset?.name]); }, [editorOpen, selectedPreset?.name]);
@ -3374,8 +3379,12 @@ function ModelsSettings({
selectedPreset && callOrder.includes(selectedPreset.name), selectedPreset && callOrder.includes(selectedPreset.name),
); );
const callOrderBusy = orderSaving || saving; const callOrderBusy = orderSaving || saving;
const selectPreset = (preset: SettingsPayload["model_presets"][number]) => { const selectPreset = (
const toggleCurrentPreset = !creating && selectedPreset?.name === preset.name; preset: SettingsPayload["model_presets"][number],
rowKey: string,
) => {
const toggleCurrentPreset =
!creating && selectedPreset?.name === preset.name && activeEditorRowKey === rowKey;
onSelectConfiguration(); onSelectConfiguration();
if (toggleCurrentPreset) { if (toggleCurrentPreset) {
setEditorOpen((open) => !open); setEditorOpen((open) => !open);
@ -3392,6 +3401,7 @@ function ModelsSettings({
temperature: preset.temperature, temperature: preset.temperature,
reasoningEffort: preset.reasoning_effort ?? "", reasoningEffort: preset.reasoning_effort ?? "",
})); }));
setEditorRowKey(rowKey);
setEditorOpen(true); setEditorOpen(true);
}; };
@ -3432,9 +3442,8 @@ function ModelsSettings({
onChangeCallOrder(next); onChangeCallOrder(next);
}; };
const renderPresetEditor = (key: string) => ( const renderPresetEditor = () => (
<div <div
key={key}
id="model-preset-editor" id="model-preset-editor"
data-testid="model-preset-editor" data-testid="model-preset-editor"
className="mx-3 mb-3 divide-y divide-border/45 overflow-hidden rounded-[18px] border border-border/45 bg-background/80 shadow-sm motion-reduce:animate-none animate-in fade-in-0 slide-in-from-top-1 duration-200 sm:mx-5 lg:mx-auto lg:w-[calc(100%-2.5rem)] lg:max-w-6xl" className="mx-3 mb-3 divide-y divide-border/45 overflow-hidden rounded-[18px] border border-border/45 bg-background/80 shadow-sm motion-reduce:animate-none animate-in fade-in-0 slide-in-from-top-1 duration-200 sm:mx-5 lg:mx-auto lg:w-[calc(100%-2.5rem)] lg:max-w-6xl"
@ -3675,11 +3684,12 @@ function ModelsSettings({
draggedCallOrderIndex !== null && draggedCallOrderIndex !== null &&
draggedCallOrderIndex < orderIndex; draggedCallOrderIndex < orderIndex;
const isSelected = const isSelected =
editorOpen && !creating && selectedPreset?.name === name; editorOpen &&
return [ !creating &&
activeEditorRowKey === key &&
selectedPreset?.name === name;
const presetRow = (
<div <div
key={key}
role="listitem"
tabIndex={ordered ? 0 : -1} tabIndex={ordered ? 0 : -1}
draggable={ordered && !callOrderBusy} draggable={ordered && !callOrderBusy}
aria-label={ aria-label={
@ -3731,7 +3741,7 @@ function ModelsSettings({
moveCallOrderItem(orderIndex, 1); moveCallOrderItem(orderIndex, 1);
} else if ((event.key === "Enter" || event.key === " ") && preset) { } else if ((event.key === "Enter" || event.key === " ") && preset) {
event.preventDefault(); event.preventDefault();
selectPreset(preset); selectPreset(preset, key);
} }
}} }}
className={cn( className={cn(
@ -3766,7 +3776,7 @@ function ModelsSettings({
aria-expanded={isSelected} aria-expanded={isSelected}
aria-controls={isSelected ? "model-preset-editor" : undefined} aria-controls={isSelected ? "model-preset-editor" : undefined}
disabled={!preset} disabled={!preset}
onClick={() => preset && selectPreset(preset)} onClick={() => preset && selectPreset(preset, key)}
className="flex min-w-0 flex-1 items-center gap-3 rounded-[12px] text-left outline-none focus-visible:ring-2 focus-visible:ring-ring" className="flex min-w-0 flex-1 items-center gap-3 rounded-[12px] text-left outline-none focus-visible:ring-2 focus-visible:ring-ring"
> >
{ordered ? ( {ordered ? (
@ -3846,9 +3856,14 @@ function ModelsSettings({
aria-hidden aria-hidden
/> />
</button> </button>
</div>, </div>
isSelected ? renderPresetEditor(`editor:${key}`) : null, );
]; return (
<div key={key} role="listitem">
{presetRow}
{isSelected ? renderPresetEditor() : null}
</div>
);
})} })}
</div> </div>
<div className="flex min-h-[58px] flex-col gap-3 px-4 py-3 sm:flex-row sm:items-center sm:justify-between sm:px-5"> <div className="flex min-h-[58px] flex-col gap-3 px-4 py-3 sm:flex-row sm:items-center sm:justify-between sm:px-5">
@ -3859,6 +3874,7 @@ function ModelsSettings({
className="rounded-full" className="rounded-full"
disabled={callOrderBusy} disabled={callOrderBusy}
onClick={() => { onClick={() => {
setEditorRowKey(null);
setEditorOpen(true); setEditorOpen(true);
onBeginCreate(); onBeginCreate();
}} }}
@ -3878,7 +3894,7 @@ function ModelsSettings({
</SettingsStatusMessage> </SettingsStatusMessage>
) : null} ) : null}
</div> </div>
{creating && editorOpen ? renderPresetEditor("editor:new") : null} {creating && editorOpen ? renderPresetEditor() : null}
</> </>
)} )}
</SettingsGroup> </SettingsGroup>

View File

@ -2166,6 +2166,8 @@ describe("SettingsView Apps catalog", () => {
expect(trigger).toHaveAttribute("aria-pressed", "true"); expect(trigger).toHaveAttribute("aria-pressed", "true");
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.parentElement).toHaveAttribute("role", "listitem");
expect(row.parentElement?.parentElement).toHaveAttribute("role", "list");
expect(row.nextElementSibling).toBe(editor); expect(row.nextElementSibling).toBe(editor);
expect(editor).toHaveClass( expect(editor).toHaveClass(
"slide-in-from-top-1", "slide-in-from-top-1",
@ -2317,7 +2319,17 @@ describe("SettingsView Apps catalog", () => {
renderSettingsView({ initialSection: "models", initialSettings: initialPayload }); renderSettingsView({ initialSection: "models", initialSettings: initialPayload });
const primaryRow = screen.getByTestId("model-call-order-row-primary"); const primaryRow = screen.getByTestId("model-call-order-row-primary");
const firstBackupRow = screen.getAllByTestId("model-call-order-row-backup")[0]; const backupRows = screen.getAllByTestId("model-call-order-row-backup");
const firstBackupRow = backupRows[0];
const secondBackupRow = backupRows[1];
const secondBackupTrigger = within(secondBackupRow).getAllByRole("button")[0];
fireEvent.click(secondBackupTrigger);
expect(screen.getAllByTestId("model-preset-editor")).toHaveLength(1);
expect(secondBackupRow.nextElementSibling).toBe(
screen.getByTestId("model-preset-editor"),
);
fireEvent.click(secondBackupTrigger);
const dataTransfer = { const dataTransfer = {
dropEffect: "move", dropEffect: "move",
effectAllowed: "move", effectAllowed: "move",