checkbox didn't enable the save button, and disabling the toggle wipes the credentials in the backend upon saving

This commit is contained in:
DaKheera47 2026-01-22 12:46:43 +00:00
parent c416e5c7ff
commit fc527b6cc8
6 changed files with 134 additions and 27 deletions

View File

@ -195,4 +195,89 @@ describe("SettingsPage", () => {
})
)
})
it("enables save button when model is changed", async () => {
vi.mocked(api.getSettings).mockResolvedValue(baseSettings)
renderPage()
const saveButton = screen.getByRole("button", { name: /^save$/i })
expect(saveButton).toBeDisabled()
const modelTrigger = await screen.findByRole("button", { name: /model/i })
fireEvent.click(modelTrigger)
const modelInput = screen.getByLabelText(/override model/i)
fireEvent.change(modelInput, { target: { value: "new-model" } })
expect(saveButton).toBeEnabled()
})
it("enables save button when numeric setting is changed", async () => {
vi.mocked(api.getSettings).mockResolvedValue(baseSettings)
renderPage()
const saveButton = screen.getByRole("button", { name: /^save$/i })
const visaTrigger = await screen.findByRole("button", { name: /ukvisajobs extractor/i })
fireEvent.click(visaTrigger)
const maxJobsInput = screen.getByLabelText(/max jobs to fetch/i)
fireEvent.change(maxJobsInput, { target: { value: "100" } })
expect(saveButton).toBeEnabled()
})
it("enables save button when display setting is changed", async () => {
vi.mocked(api.getSettings).mockResolvedValue(baseSettings)
renderPage()
const saveButton = screen.getByRole("button", { name: /^save$/i })
const displayTrigger = await screen.findByRole("button", { name: /display settings/i })
fireEvent.click(displayTrigger)
const sponsorCheckbox = screen.getByLabelText(/show visa sponsor information/i)
fireEvent.click(sponsorCheckbox)
expect(saveButton).toBeEnabled()
})
it("enables save button when basic auth toggle is changed", async () => {
vi.mocked(api.getSettings).mockResolvedValue(baseSettings)
renderPage()
const saveButton = screen.getByRole("button", { name: /^save$/i })
const envTrigger = await screen.findByRole("button", { name: /environment & accounts/i })
fireEvent.click(envTrigger)
const authCheckbox = screen.getByLabelText(/enable basic authentication/i)
fireEvent.click(authCheckbox)
expect(saveButton).toBeEnabled()
})
it("wipes basic auth credentials when toggle is disabled and saved", async () => {
// Initial state: Basic Auth is active
const activeSettings = {
...baseSettings,
basicAuthActive: true,
basicAuthUser: "admin",
basicAuthPasswordHint: "pass",
}
vi.mocked(api.getSettings).mockResolvedValue(activeSettings)
vi.mocked(api.updateSettings).mockResolvedValue(baseSettings)
renderPage()
const envTrigger = await screen.findByRole("button", { name: /environment & accounts/i })
fireEvent.click(envTrigger)
const authCheckbox = screen.getByLabelText(/enable basic authentication/i)
expect(authCheckbox).toBeChecked()
// Disable it
fireEvent.click(authCheckbox)
expect(authCheckbox).not.toBeChecked()
const saveButton = screen.getByRole("button", { name: /^save$/i })
expect(saveButton).toBeEnabled()
fireEvent.click(saveButton)
await waitFor(() => expect(api.updateSettings).toHaveBeenCalled())
expect(api.updateSettings).toHaveBeenCalledWith(
expect.objectContaining({
basicAuthUser: null,
basicAuthPassword: null,
})
)
})
})

View File

@ -49,6 +49,7 @@ const DEFAULT_FORM_VALUES: UpdateSettingsInput = {
ukvisajobsEmail: "",
ukvisajobsPassword: "",
webhookSecret: "",
enableBasicAuth: false,
}
const NULL_SETTINGS_PAYLOAD: UpdateSettingsInput = {
@ -105,6 +106,7 @@ const mapSettingsToForm = (data: AppSettings): UpdateSettingsInput => ({
ukvisajobsEmail: data.ukvisajobsEmail ?? "",
ukvisajobsPassword: "",
webhookSecret: "",
enableBasicAuth: data.basicAuthActive,
})
const normalizeString = (value: string | null | undefined) => {
@ -284,7 +286,7 @@ export const SettingsPage: React.FC = () => {
if (!settings) return
try {
setIsSaving(true)
// Prepare payload: nullify if equal to default
const resumeProjectsData = data.resumeProjects
const resumeProjectsOverride = (resumeProjectsData && defaultResumeProjects && resumeProjectsEqual(resumeProjectsData, defaultResumeProjects))
@ -301,8 +303,18 @@ export const SettingsPage: React.FC = () => {
envPayload.ukvisajobsEmail = normalizeString(data.ukvisajobsEmail)
}
if (dirtyFields.basicAuthUser) {
envPayload.basicAuthUser = normalizeString(data.basicAuthUser)
if (data.enableBasicAuth === false) {
envPayload.basicAuthUser = null
envPayload.basicAuthPassword = null
} else {
if (dirtyFields.basicAuthUser) {
envPayload.basicAuthUser = normalizeString(data.basicAuthUser)
}
if (dirtyFields.basicAuthPassword) {
const value = normalizePrivateInput(data.basicAuthPassword)
if (value !== undefined) envPayload.basicAuthPassword = value
}
}
if (dirtyFields.openrouterApiKey) {
@ -320,11 +332,6 @@ export const SettingsPage: React.FC = () => {
if (value !== undefined) envPayload.ukvisajobsPassword = value
}
if (dirtyFields.basicAuthPassword) {
const value = normalizePrivateInput(data.basicAuthPassword)
if (value !== undefined) envPayload.basicAuthPassword = value
}
if (dirtyFields.webhookSecret) {
const value = normalizePrivateInput(data.webhookSecret)
if (value !== undefined) envPayload.webhookSecret = value
@ -354,6 +361,11 @@ export const SettingsPage: React.FC = () => {
...envPayload,
}
// Remove virtual field because the backend doesn't expect it
// this exists only to toggle the UI
// need o track it so that the save button is enabled when it changes
delete payload.enableBasicAuth
const updated = await api.updateSettings(payload)
setSettings(updated)
reset(mapSettingsToForm(updated))

View File

@ -16,6 +16,7 @@ const EnvironmentSettingsHarness = () => {
ukvisajobsPassword: "",
basicAuthPassword: "",
webhookSecret: "",
enableBasicAuth: true,
}
})
@ -53,9 +54,9 @@ describe("EnvironmentSettingsSection", () => {
expect(screen.getByDisplayValue("resume@example.com")).toBeInTheDocument()
expect(screen.getByDisplayValue("visa@example.com")).toBeInTheDocument()
expect(screen.getByText("sk-1********")).toBeInTheDocument()
expect(screen.getByText("pass********")).toBeInTheDocument()
expect(screen.getByText("abcd********")).toBeInTheDocument()
expect(screen.getByText(/sk-1\*{8}/)).toBeInTheDocument()
expect(screen.getByText(/pass\*{8}/)).toBeInTheDocument()
expect(screen.getByText(/abcd\*{8}/)).toBeInTheDocument()
expect(screen.getByText("Not set")).toBeInTheDocument()
// Basic Auth

View File

@ -1,5 +1,5 @@
import React, { useState, useEffect } from "react"
import { useFormContext } from "react-hook-form"
import React from "react"
import { useFormContext, Controller } from "react-hook-form"
import { AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion"
import { Checkbox } from "@/components/ui/checkbox"
@ -21,14 +21,10 @@ export const EnvironmentSettingsSection: React.FC<EnvironmentSettingsSectionProp
isLoading,
isSaving,
}) => {
const { register, formState: { errors } } = useFormContext<UpdateSettingsInput>()
const { private: privateValues, basicAuthActive } = values
const { register, control, watch, formState: { errors } } = useFormContext<UpdateSettingsInput>()
const { private: privateValues } = values
const [isBasicAuthEnabled, setIsBasicAuthEnabled] = useState(basicAuthActive)
useEffect(() => {
setIsBasicAuthEnabled(basicAuthActive)
}, [basicAuthActive])
const isBasicAuthEnabled = watch("enableBasicAuth")
return (
<AccordionItem value="environment" className="border rounded-lg px-4">
@ -110,11 +106,17 @@ export const EnvironmentSettingsSection: React.FC<EnvironmentSettingsSectionProp
<div className="space-y-4">
<div className="text-sm font-bold uppercase tracking-wider text-muted-foreground">Security</div>
<div className="flex items-start space-x-3">
<Checkbox
id="enableBasicAuth"
checked={isBasicAuthEnabled}
onCheckedChange={(checked) => setIsBasicAuthEnabled(checked === true)}
disabled={isLoading || isSaving}
<Controller
name="enableBasicAuth"
control={control}
render={({ field }) => (
<Checkbox
id="enableBasicAuth"
checked={field.value}
onCheckedChange={field.onChange}
disabled={isLoading || isSaving}
/>
)}
/>
<div className="flex flex-col gap-1.5">
<label

View File

@ -23,10 +23,16 @@ export const SettingsInput: React.FC<SettingsInputProps> = ({
helper,
current,
}) => {
const id = inputProps.id || inputProps.name
return (
<div className="space-y-2">
<div className="text-sm font-medium">{label}</div>
<Input {...inputProps} type={type} placeholder={placeholder} disabled={disabled} />
{label && (
<label htmlFor={id} className="text-sm font-medium">
{label}
</label>
)}
<Input {...inputProps} id={id} type={type} placeholder={placeholder} disabled={disabled} />
{error && <p className="text-xs text-destructive">{error}</p>}
{helper && <div className="text-xs text-muted-foreground">{helper}</div>}
{current !== undefined && (

View File

@ -32,6 +32,7 @@ export const updateSettingsSchema = z.object({
ukvisajobsEmail: z.string().trim().max(200).nullable().optional(),
ukvisajobsPassword: z.string().trim().max(2000).nullable().optional(),
webhookSecret: z.string().trim().max(2000).nullable().optional(),
enableBasicAuth: z.boolean().optional(),
});
export type UpdateSettingsInput = z.infer<typeof updateSettingsSchema>;