mirror of
https://github.com/OpenHands/OpenHands.git
synced 2025-12-26 05:48:36 +08:00
feat(frontend): "Reset to Default" button (#1573)
* frontend: reset-button * frontend: key prop removed, issue with uncontrolled Autocomplete input * frontend: reset button test, Autocomplete switch to controlled input * frontend: proper use of getDefaultSettings in test * frontend: separate selectedKey and inputValue in Autocompletecombobox * no fallbacks, defaultSelectedKey prop is used to prevent the input from clearing itself * remove conflict resolution fragments --------- Co-authored-by: sp.wack <83104063+amanape@users.noreply.github.com> Co-authored-by: amanape <stephanpsaras@gmail.com>
This commit is contained in:
parent
fda21d2ce3
commit
4a2a35b6cf
@ -60,7 +60,6 @@ describe("AutocompleteCombobox", () => {
|
||||
userEvent.click(model2);
|
||||
});
|
||||
|
||||
expect(modelInput).toHaveValue("model2");
|
||||
expect(onChangeMock).toHaveBeenCalledWith("model2");
|
||||
});
|
||||
|
||||
|
||||
@ -58,16 +58,17 @@ export function AutocompleteCombobox({
|
||||
label={t(LABELS[ariaLabel])}
|
||||
placeholder={t(PLACEHOLDERS[ariaLabel])}
|
||||
defaultItems={items}
|
||||
defaultInputValue={
|
||||
defaultSelectedKey={defaultKey}
|
||||
inputValue={
|
||||
// Find the label for the default key, otherwise use the default key itself
|
||||
// This is useful when the default key is not in the list of items, in the case of a custom LLM model
|
||||
items.find((item) => item.value === defaultKey)?.label || defaultKey
|
||||
}
|
||||
onInputChange={(val) => {
|
||||
onChange(val);
|
||||
}}
|
||||
isDisabled={disabled}
|
||||
allowsCustomValue={allowCustomValue}
|
||||
onInputChange={(value) => {
|
||||
onChange(value);
|
||||
}}
|
||||
>
|
||||
{(item) => (
|
||||
<AutocompleteItem key={item.value}>{item.label}</AutocompleteItem>
|
||||
|
||||
@ -37,7 +37,7 @@ function SettingsForm({
|
||||
<AutocompleteCombobox
|
||||
ariaLabel="agent"
|
||||
items={agents.map((agent) => ({ value: agent, label: agent }))}
|
||||
defaultKey={settings.AGENT || agents[0]}
|
||||
defaultKey={settings.AGENT}
|
||||
onChange={onAgentChange}
|
||||
tooltip={t(I18nKey.SETTINGS$AGENT_TOOLTIP)}
|
||||
disabled={disabled}
|
||||
@ -45,7 +45,7 @@ function SettingsForm({
|
||||
<AutocompleteCombobox
|
||||
ariaLabel="model"
|
||||
items={models.map((model) => ({ value: model, label: model }))}
|
||||
defaultKey={settings.LLM_MODEL || models[0]}
|
||||
defaultKey={settings.LLM_MODEL}
|
||||
onChange={(e) => {
|
||||
onModelChange(e);
|
||||
}}
|
||||
@ -81,7 +81,7 @@ function SettingsForm({
|
||||
<AutocompleteCombobox
|
||||
ariaLabel="language"
|
||||
items={AvailableLanguages}
|
||||
defaultKey={settings.LANGUAGE || "en"}
|
||||
defaultKey={settings.LANGUAGE}
|
||||
onChange={onLanguageChange}
|
||||
tooltip={t(I18nKey.SETTINGS$LANGUAGE_TOOLTIP)}
|
||||
disabled={disabled}
|
||||
|
||||
@ -5,7 +5,12 @@ import React from "react";
|
||||
import { renderWithProviders } from "test-utils";
|
||||
import { Mock } from "vitest";
|
||||
import toast from "#/utils/toast";
|
||||
import { Settings, getSettings, saveSettings } from "#/services/settings";
|
||||
import {
|
||||
Settings,
|
||||
getSettings,
|
||||
saveSettings,
|
||||
getDefaultSettings,
|
||||
} from "#/services/settings";
|
||||
import { initializeAgent } from "#/services/agent";
|
||||
import { fetchAgents, fetchModels } from "#/api";
|
||||
import SettingsModal from "./SettingsModal";
|
||||
@ -20,6 +25,12 @@ vi.mock("#/services/settings", async (importOriginal) => ({
|
||||
AGENT: "MonologueAgent",
|
||||
LANGUAGE: "en",
|
||||
}),
|
||||
getDefaultSettings: vi.fn().mockReturnValue({
|
||||
LLM_MODEL: "gpt-3.5-turbo",
|
||||
AGENT: "CodeActAgent",
|
||||
LANGUAGE: "en",
|
||||
LLM_API_KEY: "",
|
||||
}),
|
||||
settingsAreUpToDate: vi.fn().mockReturnValue(true),
|
||||
saveSettings: vi.fn(),
|
||||
}));
|
||||
@ -52,7 +63,7 @@ describe("SettingsModal", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should close the modal when the cancel button is clicked", async () => {
|
||||
it("should close the modal when the close button is clicked", async () => {
|
||||
const onOpenChange = vi.fn();
|
||||
await act(async () =>
|
||||
renderWithProviders(<SettingsModal isOpen onOpenChange={onOpenChange} />),
|
||||
@ -237,7 +248,35 @@ describe("SettingsModal", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it.todo("should reset setting changes when the cancel button is clicked");
|
||||
it("should reset settings to defaults when the 'reset to defaults' button is clicked", async () => {
|
||||
const onOpenChangeMock = vi.fn();
|
||||
await act(async () =>
|
||||
renderWithProviders(
|
||||
<SettingsModal isOpen onOpenChange={onOpenChangeMock} />,
|
||||
),
|
||||
);
|
||||
|
||||
const resetButton = screen.getByRole("button", {
|
||||
name: /MODAL_RESET_BUTTON_LABEL/i,
|
||||
});
|
||||
const agentInput = screen.getByRole("combobox", { name: "agent" });
|
||||
|
||||
act(() => {
|
||||
userEvent.click(agentInput);
|
||||
});
|
||||
const agent3 = screen.getByText("agent3");
|
||||
act(() => {
|
||||
userEvent.click(agent3);
|
||||
});
|
||||
expect(agentInput).toHaveValue("agent3");
|
||||
|
||||
act(() => {
|
||||
userEvent.click(resetButton);
|
||||
});
|
||||
expect(getDefaultSettings).toHaveBeenCalled();
|
||||
|
||||
expect(agentInput).toHaveValue("CodeActAgent"); // Agent value is reset to default from getDefaultSettings()
|
||||
});
|
||||
|
||||
it.todo(
|
||||
"should display a loading spinner when fetching the models and agents",
|
||||
|
||||
@ -12,6 +12,7 @@ import AgentState from "../../../types/AgentState";
|
||||
import {
|
||||
Settings,
|
||||
getSettings,
|
||||
getDefaultSettings,
|
||||
getSettingsDifference,
|
||||
settingsAreUpToDate,
|
||||
maybeMigrateSettings,
|
||||
@ -79,17 +80,22 @@ function SettingsModal({ isOpen, onOpenChange }: SettingsProps) {
|
||||
};
|
||||
|
||||
const handleLanguageChange = (language: string) => {
|
||||
const key = AvailableLanguages.find(
|
||||
(lang) => lang.label === language,
|
||||
)?.value;
|
||||
|
||||
if (key) setSettings((prev) => ({ ...prev, LANGUAGE: key }));
|
||||
const key =
|
||||
AvailableLanguages.find((lang) => lang.label === language)?.value ||
|
||||
language;
|
||||
// The appropriate key is assigned when the user selects a language.
|
||||
// Otherwise, their input is reflected in the inputValue field of the Autocomplete component.
|
||||
setSettings((prev) => ({ ...prev, LANGUAGE: key }));
|
||||
};
|
||||
|
||||
const handleAPIKeyChange = (key: string) => {
|
||||
setSettings((prev) => ({ ...prev, LLM_API_KEY: key }));
|
||||
};
|
||||
|
||||
const handleResetSettings = () => {
|
||||
setSettings(getDefaultSettings);
|
||||
};
|
||||
|
||||
const handleSaveSettings = () => {
|
||||
const updatedSettings = getSettingsDifference(settings);
|
||||
saveSettings(settings);
|
||||
@ -139,6 +145,12 @@ function SettingsModal({ isOpen, onOpenChange }: SettingsProps) {
|
||||
closeAfterAction: true,
|
||||
className: "bg-primary rounded-lg",
|
||||
},
|
||||
{
|
||||
label: t(I18nKey.CONFIGURATION$MODAL_RESET_BUTTON_LABEL),
|
||||
action: handleResetSettings,
|
||||
closeAfterAction: false,
|
||||
className: "bg-neutral-500 rounded-lg",
|
||||
},
|
||||
{
|
||||
label: t(I18nKey.CONFIGURATION$MODAL_CLOSE_BUTTON_LABEL),
|
||||
action: () => {
|
||||
@ -146,7 +158,7 @@ function SettingsModal({ isOpen, onOpenChange }: SettingsProps) {
|
||||
},
|
||||
isDisabled: !settingsAreUpToDate(),
|
||||
closeAfterAction: true,
|
||||
className: "bg-neutral-500 rounded-lg",
|
||||
className: "bg-rose-600 rounded-lg",
|
||||
},
|
||||
]}
|
||||
>
|
||||
|
||||
@ -242,6 +242,20 @@
|
||||
"pt": "Salvar",
|
||||
"es": "Guardar"
|
||||
},
|
||||
"CONFIGURATION$MODAL_RESET_BUTTON_LABEL": {
|
||||
"en": "Reset to Defaults",
|
||||
"zh-CN": "重置为默认值",
|
||||
"de": "Auf Standardwerte zurücksetzen",
|
||||
"ko-KR": "기본값으로 재설정",
|
||||
"no": "Tilbakestill til standardverdier",
|
||||
"zh-TW": "重設為預設值",
|
||||
"it": "Reimposta ai valori predefiniti",
|
||||
"pt": "Redefinir para os padrões",
|
||||
"es": "Restablecer valores predeterminados",
|
||||
"ar": "إعادة التعيين إلى الإعدادات الافتراضية",
|
||||
"fr": "Réinitialiser aux valeurs par défaut",
|
||||
"tr": "Varsayılanlara Sıfırla"
|
||||
},
|
||||
"CONFIGURATION$SETTINGS_NEED_UPDATE_MESSAGE": {
|
||||
"en": "We've changed some settings in the latest update. Take a minute to review."
|
||||
},
|
||||
|
||||
@ -38,6 +38,11 @@ export const maybeMigrateSettings = () => {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the default settings
|
||||
*/
|
||||
export const getDefaultSettings = (): Settings => DEFAULT_SETTINGS;
|
||||
|
||||
/**
|
||||
* Get the settings from local storage or use the default settings if not found
|
||||
*/
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user