Merge branch 'w2p-101127_browse-by-controlled-vocabulary' into w2p-101127_browse-by-controlled-vocabulary-7.6.0-next

Conflicts:
	src/app/browse-by/browse-by-routing.module.ts
	src/app/browse-by/browse-by.module.ts
	src/app/core/browse/browse-definition-data.service.ts
	src/app/core/core.module.ts
This commit is contained in:
Nona Luypaert
2023-05-24 16:35:37 +02:00
33 changed files with 549 additions and 99 deletions

View File

@@ -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';
describe('BrowseByGuard', () => {
describe('canActivate', () => {
@@ -18,7 +18,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 = {

View File

@@ -5,8 +5,6 @@ import { BrowseByDSOBreadcrumbResolver } from './browse-by-dso-breadcrumb.resolv
import { BrowseByI18nBreadcrumbResolver } from './browse-by-i18n-breadcrumb.resolver';
import { ThemedBrowseBySwitcherComponent } from './browse-by-switcher/themed-browse-by-switcher.component';
import { DSOEditMenuResolver } from '../shared/dso-page/dso-edit-menu.resolver';
import { BrowseByTaxonomyPageComponent } from './browse-by-taxonomy-page/browse-by-taxonomy-page.component';
import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver';
@NgModule({
imports: [
@@ -18,13 +16,6 @@ import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.reso
menu: DSOEditMenuResolver
},
children: [
{
path: 'srsc',
component: BrowseByTaxonomyPageComponent,
canActivate: [BrowseByGuard],
resolve: { breadcrumb: I18nBreadcrumbResolver },
data: { title: 'browse.title.page', breadcrumbKey: 'browse.metadata.srsc' }
},
{
path: ':id',
component: ThemedBrowseBySwitcherComponent,

View File

@@ -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());

View File

@@ -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));

View File

@@ -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()))
);
}

View File

@@ -6,5 +6,5 @@
(deselect)="onDeselect($event)">
</ds-vocabulary-treeview>
</div>
<a class="btn btn-primary" [routerLink]="['/search']" [queryParams]="{ 'f.subject': filterValues }">{{ 'browse.taxonomy.button' | translate }}</a>
<a class="btn btn-primary" [routerLink]="['/search']" [queryParams]="queryParams">{{ 'browse.taxonomy.button' | translate }}</a>
</div>

View File

@@ -4,17 +4,36 @@ import { BrowseByTaxonomyPageComponent } from './browse-by-taxonomy-page.compone
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<BrowseByTaxonomyPageComponent>;
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();

View File

@@ -1,6 +1,14 @@
import { Component, OnInit } from '@angular/core';
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',
@@ -10,7 +18,7 @@ import { VocabularyEntryDetail } from '../../core/submission/vocabularies/models
/**
* Component for browsing items by metadata in a hierarchical controlled vocabulary
*/
export class BrowseByTaxonomyPageComponent implements OnInit {
export class BrowseByTaxonomyPageComponent implements OnInit, OnDestroy {
/**
* The {@link VocabularyOptions} object
@@ -27,8 +35,48 @@ export class BrowseByTaxonomyPageComponent implements OnInit {
*/
filterValues: string[];
ngOnInit() {
this.vocabularyOptions = { name: 'srsc', closed: true };
/**
* 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<any>;
/**
* Subscriptions to track
*/
browseByComponentSubs: Subscription[] = [];
public constructor( protected route: ActivatedRoute,
protected themeService: ThemeService,
@Inject(BROWSE_BY_COMPONENT_FACTORY) private getComponentByBrowseByType: (browseByType, theme) => GenericConstructor<any>) {
}
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 };
}));
}
/**
@@ -41,10 +89,30 @@ export class BrowseByTaxonomyPageComponent implements OnInit {
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 !== detail; });
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());
}
}

View File

@@ -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<BrowseByTaxonomyPageComponent>{
protected getComponentName(): string {
return 'BrowseByTaxonomyPageComponent';
}
protected importThemedComponent(themeName: string): Promise<any> {
return import(`../../../themes/${themeName}/app/browse-by/browse-by-taxonomy-page/browse-by-taxonomy-page.component`);
}
protected importUnthemedComponent(): Promise<any> {
return import(`./browse-by-taxonomy-page.component`);
}
}

View File

@@ -10,6 +10,7 @@ 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';
@@ -19,11 +20,12 @@ const ENTRY_COMPONENTS = [
BrowseByTitlePageComponent,
BrowseByMetadataPageComponent,
BrowseByDatePageComponent,
BrowseByTaxonomyPageComponent,
ThemedBrowseByMetadataPageComponent,
ThemedBrowseByDatePageComponent,
ThemedBrowseByTitlePageComponent,
ThemedBrowseByTaxonomyPageComponent,
];
@NgModule({
@@ -37,7 +39,6 @@ const ENTRY_COMPONENTS = [
declarations: [
BrowseBySwitcherComponent,
ThemedBrowseBySwitcherComponent,
BrowseByTaxonomyPageComponent,
...ENTRY_COMPONENTS
],
exports: [

View File

@@ -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<string>,
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<BrowseDefinition> {
createAndSendGetRequest(href$: string | Observable<string>, 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<BrowseDefinition> implements FindAllData<BrowseDefinition>, SearchData<BrowseDefinition> {
private findAllData: FindAllDataImpl<BrowseDefinition>;
private findAllData: BrowseDefinitionFindAllDataImpl;
private searchData: SearchDataImpl<BrowseDefinition>;
constructor(
@@ -35,7 +75,7 @@ export class BrowseDefinitionDataService extends IdentifiableDataService<BrowseD
) {
super('browses', requestService, rdbService, objectCache, halService);
this.searchData = new SearchDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, this.responseMsToLive);
this.findAllData = new FindAllDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, this.responseMsToLive);
this.findAllData = new BrowseDefinitionFindAllDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, this.responseMsToLive);
}
/**
@@ -121,5 +161,8 @@ export class BrowseDefinitionDataService extends IdentifiableDataService<BrowseD
);
}
createAndSendGetRequest(href$: string | Observable<string>, useCachedVersionIfAvailable: boolean = true) {
createAndSendBrowseDefinitionGetRequest(this.requestService, this.responseMsToLive, href$, useCachedVersionIfAvailable);
}
}

View File

@@ -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,7 +25,7 @@ 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,
sortOptions: [
@@ -50,7 +52,7 @@ describe('BrowseService', () => {
items: { href: 'https://rest.api/discover/browses/dateissued/items' }
}
}),
Object.assign(new BrowseDefinition(), {
Object.assign(new ValueListBrowseDefinition(), {
id: 'author',
metadataBrowse: true,
sortOptions: [
@@ -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;

View File

@@ -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);
})
),

View File

@@ -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,

View File

@@ -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<ObjectDomain>(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);
});
});
});

View File

@@ -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<ObjectDomain>(obj): any {
const browseType: string = obj.browseType;
if (obj.type === BROWSE_DEFINITION.value && hasValue(browseType)) {
let serializer: Serializer<BrowseDefinition>;
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.');
}
}
}

View File

@@ -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;

View File

@@ -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<ResponseParsingService> {
return BrowseResponseParsingService;
}
}
export class FindListRequest extends GetRequest {
constructor(
uuid: string,

View File

@@ -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;
}

View File

@@ -0,0 +1,29 @@
import { inheritSerialization } 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';
/**
* 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;
}
getRenderType(): string {
return this.dataType;
}
}

View File

@@ -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');

View File

@@ -0,0 +1,47 @@
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;
entries: HALLink;
items: HALLink;
vocabulary: HALLink;
};
getRenderType(): string {
return 'hierarchy';
}
}

View File

@@ -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');

View File

@@ -0,0 +1,32 @@
import { autoserialize, autoserializeAs, deserialize, inheritSerialization } from 'cerialize';
import { SortOption } from './sort-option.model';
import { BrowseByDataType } from '../../browse-by/browse-by-switcher/browse-by-decorator';
import { HALLink } from './hal-link.model';
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;
@deserialize
_links: {
self: HALLink;
entries: HALLink;
items: HALLink;
};
}

View File

@@ -0,0 +1,29 @@
import { inheritSerialization } 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';
/**
* 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;
}
getRenderType(): string {
return this.dataType;
}
}

View File

@@ -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');

View File

@@ -137,20 +137,6 @@ export class MenuResolver implements Resolve<boolean> {
} as TextMenuItemModel,
}
);
menuList.push(
{
id: 'browse_global_by_srsc',
parentID: 'browse_global',
active: false,
visible: true,
index: 99,
model: {
type: MenuItemType.LINK,
text: `menu.section.browse_global_by_srsc`,
link: `/browse/srsc`
} as LinkMenuItemModel
}
);
}
menuList.forEach((menuSection) => this.menuService.addSection(MenuID.PUBLIC, Object.assign(menuSection, {
shouldPersistOnRouteChange: true

View File

@@ -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<NavbarComponent>;
@@ -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: {

View File

@@ -248,7 +248,7 @@ export class VocabularyTreeviewComponent implements OnDestroy, OnInit {
}
/**
* Method called on entry select
* Method called on entry select/deselect
*/
onSelect(item: VocabularyEntryDetail) {
if (!this.selectedItems.includes(item.id)) {

View File

@@ -0,0 +1,15 @@
import { Component } from '@angular/core';
import { BrowseByTaxonomyPageComponent as BaseComponent } from '../../../../../app/browse-by/browse-by-taxonomy-page/browse-by-taxonomy-page.component';
@Component({
selector: 'ds-browse-by-taxonomy-page',
// templateUrl: './browse-by-taxonomy-page.component.html',
templateUrl: '../../../../../app/browse-by/browse-by-taxonomy-page/browse-by-taxonomy-page.component.html',
// styleUrls: ['./browse-by-taxonomy-page.component.scss'],
styleUrls: ['../../../../../app/browse-by/browse-by-taxonomy-page/browse-by-taxonomy-page.component.scss'],
})
/**
* Component for browsing items by metadata in a hierarchical controlled vocabulary
*/
export class BrowseByTaxonomyPageComponent extends BaseComponent {
}

View File

@@ -114,6 +114,7 @@ import { ObjectListComponent } from './app/shared/object-list/object-list.compon
import { BrowseByMetadataPageComponent } from './app/browse-by/browse-by-metadata-page/browse-by-metadata-page.component';
import { BrowseByDatePageComponent } from './app/browse-by/browse-by-date-page/browse-by-date-page.component';
import { BrowseByTitlePageComponent } from './app/browse-by/browse-by-title-page/browse-by-title-page.component';
import { BrowseByTaxonomyPageComponent } from './app/browse-by/browse-by-taxonomy-page/browse-by-taxonomy-page.component';
import {
ExternalSourceEntryImportModalComponent
} from './app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/external-source-entry-import-modal/external-source-entry-import-modal.component';
@@ -200,6 +201,7 @@ const DECLARATIONS = [
BrowseByMetadataPageComponent,
BrowseByDatePageComponent,
BrowseByTitlePageComponent,
BrowseByTaxonomyPageComponent,
ExternalSourceEntryImportModalComponent,
ResultsBackButtonComponent,
DsoEditMetadataComponent,