diff --git a/src/app/browse-by/browse-by-guard.spec.ts b/src/app/browse-by/browse-by-guard.spec.ts
index 671e098762..9ef76b1212 100644
--- a/src/app/browse-by/browse-by-guard.spec.ts
+++ b/src/app/browse-by/browse-by-guard.spec.ts
@@ -2,8 +2,8 @@ import { first } from 'rxjs/operators';
import { BrowseByGuard } from './browse-by-guard';
import { of as observableOf } from 'rxjs';
import { createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils';
-import { BrowseDefinition } from '../core/shared/browse-definition.model';
import { BrowseByDataType } from './browse-by-switcher/browse-by-decorator';
+import { ValueListBrowseDefinition } from '../core/shared/value-list-browse-definition.model';
import { DSONameServiceMock } from '../shared/mocks/dso-name.service.mock';
import { DSONameService } from '../core/breadcrumbs/dso-name.service';
@@ -20,7 +20,7 @@ describe('BrowseByGuard', () => {
const id = 'author';
const scope = '1234-65487-12354-1235';
const value = 'Filter';
- const browseDefinition = Object.assign(new BrowseDefinition(), { type: BrowseByDataType.Metadata, metadataKeys: ['dc.contributor'] });
+ const browseDefinition = Object.assign(new ValueListBrowseDefinition(), { type: BrowseByDataType.Metadata, metadataKeys: ['dc.contributor'] });
beforeEach(() => {
dsoService = {
diff --git a/src/app/browse-by/browse-by-switcher/browse-by-decorator.ts b/src/app/browse-by/browse-by-switcher/browse-by-decorator.ts
index ceb4c6a6c6..b59a46cae1 100644
--- a/src/app/browse-by/browse-by-switcher/browse-by-decorator.ts
+++ b/src/app/browse-by/browse-by-switcher/browse-by-decorator.ts
@@ -26,7 +26,7 @@ const map = new Map();
* @param browseByType The type of page
* @param theme The optional theme for the component
*/
-export function rendersBrowseBy(browseByType: BrowseByDataType, theme = DEFAULT_THEME) {
+export function rendersBrowseBy(browseByType: string, theme = DEFAULT_THEME) {
return function decorator(component: any) {
if (hasNoValue(map.get(browseByType))) {
map.set(browseByType, new Map());
diff --git a/src/app/browse-by/browse-by-switcher/browse-by-switcher.component.spec.ts b/src/app/browse-by/browse-by-switcher/browse-by-switcher.component.spec.ts
index c2e1c9cb68..c13405dd4d 100644
--- a/src/app/browse-by/browse-by-switcher/browse-by-switcher.component.spec.ts
+++ b/src/app/browse-by/browse-by-switcher/browse-by-switcher.component.spec.ts
@@ -3,9 +3,11 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { BROWSE_BY_COMPONENT_FACTORY, BrowseByDataType } from './browse-by-decorator';
-import { BrowseDefinition } from '../../core/shared/browse-definition.model';
import { BehaviorSubject } from 'rxjs';
import { ThemeService } from '../../shared/theme-support/theme.service';
+import { FlatBrowseDefinition } from '../../core/shared/flat-browse-definition.model';
+import { ValueListBrowseDefinition } from '../../core/shared/value-list-browse-definition.model';
+import { NonHierarchicalBrowseDefinition } from '../../core/shared/non-hierarchical-browse-definition';
describe('BrowseBySwitcherComponent', () => {
let comp: BrowseBySwitcherComponent;
@@ -13,33 +15,33 @@ describe('BrowseBySwitcherComponent', () => {
const types = [
Object.assign(
- new BrowseDefinition(), {
+ new FlatBrowseDefinition(), {
id: 'title',
dataType: BrowseByDataType.Title,
}
),
Object.assign(
- new BrowseDefinition(), {
+ new FlatBrowseDefinition(), {
id: 'dateissued',
dataType: BrowseByDataType.Date,
metadataKeys: ['dc.date.issued']
}
),
Object.assign(
- new BrowseDefinition(), {
+ new ValueListBrowseDefinition(), {
id: 'author',
dataType: BrowseByDataType.Metadata,
}
),
Object.assign(
- new BrowseDefinition(), {
+ new ValueListBrowseDefinition(), {
id: 'subject',
dataType: BrowseByDataType.Metadata,
}
),
];
- const data = new BehaviorSubject(createDataWithBrowseDefinition(new BrowseDefinition()));
+ const data = new BehaviorSubject(createDataWithBrowseDefinition(new FlatBrowseDefinition()));
const activatedRouteStub = {
data
@@ -70,7 +72,7 @@ describe('BrowseBySwitcherComponent', () => {
comp = fixture.componentInstance;
}));
- types.forEach((type: BrowseDefinition) => {
+ types.forEach((type: NonHierarchicalBrowseDefinition) => {
describe(`when switching to a browse-by page for "${type.id}"`, () => {
beforeEach(() => {
data.next(createDataWithBrowseDefinition(type));
diff --git a/src/app/browse-by/browse-by-switcher/browse-by-switcher.component.ts b/src/app/browse-by/browse-by-switcher/browse-by-switcher.component.ts
index 0d3a35bebf..35e4edf900 100644
--- a/src/app/browse-by/browse-by-switcher/browse-by-switcher.component.ts
+++ b/src/app/browse-by/browse-by-switcher/browse-by-switcher.component.ts
@@ -31,7 +31,7 @@ export class BrowseBySwitcherComponent implements OnInit {
*/
ngOnInit(): void {
this.browseByComponent = this.route.data.pipe(
- map((data: { browseDefinition: BrowseDefinition }) => this.getComponentByBrowseByType(data.browseDefinition.dataType, this.themeService.getThemeName()))
+ map((data: { browseDefinition: BrowseDefinition }) => this.getComponentByBrowseByType(data.browseDefinition.getRenderType(), this.themeService.getThemeName()))
);
}
diff --git a/src/app/browse-by/browse-by-taxonomy-page/browse-by-taxonomy-page.component.html b/src/app/browse-by/browse-by-taxonomy-page/browse-by-taxonomy-page.component.html
new file mode 100644
index 0000000000..87c7937b1b
--- /dev/null
+++ b/src/app/browse-by/browse-by-taxonomy-page/browse-by-taxonomy-page.component.html
@@ -0,0 +1,10 @@
+
diff --git a/src/app/browse-by/browse-by-taxonomy-page/browse-by-taxonomy-page.component.scss b/src/app/browse-by/browse-by-taxonomy-page/browse-by-taxonomy-page.component.scss
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/src/app/browse-by/browse-by-taxonomy-page/browse-by-taxonomy-page.component.spec.ts b/src/app/browse-by/browse-by-taxonomy-page/browse-by-taxonomy-page.component.spec.ts
new file mode 100644
index 0000000000..c724017b1f
--- /dev/null
+++ b/src/app/browse-by/browse-by-taxonomy-page/browse-by-taxonomy-page.component.spec.ts
@@ -0,0 +1,91 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { BrowseByTaxonomyPageComponent } from './browse-by-taxonomy-page.component';
+import { VocabularyEntryDetail } from '../../core/submission/vocabularies/models/vocabulary-entry-detail.model';
+import { TranslateModule } from '@ngx-translate/core';
+import { NO_ERRORS_SCHEMA } from '@angular/core';
+import { ActivatedRoute } from '@angular/router';
+import { BehaviorSubject } from 'rxjs';
+import { createDataWithBrowseDefinition } from '../browse-by-switcher/browse-by-switcher.component.spec';
+import { HierarchicalBrowseDefinition } from '../../core/shared/hierarchical-browse-definition.model';
+import { ThemeService } from '../../shared/theme-support/theme.service';
+
+describe('BrowseByTaxonomyPageComponent', () => {
+ let component: BrowseByTaxonomyPageComponent;
+ let fixture: ComponentFixture;
+ let themeService: ThemeService;
+ let detail1: VocabularyEntryDetail;
+ let detail2: VocabularyEntryDetail;
+
+ const data = new BehaviorSubject(createDataWithBrowseDefinition(new HierarchicalBrowseDefinition()));
+ const activatedRouteStub = {
+ data
+ };
+
+ beforeEach(async () => {
+ themeService = jasmine.createSpyObj('themeService', {
+ getThemeName: 'dspace',
+ });
+
+ await TestBed.configureTestingModule({
+ imports: [ TranslateModule.forRoot() ],
+ declarations: [ BrowseByTaxonomyPageComponent ],
+ providers: [
+ { provide: ActivatedRoute, useValue: activatedRouteStub },
+ { provide: ThemeService, useValue: themeService },
+ ],
+ schemas: [NO_ERRORS_SCHEMA]
+ })
+ .compileComponents();
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(BrowseByTaxonomyPageComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ detail1 = new VocabularyEntryDetail();
+ detail2 = new VocabularyEntryDetail();
+ detail1.value = 'HUMANITIES and RELIGION';
+ detail2.value = 'TECHNOLOGY';
+ detail1.id = 'id-1';
+ detail2.id = 'id-2';
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should handle select event', () => {
+ component.onSelect(detail1);
+ expect(component.selectedItems.length).toBe(1);
+ expect(component.selectedItems).toContain(detail1);
+ expect(component.selectedItems.length).toBe(1);
+ expect(component.filterValues).toEqual(['HUMANITIES and RELIGION,equals'] );
+ });
+
+ it('should handle select event with multiple selected items', () => {
+ component.onSelect(detail1);
+ component.onSelect(detail2);
+ expect(component.selectedItems.length).toBe(2);
+ expect(component.selectedItems).toContain(detail1, detail2);
+ expect(component.selectedItems.length).toBe(2);
+ expect(component.filterValues).toEqual(['HUMANITIES and RELIGION,equals', 'TECHNOLOGY,equals'] );
+ });
+
+ it('should handle deselect event', () => {
+ component.onSelect(detail1);
+ component.onSelect(detail2);
+ expect(component.selectedItems.length).toBe(2);
+ expect(component.selectedItems.length).toBe(2);
+ component.onDeselect(detail1);
+ expect(component.selectedItems.length).toBe(1);
+ expect(component.selectedItems).toContain(detail2);
+ expect(component.selectedItems.length).toBe(1);
+ expect(component.filterValues).toEqual(['TECHNOLOGY,equals'] );
+ });
+
+ afterEach(() => {
+ fixture.destroy();
+ component = null;
+ });
+});
diff --git a/src/app/browse-by/browse-by-taxonomy-page/browse-by-taxonomy-page.component.ts b/src/app/browse-by/browse-by-taxonomy-page/browse-by-taxonomy-page.component.ts
new file mode 100644
index 0000000000..cf6345bf39
--- /dev/null
+++ b/src/app/browse-by/browse-by-taxonomy-page/browse-by-taxonomy-page.component.ts
@@ -0,0 +1,118 @@
+import { Component, OnInit, Inject, OnDestroy } from '@angular/core';
+import { VocabularyOptions } from '../../core/submission/vocabularies/models/vocabulary-options.model';
+import { VocabularyEntryDetail } from '../../core/submission/vocabularies/models/vocabulary-entry-detail.model';
+import { ActivatedRoute } from '@angular/router';
+import { Observable, Subscription } from 'rxjs';
+import { BrowseDefinition } from '../../core/shared/browse-definition.model';
+import { GenericConstructor } from '../../core/shared/generic-constructor';
+import { BROWSE_BY_COMPONENT_FACTORY } from '../browse-by-switcher/browse-by-decorator';
+import { map } from 'rxjs/operators';
+import { ThemeService } from 'src/app/shared/theme-support/theme.service';
+import { HierarchicalBrowseDefinition } from '../../core/shared/hierarchical-browse-definition.model';
+
+@Component({
+ selector: 'ds-browse-by-taxonomy-page',
+ templateUrl: './browse-by-taxonomy-page.component.html',
+ styleUrls: ['./browse-by-taxonomy-page.component.scss']
+})
+/**
+ * Component for browsing items by metadata in a hierarchical controlled vocabulary
+ */
+export class BrowseByTaxonomyPageComponent implements OnInit, OnDestroy {
+
+ /**
+ * The {@link VocabularyOptions} object
+ */
+ vocabularyOptions: VocabularyOptions;
+
+ /**
+ * The selected vocabulary entries
+ */
+ selectedItems: VocabularyEntryDetail[] = [];
+
+ /**
+ * The query parameters, contain the selected entries
+ */
+ filterValues: string[];
+
+ /**
+ * The facet the use when filtering
+ */
+ facetType: string;
+
+ /**
+ * The used vocabulary
+ */
+ vocabularyName: string;
+
+ /**
+ * The parameters used in the URL
+ */
+ queryParams: any;
+
+ /**
+ * Resolved browse-by component
+ */
+ browseByComponent: Observable;
+
+ /**
+ * Subscriptions to track
+ */
+ browseByComponentSubs: Subscription[] = [];
+
+ public constructor( protected route: ActivatedRoute,
+ protected themeService: ThemeService,
+ @Inject(BROWSE_BY_COMPONENT_FACTORY) private getComponentByBrowseByType: (browseByType, theme) => GenericConstructor) {
+ }
+
+ ngOnInit(): void {
+ this.browseByComponent = this.route.data.pipe(
+ map((data: { browseDefinition: BrowseDefinition }) => {
+ this.getComponentByBrowseByType(data.browseDefinition.getRenderType(), this.themeService.getThemeName());
+ return data.browseDefinition;
+ })
+ );
+ this.browseByComponentSubs.push(this.browseByComponent.subscribe((browseDefinition: HierarchicalBrowseDefinition) => {
+ this.facetType = browseDefinition.facetType;
+ this.vocabularyName = browseDefinition.vocabulary;
+ this.vocabularyOptions = { name: this.vocabularyName, closed: true };
+ }));
+ }
+
+ /**
+ * Adds detail to selectedItems, transforms it to be used as query parameter
+ * and adds that to filterValues.
+ *
+ * @param detail VocabularyEntryDetail to be added
+ */
+ onSelect(detail: VocabularyEntryDetail): void {
+ this.selectedItems.push(detail);
+ this.filterValues = this.selectedItems
+ .map((item: VocabularyEntryDetail) => `${item.value},equals`);
+ this.updateQueryParams();
+ }
+
+ /**
+ * Removes detail from selectedItems and filterValues.
+ *
+ * @param detail VocabularyEntryDetail to be removed
+ */
+ onDeselect(detail: VocabularyEntryDetail): void {
+ this.selectedItems = this.selectedItems.filter((entry: VocabularyEntryDetail) => { return entry.id !== detail.id; });
+ this.filterValues = this.filterValues.filter((value: string) => { return value !== `${detail.value},equals`; });
+ this.updateQueryParams();
+ }
+
+ /**
+ * Updates queryParams based on the current facetType and filterValues.
+ */
+ private updateQueryParams(): void {
+ this.queryParams = {
+ ['f.' + this.facetType]: this.filterValues
+ };
+ }
+
+ ngOnDestroy(): void {
+ this.browseByComponentSubs.forEach((sub: Subscription) => sub.unsubscribe());
+ }
+}
diff --git a/src/app/browse-by/browse-by-taxonomy-page/themed-browse-by-taxonomy-page.component.ts b/src/app/browse-by/browse-by-taxonomy-page/themed-browse-by-taxonomy-page.component.ts
new file mode 100644
index 0000000000..212044b853
--- /dev/null
+++ b/src/app/browse-by/browse-by-taxonomy-page/themed-browse-by-taxonomy-page.component.ts
@@ -0,0 +1,28 @@
+import { Component } from '@angular/core';
+import { ThemedComponent } from '../../shared/theme-support/themed.component';
+import { rendersBrowseBy } from '../browse-by-switcher/browse-by-decorator';
+import { BrowseByTaxonomyPageComponent } from './browse-by-taxonomy-page.component';
+
+@Component({
+ selector: 'ds-themed-browse-by-taxonomy-page',
+ templateUrl: '../../shared/theme-support/themed.component.html',
+ styleUrls: []
+})
+/**
+ * Themed wrapper for BrowseByTaxonomyPageComponent
+ */
+@rendersBrowseBy('hierarchy')
+export class ThemedBrowseByTaxonomyPageComponent extends ThemedComponent{
+
+ protected getComponentName(): string {
+ return 'BrowseByTaxonomyPageComponent';
+ }
+
+ protected importThemedComponent(themeName: string): Promise {
+ return import(`../../../themes/${themeName}/app/browse-by/browse-by-taxonomy-page/browse-by-taxonomy-page.component`);
+ }
+
+ protected importUnthemedComponent(): Promise {
+ return import(`./browse-by-taxonomy-page.component`);
+ }
+}
diff --git a/src/app/browse-by/browse-by.module.ts b/src/app/browse-by/browse-by.module.ts
index 6c652376d5..c0e2d3f9ff 100644
--- a/src/app/browse-by/browse-by.module.ts
+++ b/src/app/browse-by/browse-by.module.ts
@@ -4,24 +4,28 @@ import { BrowseByTitlePageComponent } from './browse-by-title-page/browse-by-tit
import { BrowseByMetadataPageComponent } from './browse-by-metadata-page/browse-by-metadata-page.component';
import { BrowseByDatePageComponent } from './browse-by-date-page/browse-by-date-page.component';
import { BrowseBySwitcherComponent } from './browse-by-switcher/browse-by-switcher.component';
+import { BrowseByTaxonomyPageComponent } from './browse-by-taxonomy-page/browse-by-taxonomy-page.component';
import { ThemedBrowseBySwitcherComponent } from './browse-by-switcher/themed-browse-by-switcher.component';
import { ComcolModule } from '../shared/comcol/comcol.module';
import { ThemedBrowseByMetadataPageComponent } from './browse-by-metadata-page/themed-browse-by-metadata-page.component';
import { ThemedBrowseByDatePageComponent } from './browse-by-date-page/themed-browse-by-date-page.component';
import { ThemedBrowseByTitlePageComponent } from './browse-by-title-page/themed-browse-by-title-page.component';
+import { ThemedBrowseByTaxonomyPageComponent } from './browse-by-taxonomy-page/themed-browse-by-taxonomy-page.component';
import { SharedBrowseByModule } from '../shared/browse-by/shared-browse-by.module';
import { DsoPageModule } from '../shared/dso-page/dso-page.module';
+import { FormModule } from '../shared/form/form.module';
const ENTRY_COMPONENTS = [
// put only entry components that use custom decorator
BrowseByTitlePageComponent,
BrowseByMetadataPageComponent,
BrowseByDatePageComponent,
+ BrowseByTaxonomyPageComponent,
ThemedBrowseByMetadataPageComponent,
ThemedBrowseByDatePageComponent,
ThemedBrowseByTitlePageComponent,
-
+ ThemedBrowseByTaxonomyPageComponent,
];
@NgModule({
@@ -29,7 +33,8 @@ const ENTRY_COMPONENTS = [
SharedBrowseByModule,
CommonModule,
ComcolModule,
- DsoPageModule
+ DsoPageModule,
+ FormModule,
],
declarations: [
BrowseBySwitcherComponent,
diff --git a/src/app/core/browse/browse-definition-data.service.ts b/src/app/core/browse/browse-definition-data.service.ts
index 88d070000e..bc495a51f4 100644
--- a/src/app/core/browse/browse-definition-data.service.ts
+++ b/src/app/core/browse/browse-definition-data.service.ts
@@ -1,20 +1,60 @@
+// eslint-disable-next-line max-classes-per-file
import { Injectable } from '@angular/core';
import { BROWSE_DEFINITION } from '../shared/browse-definition.resource-type';
-import { BrowseDefinition } from '../shared/browse-definition.model';
import { RequestService } from '../data/request.service';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { ObjectCacheService } from '../cache/object-cache.service';
import { HALEndpointService } from '../shared/hal-endpoint.service';
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
-import { Observable } from 'rxjs';
+import { Observable, of as observableOf } from 'rxjs';
import { RemoteData } from '../data/remote-data';
import { PaginatedList } from '../data/paginated-list.model';
import { FindListOptions } from '../data/find-list-options.model';
import { IdentifiableDataService } from '../data/base/identifiable-data.service';
import { FindAllData, FindAllDataImpl } from '../data/base/find-all-data';
import { dataService } from '../data/base/data-service.decorator';
+import { isNotEmpty, isNotEmptyOperator, hasValue } from '../../shared/empty.util';
+import { take } from 'rxjs/operators';
+import { BrowseDefinitionRestRequest } from '../data/request.models';
import { RequestParam } from '../cache/models/request-param.model';
import { SearchData, SearchDataImpl } from '../data/base/search-data';
+import { BrowseDefinition } from '../shared/browse-definition.model';
+
+/**
+ * Create a GET request for the given href, and send it.
+ * Use a GET request specific for BrowseDefinitions.
+ */
+export const createAndSendBrowseDefinitionGetRequest = (requestService: RequestService,
+ responseMsToLive: number,
+ href$: string | Observable,
+ useCachedVersionIfAvailable: boolean = true): void => {
+ if (isNotEmpty(href$)) {
+ if (typeof href$ === 'string') {
+ href$ = observableOf(href$);
+ }
+
+ href$.pipe(
+ isNotEmptyOperator(),
+ take(1)
+ ).subscribe((href: string) => {
+ const requestId = requestService.generateRequestId();
+ const request = new BrowseDefinitionRestRequest(requestId, href);
+ if (hasValue(responseMsToLive)) {
+ request.responseMsToLive = responseMsToLive;
+ }
+ requestService.send(request, useCachedVersionIfAvailable);
+ });
+ }
+};
+
+/**
+ * Custom extension of {@link FindAllDataImpl} to be able to send BrowseDefinitionRestRequests
+ */
+class BrowseDefinitionFindAllDataImpl extends FindAllDataImpl {
+ createAndSendGetRequest(href$: string | Observable, useCachedVersionIfAvailable: boolean = true) {
+ createAndSendBrowseDefinitionGetRequest(this.requestService, this.responseMsToLive, href$, useCachedVersionIfAvailable);
+ }
+}
/**
* Data service responsible for retrieving browse definitions from the REST server
@@ -24,7 +64,7 @@ import { SearchData, SearchDataImpl } from '../data/base/search-data';
})
@dataService(BROWSE_DEFINITION)
export class BrowseDefinitionDataService extends IdentifiableDataService implements FindAllData, SearchData {
- private findAllData: FindAllDataImpl;
+ private findAllData: BrowseDefinitionFindAllDataImpl;
private searchData: SearchDataImpl;
constructor(
@@ -35,7 +75,7 @@ export class BrowseDefinitionDataService extends IdentifiableDataService, useCachedVersionIfAvailable: boolean = true) {
+ createAndSendBrowseDefinitionGetRequest(this.requestService, this.responseMsToLive, href$, useCachedVersionIfAvailable);
+ }
}
diff --git a/src/app/core/browse/browse.service.spec.ts b/src/app/core/browse/browse.service.spec.ts
index 46ac8c44f4..9f166e3d19 100644
--- a/src/app/core/browse/browse.service.spec.ts
+++ b/src/app/core/browse/browse.service.spec.ts
@@ -6,13 +6,15 @@ import { getMockRequestService } from '../../shared/mocks/request.service.mock';
import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { RequestService } from '../data/request.service';
-import { BrowseDefinition } from '../shared/browse-definition.model';
import { BrowseEntrySearchOptions } from './browse-entry-search-options.model';
import { BrowseService } from './browse.service';
import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
import { createPaginatedList, getFirstUsedArgumentOfSpyMethod } from '../../shared/testing/utils.test';
import { getMockHrefOnlyDataService } from '../../shared/mocks/href-only-data.service.mock';
import { RequestEntry } from '../data/request-entry.model';
+import { FlatBrowseDefinition } from '../shared/flat-browse-definition.model';
+import { ValueListBrowseDefinition } from '../shared/value-list-browse-definition.model';
+import { HierarchicalBrowseDefinition } from '../shared/hierarchical-browse-definition.model';
describe('BrowseService', () => {
let scheduler: TestScheduler;
@@ -23,9 +25,9 @@ describe('BrowseService', () => {
const browsesEndpointURL = 'https://rest.api/browses';
const halService: any = new HALEndpointServiceStub(browsesEndpointURL);
const browseDefinitions = [
- Object.assign(new BrowseDefinition(), {
+ Object.assign(new FlatBrowseDefinition(), {
id: 'date',
- metadataBrowse: false,
+ browseType: 'flatBrowse',
sortOptions: [
{
name: 'title',
@@ -50,9 +52,9 @@ describe('BrowseService', () => {
items: { href: 'https://rest.api/discover/browses/dateissued/items' }
}
}),
- Object.assign(new BrowseDefinition(), {
+ Object.assign(new ValueListBrowseDefinition(), {
id: 'author',
- metadataBrowse: true,
+ browseType: 'valueList',
sortOptions: [
{
name: 'title',
@@ -78,7 +80,23 @@ describe('BrowseService', () => {
entries: { href: 'https://rest.api/discover/browses/author/entries' },
items: { href: 'https://rest.api/discover/browses/author/items' }
}
- })
+ }),
+ Object.assign(new HierarchicalBrowseDefinition(), {
+ id: 'srsc',
+ browseType: 'hierarchicalBrowse',
+ facetType: 'subject',
+ vocabulary: 'srsc',
+ type: 'browse',
+ metadata: [
+ 'dc.subject'
+ ],
+ _links: {
+ vocabulary: { 'href': 'https://rest.api/submission/vocabularies/srsc/' },
+ items: { 'href': 'https://rest.api/discover/browses/srsc/items' },
+ entries: { 'href': 'https://rest.api/discover/browses/srsc/entries' },
+ self: { 'href': 'https://rest.api/discover/browses/srsc' }
+ }
+ }),
];
let browseDefinitionDataService;
@@ -140,7 +158,7 @@ describe('BrowseService', () => {
describe('when getBrowseEntriesFor is called with a valid browse definition id', () => {
it('should call hrefOnlyDataService.findListByHref with the expected href', () => {
- const expected = browseDefinitions[1]._links.entries.href;
+ const expected = (browseDefinitions[1] as ValueListBrowseDefinition)._links.entries.href;
scheduler.schedule(() => service.getBrowseEntriesFor(new BrowseEntrySearchOptions(browseDefinitions[1].id)).subscribe());
scheduler.flush();
diff --git a/src/app/core/browse/browse.service.ts b/src/app/core/browse/browse.service.ts
index 989213a978..b210b34949 100644
--- a/src/app/core/browse/browse.service.ts
+++ b/src/app/core/browse/browse.service.ts
@@ -7,6 +7,7 @@ import { PaginatedList } from '../data/paginated-list.model';
import { RemoteData } from '../data/remote-data';
import { RequestService } from '../data/request.service';
import { BrowseDefinition } from '../shared/browse-definition.model';
+import { FlatBrowseDefinition } from '../shared/flat-browse-definition.model';
import { BrowseEntry } from '../shared/browse-entry.model';
import { HALEndpointService } from '../shared/hal-endpoint.service';
import { Item } from '../shared/item.model';
@@ -240,7 +241,12 @@ export class BrowseService {
getPaginatedListPayload(),
map((browseDefinitions: BrowseDefinition[]) => browseDefinitions
.find((def: BrowseDefinition) => {
- const matchingKeys = def.metadataKeys.find((key: string) => searchKeyArray.indexOf(key) >= 0);
+ let matchingKeys = '';
+
+ if (Array.isArray((def as FlatBrowseDefinition).metadataKeys)) {
+ matchingKeys = (def as FlatBrowseDefinition).metadataKeys.find((key: string) => searchKeyArray.indexOf(key) >= 0);
+ }
+
return isNotEmpty(matchingKeys);
})
),
diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts
index d35900ebe2..8cd24b0140 100644
--- a/src/app/core/core.module.ts
+++ b/src/app/core/core.module.ts
@@ -177,6 +177,10 @@ import { IdentifierData } from '../shared/object-list/identifier-data/identifier
import { Subscription } from '../shared/subscriptions/models/subscription.model';
import { SupervisionOrderDataService } from './supervision-order/supervision-order-data.service';
import { ItemRequest } from './shared/item-request.model';
+import { HierarchicalBrowseDefinition } from './shared/hierarchical-browse-definition.model';
+import { FlatBrowseDefinition } from './shared/flat-browse-definition.model';
+import { ValueListBrowseDefinition } from './shared/value-list-browse-definition.model';
+import { NonHierarchicalBrowseDefinition } from './shared/non-hierarchical-browse-definition';
/**
* When not in production, endpoint responses can be mocked for testing purposes
@@ -333,6 +337,10 @@ export const models =
AuthStatus,
BrowseEntry,
BrowseDefinition,
+ NonHierarchicalBrowseDefinition,
+ FlatBrowseDefinition,
+ ValueListBrowseDefinition,
+ HierarchicalBrowseDefinition,
ClaimedTask,
TaskObject,
PoolTask,
diff --git a/src/app/core/data/bitstream-data.service.spec.ts b/src/app/core/data/bitstream-data.service.spec.ts
index c67eaa2d68..89178f8dd2 100644
--- a/src/app/core/data/bitstream-data.service.spec.ts
+++ b/src/app/core/data/bitstream-data.service.spec.ts
@@ -1,13 +1,14 @@
+import { TestBed } from '@angular/core/testing';
import { BitstreamDataService } from './bitstream-data.service';
import { ObjectCacheService } from '../cache/object-cache.service';
import { RequestService } from './request.service';
import { Bitstream } from '../shared/bitstream.model';
import { HALEndpointService } from '../shared/hal-endpoint.service';
import { BitstreamFormatDataService } from './bitstream-format-data.service';
-import { of as observableOf } from 'rxjs';
+import { Observable, of as observableOf } from 'rxjs';
import { BitstreamFormat } from '../shared/bitstream-format.model';
import { BitstreamFormatSupportLevel } from '../shared/bitstream-format-support-level';
-import { PutRequest } from './request.models';
+import { PatchRequest, PutRequest } from './request.models';
import { getMockRequestService } from '../../shared/mocks/request.service.mock';
import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
@@ -15,6 +16,11 @@ import { getMockRemoteDataBuildService } from '../../shared/mocks/remote-data-bu
import { testSearchDataImplementation } from './base/search-data.spec';
import { testPatchDataImplementation } from './base/patch-data.spec';
import { testDeleteDataImplementation } from './base/delete-data.spec';
+import { DSOChangeAnalyzer } from './dso-change-analyzer.service';
+import { NotificationsService } from '../../shared/notifications/notifications.service';
+import objectContaining = jasmine.objectContaining;
+import { RemoteData } from './remote-data';
+import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
describe('BitstreamDataService', () => {
let service: BitstreamDataService;
@@ -25,10 +31,18 @@ describe('BitstreamDataService', () => {
let rdbService: RemoteDataBuildService;
const bitstreamFormatHref = 'rest-api/bitstreamformats';
- const bitstream = Object.assign(new Bitstream(), {
- uuid: 'fake-bitstream',
+ const bitstream1 = Object.assign(new Bitstream(), {
+ id: 'fake-bitstream1',
+ uuid: 'fake-bitstream1',
_links: {
- self: { href: 'fake-bitstream-self' }
+ self: { href: 'fake-bitstream1-self' }
+ }
+ });
+ const bitstream2 = Object.assign(new Bitstream(), {
+ id: 'fake-bitstream2',
+ uuid: 'fake-bitstream2',
+ _links: {
+ self: { href: 'fake-bitstream2-self' }
}
});
const format = Object.assign(new BitstreamFormat(), {
@@ -50,7 +64,18 @@ describe('BitstreamDataService', () => {
});
rdbService = getMockRemoteDataBuildService();
- service = new BitstreamDataService(requestService, rdbService, objectCache, halService, null, bitstreamFormatService, null, null);
+ TestBed.configureTestingModule({
+ providers: [
+ { provide: ObjectCacheService, useValue: objectCache },
+ { provide: RequestService, useValue: requestService },
+ { provide: HALEndpointService, useValue: halService },
+ { provide: BitstreamFormatDataService, useValue: bitstreamFormatService },
+ { provide: RemoteDataBuildService, useValue: rdbService },
+ { provide: DSOChangeAnalyzer, useValue: {} },
+ { provide: NotificationsService, useValue: {} },
+ ],
+ });
+ service = TestBed.inject(BitstreamDataService);
});
describe('composition', () => {
@@ -62,11 +87,49 @@ describe('BitstreamDataService', () => {
describe('when updating the bitstream\'s format', () => {
beforeEach(() => {
- service.updateFormat(bitstream, format);
+ service.updateFormat(bitstream1, format);
});
it('should send a put request', () => {
expect(requestService.send).toHaveBeenCalledWith(jasmine.any(PutRequest));
});
});
+
+ describe('removeMultiple', () => {
+ function mockBuildFromRequestUUIDAndAwait(requestUUID$: string | Observable, callback: (rd?: RemoteData) => Observable, ..._linksToFollow: FollowLinkConfig[]): Observable> {
+ callback();
+ return;
+ }
+
+ beforeEach(() => {
+ spyOn(service, 'invalidateByHref');
+ spyOn(rdbService, 'buildFromRequestUUIDAndAwait').and.callFake((requestUUID$: string | Observable, callback: (rd?: RemoteData) => Observable, ...linksToFollow: FollowLinkConfig[]) => mockBuildFromRequestUUIDAndAwait(requestUUID$, callback, ...linksToFollow));
+ });
+
+ it('should be able to 1 bitstream', () => {
+ service.removeMultiple([bitstream1]);
+
+ expect(requestService.send).toHaveBeenCalledWith(objectContaining({
+ href: `${url}/bitstreams`,
+ body: [
+ { op: 'remove', path: '/bitstreams/fake-bitstream1' },
+ ],
+ } as PatchRequest));
+ expect(service.invalidateByHref).toHaveBeenCalledWith('fake-bitstream1-self');
+ });
+
+ it('should be able to delete multiple bitstreams', () => {
+ service.removeMultiple([bitstream1, bitstream2]);
+
+ expect(requestService.send).toHaveBeenCalledWith(objectContaining({
+ href: `${url}/bitstreams`,
+ body: [
+ { op: 'remove', path: '/bitstreams/fake-bitstream1' },
+ { op: 'remove', path: '/bitstreams/fake-bitstream2' },
+ ],
+ } as PatchRequest));
+ expect(service.invalidateByHref).toHaveBeenCalledWith('fake-bitstream1-self');
+ expect(service.invalidateByHref).toHaveBeenCalledWith('fake-bitstream2-self');
+ });
+ });
});
diff --git a/src/app/core/data/bitstream-data.service.ts b/src/app/core/data/bitstream-data.service.ts
index 6bdcefe187..bb4ec28166 100644
--- a/src/app/core/data/bitstream-data.service.ts
+++ b/src/app/core/data/bitstream-data.service.ts
@@ -1,7 +1,7 @@
import { HttpHeaders } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { combineLatest as observableCombineLatest, Observable } from 'rxjs';
-import { map, switchMap, take } from 'rxjs/operators';
+import { find, map, switchMap, take } from 'rxjs/operators';
import { hasValue } from '../../shared/empty.util';
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
@@ -14,7 +14,7 @@ import { Item } from '../shared/item.model';
import { BundleDataService } from './bundle-data.service';
import { buildPaginatedList, PaginatedList } from './paginated-list.model';
import { RemoteData } from './remote-data';
-import { PutRequest } from './request.models';
+import { PatchRequest, PutRequest } from './request.models';
import { RequestService } from './request.service';
import { BitstreamFormatDataService } from './bitstream-format-data.service';
import { BitstreamFormat } from '../shared/bitstream-format.model';
@@ -33,7 +33,7 @@ import { NotificationsService } from '../../shared/notifications/notifications.s
import { NoContent } from '../shared/NoContent.model';
import { IdentifiableDataService } from './base/identifiable-data.service';
import { dataService } from './base/data-service.decorator';
-import { Operation } from 'fast-json-patch';
+import { Operation, RemoveOperation } from 'fast-json-patch';
/**
* A service to retrieve {@link Bitstream}s from the REST API
@@ -277,4 +277,34 @@ export class BitstreamDataService extends IdentifiableDataService imp
deleteByHref(href: string, copyVirtualMetadata?: string[]): Observable> {
return this.deleteData.deleteByHref(href, copyVirtualMetadata);
}
+
+ /**
+ * Delete multiple {@link Bitstream}s at once by sending a PATCH request to the backend
+ *
+ * @param bitstreams The bitstreams that should be removed
+ */
+ removeMultiple(bitstreams: Bitstream[]): Observable> {
+ const operations: RemoveOperation[] = bitstreams.map((bitstream: Bitstream) => {
+ return {
+ op: 'remove',
+ path: `/bitstreams/${bitstream.id}`,
+ };
+ });
+ const requestId: string = this.requestService.generateRequestId();
+
+ const hrefObs: Observable = this.halService.getEndpoint(this.linkPath);
+
+ hrefObs.pipe(
+ find((href: string) => hasValue(href)),
+ ).subscribe((href: string) => {
+ const request = new PatchRequest(requestId, href, operations);
+ if (hasValue(this.responseMsToLive)) {
+ request.responseMsToLive = this.responseMsToLive;
+ }
+ this.requestService.send(request);
+ });
+
+ return this.rdbService.buildFromRequestUUIDAndAwait(requestId, () => observableCombineLatest(bitstreams.map((bitstream: Bitstream) => this.invalidateByHref(bitstream._links.self.href))));
+ }
+
}
diff --git a/src/app/core/data/browse-response-parsing.service.spec.ts b/src/app/core/data/browse-response-parsing.service.spec.ts
new file mode 100644
index 0000000000..9fa7239ef7
--- /dev/null
+++ b/src/app/core/data/browse-response-parsing.service.spec.ts
@@ -0,0 +1,64 @@
+import { getMockObjectCacheService } from '../../shared/mocks/object-cache.service.mock';
+import { BrowseResponseParsingService } from './browse-response-parsing.service';
+import { ObjectCacheService } from '../cache/object-cache.service';
+import { HIERARCHICAL_BROWSE_DEFINITION } from '../shared/hierarchical-browse-definition.resource-type';
+import { FLAT_BROWSE_DEFINITION } from '../shared/flat-browse-definition.resource-type';
+import { VALUE_LIST_BROWSE_DEFINITION } from '../shared/value-list-browse-definition.resource-type';
+
+class TestService extends BrowseResponseParsingService {
+ constructor(protected objectCache: ObjectCacheService) {
+ super(objectCache);
+ }
+
+ // Overwrite method to make it public for testing
+ public deserialize(obj): any {
+ return super.deserialize(obj);
+ }
+}
+
+describe('BrowseResponseParsingService', () => {
+ let service: TestService;
+
+
+ beforeEach(() => {
+ service = new TestService(getMockObjectCacheService());
+ });
+
+ describe('', () => {
+ const mockFlatBrowse = {
+ id: 'title',
+ browseType: 'flatBrowse',
+ type: 'browse',
+ };
+
+ const mockValueList = {
+ id: 'author',
+ browseType: 'valueList',
+ type: 'browse',
+ };
+
+ const mockHierarchicalBrowse = {
+ id: 'srsc',
+ browseType: 'hierarchicalBrowse',
+ type: 'browse',
+ };
+
+ it('should deserialize flatBrowses correctly', () => {
+ let deserialized = service.deserialize(mockFlatBrowse);
+ expect(deserialized.type).toBe(FLAT_BROWSE_DEFINITION);
+ expect(deserialized.id).toBe(mockFlatBrowse.id);
+ });
+
+ it('should deserialize valueList browses correctly', () => {
+ let deserialized = service.deserialize(mockValueList);
+ expect(deserialized.type).toBe(VALUE_LIST_BROWSE_DEFINITION);
+ expect(deserialized.id).toBe(mockValueList.id);
+ });
+
+ it('should deserialize hierarchicalBrowses correctly', () => {
+ let deserialized = service.deserialize(mockHierarchicalBrowse);
+ expect(deserialized.type).toBe(HIERARCHICAL_BROWSE_DEFINITION);
+ expect(deserialized.id).toBe(mockHierarchicalBrowse.id);
+ });
+ });
+});
diff --git a/src/app/core/data/browse-response-parsing.service.ts b/src/app/core/data/browse-response-parsing.service.ts
new file mode 100644
index 0000000000..a568cdb617
--- /dev/null
+++ b/src/app/core/data/browse-response-parsing.service.ts
@@ -0,0 +1,48 @@
+import { Injectable } from '@angular/core';
+import { ObjectCacheService } from '../cache/object-cache.service';
+import { hasValue } from '../../shared/empty.util';
+import {
+ HIERARCHICAL_BROWSE_DEFINITION
+} from '../shared/hierarchical-browse-definition.resource-type';
+import { FLAT_BROWSE_DEFINITION } from '../shared/flat-browse-definition.resource-type';
+import { HierarchicalBrowseDefinition } from '../shared/hierarchical-browse-definition.model';
+import { FlatBrowseDefinition } from '../shared/flat-browse-definition.model';
+import { DspaceRestResponseParsingService } from './dspace-rest-response-parsing.service';
+import { Serializer } from '../serializer';
+import { BrowseDefinition } from '../shared/browse-definition.model';
+import { BROWSE_DEFINITION } from '../shared/browse-definition.resource-type';
+import { ValueListBrowseDefinition } from '../shared/value-list-browse-definition.model';
+import { VALUE_LIST_BROWSE_DEFINITION } from '../shared/value-list-browse-definition.resource-type';
+
+/**
+ * A ResponseParsingService used to parse a REST API response to a BrowseDefinition object
+ */
+@Injectable({
+ providedIn: 'root',
+})
+export class BrowseResponseParsingService extends DspaceRestResponseParsingService {
+ constructor(
+ protected objectCache: ObjectCacheService,
+ ) {
+ super(objectCache);
+ }
+
+ protected deserialize(obj): any {
+ const browseType: string = obj.browseType;
+ if (obj.type === BROWSE_DEFINITION.value && hasValue(browseType)) {
+ let serializer: Serializer;
+ if (browseType === HIERARCHICAL_BROWSE_DEFINITION.value) {
+ serializer = new this.serializerConstructor(HierarchicalBrowseDefinition);
+ } else if (browseType === FLAT_BROWSE_DEFINITION.value) {
+ serializer = new this.serializerConstructor(FlatBrowseDefinition);
+ } else if (browseType === VALUE_LIST_BROWSE_DEFINITION.value) {
+ serializer = new this.serializerConstructor(ValueListBrowseDefinition);
+ } else {
+ throw new Error('An error occurred while retrieving the browse definitions.');
+ }
+ return serializer.deserialize(obj);
+ } else {
+ throw new Error('An error occurred while retrieving the browse definitions.');
+ }
+ }
+}
diff --git a/src/app/core/data/dso-response-parsing.service.ts b/src/app/core/data/dso-response-parsing.service.ts
index fd5a22fae9..74117e79d3 100644
--- a/src/app/core/data/dso-response-parsing.service.ts
+++ b/src/app/core/data/dso-response-parsing.service.ts
@@ -10,6 +10,10 @@ import { hasNoValue, hasValue } from '../../shared/empty.util';
import { DSpaceObject } from '../shared/dspace-object.model';
import { RestRequest } from './rest-request.model';
+/**
+ * @deprecated use DspaceRestResponseParsingService for new code, this is only left to support a
+ * few legacy use cases, and should get removed eventually
+ */
@Injectable()
export class DSOResponseParsingService extends BaseResponseParsingService implements ResponseParsingService {
protected toCache = true;
diff --git a/src/app/core/data/request.models.ts b/src/app/core/data/request.models.ts
index 6ab3f180d3..9809bc0fde 100644
--- a/src/app/core/data/request.models.ts
+++ b/src/app/core/data/request.models.ts
@@ -11,6 +11,7 @@ import { TaskResponseParsingService } from '../tasks/task-response-parsing.servi
import { ContentSourceResponseParsingService } from './content-source-response-parsing.service';
import { RestRequestWithResponseParser } from './rest-request-with-response-parser.model';
import { DspaceRestResponseParsingService } from './dspace-rest-response-parsing.service';
+import { BrowseResponseParsingService } from './browse-response-parsing.service';
import { FindListOptions } from './find-list-options.model';
@@ -118,6 +119,15 @@ export class PatchRequest extends DSpaceRestRequest {
}
}
+/**
+ * Class representing a BrowseDefinition HTTP Rest request object
+ */
+export class BrowseDefinitionRestRequest extends DSpaceRestRequest {
+ getResponseParser(): GenericConstructor {
+ return BrowseResponseParsingService;
+ }
+}
+
export class FindListRequest extends GetRequest {
constructor(
uuid: string,
diff --git a/src/app/core/shared/browse-definition.model.ts b/src/app/core/shared/browse-definition.model.ts
index 863f454422..a5bed53c9f 100644
--- a/src/app/core/shared/browse-definition.model.ts
+++ b/src/app/core/shared/browse-definition.model.ts
@@ -1,50 +1,16 @@
-import { autoserialize, autoserializeAs, deserialize } from 'cerialize';
-import { typedObject } from '../cache/builders/build-decorators';
-import { excludeFromEquals } from '../utilities/equals.decorators';
-import { BROWSE_DEFINITION } from './browse-definition.resource-type';
-import { HALLink } from './hal-link.model';
-import { ResourceType } from './resource-type';
-import { SortOption } from './sort-option.model';
+import { autoserialize } from 'cerialize';
import { CacheableObject } from '../cache/cacheable-object.model';
-import { BrowseByDataType } from '../../browse-by/browse-by-switcher/browse-by-decorator';
-@typedObject
-export class BrowseDefinition extends CacheableObject {
- static type = BROWSE_DEFINITION;
-
- /**
- * The object type
- */
- @excludeFromEquals
- @autoserialize
- type: ResourceType;
+/**
+ * Base class for BrowseDefinition models
+ */
+export abstract class BrowseDefinition extends CacheableObject {
@autoserialize
id: string;
- @autoserialize
- metadataBrowse: boolean;
-
- @autoserialize
- sortOptions: SortOption[];
-
- @autoserializeAs('order')
- defaultSortOrder: string;
-
- @autoserializeAs('metadata')
- metadataKeys: string[];
-
- @autoserialize
- dataType: BrowseByDataType;
-
- get self(): string {
- return this._links.self.href;
- }
-
- @deserialize
- _links: {
- self: HALLink;
- entries: HALLink;
- items: HALLink;
- };
+ /**
+ * Get the render type of the BrowseDefinition model
+ */
+ abstract getRenderType(): string;
}
diff --git a/src/app/core/shared/flat-browse-definition.model.ts b/src/app/core/shared/flat-browse-definition.model.ts
new file mode 100644
index 0000000000..086fca891b
--- /dev/null
+++ b/src/app/core/shared/flat-browse-definition.model.ts
@@ -0,0 +1,36 @@
+import { inheritSerialization, deserialize } from 'cerialize';
+import { typedObject } from '../cache/builders/build-decorators';
+import { excludeFromEquals } from '../utilities/equals.decorators';
+import { FLAT_BROWSE_DEFINITION } from './flat-browse-definition.resource-type';
+import { ResourceType } from './resource-type';
+import { NonHierarchicalBrowseDefinition } from './non-hierarchical-browse-definition';
+import { HALLink } from './hal-link.model';
+
+/**
+ * BrowseDefinition model for browses of type 'flatBrowse'
+ */
+@typedObject
+@inheritSerialization(NonHierarchicalBrowseDefinition)
+export class FlatBrowseDefinition extends NonHierarchicalBrowseDefinition {
+ static type = FLAT_BROWSE_DEFINITION;
+
+ /**
+ * The object type
+ */
+ @excludeFromEquals
+ type: ResourceType = FLAT_BROWSE_DEFINITION;
+
+ get self(): string {
+ return this._links.self.href;
+ }
+
+ @deserialize
+ _links: {
+ self: HALLink;
+ items: HALLink;
+ };
+
+ getRenderType(): string {
+ return this.dataType;
+ }
+}
diff --git a/src/app/core/shared/flat-browse-definition.resource-type.ts b/src/app/core/shared/flat-browse-definition.resource-type.ts
new file mode 100644
index 0000000000..bfb01cd98c
--- /dev/null
+++ b/src/app/core/shared/flat-browse-definition.resource-type.ts
@@ -0,0 +1,9 @@
+import { ResourceType } from './resource-type';
+
+/**
+ * The resource type for FlatBrowseDefinition
+ *
+ * Needs to be in a separate file to prevent circular
+ * dependencies in webpack.
+ */
+export const FLAT_BROWSE_DEFINITION = new ResourceType('flatBrowse');
diff --git a/src/app/core/shared/hierarchical-browse-definition.model.ts b/src/app/core/shared/hierarchical-browse-definition.model.ts
new file mode 100644
index 0000000000..d561fff643
--- /dev/null
+++ b/src/app/core/shared/hierarchical-browse-definition.model.ts
@@ -0,0 +1,45 @@
+import { autoserialize, autoserializeAs, deserialize, inheritSerialization } from 'cerialize';
+import { typedObject } from '../cache/builders/build-decorators';
+import { excludeFromEquals } from '../utilities/equals.decorators';
+import { HIERARCHICAL_BROWSE_DEFINITION } from './hierarchical-browse-definition.resource-type';
+import { HALLink } from './hal-link.model';
+import { ResourceType } from './resource-type';
+import { BrowseDefinition } from './browse-definition.model';
+
+/**
+ * BrowseDefinition model for browses of type 'hierarchicalBrowse'
+ */
+@typedObject
+@inheritSerialization(BrowseDefinition)
+export class HierarchicalBrowseDefinition extends BrowseDefinition {
+ static type = HIERARCHICAL_BROWSE_DEFINITION;
+
+ /**
+ * The object type
+ */
+ @excludeFromEquals
+ type: ResourceType = HIERARCHICAL_BROWSE_DEFINITION;
+
+ @autoserialize
+ facetType: string;
+
+ @autoserialize
+ vocabulary: string;
+
+ @autoserializeAs('metadata')
+ metadataKeys: string[];
+
+ get self(): string {
+ return this._links.self.href;
+ }
+
+ @deserialize
+ _links: {
+ self: HALLink;
+ vocabulary: HALLink;
+ };
+
+ getRenderType(): string {
+ return 'hierarchy';
+ }
+}
diff --git a/src/app/core/shared/hierarchical-browse-definition.resource-type.ts b/src/app/core/shared/hierarchical-browse-definition.resource-type.ts
new file mode 100644
index 0000000000..df06d67c7a
--- /dev/null
+++ b/src/app/core/shared/hierarchical-browse-definition.resource-type.ts
@@ -0,0 +1,9 @@
+import { ResourceType } from './resource-type';
+
+/**
+ * The resource type for HierarchicalBrowseDefinition
+ *
+ * Needs to be in a separate file to prevent circular
+ * dependencies in webpack.
+ */
+export const HIERARCHICAL_BROWSE_DEFINITION = new ResourceType('hierarchicalBrowse');
diff --git a/src/app/core/shared/non-hierarchical-browse-definition.ts b/src/app/core/shared/non-hierarchical-browse-definition.ts
new file mode 100644
index 0000000000..d5481fcc8d
--- /dev/null
+++ b/src/app/core/shared/non-hierarchical-browse-definition.ts
@@ -0,0 +1,24 @@
+import { autoserialize, autoserializeAs, inheritSerialization } from 'cerialize';
+import { SortOption } from './sort-option.model';
+import { BrowseByDataType } from '../../browse-by/browse-by-switcher/browse-by-decorator';
+import { BrowseDefinition } from './browse-definition.model';
+
+/**
+ * Super class for NonHierarchicalBrowseDefinition models,
+ * e.g. FlatBrowseDefinition and ValueListBrowseDefinition
+ */
+@inheritSerialization(BrowseDefinition)
+export abstract class NonHierarchicalBrowseDefinition extends BrowseDefinition {
+
+ @autoserialize
+ sortOptions: SortOption[];
+
+ @autoserializeAs('order')
+ defaultSortOrder: string;
+
+ @autoserializeAs('metadata')
+ metadataKeys: string[];
+
+ @autoserialize
+ dataType: BrowseByDataType;
+}
diff --git a/src/app/core/shared/value-list-browse-definition.model.ts b/src/app/core/shared/value-list-browse-definition.model.ts
new file mode 100644
index 0000000000..3378263962
--- /dev/null
+++ b/src/app/core/shared/value-list-browse-definition.model.ts
@@ -0,0 +1,36 @@
+import { inheritSerialization, deserialize } from 'cerialize';
+import { typedObject } from '../cache/builders/build-decorators';
+import { excludeFromEquals } from '../utilities/equals.decorators';
+import { VALUE_LIST_BROWSE_DEFINITION } from './value-list-browse-definition.resource-type';
+import { ResourceType } from './resource-type';
+import { NonHierarchicalBrowseDefinition } from './non-hierarchical-browse-definition';
+import { HALLink } from './hal-link.model';
+
+/**
+ * BrowseDefinition model for browses of type 'valueList'
+ */
+@typedObject
+@inheritSerialization(NonHierarchicalBrowseDefinition)
+export class ValueListBrowseDefinition extends NonHierarchicalBrowseDefinition {
+ static type = VALUE_LIST_BROWSE_DEFINITION;
+
+ /**
+ * The object type
+ */
+ @excludeFromEquals
+ type: ResourceType = VALUE_LIST_BROWSE_DEFINITION;
+
+ get self(): string {
+ return this._links.self.href;
+ }
+
+ @deserialize
+ _links: {
+ self: HALLink;
+ entries: HALLink;
+ };
+
+ getRenderType(): string {
+ return this.dataType;
+ }
+}
diff --git a/src/app/core/shared/value-list-browse-definition.resource-type.ts b/src/app/core/shared/value-list-browse-definition.resource-type.ts
new file mode 100644
index 0000000000..8904dc472f
--- /dev/null
+++ b/src/app/core/shared/value-list-browse-definition.resource-type.ts
@@ -0,0 +1,9 @@
+import { ResourceType } from './resource-type';
+
+/**
+ * The resource type for ValueListBrowseDefinition
+ *
+ * Needs to be in a separate file to prevent circular
+ * dependencies in webpack.
+ */
+export const VALUE_LIST_BROWSE_DEFINITION = new ResourceType('valueList');
diff --git a/src/app/core/submission/vocabularies/vocabulary.service.ts b/src/app/core/submission/vocabularies/vocabulary.service.ts
index f2c1747658..1ff5b30ee0 100644
--- a/src/app/core/submission/vocabularies/vocabulary.service.ts
+++ b/src/app/core/submission/vocabularies/vocabulary.service.ts
@@ -223,13 +223,15 @@ export class VocabularyService {
* no valid cached version. Defaults to true
* @param reRequestOnStale Whether or not the request should automatically be re-
* requested after the response becomes stale
+ * @param constructId Whether constructing the full vocabularyDetail ID
+ * ({vocabularyName}:{detailName}) is still necessary
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which
* {@link HALLink}s should be automatically resolved
* @return {Observable>}
* Return an observable that emits VocabularyEntryDetail object
*/
- findEntryDetailById(id: string, name: string, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig[]): Observable> {
- const findId = `${name}:${id}`;
+ findEntryDetailById(id: string, name: string, useCachedVersionIfAvailable = true, reRequestOnStale = true, constructId: boolean = true, ...linksToFollow: FollowLinkConfig[]): Observable> {
+ const findId: string = (constructId ? `${name}:${id}` : id);
return this.vocabularyEntryDetailDataService.findById(findId, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
}
diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.spec.ts b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.spec.ts
index 67d047d776..10e1812131 100644
--- a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.spec.ts
+++ b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.spec.ts
@@ -25,6 +25,7 @@ import { getMockRequestService } from '../../../shared/mocks/request.service.moc
import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
import { createPaginatedList } from '../../../shared/testing/utils.test';
import { FieldChangeType } from '../../../core/data/object-updates/field-change-type.model';
+import { BitstreamDataServiceStub } from '../../../shared/testing/bitstream-data-service.stub';
let comp: ItemBitstreamsComponent;
let fixture: ComponentFixture;
@@ -71,7 +72,7 @@ let objectUpdatesService: ObjectUpdatesService;
let router: any;
let route: ActivatedRoute;
let notificationsService: NotificationsService;
-let bitstreamService: BitstreamDataService;
+let bitstreamService: BitstreamDataServiceStub;
let objectCache: ObjectCacheService;
let requestService: RequestService;
let searchConfig: SearchConfigurationService;
@@ -112,9 +113,7 @@ describe('ItemBitstreamsComponent', () => {
success: successNotification
}
);
- bitstreamService = jasmine.createSpyObj('bitstreamService', {
- delete: jasmine.createSpy('delete')
- });
+ bitstreamService = new BitstreamDataServiceStub();
objectCache = jasmine.createSpyObj('objectCache', {
remove: jasmine.createSpy('remove')
});
@@ -179,15 +178,16 @@ describe('ItemBitstreamsComponent', () => {
describe('when submit is called', () => {
beforeEach(() => {
+ spyOn(bitstreamService, 'removeMultiple').and.callThrough();
comp.submit();
});
- it('should call delete on the bitstreamService for the marked field', () => {
- expect(bitstreamService.delete).toHaveBeenCalledWith(bitstream2.id);
+ it('should call removeMultiple on the bitstreamService for the marked field', () => {
+ expect(bitstreamService.removeMultiple).toHaveBeenCalledWith([bitstream2]);
});
- it('should not call delete on the bitstreamService for the unmarked field', () => {
- expect(bitstreamService.delete).not.toHaveBeenCalledWith(bitstream1.id);
+ it('should not call removeMultiple on the bitstreamService for the unmarked field', () => {
+ expect(bitstreamService.removeMultiple).not.toHaveBeenCalledWith([bitstream1]);
});
});
@@ -210,7 +210,6 @@ describe('ItemBitstreamsComponent', () => {
comp.dropBitstream(bundle, {
fromIndex: 0,
toIndex: 50,
- // eslint-disable-next-line no-empty, @typescript-eslint/no-empty-function
finish: () => {
done();
}
diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.ts b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.ts
index 0c7dfb1e34..ee53bd919c 100644
--- a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.ts
+++ b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.ts
@@ -1,7 +1,7 @@
import { ChangeDetectorRef, Component, NgZone, OnDestroy } from '@angular/core';
import { AbstractItemUpdateComponent } from '../abstract-item-update/abstract-item-update.component';
import { filter, map, switchMap, take } from 'rxjs/operators';
-import { Observable, of as observableOf, Subscription, zip as observableZip } from 'rxjs';
+import { Observable, Subscription, zip as observableZip } from 'rxjs';
import { ItemDataService } from '../../../core/data/item-data.service';
import { ObjectUpdatesService } from '../../../core/data/object-updates/object-updates.service';
import { ActivatedRoute, Router } from '@angular/router';
@@ -133,20 +133,16 @@ export class ItemBitstreamsComponent extends AbstractItemUpdateComponent impleme
);
// Send out delete requests for all deleted bitstreams
- const removedResponses$ = removedBitstreams$.pipe(
+ const removedResponses$: Observable> = removedBitstreams$.pipe(
take(1),
- switchMap((removedBistreams: Bitstream[]) => {
- if (isNotEmpty(removedBistreams)) {
- return observableZip(...removedBistreams.map((bitstream: Bitstream) => this.bitstreamService.delete(bitstream.id)));
- } else {
- return observableOf(undefined);
- }
+ switchMap((removedBitstreams: Bitstream[]) => {
+ return this.bitstreamService.removeMultiple(removedBitstreams);
})
);
// Perform the setup actions from above in order and display notifications
- removedResponses$.pipe(take(1)).subscribe((responses: RemoteData[]) => {
- this.displayNotifications('item.edit.bitstreams.notifications.remove', responses);
+ removedResponses$.subscribe((responses: RemoteData) => {
+ this.displayNotifications('item.edit.bitstreams.notifications.remove', [responses]);
this.submitting = false;
});
}
diff --git a/src/app/item-page/field-components/metadata-values/metadata-values.component.ts b/src/app/item-page/field-components/metadata-values/metadata-values.component.ts
index 49ce453fbe..cbbae9006d 100644
--- a/src/app/item-page/field-components/metadata-values/metadata-values.component.ts
+++ b/src/app/item-page/field-components/metadata-values/metadata-values.component.ts
@@ -3,6 +3,7 @@ import { MetadataValue } from '../../../core/shared/metadata.models';
import { APP_CONFIG, AppConfig } from '../../../../config/app-config.interface';
import { BrowseDefinition } from '../../../core/shared/browse-definition.model';
import { hasValue } from '../../../shared/empty.util';
+import { VALUE_LIST_BROWSE_DEFINITION } from '../../../core/shared/value-list-browse-definition.resource-type';
/**
* This component renders the configured 'values' into the ds-metadata-field-wrapper component.
@@ -84,7 +85,7 @@ export class MetadataValuesComponent implements OnChanges {
*/
getQueryParams(value) {
let queryParams = {startsWith: value};
- if (this.browseDefinition.metadataBrowse) {
+ if (this.browseDefinition.getRenderType() === VALUE_LIST_BROWSE_DEFINITION.value) {
return {value: value};
}
return queryParams;
diff --git a/src/app/item-page/orcid-page/orcid-queue/orcid-queue.component.html b/src/app/item-page/orcid-page/orcid-queue/orcid-queue.component.html
index 9358bcf835..6ba318f7fd 100644
--- a/src/app/item-page/orcid-page/orcid-queue/orcid-queue.component.html
+++ b/src/app/item-page/orcid-page/orcid-queue/orcid-queue.component.html
@@ -34,11 +34,13 @@
diff --git a/src/app/navbar/navbar.component.spec.ts b/src/app/navbar/navbar.component.spec.ts
index ada9be9d0b..983eace055 100644
--- a/src/app/navbar/navbar.component.spec.ts
+++ b/src/app/navbar/navbar.component.spec.ts
@@ -16,7 +16,6 @@ import { RouterTestingModule } from '@angular/router/testing';
import { BrowseService } from '../core/browse/browse.service';
import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils';
import { buildPaginatedList } from '../core/data/paginated-list.model';
-import { BrowseDefinition } from '../core/shared/browse-definition.model';
import { BrowseByDataType } from '../browse-by/browse-by-switcher/browse-by-decorator';
import { Item } from '../core/shared/item.model';
import { AuthorizationDataService } from '../core/data/feature-authorization/authorization-data.service';
@@ -28,6 +27,9 @@ import { authReducer } from '../core/auth/auth.reducer';
import { provideMockStore } from '@ngrx/store/testing';
import { AuthTokenInfo } from '../core/auth/models/auth-token-info.model';
import { EPersonMock } from '../shared/testing/eperson.mock';
+import { FlatBrowseDefinition } from '../core/shared/flat-browse-definition.model';
+import { ValueListBrowseDefinition } from '../core/shared/value-list-browse-definition.model';
+import { HierarchicalBrowseDefinition } from '../core/shared/hierarchical-browse-definition.model';
let comp: NavbarComponent;
let fixture: ComponentFixture;
@@ -66,30 +68,35 @@ describe('NavbarComponent', () => {
beforeEach(waitForAsync(() => {
browseDefinitions = [
Object.assign(
- new BrowseDefinition(), {
+ new FlatBrowseDefinition(), {
id: 'title',
dataType: BrowseByDataType.Title,
}
),
Object.assign(
- new BrowseDefinition(), {
+ new FlatBrowseDefinition(), {
id: 'dateissued',
dataType: BrowseByDataType.Date,
metadataKeys: ['dc.date.issued']
}
),
Object.assign(
- new BrowseDefinition(), {
+ new ValueListBrowseDefinition(), {
id: 'author',
dataType: BrowseByDataType.Metadata,
}
),
Object.assign(
- new BrowseDefinition(), {
+ new ValueListBrowseDefinition(), {
id: 'subject',
dataType: BrowseByDataType.Metadata,
}
),
+ Object.assign(
+ new HierarchicalBrowseDefinition(), {
+ id: 'srsc',
+ }
+ ),
];
initialState = {
core: {
diff --git a/src/app/shared/dso-page/dso-edit-menu.resolver.ts b/src/app/shared/dso-page/dso-edit-menu.resolver.ts
index 749d5580a4..80a69c2830 100644
--- a/src/app/shared/dso-page/dso-edit-menu.resolver.ts
+++ b/src/app/shared/dso-page/dso-edit-menu.resolver.ts
@@ -50,27 +50,32 @@ export class DSOEditMenuResolver implements Resolve<{ [key: string]: MenuSection
if (hasNoValue(id) && hasValue(route.queryParams.scope)) {
id = route.queryParams.scope;
}
- return this.dSpaceObjectDataService.findById(id, true, false).pipe(
- getFirstCompletedRemoteData(),
- switchMap((dsoRD) => {
- if (dsoRD.hasSucceeded) {
- const dso = dsoRD.payload;
- return combineLatest(this.getDsoMenus(dso, route, state)).pipe(
- // Menu sections are retrieved as an array of arrays and flattened into a single array
- map((combinedMenus) => [].concat.apply([], combinedMenus)),
- map((menus) => this.addDsoUuidToMenuIDs(menus, dso)),
- map((menus) => {
- return {
- ...route.data?.menu,
- [MenuID.DSO_EDIT]: menus
- };
- })
- );
- } else {
- return observableOf({...route.data?.menu});
- }
- })
- );
+ if (hasNoValue(id)) {
+ // If there's no ID, we're not on a DSO homepage, so pass on any pre-existing menu route data
+ return observableOf({ ...route.data?.menu });
+ } else {
+ return this.dSpaceObjectDataService.findById(id, true, false).pipe(
+ getFirstCompletedRemoteData(),
+ switchMap((dsoRD) => {
+ if (dsoRD.hasSucceeded) {
+ const dso = dsoRD.payload;
+ return combineLatest(this.getDsoMenus(dso, route, state)).pipe(
+ // Menu sections are retrieved as an array of arrays and flattened into a single array
+ map((combinedMenus) => [].concat.apply([], combinedMenus)),
+ map((menus) => this.addDsoUuidToMenuIDs(menus, dso)),
+ map((menus) => {
+ return {
+ ...route.data?.menu,
+ [MenuID.DSO_EDIT]: menus
+ };
+ })
+ );
+ } else {
+ return observableOf({...route.data?.menu});
+ }
+ })
+ );
+ }
}
/**
diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component.ts
index 251ff36a68..db278716e1 100644
--- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component.ts
+++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component.ts
@@ -30,8 +30,8 @@ import { VocabularyEntry } from '../../../../../../core/submission/vocabularies/
import { PageInfo } from '../../../../../../core/shared/page-info.model';
import { DsDynamicVocabularyComponent } from '../dynamic-vocabulary.component';
import { Vocabulary } from '../../../../../../core/submission/vocabularies/models/vocabulary.model';
-import { VocabularyTreeviewComponent } from '../../../../vocabulary-treeview/vocabulary-treeview.component';
import { VocabularyEntryDetail } from '../../../../../../core/submission/vocabularies/models/vocabulary-entry-detail.model';
+import { VocabularyTreeviewModalComponent } from '../../../../vocabulary-treeview-modal/vocabulary-treeview-modal.component';
/**
* Component representing a onebox input field.
@@ -222,10 +222,10 @@ export class DsDynamicOneboxComponent extends DsDynamicVocabularyComponent imple
map((vocabulary: Vocabulary) => vocabulary.preloadLevel),
take(1)
).subscribe((preloadLevel) => {
- const modalRef: NgbModalRef = this.modalService.open(VocabularyTreeviewComponent, { size: 'lg', windowClass: 'treeview' });
+ const modalRef: NgbModalRef = this.modalService.open(VocabularyTreeviewModalComponent, { size: 'lg', windowClass: 'treeview' });
modalRef.componentInstance.vocabularyOptions = this.model.vocabularyOptions;
modalRef.componentInstance.preloadLevel = preloadLevel;
- modalRef.componentInstance.selectedItem = this.currentValue ? this.currentValue : '';
+ modalRef.componentInstance.selectedItems = this.currentValue ? [this.currentValue.value] : [];
modalRef.result.then((result: VocabularyEntryDetail) => {
if (result) {
this.currentValue = result;
diff --git a/src/app/shared/form/form.module.ts b/src/app/shared/form/form.module.ts
index de18c53363..792de6f251 100644
--- a/src/app/shared/form/form.module.ts
+++ b/src/app/shared/form/form.module.ts
@@ -32,7 +32,7 @@ import { NumberPickerComponent } from './number-picker/number-picker.component';
import { AuthorityConfidenceStateDirective } from './directives/authority-confidence-state.directive';
import { SortablejsModule } from 'ngx-sortablejs';
import { VocabularyTreeviewComponent } from './vocabulary-treeview/vocabulary-treeview.component';
-import { VocabularyTreeviewService } from './vocabulary-treeview/vocabulary-treeview.service';
+import { VocabularyTreeviewModalComponent } from './vocabulary-treeview-modal/vocabulary-treeview-modal.component';
import { FormBuilderService } from './builder/form-builder.service';
import { DsDynamicTypeBindRelationService } from './builder/ds-dynamic-form-ui/ds-dynamic-type-bind-relation.service';
import { FormService } from './form.service';
@@ -71,7 +71,8 @@ const COMPONENTS = [
ChipsComponent,
NumberPickerComponent,
VocabularyTreeviewComponent,
- ThemedExternalSourceEntryImportModalComponent
+ VocabularyTreeviewModalComponent,
+ ThemedExternalSourceEntryImportModalComponent,
];
const DIRECTIVES = [
@@ -105,7 +106,6 @@ const DIRECTIVES = [
provide: DYNAMIC_FORM_CONTROL_MAP_FN,
useValue: dsDynamicFormControlMapFn
},
- VocabularyTreeviewService,
DynamicFormLayoutService,
DynamicFormService,
DynamicFormValidationService,
diff --git a/src/app/shared/form/vocabulary-treeview-modal/vocabulary-treeview-modal.component.html b/src/app/shared/form/vocabulary-treeview-modal/vocabulary-treeview-modal.component.html
new file mode 100644
index 0000000000..71eb8e1476
--- /dev/null
+++ b/src/app/shared/form/vocabulary-treeview-modal/vocabulary-treeview-modal.component.html
@@ -0,0 +1,16 @@
+
+
diff --git a/src/app/shared/form/vocabulary-treeview-modal/vocabulary-treeview-modal.component.scss b/src/app/shared/form/vocabulary-treeview-modal/vocabulary-treeview-modal.component.scss
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/src/app/shared/form/vocabulary-treeview-modal/vocabulary-treeview-modal.component.spec.ts b/src/app/shared/form/vocabulary-treeview-modal/vocabulary-treeview-modal.component.spec.ts
new file mode 100644
index 0000000000..590c69a159
--- /dev/null
+++ b/src/app/shared/form/vocabulary-treeview-modal/vocabulary-treeview-modal.component.spec.ts
@@ -0,0 +1,33 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { VocabularyTreeviewModalComponent } from './vocabulary-treeview-modal.component';
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+import { TranslateModule } from '@ngx-translate/core';
+
+describe('VocabularyTreeviewModalComponent', () => {
+ let component: VocabularyTreeviewModalComponent;
+ let fixture: ComponentFixture;
+
+ const modalStub = jasmine.createSpyObj('modalStub', ['close']);
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [ TranslateModule.forRoot() ],
+ declarations: [ VocabularyTreeviewModalComponent ],
+ providers: [
+ { provide: NgbActiveModal, useValue: modalStub },
+ ],
+ })
+ .compileComponents();
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(VocabularyTreeviewModalComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/shared/form/vocabulary-treeview-modal/vocabulary-treeview-modal.component.ts b/src/app/shared/form/vocabulary-treeview-modal/vocabulary-treeview-modal.component.ts
new file mode 100644
index 0000000000..c6b0bf20fe
--- /dev/null
+++ b/src/app/shared/form/vocabulary-treeview-modal/vocabulary-treeview-modal.component.ts
@@ -0,0 +1,51 @@
+import { Component, Input } from '@angular/core';
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+import { VocabularyOptions } from '../../../core/submission/vocabularies/models/vocabulary-options.model';
+import { VocabularyEntryDetail } from '../../../core/submission/vocabularies/models/vocabulary-entry-detail.model';
+
+@Component({
+ selector: 'ds-vocabulary-treeview-modal',
+ templateUrl: './vocabulary-treeview-modal.component.html',
+ styleUrls: ['./vocabulary-treeview-modal.component.scss']
+})
+/**
+ * Component that contains a modal to display a VocabularyTreeviewComponent
+ */
+export class VocabularyTreeviewModalComponent {
+
+ /**
+ * The {@link VocabularyOptions} object
+ */
+ @Input() vocabularyOptions: VocabularyOptions;
+
+ /**
+ * Representing how many tree level load at initialization
+ */
+ @Input() preloadLevel = 2;
+
+ /**
+ * The vocabulary entries already selected, if any
+ */
+ @Input() selectedItems: string[] = [];
+
+ /**
+ * Whether to allow selecting multiple values with checkboxes
+ */
+ @Input() multiSelect = false;
+
+ /**
+ * Initialize instance variables
+ *
+ * @param {NgbActiveModal} activeModal
+ */
+ constructor(
+ public activeModal: NgbActiveModal,
+ ) { }
+
+ /**
+ * Method called on entry select
+ */
+ onSelect(item: VocabularyEntryDetail) {
+ this.activeModal.close(item);
+ }
+}
diff --git a/src/app/shared/form/vocabulary-treeview/vocabulary-treeview-node.model.ts b/src/app/shared/form/vocabulary-treeview/vocabulary-treeview-node.model.ts
index c167328cab..4ac1b08425 100644
--- a/src/app/shared/form/vocabulary-treeview/vocabulary-treeview-node.model.ts
+++ b/src/app/shared/form/vocabulary-treeview/vocabulary-treeview-node.model.ts
@@ -21,7 +21,8 @@ export class TreeviewNode {
public pageInfo: PageInfo = new PageInfo(),
public loadMoreParentItem: VocabularyEntryDetail | null = null,
public isSearchNode = false,
- public isInInitValueHierarchy = false) {
+ public isInInitValueHierarchy = false,
+ public isSelected = false) {
}
updatePageInfo(pageInfo: PageInfo) {
@@ -38,7 +39,8 @@ export class TreeviewFlatNode {
public pageInfo: PageInfo = new PageInfo(),
public loadMoreParentItem: VocabularyEntryDetail | null = null,
public isSearchNode = false,
- public isInInitValueHierarchy = false) {
+ public isInInitValueHierarchy = false,
+ public isSelected = false) {
}
}
diff --git a/src/app/shared/form/vocabulary-treeview/vocabulary-treeview.component.html b/src/app/shared/form/vocabulary-treeview/vocabulary-treeview.component.html
index 39c62d6e53..fb7d162008 100644
--- a/src/app/shared/form/vocabulary-treeview/vocabulary-treeview.component.html
+++ b/src/app/shared/form/vocabulary-treeview/vocabulary-treeview.component.html
@@ -1,77 +1,98 @@
-
-