Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix total count of filter options for issue #525 #528

Draft
wants to merge 14 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/app/job-details/job-details.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ export class JobDetailsComponent implements OnInit {

public getRelatedJobs(): any {
if (this.job && this.job.publishedCategory) {
this.service.getjobs({ 'publishedCategory.id': [this.job.publishedCategory.id]}, {} , SettingsService.settings.service.batchSize).subscribe((res: any) => { this.relatedJobs = res.data; });
this.service.getJobs({ 'publishedCategory.id': [this.job.publishedCategory.id]}, {} , SettingsService.settings.service.batchSize).subscribe((res: any) => { this.relatedJobs = res.data; });
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/app/job-list/job-list.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export class JobListComponent implements OnChanges {
this.meta.updateTag({ name: 'og:description', content: description });
this.meta.updateTag({ name: 'twitter:description', content: description });
this.meta.updateTag({ name: 'description', content: description });
this.http.getjobs(this.filter, { start: this.start }).subscribe(this.onSuccess.bind(this), this.onFailure.bind(this));
this.http.getJobs(this.filter, { start: this.start }).subscribe(this.onSuccess.bind(this), this.onFailure.bind(this));
}

public loadMore(): void {
Expand Down
173 changes: 127 additions & 46 deletions src/app/services/search/search.service.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { SettingsService } from '../settings/settings.service';
import { Observable, of } from 'rxjs';
import { Observable, of, forkJoin } from 'rxjs';
import { IServiceSettings } from '../../typings/settings';
import { concatMap, map } from 'rxjs/operators';

@Injectable()
export class SearchService {
Expand All @@ -17,7 +18,7 @@ export class SearchService {
return `${scheme}://public-rest${service?.swimlane}.bullhornstaffing.com:${port}/rest-services/${service?.corpToken}`;
}

public getjobs(filter?: any, params: any = {}, count: number = 30): Observable<any> {
public getJobs(filter?: any, params: any = {}, count: number = 30): Observable<any> {
let queryArray: string[] = [];
params.query = `(isOpen:1) AND (isDeleted:0)${this.formatAdditionalCriteria(true)}${this.formatFilter(filter, true)}`;
params.fields = SettingsService.settings.service.fields;
Expand All @@ -37,55 +38,136 @@ export class SearchService {
return this.http.get(`${this.baseUrl}/query/JobBoardPost?where=(id=${id})&fields=${SettingsService.settings?.service?.fields}`);
}

public getCurrentJobIds(filter: any, ignoreFields: string[]): Observable<any> {
let queryArray: string[] = [];
let params: any = {};
public getCurrentJobIds(filter: any, ignoreFields: string[]): Observable<any[]> {
const queryString: string = this.getQueryString(filter, ignoreFields);

// Recursive function to fetch all records
const fetchAllRecords = (start: number = 0, records: any[] = []): Observable<any> => {
return this.getJobRecords(queryString, start).pipe(
concatMap((response: any) => {
// Concatenate records from the response
const updatedRecords = [...records, ...response.data];

if (updatedRecords.length < response.total) {
// Continue fetching more records if needed
return fetchAllRecords(updatedRecords.length, updatedRecords);
} else {
// Return the accumulated records when done
return of(updatedRecords);
}
}),
);
};

// Start fetching all records
return fetchAllRecords();
}

private getQueryString(filter: any, ignoreFields: string[]): string {
// Construct the query string based on filter and parameters
const params = {
query: `(isOpen:1) AND (isDeleted:0)${this.formatAdditionalCriteria(true)}${this.formatFilter(filter, true, ignoreFields)}`,
count: `500`,
fields: 'id',
sort: 'id'
};

// Join the query parameters with '&' to form the complete query string
return Object.entries(params).map(([key, value]) => `${key}=${value}`).join('&');
}

private getJobRecords(queryString: string, start: number = 0): Observable<any> {
// Fetch job records from the API with the specified query and start offset
return this.http.get(`${this.baseUrl}/search/JobOrder?start=${start}&${queryString}`);
}

params.query = `(isOpen:1) AND (isDeleted:0)${this.formatAdditionalCriteria(true)}${this.formatFilter(filter, true, ignoreFields)}`;
params.count = `500`;
params.fields = 'id';
params.sort = 'id';
// Function to get available filter options
public getAvailableFilterOptions(ids: number[], field: string): Observable<any> {
// If there are no ids, return an empty response
if(ids.length === 0) {
return of({count:0, start:0, data:[]});
}

for (let key in params) {
queryArray.push(`${key}=${params[key]}`);
// Define the batch size
const batchSize = 500;

// Create an array of observables for each batch of ids
const observables = Array(Math.ceil(ids.length / batchSize)).fill(null).map((_, index) => {
// Get the ids for the current batch
const batchIds = ids.slice(index * batchSize, (index + 1) * batchSize);

// Define the parameters for the HTTP request
const params: any = {
count: 500,
fields: `${field},count(id)`,
groupBy: field,
where: `id IN (${batchIds.toString()})`,
orderBy: this.getOrderByField(field) // Get the order by field based on the field parameter
}
let queryString: string = queryArray.join('&');

return this.http.get(`${this.baseUrl}/search/JobOrder?${queryString}`);
}
// Create the query string from the parameters
const queryString = Object.keys(params).map(key => `${key}=${params[key]}`).join('&');

// Return the observable for the HTTP request
return this.http.get(`${this.baseUrl}/query/JobBoardPost?${queryString}`);
});

// Use forkJoin to wait for all observables to complete and then process the responses
return forkJoin(observables).pipe(
map((responses: any[]) => {
// Reduce the responses to a single response by merging the data
const mergedResponse = responses.reduce((acc, response) => {
// For each item in the response data
response.data.forEach(item => {
// Find the index of the existing item in the accumulator data
const existingItemIndex = acc.data.findIndex(x => this.isSameItem(x, item, field));

// If the item exists, increment its count
if(existingItemIndex !== -1) {
acc.data[existingItemIndex].idCount += item.idCount;
} else {
// If the item does not exist, add it to the accumulator data
acc.data.push(item);
}
})

// Return the accumulator
return acc;
}, {count: 0, start: 0, data: []});

// Return the merged response
return mergedResponse;
}),
)
}

public getAvailableFilterOptions(ids: number[], field: string): Observable<any> {
let params: any = {};
let queryArray: string[] = [];
if (ids.length > 0) {
params.where = `id IN (${ids.toString()})`;
params.count = `500`;
params.fields = `${field},count(id)`;
params.groupBy = field;
switch (field) {
case 'publishedCategory(id,name)':
params.orderBy = 'publishedCategory.name';
break;
case 'address(state)':
params.orderBy = 'address.state';
break;
case 'address(city)':
params.orderBy = 'address.city';
break;
default:
params.orderBy = '-count.id';
break;
}
for (let key in params) {
queryArray.push(`${key}=${params[key]}`);
}
let queryString: string = queryArray.join('&');
// Function to get the order by field based on the field parameter
private getOrderByField(field: string): string {
switch (field) {
case 'publishedCategory(id,name)':
return 'publishedCategory.name';
case 'address(state)':
return 'address.state';
case 'address(city)':
return 'address.city';
default:
return '-count.id';
}
}

return this.http.get(`${this.baseUrl}/query/JobBoardPost?${queryString}`); // tslint:disable-line
} else {
return of({count: 0, start: 0, data: []});
}
// Function to check if two items are the same based on the field parameter
private isSameItem(item1: any, item2: any, field: string): boolean {
switch(field) {
case 'publishedCategory(id,name)':
return item1?.publishedCategory?.id === item2?.publishedCategory?.id;
case 'address(state)':
return item1?.address?.state === item2?.address?.state;
case 'address(city)':
return item1?.address?.city === item2?.address?.city;
default:
return false;
}
}

private formatAdditionalCriteria(isSearch: boolean): string {
let field: string = SettingsService.settings.additionalJobCriteria.field;
Expand Down Expand Up @@ -123,5 +205,4 @@ export class SearchService {

return additionalFilter.replace(/{\?\^\^equals}/g, isSearch ? ':' : '=').replace(/{\?\^\^delimiter}/g, isSearch ? '"' : '\'');
}

}
}
76 changes: 40 additions & 36 deletions src/app/sidebar/sidebar-filter/sidebar-filter.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export class SidebarFilterComponent implements OnChanges {
public options: any[];
public fieldName: string;

constructor(private service: SearchService, private formUtils: FormUtils) { }
constructor(private service: SearchService, private formUtils: FormUtils) {}

public ngOnChanges(changes: SimpleChanges): void {
switch (this.field) {
Expand All @@ -45,48 +45,53 @@ export class SidebarFilterComponent implements OnChanges {
}

private handleJobIdsOnSuccess(res: any): void {
let resultIds: number[] = res.data.map((result: any) => { return result.id; });
let resultIds: number[] = res.map((result: any) => {
return result.id;
});
this.service.getAvailableFilterOptions(resultIds, this.field).subscribe(this.setFieldOptionsOnSuccess.bind(this));

}

private setFieldOptionsOnSuccess(res: any): void {
let interaction: Function;
switch (this.field) {
case 'address(city)':
this.options = res.data.map((result: IAddressListResponse) => {
return {
value: result.address.city,
label: `${result.address.city} (${result.idCount})`,
};
}).filter((item: any) => {
return item.value;
});
this.options = res.data
.map((result: IAddressListResponse) => {
return {
value: result.address.city,
label: `${result.address.city} (${result.idCount})`,
};
})
.filter((item: any) => {
return item.value;
});
interaction = (API: FieldInteractionApi) => {
let values: string[] = [];
this.lastSetValue = API.getActiveValue();
if (API.getActiveValue()) {
values = API.getActiveValue().map((value: string ) => {
values = API.getActiveValue().map((value: string) => {
return `address.city{?^^equals}{?^^delimiter}${value}{?^^delimiter}`;
});
}
this.checkboxFilter.emit(values);
};
break;
case 'address(state)':
this.options = res.data.map((result: IAddressListResponse) => {
return {
value: result.address.state,
label: `${result.address.state} (${result.idCount})`,
};
}).filter((item: any) => {
return item.value;
});
this.options = res.data
.map((result: IAddressListResponse) => {
return {
value: result.address.state,
label: `${result.address.state} (${result.idCount})`,
};
})
.filter((item: any) => {
return item.value;
});
interaction = (API: FieldInteractionApi) => {
let values: string[] = [];
this.lastSetValue = API.getActiveValue();
if (API.getActiveValue()) {
values = API.getActiveValue().map((value: string ) => {
values = API.getActiveValue().map((value: string) => {
return `address.state{?^^equals}{?^^delimiter}${value}{?^^delimiter}`;
});
}
Expand All @@ -95,22 +100,22 @@ export class SidebarFilterComponent implements OnChanges {
break;
case 'publishedCategory(id,name)':
this.options = res.data
.filter((unfilteredResult: ICategoryListResponse) => {
return !!unfilteredResult.publishedCategory;
})
.map((result: ICategoryListResponse) => {
return {
value: result.publishedCategory.id,
label: `${result.publishedCategory.name} (${result.idCount})`,
};
});
.filter((unfilteredResult: ICategoryListResponse) => {
return !!unfilteredResult.publishedCategory;
})
.map((result: ICategoryListResponse) => {
return {
value: result.publishedCategory.id,
label: `${result.publishedCategory.name} (${result.idCount})`,
};
});
interaction = (API: FieldInteractionApi) => {
let values: string[] = [];
this.lastSetValue = API.getActiveValue();
if (API.getActiveValue()) {
values = API.getActiveValue().map((value: number) => {
return `publishedCategory.id{?^^equals}${value}`;
});
values = API.getActiveValue().map((value: number) => {
return `publishedCategory.id{?^^equals}${value}`;
});
}
this.checkboxFilter.emit(values);
};
Expand All @@ -122,11 +127,10 @@ export class SidebarFilterComponent implements OnChanges {
this.control = new CheckListControl({
key: 'checklist',
options: this.options,
interactions: [{event: 'change', script: interaction.bind(this), invokeOnInit: false}],
interactions: [{ event: 'change', script: interaction.bind(this), invokeOnInit: false }],
});
this.formUtils.setInitialValues([this.control], {'checklist': this.lastSetValue});
this.formUtils.setInitialValues([this.control], { checklist: this.lastSetValue });
this.form = this.formUtils.toFormGroup([this.control]);
this.loading = false;
}

}
25 changes: 15 additions & 10 deletions src/app/sidebar/sidebar.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,16 +70,19 @@ export class SidebarComponent {
}, 250);
}

public updateFilter(
field: string,
httpFormatedFilter: string | string[],
): void {
public updateFilter(field: string, httpFormatedFilter: string | string[]): void {
delete this.filter['keyword'];
this.filter[field] = httpFormatedFilter;
let filter: object = {};
Object.assign(filter, this.filter);
this.filter = filter; // triggering angular change detection
this.newFilter.emit(this.filter);
// max length of calling function changed to 500 to not break search. If more than 500, show all results until the filter is refined by user
if (Array.isArray(httpFormatedFilter) && httpFormatedFilter.length === 500) {
this.filter = {};
this.newFilter.emit(this.filter);
} else {
this.filter[field] = httpFormatedFilter;
let filter: object = {};
Object.assign(filter, this.filter);
this.filter = filter; // triggering angular change detection
this.newFilter.emit(this.filter);
}
}

public hideSidebar(): void {
Expand All @@ -101,7 +104,9 @@ export class SidebarComponent {
}

private handleJobIdsOnSuccess(res: any): void {
let resultIds: string[] = res.data.map((result: any) => {
// only show results if filter is less than 500. If more than 500, show all results until the filter is refined by user
res = res.slice(0, 500);
let resultIds: string[] = res.map((result: any) => {
return `id{?^^equals}${result.id}`;
});
if (resultIds.length === 0) {
Expand Down