objects for each section

This commit is contained in:
DaKheera47 2026-01-21 19:34:55 +00:00
parent f50f00a09a
commit eb41c6237b

View File

@ -10,6 +10,7 @@ import { Button } from "@/components/ui/button"
import type { AppSettings, JobStatus } from "@shared/types" import type { AppSettings, JobStatus } from "@shared/types"
import { updateSettingsSchema, type UpdateSettingsInput } from "@shared/settings-schema" import { updateSettingsSchema, type UpdateSettingsInput } from "@shared/settings-schema"
import * as api from "@client/api" import * as api from "@client/api"
import { arraysEqual } from "@/lib/utils"
import { resumeProjectsEqual } from "@client/pages/settings/utils" import { resumeProjectsEqual } from "@client/pages/settings/utils"
import { DangerZoneSection } from "@client/pages/settings/components/DangerZoneSection" import { DangerZoneSection } from "@client/pages/settings/components/DangerZoneSection"
import { DisplaySettingsSection } from "@client/pages/settings/components/DisplaySettingsSection" import { DisplaySettingsSection } from "@client/pages/settings/components/DisplaySettingsSection"
@ -82,6 +83,99 @@ const mapSettingsToForm = (data: AppSettings): UpdateSettingsInput => ({
showSponsorInfo: data.overrideShowSponsorInfo, showSponsorInfo: data.overrideShowSponsorInfo,
}) })
const normalizeString = (value: string | null | undefined) => {
const trimmed = value?.trim()
return trimmed ? trimmed : null
}
const isSameStringList = (left: string[] | null | undefined, right: string[] | null | undefined) => {
if (!left && !right) return true
if (!left || !right) return false
return arraysEqual(left, right)
}
const isSameSortedStringList = (left: string[] | null | undefined, right: string[] | null | undefined) => {
if (!left && !right) return true
if (!left || !right) return false
return arraysEqual(left.slice().sort(), right.slice().sort())
}
const nullIfSame = <T,>(value: T | null | undefined, defaultValue: T) =>
value === defaultValue ? null : value ?? null
const nullIfSameList = (value: string[] | null | undefined, defaultValue: string[]) =>
isSameStringList(value, defaultValue) ? null : value ?? null
const nullIfSameSortedList = (value: string[] | null | undefined, defaultValue: string[]) =>
isSameSortedStringList(value, defaultValue) ? null : value ?? null
const getDerivedSettings = (settings: AppSettings | null) => {
const profileProjects = settings?.profileProjects ?? []
return {
model: {
effective: settings?.model ?? "",
default: settings?.defaultModel ?? "",
scorer: settings?.modelScorer ?? "",
tailoring: settings?.modelTailoring ?? "",
projectSelection: settings?.modelProjectSelection ?? "",
},
pipelineWebhook: {
effective: settings?.pipelineWebhookUrl ?? "",
default: settings?.defaultPipelineWebhookUrl ?? "",
},
jobCompleteWebhook: {
effective: settings?.jobCompleteWebhookUrl ?? "",
default: settings?.defaultJobCompleteWebhookUrl ?? "",
},
ukvisajobs: {
effectiveMaxJobs: settings?.ukvisajobsMaxJobs ?? 50,
defaultMaxJobs: settings?.defaultUkvisajobsMaxJobs ?? 50,
},
gradcracker: {
effectiveMaxJobsPerTerm: settings?.gradcrackerMaxJobsPerTerm ?? 50,
defaultMaxJobsPerTerm: settings?.defaultGradcrackerMaxJobsPerTerm ?? 50,
},
searchTerms: {
effective: settings?.searchTerms ?? [],
default: settings?.defaultSearchTerms ?? [],
},
jobspy: {
location: {
effective: settings?.jobspyLocation ?? "",
default: settings?.defaultJobspyLocation ?? "",
},
resultsWanted: {
effective: settings?.jobspyResultsWanted ?? 200,
default: settings?.defaultJobspyResultsWanted ?? 200,
},
hoursOld: {
effective: settings?.jobspyHoursOld ?? 72,
default: settings?.defaultJobspyHoursOld ?? 72,
},
countryIndeed: {
effective: settings?.jobspyCountryIndeed ?? "",
default: settings?.defaultJobspyCountryIndeed ?? "",
},
sites: {
effective: settings?.jobspySites ?? ["indeed", "linkedin"],
default: settings?.defaultJobspySites ?? ["indeed", "linkedin"],
},
linkedinFetchDescription: {
effective: settings?.jobspyLinkedinFetchDescription ?? true,
default: settings?.defaultJobspyLinkedinFetchDescription ?? true,
},
},
display: {
effectiveShowSponsorInfo: settings?.showSponsorInfo ?? true,
defaultShowSponsorInfo: settings?.defaultShowSponsorInfo ?? true,
},
defaultResumeProjects: settings?.defaultResumeProjects ?? null,
profileProjects,
maxProjectsTotal: profileProjects.length,
}
}
export const SettingsPage: React.FC = () => { export const SettingsPage: React.FC = () => {
const [settings, setSettings] = useState<AppSettings | null>(null) const [settings, setSettings] = useState<AppSettings | null>(null)
const [isSaving, setIsSaving] = useState(false) const [isSaving, setIsSaving] = useState(false)
@ -120,37 +214,20 @@ export const SettingsPage: React.FC = () => {
} }
}, [reset]) }, [reset])
const effectiveModel = settings?.model ?? "" const derived = getDerivedSettings(settings)
const defaultModel = settings?.defaultModel ?? "" const {
const effectiveModelScorer = settings?.modelScorer ?? "" model,
const effectiveModelTailoring = settings?.modelTailoring ?? "" pipelineWebhook,
const effectiveModelProjectSelection = settings?.modelProjectSelection ?? "" jobCompleteWebhook,
const effectivePipelineWebhookUrl = settings?.pipelineWebhookUrl ?? "" ukvisajobs,
const defaultPipelineWebhookUrl = settings?.defaultPipelineWebhookUrl ?? "" gradcracker,
const effectiveJobCompleteWebhookUrl = settings?.jobCompleteWebhookUrl ?? "" searchTerms,
const defaultJobCompleteWebhookUrl = settings?.defaultJobCompleteWebhookUrl ?? "" jobspy,
const effectiveUkvisajobsMaxJobs = settings?.ukvisajobsMaxJobs ?? 50 display,
const defaultUkvisajobsMaxJobs = settings?.defaultUkvisajobsMaxJobs ?? 50 defaultResumeProjects,
const effectiveGradcrackerMaxJobsPerTerm = settings?.gradcrackerMaxJobsPerTerm ?? 50 profileProjects,
const defaultGradcrackerMaxJobsPerTerm = settings?.defaultGradcrackerMaxJobsPerTerm ?? 50 maxProjectsTotal,
const effectiveSearchTerms = settings?.searchTerms ?? [] } = derived
const defaultSearchTerms = settings?.defaultSearchTerms ?? []
const effectiveJobspyLocation = settings?.jobspyLocation ?? ""
const defaultJobspyLocation = settings?.defaultJobspyLocation ?? ""
const effectiveJobspyResultsWanted = settings?.jobspyResultsWanted ?? 200
const defaultJobspyResultsWanted = settings?.defaultJobspyResultsWanted ?? 200
const effectiveJobspyHoursOld = settings?.jobspyHoursOld ?? 72
const defaultJobspyHoursOld = settings?.defaultJobspyHoursOld ?? 72
const effectiveJobspyCountryIndeed = settings?.jobspyCountryIndeed ?? ""
const defaultJobspyCountryIndeed = settings?.defaultJobspyCountryIndeed ?? ""
const effectiveJobspySites = settings?.jobspySites ?? ["indeed", "linkedin"]
const defaultJobspySites = settings?.defaultJobspySites ?? ["indeed", "linkedin"]
const effectiveJobspyLinkedinFetchDescription = settings?.jobspyLinkedinFetchDescription ?? true
const defaultJobspyLinkedinFetchDescription = settings?.defaultJobspyLinkedinFetchDescription ?? true
const effectiveShowSponsorInfo = settings?.showSponsorInfo ?? true
const defaultShowSponsorInfo = settings?.defaultShowSponsorInfo ?? true
const profileProjects = settings?.profileProjects ?? []
const maxProjectsTotal = profileProjects.length
const watchedValues = watch() const watchedValues = watch()
const lockedCount = watchedValues.resumeProjects?.lockedProjectIds.length ?? 0 const lockedCount = watchedValues.resumeProjects?.lockedProjectIds.length ?? 0
@ -164,28 +241,31 @@ export const SettingsPage: React.FC = () => {
// Prepare payload: nullify if equal to default // Prepare payload: nullify if equal to default
const resumeProjectsData = data.resumeProjects const resumeProjectsData = data.resumeProjects
const resumeProjectsOverride = (resumeProjectsData && settings.defaultResumeProjects && resumeProjectsEqual(resumeProjectsData, settings.defaultResumeProjects)) const resumeProjectsOverride = (resumeProjectsData && defaultResumeProjects && resumeProjectsEqual(resumeProjectsData, defaultResumeProjects))
? null ? null
: resumeProjectsData : resumeProjectsData
const payload: UpdateSettingsInput = { const payload: UpdateSettingsInput = {
model: data.model?.trim() || null, model: normalizeString(data.model),
modelScorer: data.modelScorer?.trim() || null, modelScorer: normalizeString(data.modelScorer),
modelTailoring: data.modelTailoring?.trim() || null, modelTailoring: normalizeString(data.modelTailoring),
modelProjectSelection: data.modelProjectSelection?.trim() || null, modelProjectSelection: normalizeString(data.modelProjectSelection),
pipelineWebhookUrl: data.pipelineWebhookUrl?.trim() || null, pipelineWebhookUrl: normalizeString(data.pipelineWebhookUrl),
jobCompleteWebhookUrl: data.jobCompleteWebhookUrl?.trim() || null, jobCompleteWebhookUrl: normalizeString(data.jobCompleteWebhookUrl),
resumeProjects: resumeProjectsOverride, resumeProjects: resumeProjectsOverride,
ukvisajobsMaxJobs: data.ukvisajobsMaxJobs === defaultUkvisajobsMaxJobs ? null : data.ukvisajobsMaxJobs, ukvisajobsMaxJobs: nullIfSame(data.ukvisajobsMaxJobs, ukvisajobs.defaultMaxJobs),
gradcrackerMaxJobsPerTerm: data.gradcrackerMaxJobsPerTerm === defaultGradcrackerMaxJobsPerTerm ? null : data.gradcrackerMaxJobsPerTerm, gradcrackerMaxJobsPerTerm: nullIfSame(data.gradcrackerMaxJobsPerTerm, gradcracker.defaultMaxJobsPerTerm),
searchTerms: JSON.stringify(data.searchTerms) === JSON.stringify(defaultSearchTerms) ? null : data.searchTerms, searchTerms: nullIfSameList(data.searchTerms, searchTerms.default),
jobspyLocation: data.jobspyLocation === defaultJobspyLocation ? null : data.jobspyLocation, jobspyLocation: nullIfSame(data.jobspyLocation, jobspy.location.default),
jobspyResultsWanted: data.jobspyResultsWanted === defaultJobspyResultsWanted ? null : data.jobspyResultsWanted, jobspyResultsWanted: nullIfSame(data.jobspyResultsWanted, jobspy.resultsWanted.default),
jobspyHoursOld: data.jobspyHoursOld === defaultJobspyHoursOld ? null : data.jobspyHoursOld, jobspyHoursOld: nullIfSame(data.jobspyHoursOld, jobspy.hoursOld.default),
jobspyCountryIndeed: data.jobspyCountryIndeed === defaultJobspyCountryIndeed ? null : data.jobspyCountryIndeed, jobspyCountryIndeed: nullIfSame(data.jobspyCountryIndeed, jobspy.countryIndeed.default),
jobspySites: JSON.stringify((data.jobspySites ?? []).slice().sort()) === JSON.stringify((defaultJobspySites ?? []).slice().sort()) ? null : data.jobspySites, jobspySites: nullIfSameSortedList(data.jobspySites, jobspy.sites.default),
jobspyLinkedinFetchDescription: data.jobspyLinkedinFetchDescription === defaultJobspyLinkedinFetchDescription ? null : data.jobspyLinkedinFetchDescription, jobspyLinkedinFetchDescription: nullIfSame(
showSponsorInfo: data.showSponsorInfo === defaultShowSponsorInfo ? null : data.showSponsorInfo, data.jobspyLinkedinFetchDescription,
jobspy.linkedinFetchDescription.default
),
showSponsorInfo: nullIfSame(data.showSponsorInfo, display.defaultShowSponsorInfo),
} }
const updated = await api.updateSettings(payload) const updated = await api.updateSettings(payload)
@ -281,57 +361,57 @@ export const SettingsPage: React.FC = () => {
<main className="container mx-auto max-w-3xl space-y-6 px-4 py-6 pb-12"> <main className="container mx-auto max-w-3xl space-y-6 px-4 py-6 pb-12">
<Accordion type="multiple" className="w-full space-y-4"> <Accordion type="multiple" className="w-full space-y-4">
<ModelSettingsSection <ModelSettingsSection
effectiveModel={effectiveModel} effectiveModel={model.effective}
effectiveModelScorer={effectiveModelScorer} effectiveModelScorer={model.scorer}
effectiveModelTailoring={effectiveModelTailoring} effectiveModelTailoring={model.tailoring}
effectiveModelProjectSelection={effectiveModelProjectSelection} effectiveModelProjectSelection={model.projectSelection}
defaultModel={defaultModel} defaultModel={model.default}
isLoading={isLoading} isLoading={isLoading}
isSaving={isSaving} isSaving={isSaving}
/> />
<PipelineWebhookSection <PipelineWebhookSection
defaultPipelineWebhookUrl={defaultPipelineWebhookUrl} defaultPipelineWebhookUrl={pipelineWebhook.default}
effectivePipelineWebhookUrl={effectivePipelineWebhookUrl} effectivePipelineWebhookUrl={pipelineWebhook.effective}
isLoading={isLoading} isLoading={isLoading}
isSaving={isSaving} isSaving={isSaving}
/> />
<JobCompleteWebhookSection <JobCompleteWebhookSection
defaultJobCompleteWebhookUrl={defaultJobCompleteWebhookUrl} defaultJobCompleteWebhookUrl={jobCompleteWebhook.default}
effectiveJobCompleteWebhookUrl={effectiveJobCompleteWebhookUrl} effectiveJobCompleteWebhookUrl={jobCompleteWebhook.effective}
isLoading={isLoading} isLoading={isLoading}
isSaving={isSaving} isSaving={isSaving}
/> />
<UkvisajobsSection <UkvisajobsSection
defaultUkvisajobsMaxJobs={defaultUkvisajobsMaxJobs} defaultUkvisajobsMaxJobs={ukvisajobs.defaultMaxJobs}
effectiveUkvisajobsMaxJobs={effectiveUkvisajobsMaxJobs} effectiveUkvisajobsMaxJobs={ukvisajobs.effectiveMaxJobs}
isLoading={isLoading} isLoading={isLoading}
isSaving={isSaving} isSaving={isSaving}
/> />
<GradcrackerSection <GradcrackerSection
defaultGradcrackerMaxJobsPerTerm={defaultGradcrackerMaxJobsPerTerm} defaultGradcrackerMaxJobsPerTerm={gradcracker.defaultMaxJobsPerTerm}
effectiveGradcrackerMaxJobsPerTerm={effectiveGradcrackerMaxJobsPerTerm} effectiveGradcrackerMaxJobsPerTerm={gradcracker.effectiveMaxJobsPerTerm}
isLoading={isLoading} isLoading={isLoading}
isSaving={isSaving} isSaving={isSaving}
/> />
<SearchTermsSection <SearchTermsSection
defaultSearchTerms={defaultSearchTerms} defaultSearchTerms={searchTerms.default}
effectiveSearchTerms={effectiveSearchTerms} effectiveSearchTerms={searchTerms.effective}
isLoading={isLoading} isLoading={isLoading}
isSaving={isSaving} isSaving={isSaving}
/> />
<JobspySection <JobspySection
defaultJobspySites={defaultJobspySites} defaultJobspySites={jobspy.sites.default}
effectiveJobspySites={effectiveJobspySites} effectiveJobspySites={jobspy.sites.effective}
defaultJobspyLocation={defaultJobspyLocation} defaultJobspyLocation={jobspy.location.default}
effectiveJobspyLocation={effectiveJobspyLocation} effectiveJobspyLocation={jobspy.location.effective}
defaultJobspyResultsWanted={defaultJobspyResultsWanted} defaultJobspyResultsWanted={jobspy.resultsWanted.default}
effectiveJobspyResultsWanted={effectiveJobspyResultsWanted} effectiveJobspyResultsWanted={jobspy.resultsWanted.effective}
defaultJobspyHoursOld={defaultJobspyHoursOld} defaultJobspyHoursOld={jobspy.hoursOld.default}
effectiveJobspyHoursOld={effectiveJobspyHoursOld} effectiveJobspyHoursOld={jobspy.hoursOld.effective}
defaultJobspyCountryIndeed={defaultJobspyCountryIndeed} defaultJobspyCountryIndeed={jobspy.countryIndeed.default}
effectiveJobspyCountryIndeed={effectiveJobspyCountryIndeed} effectiveJobspyCountryIndeed={jobspy.countryIndeed.effective}
defaultJobspyLinkedinFetchDescription={defaultJobspyLinkedinFetchDescription} defaultJobspyLinkedinFetchDescription={jobspy.linkedinFetchDescription.default}
effectiveJobspyLinkedinFetchDescription={effectiveJobspyLinkedinFetchDescription} effectiveJobspyLinkedinFetchDescription={jobspy.linkedinFetchDescription.effective}
isLoading={isLoading} isLoading={isLoading}
isSaving={isSaving} isSaving={isSaving}
/> />
@ -343,8 +423,8 @@ export const SettingsPage: React.FC = () => {
isSaving={isSaving} isSaving={isSaving}
/> />
<DisplaySettingsSection <DisplaySettingsSection
defaultShowSponsorInfo={defaultShowSponsorInfo} defaultShowSponsorInfo={display.defaultShowSponsorInfo}
effectiveShowSponsorInfo={effectiveShowSponsorInfo} effectiveShowSponsorInfo={display.effectiveShowSponsorInfo}
isLoading={isLoading} isLoading={isLoading}
isSaving={isSaving} isSaving={isSaving}
/> />