diff --git a/config/config.example.yml b/config/config.example.yml index 9abf167b90..c548d6944a 100644 --- a/config/config.example.yml +++ b/config/config.example.yml @@ -310,3 +310,11 @@ info: markdown: enabled: 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 diff --git a/package.json b/package.json index 387e094a67..f6ab1274e6 100644 --- a/package.json +++ b/package.json @@ -30,8 +30,9 @@ "clean:log": "rimraf *.log*", "clean:json": "rimraf *.records.json", "clean:node": "rimraf node_modules", + "clean:cli": "rimraf .angular/cache", "clean:prod": "yarn run clean:dist && yarn run clean:log && yarn run clean:doc && yarn run clean:coverage && yarn run clean:json", - "clean": "yarn run clean:prod && yarn run clean:dev:config && yarn run clean:node", + "clean": "yarn run clean:prod && yarn run clean:dev:config && yarn run clean:cli && yarn run clean:node", "sync-i18n": "ts-node --project ./tsconfig.ts-node.json scripts/sync-i18n-files.ts", "build:mirador": "webpack --config webpack/webpack.mirador.config.ts", "merge-i18n": "ts-node --project ./tsconfig.ts-node.json scripts/merge-i18n-files.ts", diff --git a/src/app/access-control/group-registry/group-form/group-form.component.spec.ts b/src/app/access-control/group-registry/group-form/group-form.component.spec.ts index 09487a7eaa..a7a7cb5be4 100644 --- a/src/app/access-control/group-registry/group-form/group-form.component.spec.ts +++ b/src/app/access-control/group-registry/group-form/group-form.component.spec.ts @@ -266,6 +266,43 @@ describe('GroupFormComponent', () => { fixture.detectChanges(); }); + it('should edit with name and description operations', () => { + const operations = [{ + op: 'add', + path: '/metadata/dc.description', + value: 'testDescription' + }, { + op: 'replace', + path: '/name', + value: 'newGroupName' + }]; + expect(groupsDataServiceStub.patch).toHaveBeenCalledWith(expected, operations); + }); + + it('should edit with description operations', () => { + component.groupName.value = null; + component.onSubmit(); + fixture.detectChanges(); + const operations = [{ + op: 'add', + path: '/metadata/dc.description', + value: 'testDescription' + }]; + expect(groupsDataServiceStub.patch).toHaveBeenCalledWith(expected, operations); + }); + + it('should edit with name operations', () => { + component.groupDescription.value = null; + component.onSubmit(); + fixture.detectChanges(); + const operations = [{ + op: 'replace', + path: '/name', + value: 'newGroupName' + }]; + expect(groupsDataServiceStub.patch).toHaveBeenCalledWith(expected, operations); + }); + it('should emit the existing group using the correct new values', waitForAsync(() => { fixture.whenStable().then(() => { expect(component.submitForm.emit).toHaveBeenCalledWith(expected2); diff --git a/src/app/access-control/group-registry/group-form/group-form.component.ts b/src/app/access-control/group-registry/group-form/group-form.component.ts index 584b28ba1e..4302d126ea 100644 --- a/src/app/access-control/group-registry/group-form/group-form.component.ts +++ b/src/app/access-control/group-registry/group-form/group-form.component.ts @@ -346,8 +346,8 @@ export class GroupFormComponent implements OnInit, OnDestroy { if (hasValue(this.groupDescription.value)) { operations = [...operations, { - op: 'replace', - path: '/metadata/dc.description/0/value', + op: 'add', + path: '/metadata/dc.description', value: this.groupDescription.value }]; } diff --git a/src/app/shared/form/vocabulary-treeview/vocabulary-treeview.component.spec.ts b/src/app/shared/form/vocabulary-treeview/vocabulary-treeview.component.spec.ts index cf8fbd8c49..642d81bbd1 100644 --- a/src/app/shared/form/vocabulary-treeview/vocabulary-treeview.component.spec.ts +++ b/src/app/shared/form/vocabulary-treeview/vocabulary-treeview.component.spec.ts @@ -141,7 +141,7 @@ describe('VocabularyTreeviewComponent test suite', () => { comp.selectedItem = currentValue; fixture.detectChanges(); 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', () => { @@ -153,7 +153,7 @@ describe('VocabularyTreeviewComponent test suite', () => { comp.selectedItem = currentValue; fixture.detectChanges(); 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', () => { diff --git a/src/app/shared/form/vocabulary-treeview/vocabulary-treeview.component.ts b/src/app/shared/form/vocabulary-treeview/vocabulary-treeview.component.ts index 408d656f42..56909090c7 100644 --- a/src/app/shared/form/vocabulary-treeview/vocabulary-treeview.component.ts +++ b/src/app/shared/form/vocabulary-treeview/vocabulary-treeview.component.ts @@ -2,14 +2,13 @@ import { FlatTreeControl } from '@angular/cdk/tree'; import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core'; 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 { select, Store } from '@ngrx/store'; +import { Store } from '@ngrx/store'; import { TranslateService } from '@ngx-translate/core'; import { VocabularyEntryDetail } from '../../../core/submission/vocabularies/models/vocabulary-entry-detail.model'; import { hasValue, isEmpty, isNotEmpty } from '../../empty.util'; -import { isAuthenticated } from '../../../core/auth/selectors'; import { VocabularyTreeviewService } from './vocabulary-treeview.service'; import { LOAD_MORE, LOAD_MORE_ROOT, TreeviewFlatNode, TreeviewNode } from './vocabulary-treeview-node.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 { VocabularyTreeFlatDataSource } from './vocabulary-tree-flat-data-source'; import { CoreState } from '../../../core/core-state.model'; +import { lowerCase } from 'lodash/string'; /** * 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.description = this.translate.get(descriptionLabel).pipe( - filter((msg) => msg !== descriptionLabel), - startWith('') + this.translate.get(`search.filters.filter.${this.vocabularyOptions.name}.head`).pipe( + map((type) => lowerCase(type)), + ).subscribe( + (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.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/app/shared/search/search-filters/search-filter/search-hierarchy-filter/search-hierarchy-filter.component.html b/src/app/shared/search/search-filters/search-filter/search-hierarchy-filter/search-hierarchy-filter.component.html index 49ca6fe3fd..eb49235641 100644 --- a/src/app/shared/search/search-filters/search-filter/search-hierarchy-filter/search-hierarchy-filter.component.html +++ b/src/app/shared/search/search-filters/search-filter/search-hierarchy-filter/search-hierarchy-filter.component.html @@ -29,3 +29,10 @@ ngDefaultControl > + + + {{'search.filters.filter.show-tree' | translate: {name: ('search.filters.filter.' + filterConfig.name + '.head' | translate | lowercase )} }} + diff --git a/src/app/shared/search/search-filters/search-filter/search-hierarchy-filter/search-hierarchy-filter.component.spec.ts b/src/app/shared/search/search-filters/search-filter/search-hierarchy-filter/search-hierarchy-filter.component.spec.ts index 9302e66d98..e6c74d8047 100644 --- a/src/app/shared/search/search-filters/search-filter/search-hierarchy-filter/search-hierarchy-filter.component.spec.ts +++ b/src/app/shared/search/search-filters/search-filter/search-hierarchy-filter/search-hierarchy-filter.component.spec.ts @@ -1,155 +1,155 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; 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 { FILTER_CONFIG, IN_PLACE_SEARCH, - REFRESH_FILTER, - SearchFilterService + SearchFilterService, + REFRESH_FILTER } from '../../../../../core/shared/search/search-filter.service'; import { RemoteDataBuildService } from '../../../../../core/cache/builders/remote-data-build.service'; -import { SearchFiltersComponent } from '../../search-filters.component'; import { Router } from '@angular/router'; -import { RouterStub } from '../../../../testing/router.stub'; -import { SearchServiceStub } from '../../../../testing/search-service.stub'; -import { BehaviorSubject, Observable, of as observableOf } from 'rxjs'; +import { NgbModal, NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { SEARCH_CONFIG_SERVICE } from '../../../../../my-dspace-page/my-dspace-page.component'; 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 { 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', () => { - let comp: SearchHierarchyFilterComponent; + let fixture: ComponentFixture; - let searchService: SearchService; - let router; + let showVocabularyTreeLink: DebugElement; - const value1 = 'testvalue1'; - const value2 = 'test2'; - const value3 = 'another value3'; - const values: FacetValue[] = [ - { - 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 { - return observableOf(values.map((value: FacetValue) => value.value)); - }, - getPage(_paramName: string): Observable { - return observableOf(0); - }, - resetPage(_filterName: string): void { - // empty - } + const testSearchLink = 'test-search'; + const testSearchFilter = 'test-search-filter'; + const VocabularyTreeViewComponent = { + select: new EventEmitter(), }; - const remoteDataBuildServiceStub = { - aggregate(_input: Observable>[]): Observable[]>> { - return createSuccessfulRemoteDataObject$([createPaginatedList(values)]); + const searchService = { + getSearchLink: () => testSearchLink, + 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 () => { - await TestBed.configureTestingModule({ - imports: [TranslateModule.forRoot(), NoopAnimationsModule, FormsModule], + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ + CommonModule, + NgbModule, + TranslateModule.forRoot(), + ], declarations: [ SearchHierarchyFilterComponent, - SearchFiltersComponent, - FilterInputSuggestionsComponent ], providers: [ - { provide: SearchService, useValue: new SearchServiceStub() }, - { provide: SearchFilterService, useValue: searchFilterServiceStub }, - { provide: RemoteDataBuildService, useValue: remoteDataBuildServiceStub }, - { provide: Router, useValue: new RouterStub() }, + { 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: new SearchFilterConfig() }, - { provide: REFRESH_FILTER, useValue: new BehaviorSubject(false) } + { provide: FILTER_CONFIG, useValue: Object.assign(new SearchFilterConfig(), { name: testSearchFilter }) }, + { provide: REFRESH_FILTER, useValue: new BehaviorSubject(false)} ], - schemas: [NO_ERRORS_SCHEMA] - }).overrideComponent(SearchHierarchyFilterComponent, { - set: { changeDetection: ChangeDetectionStrategy.Default } + schemas: [NO_ERRORS_SCHEMA], }).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); - 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(); + 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', () => { - expect((comp as any).searchService.getFacetValuesFor).toHaveBeenCalledWith(comp.filterConfig, 0, {}, null, true); + describe('if the vocabulary exists', () => { - const searchQuery = 'MARVEL'; - comp.onSubmit(searchQuery); + beforeEach(() => { + 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({ - queryParams: Object({ [mockFilterConfig.paramName]: [...values.map((value: FacetValue) => `${value.value},equals`), `${searchQuery},query`] }), - queryParamsHandling: 'merge' - })); + it('should show the vocabulary tree link', () => { + expect(showVocabularyTreeLink).toBeTruthy(); + }); + + 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', + })); + }); + }); + }); }); }); 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 b3349a5dd9..8504237f09 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 @@ -1,7 +1,30 @@ -import { Component, OnInit } from '@angular/core'; -import { FilterType } from '../../../models/filter-type.model'; +import { Component, Inject, OnInit } from '@angular/core'; 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 { 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'; @Component({ @@ -16,6 +39,23 @@ import { addOperatorToFilterValue } from '../../../search.utils'; */ @renderFacetFor(FilterType.hierarchy) 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 + ) { + super(searchService, filterService, rdbs, router, searchConfigService, inPlaceSearch, filterConfig, refreshFilters); + } + + vocabularyExists$: Observable; + /** * 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 @@ -24,4 +64,59 @@ export class SearchHierarchyFilterComponent extends SearchFacetFilterComponent i onSubmit(data: any) { 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; + } + } } diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 25bf255bd8..4e8915fdc1 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -3686,6 +3686,8 @@ "search.filters.filter.submitter.label": "Search submitter", + "search.filters.filter.show-tree": "Browse {{ name }} tree", + "search.filters.filter.supervisedBy.head": "Supervised By", "search.filters.filter.supervisedBy.placeholder": "Supervised By", @@ -4566,7 +4568,7 @@ "vocabulary-treeview.tree.description.srsc": "Research Subject Categories", - + "vocabulary-treeview.info": "Select a subject to add as search filter", "uploader.browse": "browse", diff --git a/src/config/app-config.interface.ts b/src/config/app-config.interface.ts index ce9c8b3bf7..d62b9e5bcb 100644 --- a/src/config/app-config.interface.ts +++ b/src/config/app-config.interface.ts @@ -20,6 +20,7 @@ import { InfoConfig } from './info-config.interface'; import { CommunityListConfig } from './community-list-config.interface'; import { HomeConfig } from './homepage-config.interface'; import { MarkdownConfig } from './markdown-config.interface'; +import { FilterVocabularyConfig } from './filter-vocabulary-config'; interface AppConfig extends Config { ui: UIServerConfig; @@ -44,6 +45,7 @@ interface AppConfig extends Config { actuators: ActuatorsConfig info: InfoConfig; markdown: MarkdownConfig; + vocabularies: FilterVocabularyConfig[]; } /** diff --git a/src/config/default-app-config.ts b/src/config/default-app-config.ts index 276d2d7150..d7b3efc2ed 100644 --- a/src/config/default-app-config.ts +++ b/src/config/default-app-config.ts @@ -20,6 +20,7 @@ import { InfoConfig } from './info-config.interface'; import { CommunityListConfig } from './community-list-config.interface'; import { HomeConfig } from './homepage-config.interface'; import { MarkdownConfig } from './markdown-config.interface'; +import { FilterVocabularyConfig } from './filter-vocabulary-config'; export class DefaultAppConfig implements AppConfig { production = false; @@ -385,4 +386,15 @@ export class DefaultAppConfig implements AppConfig { enabled: 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 + } + ]; } diff --git a/src/config/filter-vocabulary-config.ts b/src/config/filter-vocabulary-config.ts new file mode 100644 index 0000000000..54e57090c8 --- /dev/null +++ b/src/config/filter-vocabulary-config.ts @@ -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; +} diff --git a/src/environments/environment.test.ts b/src/environments/environment.test.ts index 19eec26a14..b323fa464d 100644 --- a/src/environments/environment.test.ts +++ b/src/environments/environment.test.ts @@ -283,4 +283,12 @@ export const environment: BuildConfig = { enabled: false, mathjax: false, }, + + vocabularies: [ + { + filter: 'subject', + vocabulary: 'srsc', + enabled: true + } + ] }; diff --git a/src/modules/app/browser-app.module.ts b/src/modules/app/browser-app.module.ts index 6baf339003..4cdf7fbe2f 100644 --- a/src/modules/app/browser-app.module.ts +++ b/src/modules/app/browser-app.module.ts @@ -31,6 +31,7 @@ import { GoogleAnalyticsService } from '../../app/statistics/google-analytics.se import { AuthRequestService } from '../../app/core/auth/auth-request.service'; import { BrowserAuthRequestService } from '../../app/core/auth/browser-auth-request.service'; import { BrowserInitService } from './browser-init.service'; +import { VocabularyTreeviewService } from 'src/app/shared/form/vocabulary-treeview/vocabulary-treeview.service'; export const REQ_KEY = makeStateKey('req'); @@ -111,6 +112,10 @@ export function getRequest(transferState: TransferState): any { provide: LocationToken, useFactory: locationProvider, }, + { + provide: VocabularyTreeviewService, + useClass: VocabularyTreeviewService, + } ] }) export class BrowserAppModule {