mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 01:54:15 +00:00
Merge pull request #1988 from atmire/issue-815-controlled-vocabulary-hierarchical-facets-7.4
Issue 815 controlled vocabulary hierarchical facets 7.4
This commit is contained in:
@@ -310,3 +310,11 @@ info:
|
|||||||
markdown:
|
markdown:
|
||||||
enabled: false
|
enabled: false
|
||||||
mathjax: false
|
mathjax: false
|
||||||
|
|
||||||
|
# Which vocabularies should be used for which search filters
|
||||||
|
# and whether to show the filter in the search sidebar
|
||||||
|
# Take a look at the filter-vocabulary-config.ts file for documentation on how the options are obtained
|
||||||
|
vocabularies:
|
||||||
|
- filter: 'subject'
|
||||||
|
vocabulary: 'srsc'
|
||||||
|
enabled: true
|
||||||
|
@@ -141,7 +141,7 @@ describe('VocabularyTreeviewComponent test suite', () => {
|
|||||||
comp.selectedItem = currentValue;
|
comp.selectedItem = currentValue;
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
expect(comp.dataSource.data).toEqual([]);
|
expect(comp.dataSource.data).toEqual([]);
|
||||||
expect(vocabularyTreeviewServiceStub.initialize).toHaveBeenCalledWith(comp.vocabularyOptions, new PageInfo(), 'entryID');
|
expect(vocabularyTreeviewServiceStub.initialize).toHaveBeenCalledWith(comp.vocabularyOptions, new PageInfo(), null);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should should init component properly with init value as VocabularyEntry', () => {
|
it('should should init component properly with init value as VocabularyEntry', () => {
|
||||||
@@ -153,7 +153,7 @@ describe('VocabularyTreeviewComponent test suite', () => {
|
|||||||
comp.selectedItem = currentValue;
|
comp.selectedItem = currentValue;
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
expect(comp.dataSource.data).toEqual([]);
|
expect(comp.dataSource.data).toEqual([]);
|
||||||
expect(vocabularyTreeviewServiceStub.initialize).toHaveBeenCalledWith(comp.vocabularyOptions, new PageInfo(), 'entryID');
|
expect(vocabularyTreeviewServiceStub.initialize).toHaveBeenCalledWith(comp.vocabularyOptions, new PageInfo(), null);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call loadMore function', () => {
|
it('should call loadMore function', () => {
|
||||||
|
@@ -2,14 +2,13 @@ import { FlatTreeControl } from '@angular/cdk/tree';
|
|||||||
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core';
|
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core';
|
||||||
|
|
||||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import { filter, find, startWith } from 'rxjs/operators';
|
import { map } from 'rxjs/operators';
|
||||||
import { Observable, Subscription } from 'rxjs';
|
import { Observable, Subscription } from 'rxjs';
|
||||||
import { select, Store } from '@ngrx/store';
|
import { Store } from '@ngrx/store';
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
|
||||||
import { VocabularyEntryDetail } from '../../../core/submission/vocabularies/models/vocabulary-entry-detail.model';
|
import { VocabularyEntryDetail } from '../../../core/submission/vocabularies/models/vocabulary-entry-detail.model';
|
||||||
import { hasValue, isEmpty, isNotEmpty } from '../../empty.util';
|
import { hasValue, isEmpty, isNotEmpty } from '../../empty.util';
|
||||||
import { isAuthenticated } from '../../../core/auth/selectors';
|
|
||||||
import { VocabularyTreeviewService } from './vocabulary-treeview.service';
|
import { VocabularyTreeviewService } from './vocabulary-treeview.service';
|
||||||
import { LOAD_MORE, LOAD_MORE_ROOT, TreeviewFlatNode, TreeviewNode } from './vocabulary-treeview-node.model';
|
import { LOAD_MORE, LOAD_MORE_ROOT, TreeviewFlatNode, TreeviewNode } from './vocabulary-treeview-node.model';
|
||||||
import { VocabularyOptions } from '../../../core/submission/vocabularies/models/vocabulary-options.model';
|
import { VocabularyOptions } from '../../../core/submission/vocabularies/models/vocabulary-options.model';
|
||||||
@@ -18,6 +17,7 @@ import { VocabularyEntry } from '../../../core/submission/vocabularies/models/vo
|
|||||||
import { VocabularyTreeFlattener } from './vocabulary-tree-flattener';
|
import { VocabularyTreeFlattener } from './vocabulary-tree-flattener';
|
||||||
import { VocabularyTreeFlatDataSource } from './vocabulary-tree-flat-data-source';
|
import { VocabularyTreeFlatDataSource } from './vocabulary-tree-flat-data-source';
|
||||||
import { CoreState } from '../../../core/core-state.model';
|
import { CoreState } from '../../../core/core-state.model';
|
||||||
|
import { lowerCase } from 'lodash/string';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component that show a hierarchical vocabulary in a tree view
|
* Component that show a hierarchical vocabulary in a tree view
|
||||||
@@ -203,23 +203,15 @@ export class VocabularyTreeviewComponent implements OnDestroy, OnInit {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
const descriptionLabel = 'vocabulary-treeview.tree.description.' + this.vocabularyOptions.name;
|
this.translate.get(`search.filters.filter.${this.vocabularyOptions.name}.head`).pipe(
|
||||||
this.description = this.translate.get(descriptionLabel).pipe(
|
map((type) => lowerCase(type)),
|
||||||
filter((msg) => msg !== descriptionLabel),
|
).subscribe(
|
||||||
startWith('')
|
(type) => this.description = this.translate.get('vocabulary-treeview.info', { type })
|
||||||
);
|
);
|
||||||
|
|
||||||
// set isAuthenticated
|
|
||||||
this.isAuthenticated = this.store.pipe(select(isAuthenticated));
|
|
||||||
|
|
||||||
this.loading = this.vocabularyTreeviewService.isLoading();
|
this.loading = this.vocabularyTreeviewService.isLoading();
|
||||||
|
|
||||||
this.isAuthenticated.pipe(
|
this.vocabularyTreeviewService.initialize(this.vocabularyOptions, new PageInfo(), null);
|
||||||
find((isAuth) => isAuth)
|
|
||||||
).subscribe(() => {
|
|
||||||
const entryId: string = (this.selectedItem) ? this.getEntryId(this.selectedItem) : null;
|
|
||||||
this.vocabularyTreeviewService.initialize(this.vocabularyOptions, new PageInfo(), entryId);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -29,3 +29,10 @@
|
|||||||
ngDefaultControl
|
ngDefaultControl
|
||||||
></ds-filter-input-suggestions>
|
></ds-filter-input-suggestions>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<a *ngIf="vocabularyExists$ | async"
|
||||||
|
href="javascript:void(0);"
|
||||||
|
id="show-{{filterConfig.name}}-tree"
|
||||||
|
(click)="showVocabularyTree()">
|
||||||
|
{{'search.filters.filter.show-tree' | translate: {name: ('search.filters.filter.' + filterConfig.name + '.head' | translate | lowercase )} }}
|
||||||
|
</a>
|
||||||
|
@@ -1,155 +1,155 @@
|
|||||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|
||||||
import { SearchHierarchyFilterComponent } from './search-hierarchy-filter.component';
|
import { SearchHierarchyFilterComponent } from './search-hierarchy-filter.component';
|
||||||
|
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||||
|
import { DebugElement, EventEmitter, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
import { VocabularyService } from '../../../../../core/submission/vocabularies/vocabulary.service';
|
||||||
|
import { of as observableOf, BehaviorSubject } from 'rxjs';
|
||||||
|
import { RemoteData } from '../../../../../core/data/remote-data';
|
||||||
|
import { RequestEntryState } from '../../../../../core/data/request-entry-state.model';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { RouterStub } from '../../../../testing/router.stub';
|
||||||
|
import { buildPaginatedList } from '../../../../../core/data/paginated-list.model';
|
||||||
|
import { PageInfo } from '../../../../../core/shared/page-info.model';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
import { SearchService } from '../../../../../core/shared/search/search.service';
|
import { SearchService } from '../../../../../core/shared/search/search.service';
|
||||||
import {
|
import {
|
||||||
FILTER_CONFIG,
|
FILTER_CONFIG,
|
||||||
IN_PLACE_SEARCH,
|
IN_PLACE_SEARCH,
|
||||||
REFRESH_FILTER,
|
SearchFilterService,
|
||||||
SearchFilterService
|
REFRESH_FILTER
|
||||||
} from '../../../../../core/shared/search/search-filter.service';
|
} from '../../../../../core/shared/search/search-filter.service';
|
||||||
import { RemoteDataBuildService } from '../../../../../core/cache/builders/remote-data-build.service';
|
import { RemoteDataBuildService } from '../../../../../core/cache/builders/remote-data-build.service';
|
||||||
import { SearchFiltersComponent } from '../../search-filters.component';
|
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { RouterStub } from '../../../../testing/router.stub';
|
import { NgbModal, NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import { SearchServiceStub } from '../../../../testing/search-service.stub';
|
|
||||||
import { BehaviorSubject, Observable, of as observableOf } from 'rxjs';
|
|
||||||
import { SEARCH_CONFIG_SERVICE } from '../../../../../my-dspace-page/my-dspace-page.component';
|
import { SEARCH_CONFIG_SERVICE } from '../../../../../my-dspace-page/my-dspace-page.component';
|
||||||
import { SearchConfigurationServiceStub } from '../../../../testing/search-configuration-service.stub';
|
import { SearchConfigurationServiceStub } from '../../../../testing/search-configuration-service.stub';
|
||||||
|
import { VocabularyEntryDetail } from '../../../../../core/submission/vocabularies/models/vocabulary-entry-detail.model';
|
||||||
|
import { FacetValue} from '../../../models/facet-value.model';
|
||||||
import { SearchFilterConfig } from '../../../models/search-filter-config.model';
|
import { SearchFilterConfig } from '../../../models/search-filter-config.model';
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
|
||||||
import {
|
|
||||||
FilterInputSuggestionsComponent
|
|
||||||
} from '../../../../input-suggestions/filter-suggestions/filter-input-suggestions.component';
|
|
||||||
import { FormsModule } from '@angular/forms';
|
|
||||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
|
||||||
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
|
|
||||||
import { createSuccessfulRemoteDataObject$ } from '../../../../remote-data.utils';
|
|
||||||
import { FacetValue } from '../../../models/facet-value.model';
|
|
||||||
import { FilterType } from '../../../models/filter-type.model';
|
|
||||||
import { createPaginatedList } from '../../../../testing/utils.test';
|
|
||||||
import { RemoteData } from '../../../../../core/data/remote-data';
|
|
||||||
import { PaginatedList } from '../../../../../core/data/paginated-list.model';
|
|
||||||
|
|
||||||
describe('SearchHierarchyFilterComponent', () => {
|
describe('SearchHierarchyFilterComponent', () => {
|
||||||
let comp: SearchHierarchyFilterComponent;
|
|
||||||
let fixture: ComponentFixture<SearchHierarchyFilterComponent>;
|
let fixture: ComponentFixture<SearchHierarchyFilterComponent>;
|
||||||
let searchService: SearchService;
|
let showVocabularyTreeLink: DebugElement;
|
||||||
let router;
|
|
||||||
|
|
||||||
const value1 = 'testvalue1';
|
const testSearchLink = 'test-search';
|
||||||
const value2 = 'test2';
|
const testSearchFilter = 'test-search-filter';
|
||||||
const value3 = 'another value3';
|
const VocabularyTreeViewComponent = {
|
||||||
const values: FacetValue[] = [
|
select: new EventEmitter<VocabularyEntryDetail>(),
|
||||||
{
|
|
||||||
label: value1,
|
|
||||||
value: value1,
|
|
||||||
count: 52,
|
|
||||||
_links: {
|
|
||||||
self: {
|
|
||||||
href: ''
|
|
||||||
},
|
|
||||||
search: {
|
|
||||||
href: ''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
label: value2,
|
|
||||||
value: value2,
|
|
||||||
count: 20,
|
|
||||||
_links: {
|
|
||||||
self: {
|
|
||||||
href: ''
|
|
||||||
},
|
|
||||||
search: {
|
|
||||||
href: ''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
label: value3,
|
|
||||||
value: value3,
|
|
||||||
count: 5,
|
|
||||||
_links: {
|
|
||||||
self: {
|
|
||||||
href: ''
|
|
||||||
},
|
|
||||||
search: {
|
|
||||||
href: ''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
];
|
|
||||||
const mockValues = createSuccessfulRemoteDataObject$(createPaginatedList(values));
|
|
||||||
|
|
||||||
const searchFilterServiceStub = {
|
|
||||||
getSelectedValuesForFilter(_filterConfig: SearchFilterConfig): Observable<string[]> {
|
|
||||||
return observableOf(values.map((value: FacetValue) => value.value));
|
|
||||||
},
|
|
||||||
getPage(_paramName: string): Observable<number> {
|
|
||||||
return observableOf(0);
|
|
||||||
},
|
|
||||||
resetPage(_filterName: string): void {
|
|
||||||
// empty
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const remoteDataBuildServiceStub = {
|
const searchService = {
|
||||||
aggregate(_input: Observable<RemoteData<FacetValue>>[]): Observable<RemoteData<PaginatedList<FacetValue>[]>> {
|
getSearchLink: () => testSearchLink,
|
||||||
return createSuccessfulRemoteDataObject$([createPaginatedList(values)]);
|
getFacetValuesFor: () => observableOf([]),
|
||||||
|
};
|
||||||
|
const searchFilterService = {
|
||||||
|
getPage: () => observableOf(0),
|
||||||
|
};
|
||||||
|
const router = new RouterStub();
|
||||||
|
const ngbModal = jasmine.createSpyObj('modal', {
|
||||||
|
open: {
|
||||||
|
componentInstance: VocabularyTreeViewComponent,
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
const vocabularyService = {
|
||||||
|
searchTopEntries: () => undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(() => {
|
||||||
await TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [TranslateModule.forRoot(), NoopAnimationsModule, FormsModule],
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
NgbModule,
|
||||||
|
TranslateModule.forRoot(),
|
||||||
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
SearchHierarchyFilterComponent,
|
SearchHierarchyFilterComponent,
|
||||||
SearchFiltersComponent,
|
|
||||||
FilterInputSuggestionsComponent
|
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: SearchService, useValue: new SearchServiceStub() },
|
{ provide: SearchService, useValue: searchService },
|
||||||
{ provide: SearchFilterService, useValue: searchFilterServiceStub },
|
{ provide: SearchFilterService, useValue: searchFilterService },
|
||||||
{ provide: RemoteDataBuildService, useValue: remoteDataBuildServiceStub },
|
{ provide: RemoteDataBuildService, useValue: {} },
|
||||||
{ provide: Router, useValue: new RouterStub() },
|
{ provide: Router, useValue: router },
|
||||||
|
{ provide: NgbModal, useValue: ngbModal },
|
||||||
|
{ provide: VocabularyService, useValue: vocabularyService },
|
||||||
{ provide: SEARCH_CONFIG_SERVICE, useValue: new SearchConfigurationServiceStub() },
|
{ provide: SEARCH_CONFIG_SERVICE, useValue: new SearchConfigurationServiceStub() },
|
||||||
{ provide: IN_PLACE_SEARCH, useValue: false },
|
{ provide: IN_PLACE_SEARCH, useValue: false },
|
||||||
{ provide: FILTER_CONFIG, useValue: new SearchFilterConfig() },
|
{ provide: FILTER_CONFIG, useValue: Object.assign(new SearchFilterConfig(), { name: testSearchFilter }) },
|
||||||
{ provide: REFRESH_FILTER, useValue: new BehaviorSubject<boolean>(false) }
|
{ provide: REFRESH_FILTER, useValue: new BehaviorSubject<boolean>(false)}
|
||||||
],
|
],
|
||||||
schemas: [NO_ERRORS_SCHEMA]
|
schemas: [NO_ERRORS_SCHEMA],
|
||||||
}).overrideComponent(SearchHierarchyFilterComponent, {
|
|
||||||
set: { changeDetection: ChangeDetectionStrategy.Default }
|
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
})
|
|
||||||
;
|
|
||||||
const mockFilterConfig: SearchFilterConfig = Object.assign(new SearchFilterConfig(), {
|
|
||||||
name: 'filterName1',
|
|
||||||
filterType: FilterType.text,
|
|
||||||
hasFacets: false,
|
|
||||||
isOpenByDefault: false,
|
|
||||||
pageSize: 2
|
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(async () => {
|
function init() {
|
||||||
fixture = TestBed.createComponent(SearchHierarchyFilterComponent);
|
fixture = TestBed.createComponent(SearchHierarchyFilterComponent);
|
||||||
comp = fixture.componentInstance; // SearchHierarchyFilterComponent test instance
|
|
||||||
comp.filterConfig = mockFilterConfig;
|
|
||||||
searchService = (comp as any).searchService;
|
|
||||||
// @ts-ignore
|
|
||||||
spyOn(searchService, 'getFacetValuesFor').and.returnValue(mockValues);
|
|
||||||
router = (comp as any).router;
|
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
showVocabularyTreeLink = fixture.debugElement.query(By.css('a#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();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should navigate to the correct filter with the query operator', () => {
|
describe('if the vocabulary exists', () => {
|
||||||
expect((comp as any).searchService.getFacetValuesFor).toHaveBeenCalledWith(comp.filterConfig, 0, {}, null, true);
|
|
||||||
|
|
||||||
const searchQuery = 'MARVEL';
|
beforeEach(() => {
|
||||||
comp.onSubmit(searchQuery);
|
spyOn(vocabularyService, 'searchTopEntries').and.returnValue(observableOf(new RemoteData(
|
||||||
|
undefined, 0, 0, RequestEntryState.Success, undefined, buildPaginatedList(new PageInfo(), []), 200
|
||||||
|
)));
|
||||||
|
init();
|
||||||
|
});
|
||||||
|
|
||||||
expect(router.navigate).toHaveBeenCalledWith(['', 'search'], Object({
|
it('should show the vocabulary tree link', () => {
|
||||||
queryParams: Object({ [mockFilterConfig.paramName]: [...values.map((value: FacetValue) => `${value.value},equals`), `${searchQuery},query`] }),
|
expect(showVocabularyTreeLink).toBeTruthy();
|
||||||
queryParamsHandling: 'merge'
|
});
|
||||||
}));
|
|
||||||
|
describe('when clicking the vocabulary tree link', () => {
|
||||||
|
|
||||||
|
const alreadySelectedValues = [
|
||||||
|
'already-selected-value-1',
|
||||||
|
'already-selected-value-2',
|
||||||
|
];
|
||||||
|
const newSelectedValue = 'new-selected-value';
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
showVocabularyTreeLink.nativeElement.click();
|
||||||
|
fixture.componentInstance.selectedValues$ = observableOf(
|
||||||
|
alreadySelectedValues.map(value => Object.assign(new FacetValue(), { value }))
|
||||||
|
);
|
||||||
|
VocabularyTreeViewComponent.select.emit(Object.assign(new VocabularyEntryDetail(), {
|
||||||
|
value: newSelectedValue,
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should open the vocabulary tree modal', () => {
|
||||||
|
expect(ngbModal.open).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when selecting a value from the vocabulary tree', () => {
|
||||||
|
|
||||||
|
it('should add a new search filter to the existing search filters', () => {
|
||||||
|
waitForAsync(() => expect(router.navigate).toHaveBeenCalledWith([testSearchLink], {
|
||||||
|
queryParams: {
|
||||||
|
[`f.${testSearchFilter}`]: [
|
||||||
|
...alreadySelectedValues,
|
||||||
|
newSelectedValue,
|
||||||
|
].map((value => `${value},equals`)),
|
||||||
|
},
|
||||||
|
queryParamsHandling: 'merge',
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -1,7 +1,30 @@
|
|||||||
import { Component, OnInit } from '@angular/core';
|
import { Component, Inject, OnInit } from '@angular/core';
|
||||||
import { FilterType } from '../../../models/filter-type.model';
|
|
||||||
import { renderFacetFor } from '../search-filter-type-decorator';
|
import { renderFacetFor } from '../search-filter-type-decorator';
|
||||||
|
import { FilterType } from '../../../models/filter-type.model';
|
||||||
import { facetLoad, SearchFacetFilterComponent } from '../search-facet-filter/search-facet-filter.component';
|
import { facetLoad, SearchFacetFilterComponent } from '../search-facet-filter/search-facet-filter.component';
|
||||||
|
import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
import { VocabularyTreeviewComponent } from '../../../../form/vocabulary-treeview/vocabulary-treeview.component';
|
||||||
|
import {
|
||||||
|
VocabularyEntryDetail
|
||||||
|
} from '../../../../../core/submission/vocabularies/models/vocabulary-entry-detail.model';
|
||||||
|
import { SearchService } from '../../../../../core/shared/search/search.service';
|
||||||
|
import {
|
||||||
|
FILTER_CONFIG,
|
||||||
|
IN_PLACE_SEARCH,
|
||||||
|
SearchFilterService, REFRESH_FILTER
|
||||||
|
} from '../../../../../core/shared/search/search-filter.service';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
import { RemoteDataBuildService } from '../../../../../core/cache/builders/remote-data-build.service';
|
||||||
|
import { SEARCH_CONFIG_SERVICE } from '../../../../../my-dspace-page/my-dspace-page.component';
|
||||||
|
import { SearchConfigurationService } from '../../../../../core/shared/search/search-configuration.service';
|
||||||
|
import { SearchFilterConfig } from '../../../models/search-filter-config.model';
|
||||||
|
import { FacetValue } from '../../../models/facet-value.model';
|
||||||
|
import { getFacetValueForType } from '../../../search.utils';
|
||||||
|
import { filter, map, take } from 'rxjs/operators';
|
||||||
|
import { VocabularyService } from '../../../../../core/submission/vocabularies/vocabulary.service';
|
||||||
|
import { Observable, BehaviorSubject } from 'rxjs';
|
||||||
|
import { PageInfo } from '../../../../../core/shared/page-info.model';
|
||||||
|
import { environment } from '../../../../../../environments/environment';
|
||||||
import { addOperatorToFilterValue } from '../../../search.utils';
|
import { addOperatorToFilterValue } from '../../../search.utils';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -16,6 +39,23 @@ import { addOperatorToFilterValue } from '../../../search.utils';
|
|||||||
*/
|
*/
|
||||||
@renderFacetFor(FilterType.hierarchy)
|
@renderFacetFor(FilterType.hierarchy)
|
||||||
export class SearchHierarchyFilterComponent extends SearchFacetFilterComponent implements OnInit {
|
export class SearchHierarchyFilterComponent extends SearchFacetFilterComponent implements OnInit {
|
||||||
|
|
||||||
|
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,
|
||||||
|
@Inject(REFRESH_FILTER) public refreshFilters: BehaviorSubject<boolean>
|
||||||
|
) {
|
||||||
|
super(searchService, filterService, rdbs, router, searchConfigService, inPlaceSearch, filterConfig, refreshFilters);
|
||||||
|
}
|
||||||
|
|
||||||
|
vocabularyExists$: Observable<boolean>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Submits a new active custom value to the filter from the input field
|
* Submits a new active custom value to the filter from the input field
|
||||||
* Overwritten method from parent component, adds the "query" operator to the received data before passing it on
|
* Overwritten method from parent component, adds the "query" operator to the received data before passing it on
|
||||||
@@ -24,4 +64,59 @@ export class SearchHierarchyFilterComponent extends SearchFacetFilterComponent i
|
|||||||
onSubmit(data: any) {
|
onSubmit(data: any) {
|
||||||
super.onSubmit(addOperatorToFilterValue(data, 'query'));
|
super.onSubmit(addOperatorToFilterValue(data, 'query'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
super.ngOnInit();
|
||||||
|
this.vocabularyExists$ = this.vocabularyService.searchTopEntries(
|
||||||
|
this.getVocabularyEntry(), 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(VocabularyTreeviewComponent, {
|
||||||
|
size: 'lg',
|
||||||
|
windowClass: 'treeview'
|
||||||
|
});
|
||||||
|
modalRef.componentInstance.vocabularyOptions = {
|
||||||
|
name: this.getVocabularyEntry(),
|
||||||
|
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',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the matching vocabulary entry for the given search filter.
|
||||||
|
* These are configurable in the config file.
|
||||||
|
*/
|
||||||
|
getVocabularyEntry() {
|
||||||
|
const foundVocabularyConfig = environment.vocabularies.filter((v) => v.filter === this.filterConfig.name);
|
||||||
|
if (foundVocabularyConfig.length > 0 && foundVocabularyConfig[0].enabled === true) {
|
||||||
|
return foundVocabularyConfig[0].vocabulary;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -3640,6 +3640,7 @@
|
|||||||
|
|
||||||
"search.filters.filter.submitter.label": "Search submitter",
|
"search.filters.filter.submitter.label": "Search submitter",
|
||||||
|
|
||||||
|
"search.filters.filter.show-tree": "Browse {{ name }} tree",
|
||||||
|
|
||||||
|
|
||||||
"search.filters.entityType.JournalIssue": "Journal Issue",
|
"search.filters.entityType.JournalIssue": "Journal Issue",
|
||||||
@@ -4514,7 +4515,7 @@
|
|||||||
|
|
||||||
"vocabulary-treeview.tree.description.srsc": "Research Subject Categories",
|
"vocabulary-treeview.tree.description.srsc": "Research Subject Categories",
|
||||||
|
|
||||||
|
"vocabulary-treeview.info": "Select a subject to add as search filter",
|
||||||
|
|
||||||
"uploader.browse": "browse",
|
"uploader.browse": "browse",
|
||||||
|
|
||||||
|
@@ -20,6 +20,7 @@ import { InfoConfig } from './info-config.interface';
|
|||||||
import { CommunityListConfig } from './community-list-config.interface';
|
import { CommunityListConfig } from './community-list-config.interface';
|
||||||
import { HomeConfig } from './homepage-config.interface';
|
import { HomeConfig } from './homepage-config.interface';
|
||||||
import { MarkdownConfig } from './markdown-config.interface';
|
import { MarkdownConfig } from './markdown-config.interface';
|
||||||
|
import { FilterVocabularyConfig } from './filter-vocabulary-config';
|
||||||
|
|
||||||
interface AppConfig extends Config {
|
interface AppConfig extends Config {
|
||||||
ui: UIServerConfig;
|
ui: UIServerConfig;
|
||||||
@@ -44,6 +45,7 @@ interface AppConfig extends Config {
|
|||||||
actuators: ActuatorsConfig
|
actuators: ActuatorsConfig
|
||||||
info: InfoConfig;
|
info: InfoConfig;
|
||||||
markdown: MarkdownConfig;
|
markdown: MarkdownConfig;
|
||||||
|
vocabularies: FilterVocabularyConfig[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -20,6 +20,7 @@ import { InfoConfig } from './info-config.interface';
|
|||||||
import { CommunityListConfig } from './community-list-config.interface';
|
import { CommunityListConfig } from './community-list-config.interface';
|
||||||
import { HomeConfig } from './homepage-config.interface';
|
import { HomeConfig } from './homepage-config.interface';
|
||||||
import { MarkdownConfig } from './markdown-config.interface';
|
import { MarkdownConfig } from './markdown-config.interface';
|
||||||
|
import { FilterVocabularyConfig } from './filter-vocabulary-config';
|
||||||
|
|
||||||
export class DefaultAppConfig implements AppConfig {
|
export class DefaultAppConfig implements AppConfig {
|
||||||
production = false;
|
production = false;
|
||||||
@@ -385,4 +386,15 @@ export class DefaultAppConfig implements AppConfig {
|
|||||||
enabled: false,
|
enabled: false,
|
||||||
mathjax: false,
|
mathjax: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Which vocabularies should be used for which search filters
|
||||||
|
// and whether to show the filter in the search sidebar
|
||||||
|
// Take a look at the filter-vocabulary-config.ts file for documentation on how the options are obtained
|
||||||
|
vocabularies: FilterVocabularyConfig[] = [
|
||||||
|
{
|
||||||
|
filter: 'subject',
|
||||||
|
vocabulary: 'srsc',
|
||||||
|
enabled: false
|
||||||
|
}
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
22
src/config/filter-vocabulary-config.ts
Normal file
22
src/config/filter-vocabulary-config.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { Config } from './config.interface';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration that can be used to enable a vocabulary tree to be used as search filter
|
||||||
|
*/
|
||||||
|
export interface FilterVocabularyConfig extends Config {
|
||||||
|
/**
|
||||||
|
* The name of the filter where the vocabulary tree should be used
|
||||||
|
* This is the name of the filter as it's configured in the facet in discovery.xml
|
||||||
|
* (can also be seen on the /server/api/discover/facets endpoint)
|
||||||
|
*/
|
||||||
|
filter: string;
|
||||||
|
/**
|
||||||
|
* name of the vocabulary tree to use
|
||||||
|
* ( name of the file as stored in the dspace/config/controlled-vocabularies folder without file extension )
|
||||||
|
*/
|
||||||
|
vocabulary: string;
|
||||||
|
/**
|
||||||
|
* Whether to show the vocabulary tree in the sidebar
|
||||||
|
*/
|
||||||
|
enabled: boolean;
|
||||||
|
}
|
@@ -283,4 +283,12 @@ export const environment: BuildConfig = {
|
|||||||
enabled: false,
|
enabled: false,
|
||||||
mathjax: false,
|
mathjax: false,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
vocabularies: [
|
||||||
|
{
|
||||||
|
filter: 'subject',
|
||||||
|
vocabulary: 'srsc',
|
||||||
|
enabled: true
|
||||||
|
}
|
||||||
|
]
|
||||||
};
|
};
|
||||||
|
@@ -31,6 +31,7 @@ import { GoogleAnalyticsService } from '../../app/statistics/google-analytics.se
|
|||||||
import { AuthRequestService } from '../../app/core/auth/auth-request.service';
|
import { AuthRequestService } from '../../app/core/auth/auth-request.service';
|
||||||
import { BrowserAuthRequestService } from '../../app/core/auth/browser-auth-request.service';
|
import { BrowserAuthRequestService } from '../../app/core/auth/browser-auth-request.service';
|
||||||
import { BrowserInitService } from './browser-init.service';
|
import { BrowserInitService } from './browser-init.service';
|
||||||
|
import { VocabularyTreeviewService } from 'src/app/shared/form/vocabulary-treeview/vocabulary-treeview.service';
|
||||||
|
|
||||||
export const REQ_KEY = makeStateKey<string>('req');
|
export const REQ_KEY = makeStateKey<string>('req');
|
||||||
|
|
||||||
@@ -111,6 +112,10 @@ export function getRequest(transferState: TransferState): any {
|
|||||||
provide: LocationToken,
|
provide: LocationToken,
|
||||||
useFactory: locationProvider,
|
useFactory: locationProvider,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: VocabularyTreeviewService,
|
||||||
|
useClass: VocabularyTreeviewService,
|
||||||
|
}
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class BrowserAppModule {
|
export class BrowserAppModule {
|
||||||
|
Reference in New Issue
Block a user