objects for each section
This commit is contained in:
parent
f50f00a09a
commit
eb41c6237b
@ -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}
|
||||||
/>
|
/>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user