Просмотр исходного кода

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>
Lev 1 год назад
Родитель
Сommit
4a2a35b6cf

+ 0 - 1
frontend/src/components/modals/settings/AutocompleteCombobox.test.tsx

@@ -60,7 +60,6 @@ describe("AutocompleteCombobox", () => {
       userEvent.click(model2);
     });
 
-    expect(modelInput).toHaveValue("model2");
     expect(onChangeMock).toHaveBeenCalledWith("model2");
   });
 

+ 5 - 4
frontend/src/components/modals/settings/AutocompleteCombobox.tsx

@@ -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>

+ 3 - 3
frontend/src/components/modals/settings/SettingsForm.tsx

@@ -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}

+ 42 - 3
frontend/src/components/modals/settings/SettingsModal.test.tsx

@@ -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",

+ 18 - 6
frontend/src/components/modals/settings/SettingsModal.tsx

@@ -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",
         },
       ]}
     >

+ 14 - 0
frontend/src/i18n/translation.json

@@ -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."
   },

+ 5 - 0
frontend/src/services/settings.ts

@@ -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
  */