mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-05 08:10:14 +00:00
feat: add ResetToDefault component for settings with defaults
- Add ResetToDefault component with ↺ icon that shows when a setting differs from its default value - Tooltip shows the default value (e.g., 'Reset to default (true)') - Component only renders when current value !== default - Add translation keys for boolean/empty/resetToDefault formatting - Integrate with BrowserSettings as proof of concept: - browserToolEnabled checkbox - browserViewportSize dropdown - screenshotQuality slider - Add comprehensive tests for visibility, functionality, and styling
This commit is contained in:
parent
f8a20c3154
commit
504a71ac72
4 changed files with 230 additions and 10 deletions
|
|
@ -18,6 +18,7 @@ import { useAppTranslation } from "@/i18n/TranslationContext"
|
|||
import { vscode } from "@/utils/vscode"
|
||||
import { buildDocLink } from "@src/utils/docLinks"
|
||||
|
||||
import { ResetToDefault } from "./ResetToDefault"
|
||||
import { SearchableSetting } from "./SearchableSetting"
|
||||
import { Section } from "./Section"
|
||||
import { SectionHeader } from "./SectionHeader"
|
||||
|
|
@ -117,11 +118,18 @@ export const BrowserSettings = ({
|
|||
settingId="browser-enable"
|
||||
section="browser"
|
||||
label={t("settings:browser.enable.label")}>
|
||||
<VSCodeCheckbox
|
||||
checked={browserToolEnabled}
|
||||
onChange={(e: any) => setCachedStateField("browserToolEnabled", e.target.checked)}>
|
||||
<span className="font-medium">{t("settings:browser.enable.label")}</span>
|
||||
</VSCodeCheckbox>
|
||||
<div className="flex items-center gap-1">
|
||||
<VSCodeCheckbox
|
||||
checked={browserToolEnabled}
|
||||
onChange={(e: any) => setCachedStateField("browserToolEnabled", e.target.checked)}>
|
||||
<span className="font-medium">{t("settings:browser.enable.label")}</span>
|
||||
</VSCodeCheckbox>
|
||||
<ResetToDefault
|
||||
settingKey="browserToolEnabled"
|
||||
currentValue={browserToolEnabled}
|
||||
onReset={() => setCachedStateField("browserToolEnabled", undefined)}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-vscode-descriptionForeground text-sm mt-1">
|
||||
<Trans i18nKey="settings:browser.enable.description">
|
||||
<VSCodeLink
|
||||
|
|
@ -139,7 +147,14 @@ export const BrowserSettings = ({
|
|||
settingId="browser-viewport"
|
||||
section="browser"
|
||||
label={t("settings:browser.viewport.label")}>
|
||||
<label className="block font-medium mb-1">{t("settings:browser.viewport.label")}</label>
|
||||
<div className="flex items-center gap-1 mb-1">
|
||||
<label className="block font-medium">{t("settings:browser.viewport.label")}</label>
|
||||
<ResetToDefault
|
||||
settingKey="browserViewportSize"
|
||||
currentValue={browserViewportSize}
|
||||
onReset={() => setCachedStateField("browserViewportSize", undefined)}
|
||||
/>
|
||||
</div>
|
||||
<Select
|
||||
value={browserViewportSize}
|
||||
onValueChange={(value) => setCachedStateField("browserViewportSize", value)}>
|
||||
|
|
@ -165,9 +180,16 @@ export const BrowserSettings = ({
|
|||
settingId="browser-screenshot-quality"
|
||||
section="browser"
|
||||
label={t("settings:browser.screenshotQuality.label")}>
|
||||
<label className="block font-medium mb-1">
|
||||
{t("settings:browser.screenshotQuality.label")}
|
||||
</label>
|
||||
<div className="flex items-center gap-1 mb-1">
|
||||
<label className="block font-medium">
|
||||
{t("settings:browser.screenshotQuality.label")}
|
||||
</label>
|
||||
<ResetToDefault
|
||||
settingKey="screenshotQuality"
|
||||
currentValue={screenshotQuality}
|
||||
onReset={() => setCachedStateField("screenshotQuality", undefined)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Slider
|
||||
min={1}
|
||||
|
|
|
|||
80
webview-ui/src/components/settings/ResetToDefault.tsx
Normal file
80
webview-ui/src/components/settings/ResetToDefault.tsx
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
import { RotateCcw } from "lucide-react"
|
||||
|
||||
import { settingDefaults, type SettingWithDefault } from "@roo-code/types"
|
||||
|
||||
import { Button, StandardTooltip } from "@/components/ui"
|
||||
import { useAppTranslation } from "@/i18n/TranslationContext"
|
||||
|
||||
// Widen literal types to their base types for comparison
|
||||
type WidenType<T> = T extends boolean ? boolean : T extends number ? number : T extends string ? string : T
|
||||
|
||||
interface ResetToDefaultProps<K extends SettingWithDefault> {
|
||||
/** The setting key from settingDefaults */
|
||||
settingKey: K
|
||||
/** The current value of the setting (accepts wider types for flexibility) */
|
||||
currentValue: WidenType<(typeof settingDefaults)[K]> | undefined
|
||||
/** Callback to reset the value (called with undefined to reset) */
|
||||
onReset: () => void
|
||||
/** Optional className for the button */
|
||||
className?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* A small reset button that appears only when a setting differs from its default.
|
||||
* Shows a ↺ icon with a tooltip displaying the default value.
|
||||
*
|
||||
* @example
|
||||
* <ResetToDefault
|
||||
* settingKey="browserToolEnabled"
|
||||
* currentValue={browserToolEnabled}
|
||||
* onReset={() => setCachedStateField("browserToolEnabled", undefined)}
|
||||
* />
|
||||
*/
|
||||
export function ResetToDefault<K extends SettingWithDefault>({
|
||||
settingKey,
|
||||
currentValue,
|
||||
onReset,
|
||||
className,
|
||||
}: ResetToDefaultProps<K>) {
|
||||
const { t } = useAppTranslation()
|
||||
const defaultValue = settingDefaults[settingKey]
|
||||
|
||||
// Don't show the button if the current value matches the default
|
||||
// undefined is treated as "using default"
|
||||
const isDefault = currentValue === undefined || currentValue === defaultValue
|
||||
|
||||
if (isDefault) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Format the default value for display in the tooltip
|
||||
const formatDefaultValue = (value: unknown): string => {
|
||||
if (typeof value === "boolean") {
|
||||
return value ? t("settings:common.true") : t("settings:common.false")
|
||||
}
|
||||
if (typeof value === "number") {
|
||||
return String(value)
|
||||
}
|
||||
if (typeof value === "string") {
|
||||
return value || t("settings:common.empty")
|
||||
}
|
||||
return JSON.stringify(value)
|
||||
}
|
||||
|
||||
const tooltipContent = t("settings:common.resetToDefault", {
|
||||
defaultValue: formatDefaultValue(defaultValue),
|
||||
})
|
||||
|
||||
return (
|
||||
<StandardTooltip content={tooltipContent}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className={className ?? "h-6 w-6 p-1"}
|
||||
onClick={onReset}
|
||||
data-testid={`reset-${settingKey}`}>
|
||||
<RotateCcw className="h-3.5 w-3.5 text-vscode-descriptionForeground hover:text-vscode-foreground" />
|
||||
</Button>
|
||||
</StandardTooltip>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,114 @@
|
|||
import { render, screen, fireEvent } from "@testing-library/react"
|
||||
import { ReactNode } from "react"
|
||||
|
||||
import { TooltipProvider } from "@/components/ui"
|
||||
|
||||
import { ResetToDefault } from "../ResetToDefault"
|
||||
|
||||
// Mock the translation hook
|
||||
vi.mock("@/i18n/TranslationContext", () => ({
|
||||
useAppTranslation: () => ({
|
||||
t: (key: string, options?: { defaultValue?: string }) =>
|
||||
options?.defaultValue ? `${key} (${JSON.stringify(options)})` : key,
|
||||
}),
|
||||
}))
|
||||
|
||||
// Wrapper component to provide TooltipProvider context
|
||||
const TestWrapper = ({ children }: { children: ReactNode }) => <TooltipProvider>{children}</TooltipProvider>
|
||||
|
||||
const renderWithWrapper = (ui: ReactNode) => render(ui, { wrapper: TestWrapper })
|
||||
|
||||
describe("ResetToDefault", () => {
|
||||
const mockOnReset = vi.fn()
|
||||
|
||||
beforeEach(() => {
|
||||
mockOnReset.mockClear()
|
||||
})
|
||||
|
||||
describe("visibility", () => {
|
||||
it("should not render when currentValue matches default", () => {
|
||||
// browserToolEnabled defaults to true in settingDefaults
|
||||
const { container } = render(
|
||||
<ResetToDefault settingKey="browserToolEnabled" currentValue={true} onReset={mockOnReset} />,
|
||||
)
|
||||
|
||||
expect(container.firstChild).toBeNull()
|
||||
})
|
||||
|
||||
it("should not render when currentValue is undefined (using default)", () => {
|
||||
const { container } = render(
|
||||
<ResetToDefault settingKey="browserToolEnabled" currentValue={undefined} onReset={mockOnReset} />,
|
||||
)
|
||||
|
||||
expect(container.firstChild).toBeNull()
|
||||
})
|
||||
|
||||
it("should render when currentValue differs from default (boolean)", () => {
|
||||
// browserToolEnabled defaults to true, so false should show reset button
|
||||
renderWithWrapper(
|
||||
<ResetToDefault settingKey="browserToolEnabled" currentValue={false} onReset={mockOnReset} />,
|
||||
)
|
||||
|
||||
expect(screen.getByTestId("reset-browserToolEnabled")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("should render when currentValue differs from default (number)", () => {
|
||||
// screenshotQuality defaults to 75
|
||||
renderWithWrapper(<ResetToDefault settingKey="screenshotQuality" currentValue={50} onReset={mockOnReset} />)
|
||||
|
||||
expect(screen.getByTestId("reset-screenshotQuality")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("should render when currentValue differs from default (string)", () => {
|
||||
// browserViewportSize defaults to "900x600"
|
||||
renderWithWrapper(
|
||||
<ResetToDefault settingKey="browserViewportSize" currentValue="1280x800" onReset={mockOnReset} />,
|
||||
)
|
||||
|
||||
expect(screen.getByTestId("reset-browserViewportSize")).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe("functionality", () => {
|
||||
it("should call onReset when clicked", () => {
|
||||
renderWithWrapper(
|
||||
<ResetToDefault settingKey="browserToolEnabled" currentValue={false} onReset={mockOnReset} />,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByTestId("reset-browserToolEnabled"))
|
||||
|
||||
expect(mockOnReset).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it("should have the correct aria role (button)", () => {
|
||||
renderWithWrapper(<ResetToDefault settingKey="screenshotQuality" currentValue={50} onReset={mockOnReset} />)
|
||||
|
||||
expect(screen.getByRole("button")).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe("custom className", () => {
|
||||
it("should apply custom className when provided", () => {
|
||||
renderWithWrapper(
|
||||
<ResetToDefault
|
||||
settingKey="browserToolEnabled"
|
||||
currentValue={false}
|
||||
onReset={mockOnReset}
|
||||
className="custom-class"
|
||||
/>,
|
||||
)
|
||||
|
||||
const button = screen.getByTestId("reset-browserToolEnabled")
|
||||
expect(button).toHaveClass("custom-class")
|
||||
})
|
||||
|
||||
it("should use default className when not provided", () => {
|
||||
renderWithWrapper(
|
||||
<ResetToDefault settingKey="browserToolEnabled" currentValue={false} onReset={mockOnReset} />,
|
||||
)
|
||||
|
||||
const button = screen.getByTestId("reset-browserToolEnabled")
|
||||
expect(button).toHaveClass("h-6", "w-6", "p-1")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -7,7 +7,11 @@
|
|||
"reset": "Reset",
|
||||
"select": "Select",
|
||||
"add": "Add Header",
|
||||
"remove": "Remove"
|
||||
"remove": "Remove",
|
||||
"true": "true",
|
||||
"false": "false",
|
||||
"empty": "(empty)",
|
||||
"resetToDefault": "Reset to default ({{defaultValue}})"
|
||||
},
|
||||
"header": {
|
||||
"title": "Settings",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue