Skip to content

Commit

Permalink
search: add nested aggregations support
Browse files Browse the repository at this point in the history
* Adds nested aggregations support.
* Refactors RecordSearchComponent.
* Unsubscribes from observables when components are destroyed.
* Creates a RecordSearchService for handling aggregations filters.
* Adapts menu component in order to include query parameters.
* Sets aggregations buckets size to 8 in tester application.
* Fixes "combineLatest" RxJS function deprecation warnings.
* Exports RecordSearchService in public-api to use it outside ng-core.
* Initializes filters to launch first search on homepage.

Co-Authored-by: Alicia Zangger alicia.zangger@rero.ch
Co-Authored-by: Johnny Mariéthoz Johnny.Mariethoz@rero.ch
Co-Authored-by: Sébastien Délèze sebastien.deleze@rero.ch
  • Loading branch information
Sébastien Délèze committed Apr 7, 2020
1 parent 43e4bcc commit fd0fd70
Show file tree
Hide file tree
Showing 22 changed files with 926 additions and 547 deletions.
2 changes: 2 additions & 0 deletions projects/ng-core-tester/src/app/app-routing.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,8 @@ const routes: Routes = [
canRead,
aggregations,
adminMode: adminModeCan,
aggregationsBucketSize: 8,
aggregationsExpand: ['language'],
formFieldMap,
listHeaders: {
'Content-Type': 'application/rero+json'
Expand Down
11 changes: 7 additions & 4 deletions projects/ng-core-tester/src/app/home/home.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,9 @@
*/

import { Component } from '@angular/core';

import { DialogService, ApiService, TranslateLanguageService } from '@rero/ng-core';
import { DocumentComponent } from '../record/document/document.component';
import { ApiService, DialogService, RecordSearchService, TranslateLanguageService } from '@rero/ng-core';
import { ToastrService } from 'ngx-toastr';
import { DocumentComponent } from '../record/document/document.component';

@Component({
selector: 'app-home',
Expand Down Expand Up @@ -65,14 +64,18 @@ export class HomeComponent {
private dialogService: DialogService,
private apiService: ApiService,
private translateLanguageService: TranslateLanguageService,
private toastrService: ToastrService
private toastrService: ToastrService,
private _recordSearchService: RecordSearchService
) {
this.apiData = {
relative: this.apiService.getEndpointByType('documents'),
absolute: this.apiService.getEndpointByType('documents', true),
};

this.testLanguageTranslation = this.translateLanguageService.translate('fr', 'fr');

// Initializes aggregations filters to launch the first search.
this._recordSearchService.setAggregationsFilters([]);
}

showDialog() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ export class EditorComponent implements OnInit, OnDestroy {
* Component initialisation
*/
ngOnInit() {
combineLatest(this.route.params, this.route.queryParams)
combineLatest([this.route.params, this.route.queryParams])
.pipe(map(results => ({ params: results[0], query: results[1] })))
.subscribe(results => {
const params = results.params;
Expand Down
4 changes: 3 additions & 1 deletion projects/rero/ng-core/src/lib/record/record.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import { MultiSchemaTypeComponent } from './editor/multischema/multischema.compo
import { DatepickerTypeComponent } from './editor/type/datepicker-type.component';
import {ToggleWrapperComponent} from './editor/toggle-wrapper/toggle-wrappers.component';
import { hooksFormlyExtension } from './editor/extensions';
import { BucketsComponent } from './search/aggregation/buckets/buckets.component';


@NgModule({
Expand All @@ -75,7 +76,8 @@ import { hooksFormlyExtension } from './editor/extensions';
SwitchComponent,
MultiSchemaTypeComponent,
DatepickerTypeComponent,
ToggleWrapperComponent
ToggleWrapperComponent,
BucketsComponent
],
imports: [
CoreModule,
Expand Down
6 changes: 3 additions & 3 deletions projects/rero/ng-core/src/lib/record/record.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,15 +88,15 @@ export class RecordService {
* @param query - string, keyword to search for
* @param page - number, return records corresponding to this page
* @param itemsPerPage - number, number of records to return
* @param aggFilters - number, option list of filters
* @param aggregationsFilters - number, option list of filters
* @param sort - parameter for sorting records (eg. 'mostrecent' or '-mostrecent')
*/
public getRecords(
type: string,
query: string = '',
page = 1,
itemsPerPage = RecordService.DEFAULT_REST_RESULTS_SIZE,
aggFilters: any[] = [],
aggregationsFilters: any[] = [],
preFilters: object = {},
headers: any = null,
sort: string = null
Expand All @@ -110,7 +110,7 @@ export class RecordService {
httpParams = httpParams.append('sort', sort);
}

aggFilters.forEach((filter) => {
aggregationsFilters.forEach((filter) => {
filter.values.forEach((value: string) => {
httpParams = httpParams.append(filter.key, value);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,21 +24,11 @@ <h5 class="mb-0">
</div>
<div class="collapse" [class.show]="showAggregation()">
<div class="card-body">
<ul class="list-unstyled m-0">
<li class="form-check" *ngFor="let bucket of aggregation.value.buckets|slice:0:bucketSize">
<input class="form-check-input" type="checkbox" [checked]="isSelected(bucket.key)"
(click)="updateFilter(bucket.key) ">
<label class="form-check-label">
<span *ngIf="bucket.name">{{ bucket.name }}</span>
<span *ngIf="!bucket.name && aggregation.key != 'language'">{{ bucket.key | translate }}</span>
<span *ngIf="!bucket.name && aggregation.key == 'language'">{{ bucket.key | translateLanguage:language }}</span> ({{ bucket.doc_count }})
</label>
</li>
</ul>
<div *ngIf="displayMoreAndLessLink()">
<button class="btn btn-link ml-2" *ngIf="moreMode" (click)="setMoreMode(false)" translate>more…</button>
<button class="btn btn-link ml-2" *ngIf="!moreMode" (click)="setMoreMode(true)" translate>less…</button>
</div>
<ng-core-record-search-aggregation-buckets
[buckets]="aggregation.value.buckets"
[aggregationKey]="aggregation.key"
[size]="aggregation.bucketSize"
></ng-core-record-search-aggregation-buckets>
</div>
</div>
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,12 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { TranslateModule, TranslateLoader, TranslateFakeLoader } from '@ngx-translate/core';

import { RecordSearchAggregationComponent } from './aggregation.component';
import { TranslateFakeLoader, TranslateLoader, TranslateModule } from '@ngx-translate/core';
import { UpperCaseFirstPipe } from '../../../pipe/ucfirst.pipe';
import { TranslateLanguagePipe } from '../../../translate/translate-language.pipe';
import { RecordSearchService } from '../record-search.service';
import { RecordSearchAggregationComponent } from './aggregation.component';
import { BucketsComponent } from './buckets/buckets.component';

describe('RecordSearchAggregationComponent', () => {
let component: RecordSearchAggregationComponent;
Expand All @@ -30,13 +31,15 @@ describe('RecordSearchAggregationComponent', () => {
declarations: [
RecordSearchAggregationComponent,
UpperCaseFirstPipe,
TranslateLanguagePipe
TranslateLanguagePipe,
BucketsComponent
],
imports: [
TranslateModule.forRoot({
loader: { provide: TranslateLoader, useClass: TranslateFakeLoader }
})
]
],
providers: [RecordSearchService]
})
.compileComponents();
}));
Expand All @@ -60,52 +63,10 @@ describe('RecordSearchAggregationComponent', () => {
]
}
};
component.selectedValues = ['Filippini, Massimo'];
fixture.detectChanges();
});

it('should create', () => {
expect(component).toBeTruthy();
});

it('should return true if value is selected', () => {
expect(component.isSelected('Filippini, Massimo')).toBe(true);
});

it('should show aggregation filter', () => {
expect(component.showAggregation()).toBe(true);

component.expand = false;
expect(component.showAggregation()).toBe(true);
});

it('should add value to selected filters', () => {
component.updateFilter('Botturi, Luca');
expect(component.selectedValues.includes('Botturi, Luca')).toBe(true);
});

it('should remove value from selected filters', () => {
component.updateFilter('Filippini, Massimo');
expect(component.selectedValues.includes('Filippini, Massimo')).toBe(false);
});

it('should return a correct bucket size', () => {
component.aggregation.bucketSize = null;
expect(component.bucketSize).toBe(2);

component.aggregation.bucketSize = 1;
expect(component.bucketSize).toBe(1);

component.moreMode = false;
component.aggregation.bucketSize = 1;
expect(component.bucketSize).toBe(2);
});

it('should display link', () => {
component.aggregation.bucketSize = 1;
expect(component.displayMoreAndLessLink).toBeTruthy();

component.aggregation.bucketSize = 5;
expect(component.displayMoreAndLessLink).toBeTruthy();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,7 @@
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { Component, Input, Output, EventEmitter } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { Component, Input } from '@angular/core';

@Component({
selector: 'ng-core-record-search-aggregation',
Expand All @@ -26,107 +25,39 @@ export class RecordSearchAggregationComponent {
* Aggregation data
*/
@Input()
public aggregation: { key: string, bucketSize: any, value: { buckets: {}[] } };
aggregation: { key: string, bucketSize: any, value: { buckets: Array<any> } };

/**
* Selected value for filter
* Current selected values
*/
@Input()
public selectedValues: string[] = [];
aggregationsFilters = [];

/**
* Show or hide filter items
* If true, by default buckets are displayed.
*/
@Input()
public expand = true;
expand = true;

/**
* Emit event to parent when a value is clicked
* Returns aggregations filters corresponding to the aggregation key.
* @return List of aggregation filters
*/
@Output()
public updateAggregationFilter = new EventEmitter<{ term: string, values: string[] }>();
get aggregationFilters(): Array<string> {
const aggregationFilters = this.aggregationsFilters.find((item: any) => item.key === this.aggregation.key);

/**
* More and less on aggregation content (facet)
*/
moreMode = true;

/**
* Constructor
* @param translate TranslateService
*/
constructor(private translate: TranslateService) {}

/**
* Interface language
*/
get language() {
return this.translate.currentLang;
}

/**
* Check if a value is already registered in filters.
* @param value - string, filter value
*/
isSelected(value: string) {
return this.selectedValues.includes(value);
}

/**
* Update selected values with given value and emit event to parent
* @param value - string, filter value
*/
updateFilter(value: string) {
if (this.isSelected(value)) {
this.selectedValues = this.selectedValues.filter(selectedValue => selectedValue !== value);
} else {
this.selectedValues.push(value);
}

this.updateAggregationFilter.emit({ term: this.aggregation.key, values: this.selectedValues });
}

/**
* Return bucket size
*/
get bucketSize() {
const aggregationBucketSize = this.aggregation.value.buckets.length;
if (this.aggregation.bucketSize === null) {
return aggregationBucketSize;
} else {
if (this.moreMode) {
return this.aggregation.bucketSize;
} else {
return aggregationBucketSize;
}
if (aggregationFilters === undefined) {
return [];
}
}

/**
* Show filter values
* @return boolean
*/
showAggregation() {
return this.expand || this.selectedValues.length > 0;
}

/**
* Display more or less link
* @return boolean
*/
displayMoreAndLessLink(): boolean {
if (this.aggregation.bucketSize === null) {
return false;
}
return this.aggregation.value.buckets.length > this.aggregation.bucketSize;
return aggregationFilters.values;
}

/**
* Set More mode
* @param state - boolean
* @return void
* Display buckets for the aggregation or not.
* @return Boolean
*/
setMoreMode(state: boolean) {
this.moreMode = state;
showAggregation(): boolean {
return this.expand || this.aggregationFilters.length > 0;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<!--
Invenio angular core
Copyright (C) 2019 RERO
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, version 3 of the License.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<ul *ngIf="buckets" class="list-unstyled m-0">
<li class="form-check" *ngFor="let bucket of buckets|slice:0:bucketSize">
<input class="form-check-input" type="checkbox" [checked]="isSelected(bucket.key)"
(click)="updateFilter(bucket)">
<label class="form-check-label">
{{ getBucketName(bucket) }} ({{ bucket.doc_count }})
</label>
<ng-container *ngIf="isSelected(bucket.key)">
<ng-core-record-search-aggregation-buckets
*ngFor="let aggregation of bucketChildren(bucket)"
[buckets]="aggregation.buckets"
[aggregationKey]="aggregation.key"
></ng-core-record-search-aggregation-buckets>
</ng-container>
</li>
</ul>
<div *ngIf="displayMoreAndLessLink()">
<button class="btn btn-link ml-2"
(click)="moreMode = !moreMode">{{ (moreMode ? 'more…' : 'less…') | translate }}</button>
</div>
Loading

0 comments on commit fd0fd70

Please sign in to comment.