taskid 85843 Add a tree to browse hierarchical facets on the search page

This commit is contained in:
Samuel
2021-12-16 09:52:43 +01:00
committed by Jens Vannerum
parent 9ff1a6a642
commit d5578accea
7 changed files with 337 additions and 12 deletions

View File

@@ -13,6 +13,6 @@ import { facetLoad, SearchFacetFilterComponent } from '../search-facet-filter/se
/**
* Component that represents a hierarchy facet for a specific filter configuration
*/
@renderFacetFor(FilterType.hierarchy)
// @renderFacetFor(FilterType.hierarchy)
export class SearchHierarchyFilterComponent extends SearchFacetFilterComponent implements OnInit {
}

View File

@@ -1,10 +1,11 @@
import { Component } from '@angular/core';
import { VocabularyTreeviewComponent } from '../../../../../app/shared/vocabulary-treeview/vocabulary-treeview.component';
import { filter, find, startWith } from 'rxjs/operators';
import { filter, startWith } from 'rxjs/operators';
import { PageInfo } from '../../../../../app/core/shared/page-info.model';
/**
* Component that show a hierarchical vocabulary in a tree view
* Component that show a hierarchical vocabulary in a tree view.
* Worldbank customization which omits the authentication check.
*/
@Component({
selector: 'ds-okr-vocabulary-treeview',
@@ -32,16 +33,8 @@ export class OkrVocabularyTreeviewComponent extends VocabularyTreeviewComponent
startWith('')
);
// set isAuthenticated
this.isAuthenticated = this.store.pipe(select(isAuthenticated));
this.loading = this.vocabularyTreeviewService.isLoading();
this.isAuthenticated.pipe(
find((isAuth) => isAuth)
).subscribe(() => {
const entryId: string = (this.selectedItem) ? this.getEntryId(this.selectedItem) : null;
this.vocabularyTreeviewService.initialize(this.vocabularyOptions, new PageInfo(), entryId);
});
this.vocabularyTreeviewService.initialize(this.vocabularyOptions, new PageInfo(), null);
}
}

View File

@@ -0,0 +1,7 @@
<ds-search-hierarchy-filter></ds-search-hierarchy-filter>
<div *ngIf="vocabularyExists$ | async"
id="show-{{filterConfig.name}}-tree"
(click)="showVocabularyTree()">
{{'search.filters.filter.show-tree' | translate: {name: filterConfig.name} }}
</div>

View File

@@ -0,0 +1,156 @@
import { OkrSearchHierarchyFilterComponent } from './okr-search-hierarchy-filter.component';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { DebugElement, EventEmitter, NO_ERRORS_SCHEMA } from '@angular/core';
import { By } from '@angular/platform-browser';
import { VocabularyService } from '../../../../../../../../app/core/submission/vocabularies/vocabulary.service';
import { of as observableOf } from 'rxjs';
import { RemoteData } from '../../../../../../../../app/core/data/remote-data';
import { RequestEntryState } from '../../../../../../../../app/core/data/request.reducer';
import { TranslateModule } from '@ngx-translate/core';
import { RouterStub } from '../../../../../../../../app/shared/testing/router.stub';
import { buildPaginatedList } from '../../../../../../../../app/core/data/paginated-list.model';
import { PageInfo } from '../../../../../../../../app/core/shared/page-info.model';
import { CommonModule } from '@angular/common';
import { SearchService } from '../../../../../../../../app/core/shared/search/search.service';
import {
FILTER_CONFIG,
IN_PLACE_SEARCH,
SearchFilterService
} from '../../../../../../../../app/core/shared/search/search-filter.service';
import { RemoteDataBuildService } from '../../../../../../../../app/core/cache/builders/remote-data-build.service';
import { Router } from '@angular/router';
import { NgbModal, NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { SEARCH_CONFIG_SERVICE } from '../../../../../../../../app/my-dspace-page/my-dspace-page.component';
import { SearchConfigurationServiceStub } from '../../../../../../../../app/shared/testing/search-configuration-service.stub';
import { VocabularyEntryDetail } from '../../../../../../../../app/core/submission/vocabularies/models/vocabulary-entry-detail.model';
import { FacetValue } from '../../../../../../../../app/shared/search/facet-value.model';
import { SearchFilterConfig } from '../../../../../../../../app/shared/search/search-filter-config.model';
describe('OkrSearchHierarchyFilterComponent', () => {
let fixture: ComponentFixture<OkrSearchHierarchyFilterComponent>;
let showVocabularyTreeLink: DebugElement;
const testSearchLink = 'test-search';
const testSearchFilter = 'test-search-filter';
const okrVocabularyTreeViewComponent = {
select: new EventEmitter<VocabularyEntryDetail>(),
};
const searchService = {
getSearchLink: () => testSearchLink,
getFacetValuesFor: () => observableOf([]),
};
const searchFilterService = {
getPage: () => observableOf(0),
};
const router = new RouterStub();
const ngbModal = jasmine.createSpyObj('modal', {
open: {
componentInstance: okrVocabularyTreeViewComponent,
}
});
const vocabularyService = {
searchTopEntries: () => undefined,
};
beforeEach(() => {
TestBed.configureTestingModule({
imports: [
CommonModule,
NgbModule,
TranslateModule.forRoot(),
],
declarations: [
OkrSearchHierarchyFilterComponent,
],
providers: [
{ provide: SearchService, useValue: searchService },
{ provide: SearchFilterService, useValue: searchFilterService },
{ provide: RemoteDataBuildService, useValue: {} },
{ provide: Router, useValue: router },
{ provide: NgbModal, useValue: ngbModal },
{ provide: VocabularyService, useValue: vocabularyService },
{ provide: SEARCH_CONFIG_SERVICE, useValue: new SearchConfigurationServiceStub() },
{ provide: IN_PLACE_SEARCH, useValue: false },
{ provide: FILTER_CONFIG, useValue: Object.assign(new SearchFilterConfig(), { name: testSearchFilter }) },
],
schemas: [NO_ERRORS_SCHEMA],
}).compileComponents();
});
function init() {
fixture = TestBed.createComponent(OkrSearchHierarchyFilterComponent);
fixture.detectChanges();
showVocabularyTreeLink = fixture.debugElement.query(By.css('div#show-test-search-filter-tree'));
}
describe('if the vocabulary doesn\'t exist', () => {
beforeEach(() => {
spyOn(vocabularyService, 'searchTopEntries').and.returnValue(observableOf(new RemoteData(
undefined, 0, 0, RequestEntryState.Error, undefined, undefined, 404
)));
init();
});
it('should not show the vocabulary tree link', () => {
expect(showVocabularyTreeLink).toBeNull();
});
});
describe('if the vocabulary exists', () => {
beforeEach(() => {
spyOn(vocabularyService, 'searchTopEntries').and.returnValue(observableOf(new RemoteData(
undefined, 0, 0, RequestEntryState.Success, undefined, buildPaginatedList(new PageInfo(), []), 200
)));
init();
});
it('should show the vocabulary tree link', () => {
expect(showVocabularyTreeLink).toBeTruthy();
});
describe('when clicking the vocabulary tree link', () => {
beforeEach(async () => {
showVocabularyTreeLink.nativeElement.click();
});
it('should open the vocabulary tree modal', () => {
expect(ngbModal.open).toHaveBeenCalled();
});
describe('when selecting a value from the vocabulary tree', () => {
const alreadySelectedValues = [
'already-selected-value-1',
'already-selected-value-2',
];
const newSelectedValue = 'new-selected-value';
beforeEach(() => {
fixture.componentInstance.selectedValues$ = observableOf(
alreadySelectedValues.map(value => Object.assign(new FacetValue(), { value }))
);
okrVocabularyTreeViewComponent.select.emit(Object.assign(new VocabularyEntryDetail(), {
value: newSelectedValue,
}));
});
it('should add a new search filter to the existing search filters', () => {
expect(router.navigate).toHaveBeenCalledWith([testSearchLink], {
queryParams: {
[`f.${testSearchFilter}`]: [
...alreadySelectedValues,
newSelectedValue,
].map((value => `${value},equals`)),
},
queryParamsHandling: 'merge',
});
});
});
});
});
});

View File

@@ -0,0 +1,106 @@
import { Component, Inject } from '@angular/core';
import { renderFacetFor } from 'src/app/shared/search/search-filters/search-filter/search-filter-type-decorator';
import { FilterType } from '../../../../../../../../app/shared/search/filter-type.model';
import { facetLoad } from '../../../../../../../../app/shared/search/search-filters/search-filter/search-facet-filter/search-facet-filter.component';
import { SearchHierarchyFilterComponent } from '../../../../../../../../app/shared/search/search-filters/search-filter/search-hierarchy-filter/search-hierarchy-filter.component';
import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
import { OkrVocabularyTreeviewComponent } from '../../../../okr-vocabulary-treeview/okr-vocabulary-treeview.component';
import { VocabularyEntryDetail } from '../../../../../../../../app/core/submission/vocabularies/models/vocabulary-entry-detail.model';
import { SearchService } from '../../../../../../../../app/core/shared/search/search.service';
import {
FILTER_CONFIG,
IN_PLACE_SEARCH,
SearchFilterService
} from '../../../../../../../../app/core/shared/search/search-filter.service';
import { Router } from '@angular/router';
import { RemoteDataBuildService } from '../../../../../../../../app/core/cache/builders/remote-data-build.service';
import { SEARCH_CONFIG_SERVICE } from '../../../../../../../../app/my-dspace-page/my-dspace-page.component';
import { SearchConfigurationService } from '../../../../../../../../app/core/shared/search/search-configuration.service';
import { SearchFilterConfig } from '../../../../../../../../app/shared/search/search-filter-config.model';
import { FacetValue } from '../../../../../../../../app/shared/search/facet-value.model';
import { getFacetValueForType } from '../../../../../../../../app/shared/search/search.utils';
import { filter, map, take } from 'rxjs/operators';
import { VocabularyService } from '../../../../../../../../app/core/submission/vocabularies/vocabulary.service';
import { Observable } from 'rxjs';
import { PageInfo } from '../../../../../../../../app/core/shared/page-info.model';
/**
* Component that represents a hierarchy facet for a specific filter configuration.
* Worldbank customization which features a link at the bottom to open a vocabulary popup,
*/
@Component({
selector: 'ds-okr-search-hierarchy-filter',
styleUrls: ['./okr-search-hierarchy-filter.component.scss'],
templateUrl: './okr-search-hierarchy-filter.component.html',
animations: [facetLoad]
})
@renderFacetFor(FilterType.hierarchy)
export class OkrSearchHierarchyFilterComponent extends SearchHierarchyFilterComponent {
constructor(protected searchService: SearchService,
protected filterService: SearchFilterService,
protected rdbs: RemoteDataBuildService,
protected router: Router,
protected modalService: NgbModal,
protected vocabularyService: VocabularyService,
@Inject(SEARCH_CONFIG_SERVICE) public searchConfigService: SearchConfigurationService,
@Inject(IN_PLACE_SEARCH) public inPlaceSearch: boolean,
@Inject(FILTER_CONFIG) public filterConfig: SearchFilterConfig,
) {
super(
searchService,
filterService,
rdbs,
router,
searchConfigService,
inPlaceSearch,
filterConfig,
);
}
vocabularyExists$: Observable<boolean>;
ngOnInit() {
super.ngOnInit();
this.vocabularyExists$ = this.vocabularyService.searchTopEntries(
this.filterConfig.name, new PageInfo(), true, false,
).pipe(
filter(rd => rd.hasCompleted),
take(1),
map(rd => {
return rd.hasSucceeded;
}),
);
}
/**
* Open the vocabulary tree modal popup.
* When an entry is selected, add the filter query to the search options.
*/
showVocabularyTree() {
const modalRef: NgbModalRef = this.modalService.open(OkrVocabularyTreeviewComponent, {
size: 'lg',
windowClass: 'treeview'
});
modalRef.componentInstance.vocabularyOptions = {
name: this.filterConfig.name,
closed: true
};
modalRef.componentInstance.select.subscribe((detail: VocabularyEntryDetail) => {
this.selectedValues$
.pipe(take(1))
.subscribe((selectedValues) => {
this.router.navigate(
[this.searchService.getSearchLink()],
{
queryParams: {
[this.filterConfig.paramName]: [...selectedValues, {value: detail.value}]
.map((facetValue: FacetValue) => getFacetValueForType(facetValue, this.filterConfig)),
},
queryParamsHandling: 'merge',
},
);
});
});
}
}

View File

@@ -0,0 +1,63 @@
import { PublicationComponent } from './app/item-page/simple/item-types/publication/publication.component';
import { ItemSearchResultListElementComponent } from './app/shared/object-list/search-result-list-element/item-search-result/item-types/item/item-search-result-list-element.component';
import { PersonSearchResultListElementComponent } from './app/entity-groups/research-entities/item-list-elements/search-result-list-elements/person/person-search-result-list-element.component';
import { JournalSearchResultListElementComponent } from './app/entity-groups/journal-entities/item-list-elements/search-result-list-elements/journal/journal-search-result-list-element.component';
import { JournalIssueSearchResultListElementComponent } from './app/entity-groups/journal-entities/item-list-elements/search-result-list-elements/journal-issue/journal-issue-search-result-list-element.component';
import { JournalVolumeSearchResultListElementComponent } from './app/entity-groups/journal-entities/item-list-elements/search-result-list-elements/journal-volume/journal-volume-search-result-list-element.component';
import { CitationsSectionComponent } from './app/item-page/field-components/citation/citations-section.component';
import { CcIconsComponent } from './app/item-page/field-components/cc-icons/cc-icons.component';
import { AltmetricDonutComponent } from './app/item-page/field-components/citation/altmetric-donut/altmetric-donut.component';
import { ItemPageWbDateFieldComponent } from './app/item-page/field-components/specific-field/wb-date/item-page-wb-date-field.component';
import { ItemPageWbGenericWithFallbackComponent } from './app/item-page/field-components/specific-field/wb-generic-with-fallback/item-page-wb-generic-with-fallback.component';
import { ItemPageWbExternalContentComponent } from './app/item-page/field-components/specific-field/wb-external-content/item-page-wb-external-content.component';
import { UntypedItemComponent } from './app/item-page/simple/item-types/untyped-item/untyped-item.component';
import { JournalComponent } from './app/entity-groups/journal-entities/item-pages/journal/journal.component';
import { JournalIssueComponent } from './app/entity-groups/journal-entities/item-pages/journal-issue/journal-issue.component';
import { JournalVolumeComponent } from './app/entity-groups/journal-entities/item-pages/journal-volume/journal-volume.component';
import { JournalVolumeGridElementComponent } from './app/entity-groups/journal-entities/item-grid-elements/journal-volume/journal-volume-grid-element.component';
import { JournalVolumeSearchResultGridElementComponent } from './app/entity-groups/journal-entities/item-grid-elements/search-result-grid-elements/journal-volume/journal-volume-search-result-grid-element.component';
import { JournalIssueSearchResultGridElementComponent } from './app/entity-groups/journal-entities/item-grid-elements/search-result-grid-elements/journal-issue/journal-issue-search-result-grid-element.component';
import { JournalIssueGridElementComponent } from './app/entity-groups/journal-entities/item-grid-elements/journal-issue/journal-issue-grid-element.component';
import { JournalVolumeSidebarSearchListElementComponent } from './app/entity-groups/journal-entities/item-list-elements/sidebar-search-list-elements/journal-volume/journal-volume-sidebar-search-list-element.component';
import { JournalIssueSidebarSearchListElementComponent } from './app/entity-groups/journal-entities/item-list-elements/sidebar-search-list-elements/journal-issue/journal-issue-sidebar-search-list-element.component';
import { JournalIssueListElementComponent } from './app/entity-groups/journal-entities/item-list-elements/journal-issue/journal-issue-list-element.component';
import { JournalVolumeListElementComponent } from './app/entity-groups/journal-entities/item-list-elements/journal-volume/journal-volume-list-element.component';
import { FeaturedPublicationsListElementComponent } from './app/featured-publications/featured-publications-list-element/featured-publications-list-element.component';
import { ClaimedTaskActionsMarkDuplicateComponent } from './app/shared/mydspace-actions/claimed-task/mark-duplicate/claimed-task-actions-mark-duplicate.component';
import { ClaimedTaskActionsReportProblemComponent } from './app/shared/mydspace-actions/claimed-task/report-problem/claimed-task-actions-report-problem.component';
import { SingleStatletTableComponent } from './app/atmire-cua/statlets/shared/single-statlet/graph-types/single-statlet-table/single-statlet-table.component';
import { OkrSearchHierarchyFilterComponent } from './app/shared/search/search-filters/search-filter/okr-search-hierarchy-filter/okr-search-hierarchy-filter.component';
import { OkrVocabularyTreeviewComponent } from './app/shared/okr-vocabulary-treeview/okr-vocabulary-treeview.component';
export const ENTRY_COMPONENTS = [
PublicationComponent,
ItemSearchResultListElementComponent,
PersonSearchResultListElementComponent,
JournalSearchResultListElementComponent,
JournalVolumeSearchResultListElementComponent,
JournalIssueSearchResultListElementComponent,
UntypedItemComponent,
AltmetricDonutComponent,
CitationsSectionComponent,
CcIconsComponent,
ItemPageWbDateFieldComponent,
ItemPageWbGenericWithFallbackComponent,
ItemPageWbExternalContentComponent,
JournalComponent,
JournalIssueComponent,
JournalVolumeComponent,
JournalVolumeGridElementComponent,
JournalVolumeSearchResultGridElementComponent,
JournalIssueSearchResultGridElementComponent,
JournalIssueGridElementComponent,
JournalVolumeSidebarSearchListElementComponent,
JournalIssueSidebarSearchListElementComponent,
JournalIssueListElementComponent,
JournalVolumeListElementComponent,
FeaturedPublicationsListElementComponent,
ClaimedTaskActionsMarkDuplicateComponent,
ClaimedTaskActionsReportProblemComponent,
SingleStatletTableComponent,
OkrSearchHierarchyFilterComponent,
OkrVocabularyTreeviewComponent,
];