From d5578accea27a941b7d11c9f26b8fa7398e8519c Mon Sep 17 00:00:00 2001 From: Samuel Date: Thu, 16 Dec 2021 09:52:43 +0100 Subject: [PATCH] taskid 85843 Add a tree to browse hierarchical facets on the search page --- .../search-hierarchy-filter.component.ts | 2 +- .../okr-vocabulary-treeview.component.ts | 15 +- ...okr-search-hierarchy-filter.component.html | 7 + ...okr-search-hierarchy-filter.component.scss | 0 ...-search-hierarchy-filter.component.spec.ts | 156 ++++++++++++++++++ .../okr-search-hierarchy-filter.component.ts | 106 ++++++++++++ src/themes/okr/entry-components.ts | 63 +++++++ 7 files changed, 337 insertions(+), 12 deletions(-) create mode 100644 src/themes/okr/app/shared/search/search-filters/search-filter/okr-search-hierarchy-filter/okr-search-hierarchy-filter.component.html create mode 100644 src/themes/okr/app/shared/search/search-filters/search-filter/okr-search-hierarchy-filter/okr-search-hierarchy-filter.component.scss create mode 100644 src/themes/okr/app/shared/search/search-filters/search-filter/okr-search-hierarchy-filter/okr-search-hierarchy-filter.component.spec.ts create mode 100644 src/themes/okr/app/shared/search/search-filters/search-filter/okr-search-hierarchy-filter/okr-search-hierarchy-filter.component.ts create mode 100644 src/themes/okr/entry-components.ts diff --git a/src/app/shared/search/search-filters/search-filter/search-hierarchy-filter/search-hierarchy-filter.component.ts b/src/app/shared/search/search-filters/search-filter/search-hierarchy-filter/search-hierarchy-filter.component.ts index d2f3de2dc3..7aa81116de 100644 --- a/src/app/shared/search/search-filters/search-filter/search-hierarchy-filter/search-hierarchy-filter.component.ts +++ b/src/app/shared/search/search-filters/search-filter/search-hierarchy-filter/search-hierarchy-filter.component.ts @@ -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 { } diff --git a/src/themes/okr/app/shared/okr-vocabulary-treeview/okr-vocabulary-treeview.component.ts b/src/themes/okr/app/shared/okr-vocabulary-treeview/okr-vocabulary-treeview.component.ts index f340cdfccf..f33f7d89e9 100644 --- a/src/themes/okr/app/shared/okr-vocabulary-treeview/okr-vocabulary-treeview.component.ts +++ b/src/themes/okr/app/shared/okr-vocabulary-treeview/okr-vocabulary-treeview.component.ts @@ -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); } } diff --git a/src/themes/okr/app/shared/search/search-filters/search-filter/okr-search-hierarchy-filter/okr-search-hierarchy-filter.component.html b/src/themes/okr/app/shared/search/search-filters/search-filter/okr-search-hierarchy-filter/okr-search-hierarchy-filter.component.html new file mode 100644 index 0000000000..e62006538b --- /dev/null +++ b/src/themes/okr/app/shared/search/search-filters/search-filter/okr-search-hierarchy-filter/okr-search-hierarchy-filter.component.html @@ -0,0 +1,7 @@ + + +
+ {{'search.filters.filter.show-tree' | translate: {name: filterConfig.name} }} +
diff --git a/src/themes/okr/app/shared/search/search-filters/search-filter/okr-search-hierarchy-filter/okr-search-hierarchy-filter.component.scss b/src/themes/okr/app/shared/search/search-filters/search-filter/okr-search-hierarchy-filter/okr-search-hierarchy-filter.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/themes/okr/app/shared/search/search-filters/search-filter/okr-search-hierarchy-filter/okr-search-hierarchy-filter.component.spec.ts b/src/themes/okr/app/shared/search/search-filters/search-filter/okr-search-hierarchy-filter/okr-search-hierarchy-filter.component.spec.ts new file mode 100644 index 0000000000..ce4caa23eb --- /dev/null +++ b/src/themes/okr/app/shared/search/search-filters/search-filter/okr-search-hierarchy-filter/okr-search-hierarchy-filter.component.spec.ts @@ -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; + let showVocabularyTreeLink: DebugElement; + + const testSearchLink = 'test-search'; + const testSearchFilter = 'test-search-filter'; + const okrVocabularyTreeViewComponent = { + select: new EventEmitter(), + }; + + 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', + }); + }); + }); + }); + }); +}); diff --git a/src/themes/okr/app/shared/search/search-filters/search-filter/okr-search-hierarchy-filter/okr-search-hierarchy-filter.component.ts b/src/themes/okr/app/shared/search/search-filters/search-filter/okr-search-hierarchy-filter/okr-search-hierarchy-filter.component.ts new file mode 100644 index 0000000000..c637f576eb --- /dev/null +++ b/src/themes/okr/app/shared/search/search-filters/search-filter/okr-search-hierarchy-filter/okr-search-hierarchy-filter.component.ts @@ -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; + + 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', + }, + ); + }); + }); + } +} diff --git a/src/themes/okr/entry-components.ts b/src/themes/okr/entry-components.ts new file mode 100644 index 0000000000..bfc93ad216 --- /dev/null +++ b/src/themes/okr/entry-components.ts @@ -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, +];