feat: add support for indicating workplaceTypes (#296)

This commit is contained in:
Ryan Foote
2026-03-21 20:43:43 +00:00
committed by GitHub
parent 8274ec4e14
commit 0b22c08d7d
25 changed files with 497 additions and 5 deletions
+13
View File
@@ -87,6 +87,19 @@ describe("settingsRegistry helpers", () => {
);
expect(settingsRegistry.searchTerms.serialize(null)).toBeNull();
});
it("parses valid workplace type arrays", () => {
expect(
settingsRegistry.workplaceTypes.parse('["remote","onsite"]'),
).toEqual(["remote", "onsite"]);
});
it("rejects invalid workplace type arrays", () => {
expect(
settingsRegistry.workplaceTypes.parse('["remote","satellite"]'),
).toBeNull();
expect(settingsRegistry.workplaceTypes.parse("[]")).toBeNull();
});
});
describe("Resume projects settings", () => {
+43
View File
@@ -90,6 +90,35 @@ function createEnumParser<const TValues extends readonly [string, ...string[]]>(
};
}
function createEnumArrayParser<
const TValues extends readonly [string, ...string[]],
>(values: TValues): (raw: string | undefined) => TValues[number][] | null {
const allowedValues = new Set<string>(values);
return (raw: string | undefined): TValues[number][] | null => {
if (!raw) return null;
try {
const parsed = JSON.parse(raw) as unknown;
if (!Array.isArray(parsed)) return null;
const out: TValues[number][] = [];
const seen = new Set<string>();
for (const value of parsed) {
if (typeof value !== "string" || !allowedValues.has(value)) {
return null;
}
if (seen.has(value)) continue;
seen.add(value);
out.push(value as TValues[number]);
}
if (out.length === 0) return null;
return out;
} catch {
return null;
}
};
}
const parseChatStyleLanguageModeOrNull = createEnumParser(
CHAT_STYLE_LANGUAGE_MODE_VALUES,
);
@@ -98,6 +127,9 @@ const parseChatStyleManualLanguageOrNull = createEnumParser(
CHAT_STYLE_MANUAL_LANGUAGE_VALUES,
);
const WORKPLACE_TYPE_VALUES = ["remote", "hybrid", "onsite"] as const;
const parseWorkplaceTypesOrNull = createEnumArrayParser(WORKPLACE_TYPE_VALUES);
export const resumeProjectsSchema = z.object({
maxProjects: z.number().int().min(0).max(100),
lockedProjectIds: z.array(z.string().trim().min(1)).max(200),
@@ -271,6 +303,17 @@ export const settingsRegistry = {
parse: parseJsonArrayOrNull,
serialize: serializeNullableJsonArray,
},
workplaceTypes: {
kind: "typed" as const,
schema: z.array(z.enum(WORKPLACE_TYPE_VALUES)).min(1).max(3),
default: (): Array<(typeof WORKPLACE_TYPE_VALUES)[number]> => [
"remote",
"hybrid",
"onsite",
],
parse: parseWorkplaceTypesOrNull,
serialize: serializeNullableJsonArray,
},
blockedCompanyKeywords: {
kind: "typed" as const,
schema: z.array(z.string().trim().min(1).max(200)).max(200),
+5
View File
@@ -159,6 +159,11 @@ export const createAppSettings = (
default: ["Software Engineer"],
override: null,
},
workplaceTypes: {
value: ["remote", "hybrid", "onsite"],
default: ["remote", "hybrid", "onsite"],
override: null,
},
blockedCompanyKeywords: {
value: [],
default: [],
+1
View File
@@ -155,6 +155,7 @@ export interface AppSettings {
gradcrackerMaxJobsPerTerm: Resolved<number>;
startupjobsMaxJobsPerTerm: Resolved<number>;
searchTerms: Resolved<string[]>;
workplaceTypes: Resolved<Array<"remote" | "hybrid" | "onsite">>;
blockedCompanyKeywords: Resolved<string[]>;
scoringInstructions: Resolved<string>;
searchCities: Resolved<string>;