checkbox didn't enable the save button, and disabling the toggle wipes the credentials in the backend upon saving
This commit is contained in:
parent
c416e5c7ff
commit
fc527b6cc8
@ -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,
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 && (
|
||||
|
||||
@ -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>;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user