Created
June 5, 2021 14:15
-
-
Save geovanisouza92/47331bc73cb4df826d2823491747f707 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* eslint-disable @typescript-eslint/no-non-null-assertion */ | |
type Job = { | |
id: number; | |
code: string; | |
name: string; | |
jobType: string; | |
status: string; | |
publicationType: string; | |
recruiterId: number; | |
recruiterName: string; | |
recruiterEmail: string; | |
managerId: number; | |
managerName: string; | |
managerEmail: string; | |
departmentId: number; | |
departmentName: string; | |
roleId: number; | |
roleName: string; | |
companyBranchId: number; | |
branchPath: string; | |
branchLabel: string; | |
}; | |
type Option = { | |
label: string; | |
value: string | null; | |
} | |
type FilterName = string; | |
export type FilterValue = string | number; | |
export type FilterValues = Record<FilterName, Array<string | number>>; | |
export type FilterOptions = Record<FilterName, Option[]>; | |
type FilterFn = (job: Job) => boolean; | |
interface FilterHandler { | |
filterFnFactory: (filterValues: FilterValue[]) => FilterFn; | |
asOption: (job: Job) => Option | Option[]; | |
comparator?: (a: Option, b: Option) => number; | |
} | |
type FilterOptionsMap = Record<FilterName, Map<string | number, Option | Option[]>>; | |
type FilterLabel = Option & { | |
filterName: FilterName; | |
parentValue: FilterValue; | |
}; | |
type FilterLabelsMap = Record<FilterName, Map<FilterValue, FilterLabel>>; | |
function createSet(filterValues: unknown[]): Set<string> { | |
return new Set(filterValues.map((value) => `${value}`)); | |
} | |
function createIntegerSet(filterValues: Array<string | number>): Set<number> { | |
return new Set(filterValues.map((filterValue) => (typeof filterValue === 'number' | |
? filterValue | |
: parseInt(filterValue, 10)))); | |
} | |
function asMap(list: Option[]): Map<string, Option> { | |
return Array.isArray(list) | |
? new Map(list.map((it) => [it.value, it])) | |
: new Map(); | |
} | |
// The Design Team asked for a fixed order for better UX | |
const jobStatusOrder = [ | |
'published', | |
'closed', | |
'frozen', | |
'canceled', | |
]; | |
type FilterContextCtor = { | |
userId: number; | |
ownRecruiterLabel: string; | |
ownManagerLabel: string; | |
filterContext: { | |
jobList: Job[]; | |
branches: Option[]; | |
jobTypesList: Option[]; | |
jobStatusList: Option[]; | |
jobPublicationTypesList: Option[]; | |
}; | |
}; | |
/** | |
* This class holds all the machinery for producing "filter options" and "filter labels" based | |
* on "filter values" on a consistent and _fast_ fashion. | |
*/ | |
export class FilterContext { | |
private userId: number; | |
private ownRecruiterLabel: string; | |
private ownManagerLabel: string; | |
private jobList: Job[]; | |
// private branchesMap: Map<string, Option>; | |
private jobTypesMap: Map<string, Option>; | |
private jobStatusMap: Map<string, Option>; | |
private jobPublicationTypesMap: Map<string, Option>; | |
private filterRegistry!: Record<FilterName, FilterHandler>; | |
private filterNames!: FilterName[]; | |
private filterOptionsIndex!: FilterOptionsMap; | |
private filterLabelsIndex!: FilterLabelsMap; | |
constructor({ | |
userId, | |
ownRecruiterLabel, | |
ownManagerLabel, | |
filterContext, | |
}: FilterContextCtor) { | |
this.userId = userId; | |
this.ownRecruiterLabel = ownRecruiterLabel; | |
this.ownManagerLabel = ownManagerLabel; | |
this.jobList = filterContext.jobList; | |
// this.branchesMap = asMap(filterContext.branches); | |
this.jobTypesMap = asMap(filterContext.jobTypesList); | |
this.jobStatusMap = asMap(filterContext.jobStatusList); | |
this.jobPublicationTypesMap = asMap(filterContext.jobPublicationTypesList); | |
this.initFilterRegistry(); | |
this.initFilterOptionsIndex(); | |
this.initFilterLabelsIndex(); | |
} | |
private initFilterRegistry(): void { | |
this.filterRegistry = { | |
jobs: { | |
filterFnFactory: (filterValues): FilterFn => { | |
const values = createIntegerSet(filterValues); | |
return (job) => values.has(job.id); | |
}, | |
asOption: (job) => ({ | |
value: job.id ? `${job.id}` : null, | |
label: [job.code, job.name].filter(Boolean).join(' - '), | |
}), | |
}, | |
jobTypes: { | |
filterFnFactory: (filterValues): FilterFn => { | |
const values = createSet(filterValues); | |
return (job) => values.has(job.jobType); | |
}, | |
asOption: (job) => this.jobTypesMap.get(job.jobType)!, | |
}, | |
jobStatus: { | |
filterFnFactory: (filterValues): FilterFn => { | |
const values = createSet(filterValues); | |
return (job) => values.has(job.status); | |
}, | |
asOption: (job) => this.jobStatusMap.get(job.status)!, | |
comparator: (a, b) => jobStatusOrder.indexOf(a.value!) - jobStatusOrder.indexOf(b.value!), | |
}, | |
jobPublicationTypes: { | |
filterFnFactory: (filterValues): FilterFn => { | |
const values = createSet(filterValues); | |
return (job) => values.has(job.publicationType); | |
}, | |
asOption: (job) => this.jobPublicationTypesMap.get(job.publicationType)!, | |
}, | |
recruiters: { | |
filterFnFactory: (filterValues): FilterFn => { | |
const values = createIntegerSet(filterValues); | |
return (job) => values.has(job.recruiterId); | |
}, | |
asOption: (job) => ({ | |
value: job.recruiterId ? `${job.recruiterId}` : null, | |
label: job.recruiterId === this.userId | |
? this.ownRecruiterLabel | |
: [job.recruiterName, job.recruiterEmail].filter(Boolean).join(' - '), | |
}), | |
}, | |
managers: { | |
filterFnFactory: (filterValues): FilterFn => { | |
const values = createIntegerSet(filterValues); | |
return (job) => values.has(job.managerId); | |
}, | |
asOption: (job) => ({ | |
value: job.managerId ? `${job.managerId}` : null, | |
label: job.managerId === this.userId | |
? this.ownManagerLabel | |
: [job.managerName, job.managerEmail].filter(Boolean).join(' - '), | |
}), | |
}, | |
departments: { | |
filterFnFactory: (filterValues): FilterFn => { | |
const values = createIntegerSet(filterValues); | |
return (job) => values.has(job.departmentId); | |
}, | |
asOption: (job) => ({ | |
value: job.departmentId ? `${job.departmentId}` : null, | |
label: job.departmentName, | |
}), | |
}, | |
roles: { | |
filterFnFactory: (filterValues): FilterFn => { | |
const values = createIntegerSet(filterValues); | |
return (job) => values.has(job.roleId); | |
}, | |
asOption: (job) => ({ | |
value: job.roleId ? `${job.roleId}` : null, | |
label: job.roleName, | |
}), | |
}, | |
/* | |
subsidiaries: { | |
filterFnFactory: (filterValues): FilterFn => { | |
const values = createIntegerSet(filterValues); | |
return (job) => values.has(job.companyBranchId); | |
}, | |
asOption: (job) => ({ | |
value: job.companyBranchId ? `${job.companyBranchId}` : null, | |
label: job.branchLabel, | |
}), | |
}, | |
branches: { | |
filterFnFactory: (filterValues): FilterFn => { | |
const values = [...createSet(filterValues)]; | |
return (job) => (job.branchPath | |
? values.some((value) => job.branchPath.startsWith(value)) | |
: false); | |
}, | |
asOption: (job) => { | |
const branch = this.branchesMap.get(job.branchPath); | |
if (!branch) return []; | |
const parents: Option[] = (branch as any).parents | |
.map((parent) => this.branchesMap.get(parent.value)); | |
return parents.concat(branch); | |
}, | |
}, | |
*/ | |
}; | |
this.filterNames = Object.keys(this.filterRegistry); | |
} | |
private initFilterOptionsIndex(): void { | |
this.filterOptionsIndex = {}; | |
this.filterNames.forEach((filterName) => { | |
const { [filterName]: handler } = this.filterRegistry; | |
const byJob = new Map(this.jobList.map((job) => { | |
const option = handler.asOption(job); | |
return [job.id, option]; | |
})); | |
this.filterOptionsIndex[filterName] = byJob; | |
}); | |
} | |
private initFilterLabelsIndex(): void { | |
this.filterLabelsIndex = {}; | |
this.filterNames.forEach((filterName) => { | |
this.filterLabelsIndex[filterName] = new Map(); | |
}); | |
} | |
/** | |
* Return a list of jobs that matches all filter values. Internally it builds a matching | |
* functions that uses a list of per-filter matching functions that compare specific job | |
* properties with corresponding values for each filter. | |
* | |
* Complexity: O(n) where n is the number of jobs. | |
*/ | |
private selectJobsBy(filterValues: FilterValues): Job[] { | |
const filterFns = this.filterNames | |
.filter((filterName) => filterName in filterValues) | |
.map((filterName) => { | |
const { [filterName]: handler } = this.filterRegistry; | |
const { [filterName]: values } = filterValues; | |
return handler.filterFnFactory(values); | |
}); | |
const doesMatch = (job): boolean => filterFns.every((doesFilterMatch) => doesFilterMatch(job)); | |
return this.jobList.filter(doesMatch); | |
} | |
/** | |
* Find available options considering filterValues as criteria. For each filter value provided, | |
* it applies all other filter values except the filter being calculated and then uses the | |
* filter name and job ID as keys to find the option on the pre-calculated mapping. | |
* | |
* Complexity: O(n log n) in best scenario, O(n²) in worst scenario | |
*/ | |
getFilterOptionsBy(filterValues: FilterValues): FilterOptions { | |
const filterOptions: FilterOptions = {}; | |
return this.filterNames | |
.reduce((prevOptions, filterName) => { | |
const { [filterName]: _, ...filterValuesEx } = filterValues; | |
const { [filterName]: byJob } = this.filterOptionsIndex; | |
const { [filterName]: handler } = this.filterRegistry; | |
const unique = new Map(); | |
this.selectJobsBy(filterValuesEx).forEach((job) => { | |
const options: Option[] = ([] as Option[]).concat(byJob.get(job.id)!); | |
options.forEach((option) => { | |
if (option && option.value && !unique.has(option.value)) { | |
unique.set(option.value, option); | |
} | |
}); | |
}); | |
const options = handler.comparator | |
? [...unique.values()].sort(handler.comparator) | |
: [...unique.values()]; | |
return { | |
...prevOptions, | |
[filterName]: options, | |
}; | |
}, filterOptions); | |
} | |
/** | |
* Convert applied filter values to human-readable labels. For each applied filter we get | |
* (or generate) the corresponding label (same as option). | |
* | |
* Complexity: O(n log n) in best scenario, O(n²) in worst-scenario | |
*/ | |
getFilterLabelsBy(filterValues: FilterValues): FilterLabel[] { | |
return this.filterNames | |
.filter((filterName) => filterName in filterValues) | |
.flatMap((filterName) => { | |
const { [filterName]: byValue } = this.filterLabelsIndex; | |
const { [filterName]: values } = filterValues; | |
return values.map((value) => { | |
if (!byValue.has(value)) { | |
const { [filterName]: byJob } = this.filterOptionsIndex; | |
const option: Option = this.selectJobsBy({ [filterName]: [value] }) | |
.flatMap((job): Option[] => ([] as Option[]).concat(byJob.get(job.id)!)) | |
.filter(Boolean) | |
.find((it) => it.value === `${value}`)!; | |
byValue.set(value, { | |
...option, | |
filterName, | |
parentValue: value, | |
}); | |
} | |
return byValue.get(value)!; | |
}); | |
}); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment