Merge branch 'main' of github.com:DSpace/dspace-angular; branch '1422-deploy-time-config' of github.com:wwelling/dspace-angular into 1422-deploy-time-config

This commit is contained in:
William Welling
2021-12-22 09:18:14 -06:00
27 changed files with 815 additions and 657 deletions

View File

@@ -12,7 +12,7 @@ import { ActivatedRoute, Params, Router } from '@angular/router';
import { BrowseService } from '../../core/browse/browse.service'; import { BrowseService } from '../../core/browse/browse.service';
import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service'; import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
import { StartsWithType } from '../../shared/starts-with/starts-with-decorator'; import { StartsWithType } from '../../shared/starts-with/starts-with-decorator';
import { BrowseByType, rendersBrowseBy } from '../browse-by-switcher/browse-by-decorator'; import { BrowseByDataType, rendersBrowseBy } from '../browse-by-switcher/browse-by-decorator';
import { environment } from '../../../environments/environment'; import { environment } from '../../../environments/environment';
import { PaginationService } from '../../core/pagination/pagination.service'; import { PaginationService } from '../../core/pagination/pagination.service';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
@@ -29,13 +29,13 @@ import { SortDirection, SortOptions } from '../../core/cache/models/sort-options
* A metadata definition (a.k.a. browse id) is a short term used to describe one or multiple metadata fields. * A metadata definition (a.k.a. browse id) is a short term used to describe one or multiple metadata fields.
* An example would be 'dateissued' for 'dc.date.issued' * An example would be 'dateissued' for 'dc.date.issued'
*/ */
@rendersBrowseBy(BrowseByType.Date) @rendersBrowseBy(BrowseByDataType.Date)
export class BrowseByDatePageComponent extends BrowseByMetadataPageComponent { export class BrowseByDatePageComponent extends BrowseByMetadataPageComponent {
/** /**
* The default metadata-field to use for determining the lower limit of the StartsWith dropdown options * The default metadata keys to use for determining the lower limit of the StartsWith dropdown options
*/ */
defaultMetadataField = 'dc.date.issued'; defaultMetadataKeys = ['dc.date.issued'];
public constructor(protected route: ActivatedRoute, public constructor(protected route: ActivatedRoute,
protected browseService: BrowseService, protected browseService: BrowseService,
@@ -59,13 +59,13 @@ export class BrowseByDatePageComponent extends BrowseByMetadataPageComponent {
return [Object.assign({}, routeParams, queryParams, data), currentPage, currentSort]; return [Object.assign({}, routeParams, queryParams, data), currentPage, currentSort];
}) })
).subscribe(([params, currentPage, currentSort]: [Params, PaginationComponentOptions, SortOptions]) => { ).subscribe(([params, currentPage, currentSort]: [Params, PaginationComponentOptions, SortOptions]) => {
const metadataField = params.metadataField || this.defaultMetadataField; const metadataKeys = params.browseDefinition ? params.browseDefinition.metadataKeys : this.defaultMetadataKeys;
this.browseId = params.id || this.defaultBrowseId; this.browseId = params.id || this.defaultBrowseId;
this.startsWith = +params.startsWith || params.startsWith; this.startsWith = +params.startsWith || params.startsWith;
const searchOptions = browseParamsToOptions(params, currentPage, currentSort, this.browseId); const searchOptions = browseParamsToOptions(params, currentPage, currentSort, this.browseId);
this.updatePageWithItems(searchOptions, this.value); this.updatePageWithItems(searchOptions, this.value);
this.updateParent(params.scope); this.updateParent(params.scope);
this.updateStartsWithOptions(this.browseId, metadataField, params.scope); this.updateStartsWithOptions(this.browseId, metadataKeys, params.scope);
})); }));
} }
@@ -76,15 +76,15 @@ export class BrowseByDatePageComponent extends BrowseByMetadataPageComponent {
* extremely long lists with a one-year difference. * extremely long lists with a one-year difference.
* To determine the change in years, the config found under GlobalConfig.BrowseBy is used for this. * To determine the change in years, the config found under GlobalConfig.BrowseBy is used for this.
* @param definition The metadata definition to fetch the first item for * @param definition The metadata definition to fetch the first item for
* @param metadataField The metadata field to fetch the earliest date from (expects a date field) * @param metadataKeys The metadata fields to fetch the earliest date from (expects a date field)
* @param scope The scope under which to fetch the earliest item for * @param scope The scope under which to fetch the earliest item for
*/ */
updateStartsWithOptions(definition: string, metadataField: string, scope?: string) { updateStartsWithOptions(definition: string, metadataKeys: string[], scope?: string) {
this.subs.push( this.subs.push(
this.browseService.getFirstItemFor(definition, scope).subscribe((firstItemRD: RemoteData<Item>) => { this.browseService.getFirstItemFor(definition, scope).subscribe((firstItemRD: RemoteData<Item>) => {
let lowerLimit = environment.browseBy.defaultLowerLimit; let lowerLimit = environment.browseBy.defaultLowerLimit;
if (hasValue(firstItemRD.payload)) { if (hasValue(firstItemRD.payload)) {
const date = firstItemRD.payload.firstMetadataValue(metadataField); const date = firstItemRD.payload.firstMetadataValue(metadataKeys);
if (hasValue(date)) { if (hasValue(date)) {
const dateObj = new Date(date); const dateObj = new Date(date);
// TODO: it appears that getFullYear (based on local time) is sometimes unreliable. Switching to UTC. // TODO: it appears that getFullYear (based on local time) is sometimes unreliable. Switching to UTC.
@@ -120,5 +120,4 @@ export class BrowseByDatePageComponent extends BrowseByMetadataPageComponent {
}) })
); );
} }
} }

View File

@@ -1,20 +1,25 @@
import { first } from 'rxjs/operators'; import { first } from 'rxjs/operators';
import { BrowseByGuard } from './browse-by-guard'; import { BrowseByGuard } from './browse-by-guard';
import { of as observableOf } from 'rxjs'; import { of as observableOf } from 'rxjs';
import { BrowseDefinitionDataService } from '../core/browse/browse-definition-data.service';
import { createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils';
import { BrowseDefinition } from '../core/shared/browse-definition.model';
import { BrowseByDataType } from './browse-by-switcher/browse-by-decorator';
describe('BrowseByGuard', () => { describe('BrowseByGuard', () => {
describe('canActivate', () => { describe('canActivate', () => {
let guard: BrowseByGuard; let guard: BrowseByGuard;
let dsoService: any; let dsoService: any;
let translateService: any; let translateService: any;
let browseDefinitionService: any;
const name = 'An interesting DSO'; const name = 'An interesting DSO';
const title = 'Author'; const title = 'Author';
const field = 'Author'; const field = 'Author';
const id = 'author'; const id = 'author';
const metadataField = 'dc.contributor';
const scope = '1234-65487-12354-1235'; const scope = '1234-65487-12354-1235';
const value = 'Filter'; const value = 'Filter';
const browseDefinition = Object.assign(new BrowseDefinition(), { type: BrowseByDataType.Metadata, metadataKeys: ['dc.contributor'] });
beforeEach(() => { beforeEach(() => {
dsoService = { dsoService = {
@@ -24,14 +29,19 @@ describe('BrowseByGuard', () => {
translateService = { translateService = {
instant: () => field instant: () => field
}; };
guard = new BrowseByGuard(dsoService, translateService);
browseDefinitionService = {
findById: () => createSuccessfulRemoteDataObject$(browseDefinition)
};
guard = new BrowseByGuard(dsoService, translateService, browseDefinitionService);
}); });
it('should return true, and sets up the data correctly, with a scope and value', () => { it('should return true, and sets up the data correctly, with a scope and value', () => {
const scopedRoute = { const scopedRoute = {
data: { data: {
title: field, title: field,
metadataField, browseDefinition,
}, },
params: { params: {
id, id,
@@ -48,7 +58,7 @@ describe('BrowseByGuard', () => {
const result = { const result = {
title, title,
id, id,
metadataField, browseDefinition,
collection: name, collection: name,
field, field,
value: '"' + value + '"' value: '"' + value + '"'
@@ -63,7 +73,7 @@ describe('BrowseByGuard', () => {
const scopedNoValueRoute = { const scopedNoValueRoute = {
data: { data: {
title: field, title: field,
metadataField, browseDefinition,
}, },
params: { params: {
id, id,
@@ -80,7 +90,7 @@ describe('BrowseByGuard', () => {
const result = { const result = {
title, title,
id, id,
metadataField, browseDefinition,
collection: name, collection: name,
field, field,
value: '' value: ''
@@ -95,7 +105,7 @@ describe('BrowseByGuard', () => {
const route = { const route = {
data: { data: {
title: field, title: field,
metadataField, browseDefinition,
}, },
params: { params: {
id, id,
@@ -111,7 +121,7 @@ describe('BrowseByGuard', () => {
const result = { const result = {
title, title,
id, id,
metadataField, browseDefinition,
collection: '', collection: '',
field, field,
value: '"' + value + '"' value: '"' + value + '"'

View File

@@ -2,11 +2,12 @@ import { ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot } from '@angul
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { DSpaceObjectDataService } from '../core/data/dspace-object-data.service'; import { DSpaceObjectDataService } from '../core/data/dspace-object-data.service';
import { hasNoValue, hasValue } from '../shared/empty.util'; import { hasNoValue, hasValue } from '../shared/empty.util';
import { map } from 'rxjs/operators'; import { map, switchMap } from 'rxjs/operators';
import { getFirstSucceededRemoteData } from '../core/shared/operators'; import { getFirstSucceededRemoteData, getFirstSucceededRemoteDataPayload } from '../core/shared/operators';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { of as observableOf } from 'rxjs'; import { Observable, of as observableOf } from 'rxjs';
import { environment } from '../../environments/environment'; import { BrowseDefinitionDataService } from '../core/browse/browse-definition-data.service';
import { BrowseDefinition } from '../core/shared/browse-definition.model';
@Injectable() @Injectable()
/** /**
@@ -15,42 +16,46 @@ import { environment } from '../../environments/environment';
export class BrowseByGuard implements CanActivate { export class BrowseByGuard implements CanActivate {
constructor(protected dsoService: DSpaceObjectDataService, constructor(protected dsoService: DSpaceObjectDataService,
protected translate: TranslateService) { protected translate: TranslateService,
protected browseDefinitionService: BrowseDefinitionDataService) {
} }
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) { canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
const title = route.data.title; const title = route.data.title;
const id = route.params.id || route.queryParams.id || route.data.id; const id = route.params.id || route.queryParams.id || route.data.id;
let metadataField = route.data.metadataField; let browseDefinition$: Observable<BrowseDefinition>;
if (hasNoValue(metadataField) && hasValue(id)) { if (hasNoValue(route.data.browseDefinition) && hasValue(id)) {
const config = environment.browseBy.types.find((conf) => conf.id === id); browseDefinition$ = this.browseDefinitionService.findById(id).pipe(getFirstSucceededRemoteDataPayload());
if (hasValue(config) && hasValue(config.metadataField)) { } else {
metadataField = config.metadataField; browseDefinition$ = observableOf(route.data.browseDefinition);
}
} }
const scope = route.queryParams.scope; const scope = route.queryParams.scope;
const value = route.queryParams.value; const value = route.queryParams.value;
const metadataTranslated = this.translate.instant('browse.metadata.' + id); const metadataTranslated = this.translate.instant('browse.metadata.' + id);
if (hasValue(scope)) { return browseDefinition$.pipe(
const dsoAndMetadata$ = this.dsoService.findById(scope).pipe(getFirstSucceededRemoteData()); switchMap((browseDefinition) => {
return dsoAndMetadata$.pipe( if (hasValue(scope)) {
map((dsoRD) => { const dsoAndMetadata$ = this.dsoService.findById(scope).pipe(getFirstSucceededRemoteData());
const name = dsoRD.payload.name; return dsoAndMetadata$.pipe(
route.data = this.createData(title, id, metadataField, name, metadataTranslated, value, route); map((dsoRD) => {
return true; const name = dsoRD.payload.name;
}) route.data = this.createData(title, id, browseDefinition, name, metadataTranslated, value, route);
); return true;
} else { })
route.data = this.createData(title, id, metadataField, '', metadataTranslated, value, route); );
return observableOf(true); } else {
} route.data = this.createData(title, id, browseDefinition, '', metadataTranslated, value, route);
return observableOf(true);
}
})
);
} }
private createData(title, id, metadataField, collection, field, value, route) { private createData(title, id, browseDefinition, collection, field, value, route) {
return Object.assign({}, route.data, { return Object.assign({}, route.data, {
title: title, title: title,
id: id, id: id,
metadataField: metadataField, browseDefinition: browseDefinition,
collection: collection, collection: collection,
field: field, field: field,
value: hasValue(value) ? `"${value}"` : '' value: hasValue(value) ? `"${value}"` : ''

View File

@@ -14,7 +14,7 @@ import { getFirstSucceededRemoteData } from '../../core/shared/operators';
import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service'; import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
import { DSpaceObject } from '../../core/shared/dspace-object.model'; import { DSpaceObject } from '../../core/shared/dspace-object.model';
import { StartsWithType } from '../../shared/starts-with/starts-with-decorator'; import { StartsWithType } from '../../shared/starts-with/starts-with-decorator';
import { BrowseByType, rendersBrowseBy } from '../browse-by-switcher/browse-by-decorator'; import { BrowseByDataType, rendersBrowseBy } from '../browse-by-switcher/browse-by-decorator';
import { PaginationService } from '../../core/pagination/pagination.service'; import { PaginationService } from '../../core/pagination/pagination.service';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
@@ -28,7 +28,7 @@ import { map } from 'rxjs/operators';
* A metadata definition (a.k.a. browse id) is a short term used to describe one or multiple metadata fields. * A metadata definition (a.k.a. browse id) is a short term used to describe one or multiple metadata fields.
* An example would be 'author' for 'dc.contributor.*' * An example would be 'author' for 'dc.contributor.*'
*/ */
@rendersBrowseBy(BrowseByType.Metadata) @rendersBrowseBy(BrowseByDataType.Metadata)
export class BrowseByMetadataPageComponent implements OnInit { export class BrowseByMetadataPageComponent implements OnInit {
/** /**

View File

@@ -1,9 +1,9 @@
import { BrowseByType, rendersBrowseBy } from './browse-by-decorator'; import { BrowseByDataType, rendersBrowseBy } from './browse-by-decorator';
describe('BrowseByDecorator', () => { describe('BrowseByDecorator', () => {
const titleDecorator = rendersBrowseBy(BrowseByType.Title); const titleDecorator = rendersBrowseBy(BrowseByDataType.Title);
const dateDecorator = rendersBrowseBy(BrowseByType.Date); const dateDecorator = rendersBrowseBy(BrowseByDataType.Date);
const metadataDecorator = rendersBrowseBy(BrowseByType.Metadata); const metadataDecorator = rendersBrowseBy(BrowseByDataType.Metadata);
it('should have a decorator for all types', () => { it('should have a decorator for all types', () => {
expect(titleDecorator.length).not.toEqual(0); expect(titleDecorator.length).not.toEqual(0);
expect(dateDecorator.length).not.toEqual(0); expect(dateDecorator.length).not.toEqual(0);

View File

@@ -2,13 +2,13 @@ import { hasNoValue } from '../../shared/empty.util';
import { InjectionToken } from '@angular/core'; import { InjectionToken } from '@angular/core';
import { GenericConstructor } from '../../core/shared/generic-constructor'; import { GenericConstructor } from '../../core/shared/generic-constructor';
export enum BrowseByType { export enum BrowseByDataType {
Title = 'title', Title = 'title',
Metadata = 'metadata', Metadata = 'text',
Date = 'date' Date = 'date'
} }
export const DEFAULT_BROWSE_BY_TYPE = BrowseByType.Metadata; export const DEFAULT_BROWSE_BY_TYPE = BrowseByDataType.Metadata;
export const BROWSE_BY_COMPONENT_FACTORY = new InjectionToken<(browseByType) => GenericConstructor<any>>('getComponentByBrowseByType', { export const BROWSE_BY_COMPONENT_FACTORY = new InjectionToken<(browseByType) => GenericConstructor<any>>('getComponentByBrowseByType', {
providedIn: 'root', providedIn: 'root',
@@ -21,7 +21,7 @@ const map = new Map();
* Decorator used for rendering Browse-By pages by type * Decorator used for rendering Browse-By pages by type
* @param browseByType The type of page * @param browseByType The type of page
*/ */
export function rendersBrowseBy(browseByType: BrowseByType) { export function rendersBrowseBy(browseByType: BrowseByDataType) {
return function decorator(component: any) { return function decorator(component: any) {
if (hasNoValue(map.get(browseByType))) { if (hasNoValue(map.get(browseByType))) {
map.set(browseByType, component); map.set(browseByType, component);

View File

@@ -2,20 +2,46 @@ import { BrowseBySwitcherComponent } from './browse-by-switcher.component';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { NO_ERRORS_SCHEMA } from '@angular/core'; import { NO_ERRORS_SCHEMA } from '@angular/core';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
import { BehaviorSubject } from 'rxjs'; import { BROWSE_BY_COMPONENT_FACTORY, BrowseByDataType } from './browse-by-decorator';
import { environment } from '../../../environments/environment'; import { BrowseDefinition } from '../../core/shared/browse-definition.model';
import { BROWSE_BY_COMPONENT_FACTORY } from './browse-by-decorator'; import { BehaviorSubject, of as observableOf } from 'rxjs';
describe('BrowseBySwitcherComponent', () => { describe('BrowseBySwitcherComponent', () => {
let comp: BrowseBySwitcherComponent; let comp: BrowseBySwitcherComponent;
let fixture: ComponentFixture<BrowseBySwitcherComponent>; let fixture: ComponentFixture<BrowseBySwitcherComponent>;
const types = environment.browseBy.types; const types = [
Object.assign(
new BrowseDefinition(), {
id: 'title',
dataType: BrowseByDataType.Title,
}
),
Object.assign(
new BrowseDefinition(), {
id: 'dateissued',
dataType: BrowseByDataType.Date,
metadataKeys: ['dc.date.issued']
}
),
Object.assign(
new BrowseDefinition(), {
id: 'author',
dataType: BrowseByDataType.Metadata,
}
),
Object.assign(
new BrowseDefinition(), {
id: 'subject',
dataType: BrowseByDataType.Metadata,
}
),
];
const params = new BehaviorSubject(createParamsWithId('initialValue')); const data = new BehaviorSubject(createDataWithBrowseDefinition(new BrowseDefinition()));
const activatedRouteStub = { const activatedRouteStub = {
params: params data
}; };
beforeEach(waitForAsync(() => { beforeEach(waitForAsync(() => {
@@ -34,20 +60,20 @@ describe('BrowseBySwitcherComponent', () => {
comp = fixture.componentInstance; comp = fixture.componentInstance;
})); }));
types.forEach((type) => { types.forEach((type: BrowseDefinition) => {
describe(`when switching to a browse-by page for "${type.id}"`, () => { describe(`when switching to a browse-by page for "${type.id}"`, () => {
beforeEach(() => { beforeEach(() => {
params.next(createParamsWithId(type.id)); data.next(createDataWithBrowseDefinition(type));
fixture.detectChanges(); fixture.detectChanges();
}); });
it(`should call getComponentByBrowseByType with type "${type.type}"`, () => { it(`should call getComponentByBrowseByType with type "${type.dataType}"`, () => {
expect((comp as any).getComponentByBrowseByType).toHaveBeenCalledWith(type.type); expect((comp as any).getComponentByBrowseByType).toHaveBeenCalledWith(type.dataType);
}); });
}); });
}); });
}); });
export function createParamsWithId(id) { export function createDataWithBrowseDefinition(browseDefinition) {
return { id: id }; return { browseDefinition: browseDefinition };
} }

View File

@@ -1,11 +1,10 @@
import { Component, Inject, OnInit } from '@angular/core'; import { Component, Inject, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { BrowseByTypeConfig } from '../../../config/browse-by-type-config.interface';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
import { BROWSE_BY_COMPONENT_FACTORY } from './browse-by-decorator'; import { BROWSE_BY_COMPONENT_FACTORY } from './browse-by-decorator';
import { environment } from '../../../environments/environment';
import { GenericConstructor } from '../../core/shared/generic-constructor'; import { GenericConstructor } from '../../core/shared/generic-constructor';
import { BrowseDefinition } from '../../core/shared/browse-definition.model';
@Component({ @Component({
selector: 'ds-browse-by-switcher', selector: 'ds-browse-by-switcher',
@@ -26,15 +25,11 @@ export class BrowseBySwitcherComponent implements OnInit {
} }
/** /**
* Fetch the correct browse-by component by using the relevant config from environment.js * Fetch the correct browse-by component by using the relevant config from the route data
*/ */
ngOnInit(): void { ngOnInit(): void {
this.browseByComponent = this.route.params.pipe( this.browseByComponent = this.route.data.pipe(
map((params) => { map((data: { browseDefinition: BrowseDefinition }) => this.getComponentByBrowseByType(data.browseDefinition.dataType))
const id = params.id;
return environment.browseBy.types.find((config: BrowseByTypeConfig) => config.id === id);
}),
map((config: BrowseByTypeConfig) => this.getComponentByBrowseByType(config.type))
); );
} }

View File

@@ -10,7 +10,7 @@ import { BrowseEntrySearchOptions } from '../../core/browse/browse-entry-search-
import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service'; import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
import { BrowseService } from '../../core/browse/browse.service'; import { BrowseService } from '../../core/browse/browse.service';
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model'; import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
import { BrowseByType, rendersBrowseBy } from '../browse-by-switcher/browse-by-decorator'; import { BrowseByDataType, rendersBrowseBy } from '../browse-by-switcher/browse-by-decorator';
import { PaginationService } from '../../core/pagination/pagination.service'; import { PaginationService } from '../../core/pagination/pagination.service';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
@@ -23,7 +23,7 @@ import { PaginationComponentOptions } from '../../shared/pagination/pagination-c
/** /**
* Component for browsing items by title (dc.title) * Component for browsing items by title (dc.title)
*/ */
@rendersBrowseBy(BrowseByType.Title) @rendersBrowseBy(BrowseByDataType.Title)
export class BrowseByTitlePageComponent extends BrowseByMetadataPageComponent { export class BrowseByTitlePageComponent extends BrowseByMetadataPageComponent {
public constructor(protected route: ActivatedRoute, public constructor(protected route: ActivatedRoute,

View File

@@ -9,9 +9,11 @@ describe(`BrowseDefinitionDataService`, () => {
findAll: EMPTY, findAll: EMPTY,
findByHref: EMPTY, findByHref: EMPTY,
findAllByHref: EMPTY, findAllByHref: EMPTY,
findById: EMPTY,
}); });
const hrefAll = 'https://rest.api/server/api/discover/browses'; const hrefAll = 'https://rest.api/server/api/discover/browses';
const hrefSingle = 'https://rest.api/server/api/discover/browses/author'; const hrefSingle = 'https://rest.api/server/api/discover/browses/author';
const id = 'author';
const options = new FindListOptions(); const options = new FindListOptions();
const linksToFollow = [ const linksToFollow = [
followLink('entries'), followLink('entries'),
@@ -44,4 +46,10 @@ describe(`BrowseDefinitionDataService`, () => {
}); });
}); });
describe(`findById`, () => {
it(`should call findById on DataServiceImpl`, () => {
service.findAllByHref(id, options, true, false, ...linksToFollow);
expect(dataServiceImplSpy.findAllByHref).toHaveBeenCalledWith(id, options, true, false, ...linksToFollow);
});
});
}); });

View File

@@ -106,6 +106,21 @@ export class BrowseDefinitionDataService {
findAllByHref(href: string, findListOptions: FindListOptions = {}, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<BrowseDefinition>[]): Observable<RemoteData<PaginatedList<BrowseDefinition>>> { findAllByHref(href: string, findListOptions: FindListOptions = {}, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<BrowseDefinition>[]): Observable<RemoteData<PaginatedList<BrowseDefinition>>> {
return this.dataService.findAllByHref(href, findListOptions, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow); return this.dataService.findAllByHref(href, findListOptions, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
} }
/**
* Returns an observable of {@link RemoteData} of an object, based on its ID, with a list of
* {@link FollowLinkConfig}, to automatically resolve {@link HALLink}s of the object
* @param id ID of object we want to retrieve
* @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's
* 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 linksToFollow List of {@link FollowLinkConfig} that indicate which
* {@link HALLink}s should be automatically resolved
*/
findById(id: string, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<BrowseDefinition>[]): Observable<RemoteData<BrowseDefinition>> {
return this.dataService.findById(id, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
}
} }
/* tslint:enable:max-classes-per-file */ /* tslint:enable:max-classes-per-file */

View File

@@ -6,6 +6,7 @@ import { BROWSE_DEFINITION } from './browse-definition.resource-type';
import { HALLink } from './hal-link.model'; import { HALLink } from './hal-link.model';
import { ResourceType } from './resource-type'; import { ResourceType } from './resource-type';
import { SortOption } from './sort-option.model'; import { SortOption } from './sort-option.model';
import { BrowseByDataType } from '../../browse-by/browse-by-switcher/browse-by-decorator';
@typedObject @typedObject
export class BrowseDefinition extends CacheableObject { export class BrowseDefinition extends CacheableObject {
@@ -33,6 +34,9 @@ export class BrowseDefinition extends CacheableObject {
@autoserializeAs('metadata') @autoserializeAs('metadata')
metadataKeys: string[]; metadataKeys: string[];
@autoserialize
dataType: BrowseByDataType;
get self(): string { get self(): string {
return this._links.self.href; return this._links.self.href;
} }

View File

@@ -13,15 +13,48 @@ import { MenuService } from '../shared/menu/menu.service';
import { MenuServiceStub } from '../shared/testing/menu-service.stub'; import { MenuServiceStub } from '../shared/testing/menu-service.stub';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
import { RouterTestingModule } from '@angular/router/testing'; import { RouterTestingModule } from '@angular/router/testing';
import { BrowseService } from '../core/browse/browse.service';
import { 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';
let comp: NavbarComponent; let comp: NavbarComponent;
let fixture: ComponentFixture<NavbarComponent>; let fixture: ComponentFixture<NavbarComponent>;
describe('NavbarComponent', () => { describe('NavbarComponent', () => {
const menuService = new MenuServiceStub(); const menuService = new MenuServiceStub();
let browseDefinitions;
// waitForAsync beforeEach // waitForAsync beforeEach
beforeEach(waitForAsync(() => { beforeEach(waitForAsync(() => {
browseDefinitions = [
Object.assign(
new BrowseDefinition(), {
id: 'title',
dataType: BrowseByDataType.Title,
}
),
Object.assign(
new BrowseDefinition(), {
id: 'dateissued',
dataType: BrowseByDataType.Date,
metadataKeys: ['dc.date.issued']
}
),
Object.assign(
new BrowseDefinition(), {
id: 'author',
dataType: BrowseByDataType.Metadata,
}
),
Object.assign(
new BrowseDefinition(), {
id: 'subject',
dataType: BrowseByDataType.Metadata,
}
),
];
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [ imports: [
TranslateModule.forRoot(), TranslateModule.forRoot(),
@@ -33,7 +66,8 @@ describe('NavbarComponent', () => {
Injector, Injector,
{ provide: MenuService, useValue: menuService }, { provide: MenuService, useValue: menuService },
{ provide: HostWindowService, useValue: new HostWindowServiceStub(800) }, { provide: HostWindowService, useValue: new HostWindowServiceStub(800) },
{ provide: ActivatedRoute, useValue: {} } { provide: ActivatedRoute, useValue: {} },
{ provide: BrowseService, useValue: { getBrowseDefinitions: createSuccessfulRemoteDataObject$(buildPaginatedList(undefined, browseDefinitions)) } }
], ],
schemas: [NO_ERRORS_SCHEMA] schemas: [NO_ERRORS_SCHEMA]
}) })

View File

@@ -6,7 +6,11 @@ import { MenuID, MenuItemType } from '../shared/menu/initial-menus-state';
import { TextMenuItemModel } from '../shared/menu/menu-item/models/text.model'; import { TextMenuItemModel } from '../shared/menu/menu-item/models/text.model';
import { LinkMenuItemModel } from '../shared/menu/menu-item/models/link.model'; import { LinkMenuItemModel } from '../shared/menu/menu-item/models/link.model';
import { HostWindowService } from '../shared/host-window.service'; import { HostWindowService } from '../shared/host-window.service';
import { environment } from '../../environments/environment'; import { BrowseService } from '../core/browse/browse.service';
import { getFirstCompletedRemoteData } from '../core/shared/operators';
import { PaginatedList } from '../core/data/paginated-list.model';
import { BrowseDefinition } from '../core/shared/browse-definition.model';
import { RemoteData } from '../core/data/remote-data';
/** /**
* Component representing the public navbar * Component representing the public navbar
@@ -26,7 +30,8 @@ export class NavbarComponent extends MenuComponent {
constructor(protected menuService: MenuService, constructor(protected menuService: MenuService,
protected injector: Injector, protected injector: Injector,
public windowService: HostWindowService public windowService: HostWindowService,
public browseService: BrowseService
) { ) {
super(menuService, injector); super(menuService, injector);
} }
@@ -52,37 +57,44 @@ export class NavbarComponent extends MenuComponent {
text: `menu.section.browse_global_communities_and_collections`, text: `menu.section.browse_global_communities_and_collections`,
link: `/community-list` link: `/community-list`
} as LinkMenuItemModel } as LinkMenuItemModel
}, }
/* News */
{
id: 'browse_global',
active: false,
visible: true,
index: 1,
model: {
type: MenuItemType.TEXT,
text: 'menu.section.browse_global'
} as TextMenuItemModel,
},
]; ];
// Read the different Browse-By types from config and add them to the browse menu // Read the different Browse-By types from config and add them to the browse menu
const types = environment.browseBy.types; this.browseService.getBrowseDefinitions()
types.forEach((typeConfig) => { .pipe(getFirstCompletedRemoteData<PaginatedList<BrowseDefinition>>())
menuList.push({ .subscribe((browseDefListRD: RemoteData<PaginatedList<BrowseDefinition>>) => {
id: `browse_global_by_${typeConfig.id}`, if (browseDefListRD.hasSucceeded) {
parentID: 'browse_global', browseDefListRD.payload.page.forEach((browseDef: BrowseDefinition) => {
active: false, menuList.push({
visible: true, id: `browse_global_by_${browseDef.id}`,
model: { parentID: 'browse_global',
type: MenuItemType.LINK, active: false,
text: `menu.section.browse_global_by_${typeConfig.id}`, visible: true,
link: `/browse/${typeConfig.id}` model: {
} as LinkMenuItemModel type: MenuItemType.LINK,
text: `menu.section.browse_global_by_${browseDef.id}`,
link: `/browse/${browseDef.id}`
} as LinkMenuItemModel
});
});
menuList.push(
/* Browse */
{
id: 'browse_global',
active: false,
visible: true,
index: 1,
model: {
type: MenuItemType.TEXT,
text: 'menu.section.browse_global'
} as TextMenuItemModel,
}
);
}
menuList.forEach((menuSection) => this.menuService.addSection(this.menuID, Object.assign(menuSection, {
shouldPersistOnRouteChange: true
})));
}); });
});
menuList.forEach((menuSection) => this.menuService.addSection(this.menuID, Object.assign(menuSection, {
shouldPersistOnRouteChange: true
})));
} }
} }

View File

@@ -2,10 +2,13 @@ import { Component, Input, OnInit } from '@angular/core';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
import { ActivatedRoute, Params, Router } from '@angular/router'; import { ActivatedRoute, Params, Router } from '@angular/router';
import { BrowseByTypeConfig } from '../../../config/browse-by-type-config.interface';
import { environment } from '../../../environments/environment';
import { getCommunityPageRoute } from '../../community-page/community-page-routing-paths'; import { getCommunityPageRoute } from '../../community-page/community-page-routing-paths';
import { getCollectionPageRoute } from '../../collection-page/collection-page-routing-paths'; import { getCollectionPageRoute } from '../../collection-page/collection-page-routing-paths';
import { getFirstCompletedRemoteData } from '../../core/shared/operators';
import { PaginatedList } from '../../core/data/paginated-list.model';
import { BrowseDefinition } from '../../core/shared/browse-definition.model';
import { RemoteData } from '../../core/data/remote-data';
import { BrowseService } from '../../core/browse/browse.service';
export interface ComColPageNavOption { export interface ComColPageNavOption {
id: string; id: string;
@@ -29,10 +32,6 @@ export class ComcolPageBrowseByComponent implements OnInit {
*/ */
@Input() id: string; @Input() id: string;
@Input() contentType: string; @Input() contentType: string;
/**
* List of currently active browse configurations
*/
types: BrowseByTypeConfig[];
allOptions: ComColPageNavOption[]; allOptions: ComColPageNavOption[];
@@ -40,31 +39,39 @@ export class ComcolPageBrowseByComponent implements OnInit {
constructor( constructor(
private route: ActivatedRoute, private route: ActivatedRoute,
private router: Router) { private router: Router,
private browseService: BrowseService
) {
} }
ngOnInit(): void { ngOnInit(): void {
this.allOptions = environment.browseBy.types this.browseService.getBrowseDefinitions()
.map((config: BrowseByTypeConfig) => ({ .pipe(getFirstCompletedRemoteData<PaginatedList<BrowseDefinition>>())
id: config.id, .subscribe((browseDefListRD: RemoteData<PaginatedList<BrowseDefinition>>) => {
label: `browse.comcol.by.${config.id}`, if (browseDefListRD.hasSucceeded) {
routerLink: `/browse/${config.id}`, this.allOptions = browseDefListRD.payload.page
params: { scope: this.id } .map((config: BrowseDefinition) => ({
})); id: config.id,
label: `browse.comcol.by.${config.id}`,
routerLink: `/browse/${config.id}`,
params: { scope: this.id }
}));
if (this.contentType === 'collection') { if (this.contentType === 'collection') {
this.allOptions = [ { this.allOptions = [{
id: this.id, id: this.id,
label: 'collection.page.browse.recent.head', label: 'collection.page.browse.recent.head',
routerLink: getCollectionPageRoute(this.id) routerLink: getCollectionPageRoute(this.id)
}, ...this.allOptions ]; }, ...this.allOptions];
} else if (this.contentType === 'community') { } else if (this.contentType === 'community') {
this.allOptions = [{ this.allOptions = [{
id: this.id, id: this.id,
label: 'community.all-lists.head', label: 'community.all-lists.head',
routerLink: getCommunityPageRoute(this.id) routerLink: getCommunityPageRoute(this.id)
}, ...this.allOptions ]; }, ...this.allOptions];
} }
}
});
this.currentOptionId$ = this.route.params.pipe( this.currentOptionId$ = this.route.params.pipe(
map((params: Params) => params.id) map((params: Params) => params.id)

View File

@@ -1,9 +1,21 @@
<div> <div>
<ds-form *ngIf="formModel" <div class="modal-header">
#formRef="formComponent" <h4 class="modal-title">{{'submission.sections.upload.edit.title' | translate}}</h4>
[formId]="formId" <button type="button" class="close" (click)="onModalClose()" aria-label="Close" [disabled]="isSaving">
[formModel]="formModel" <span aria-hidden="true">×</span>
[displaySubmit]="false" </button>
[displayCancel]="false" </div>
(dfChange)="onChange($event)"></ds-form> <div class="modal-body">
<ds-form *ngIf="formModel"
#formRef="formComponent"
[formId]="formId"
[formModel]="formModel"
[displaySubmit]="!isSaving"
[displayCancel]="!isSaving"
(submitForm)="onSubmit()"
(cancel)="onModalClose()"
(dfChange)="onChange($event)"></ds-form>
</div>
</div> </div>

View File

@@ -1,5 +1,5 @@
import { ChangeDetectorRef, Component, NO_ERRORS_SCHEMA } from '@angular/core'; import { ChangeDetectorRef, Component, NO_ERRORS_SCHEMA } from '@angular/core';
import { waitForAsync, ComponentFixture, inject, TestBed } from '@angular/core/testing'; import { waitForAsync, ComponentFixture, inject, TestBed, fakeAsync, tick } from '@angular/core/testing';
import { BrowserModule } from '@angular/platform-browser'; import { BrowserModule } from '@angular/platform-browser';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
@@ -17,19 +17,37 @@ import { SubmissionService } from '../../../../submission.service';
import { SubmissionSectionUploadFileEditComponent } from './section-upload-file-edit.component'; import { SubmissionSectionUploadFileEditComponent } from './section-upload-file-edit.component';
import { POLICY_DEFAULT_WITH_LIST } from '../../section-upload.component'; import { POLICY_DEFAULT_WITH_LIST } from '../../section-upload.component';
import { import {
mockGroup,
mockSubmissionCollectionId, mockSubmissionCollectionId,
mockSubmissionId, mockSubmissionId,
mockUploadConfigResponse, mockUploadConfigResponse,
mockUploadConfigResponseMetadata, mockUploadConfigResponseMetadata,
mockUploadFiles mockUploadFiles,
mockFileFormData,
mockSubmissionObject,
} from '../../../../../shared/mocks/submission.mock'; } from '../../../../../shared/mocks/submission.mock';
import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { FormComponent } from '../../../../../shared/form/form.component'; import { FormComponent } from '../../../../../shared/form/form.component';
import { FormService } from '../../../../../shared/form/form.service'; import { FormService } from '../../../../../shared/form/form.service';
import { getMockFormService } from '../../../../../shared/mocks/form-service.mock'; import { getMockFormService } from '../../../../../shared/mocks/form-service.mock';
import { Group } from '../../../../../core/eperson/models/group.model';
import { createTestComponent } from '../../../../../shared/testing/utils.test'; import { createTestComponent } from '../../../../../shared/testing/utils.test';
import { NgbActiveModal, NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { JsonPatchOperationsBuilder } from '../../../../../core/json-patch/builder/json-patch-operations-builder';
import { SubmissionJsonPatchOperationsServiceStub } from '../../../../../shared/testing/submission-json-patch-operations-service.stub';
import { SubmissionJsonPatchOperationsService } from '../../../../../core/submission/submission-json-patch-operations.service';
import { SectionUploadService } from '../../section-upload.service';
import { getMockSectionUploadService } from '../../../../../shared/mocks/section-upload.service.mock';
import { FormFieldMetadataValueObject } from '../../../../../shared/form/builder/models/form-field-metadata-value.model';
import { JsonPatchOperationPathCombiner } from '../../../../../core/json-patch/builder/json-patch-operation-path-combiner';
import { dateToISOFormat } from '../../../../../shared/date.util';
import { of } from 'rxjs';
const jsonPatchOpBuilder: any = jasmine.createSpyObj('jsonPatchOpBuilder', {
add: jasmine.createSpy('add'),
replace: jasmine.createSpy('replace'),
remove: jasmine.createSpy('remove'),
});
const formMetadataMock = ['dc.title', 'dc.description'];
describe('SubmissionSectionUploadFileEditComponent test suite', () => { describe('SubmissionSectionUploadFileEditComponent test suite', () => {
@@ -38,7 +56,12 @@ describe('SubmissionSectionUploadFileEditComponent test suite', () => {
let fixture: ComponentFixture<SubmissionSectionUploadFileEditComponent>; let fixture: ComponentFixture<SubmissionSectionUploadFileEditComponent>;
let submissionServiceStub: SubmissionServiceStub; let submissionServiceStub: SubmissionServiceStub;
let formbuilderService: any; let formbuilderService: any;
let operationsBuilder: any;
let operationsService: any;
let formService: any;
let uploadService: any;
const submissionJsonPatchOperationsServiceStub = new SubmissionJsonPatchOperationsServiceStub();
const submissionId = mockSubmissionId; const submissionId = mockSubmissionId;
const sectionId = 'upload'; const sectionId = 'upload';
const collectionId = mockSubmissionCollectionId; const collectionId = mockSubmissionCollectionId;
@@ -48,6 +71,7 @@ describe('SubmissionSectionUploadFileEditComponent test suite', () => {
const fileIndex = '0'; const fileIndex = '0';
const fileId = '123456-test-upload'; const fileId = '123456-test-upload';
const fileData: any = mockUploadFiles[0]; const fileData: any = mockUploadFiles[0];
const pathCombiner = new JsonPatchOperationPathCombiner('sections', sectionId, 'files', fileIndex);
beforeEach(waitForAsync(() => { beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
@@ -66,9 +90,15 @@ describe('SubmissionSectionUploadFileEditComponent test suite', () => {
providers: [ providers: [
{ provide: FormService, useValue: getMockFormService() }, { provide: FormService, useValue: getMockFormService() },
{ provide: SubmissionService, useClass: SubmissionServiceStub }, { provide: SubmissionService, useClass: SubmissionServiceStub },
{ provide: SubmissionJsonPatchOperationsService, useValue: submissionJsonPatchOperationsServiceStub },
{ provide: JsonPatchOperationsBuilder, useValue: jsonPatchOpBuilder },
{ provide: SectionUploadService, useValue: getMockSectionUploadService() },
FormBuilderService, FormBuilderService,
ChangeDetectorRef, ChangeDetectorRef,
SubmissionSectionUploadFileEditComponent SubmissionSectionUploadFileEditComponent,
NgbModal,
NgbActiveModal,
FormComponent,
], ],
schemas: [NO_ERRORS_SCHEMA] schemas: [NO_ERRORS_SCHEMA]
}).compileComponents().then(); }).compileComponents().then();
@@ -114,6 +144,10 @@ describe('SubmissionSectionUploadFileEditComponent test suite', () => {
compAsAny = comp; compAsAny = comp;
submissionServiceStub = TestBed.inject(SubmissionService as any); submissionServiceStub = TestBed.inject(SubmissionService as any);
formbuilderService = TestBed.inject(FormBuilderService); formbuilderService = TestBed.inject(FormBuilderService);
operationsBuilder = TestBed.inject(JsonPatchOperationsBuilder);
operationsService = TestBed.inject(SubmissionJsonPatchOperationsService);
formService = TestBed.inject(FormService);
uploadService = TestBed.inject(SectionUploadService);
comp.submissionId = submissionId; comp.submissionId = submissionId;
comp.collectionId = collectionId; comp.collectionId = collectionId;
@@ -123,6 +157,9 @@ describe('SubmissionSectionUploadFileEditComponent test suite', () => {
comp.fileIndex = fileIndex; comp.fileIndex = fileIndex;
comp.fileId = fileId; comp.fileId = fileId;
comp.configMetadataForm = configMetadataForm; comp.configMetadataForm = configMetadataForm;
comp.formMetadata = formMetadataMock;
formService.isValid.and.returnValue(of(true));
}); });
afterEach(() => { afterEach(() => {
@@ -135,7 +172,7 @@ describe('SubmissionSectionUploadFileEditComponent test suite', () => {
comp.fileData = fileData; comp.fileData = fileData;
comp.formId = 'testFileForm'; comp.formId = 'testFileForm';
comp.ngOnChanges(); comp.ngOnInit();
expect(comp.formModel).toBeDefined(); expect(comp.formModel).toBeDefined();
expect(comp.formModel.length).toBe(2); expect(comp.formModel.length).toBe(2);
@@ -165,7 +202,7 @@ describe('SubmissionSectionUploadFileEditComponent test suite', () => {
comp.fileData = fileData; comp.fileData = fileData;
comp.formId = 'testFileForm'; comp.formId = 'testFileForm';
comp.ngOnChanges(); comp.ngOnInit();
const model: DynamicSelectModel<string> = formbuilderService.findById('name', comp.formModel, 0); const model: DynamicSelectModel<string> = formbuilderService.findById('name', comp.formModel, 0);
const formGroup = formbuilderService.createFormGroup(comp.formModel); const formGroup = formbuilderService.createFormGroup(comp.formModel);
@@ -186,6 +223,82 @@ describe('SubmissionSectionUploadFileEditComponent test suite', () => {
comp.setOptions(model, control); comp.setOptions(model, control);
expect(formbuilderService.findById).toHaveBeenCalledWith('startDate', (model.parent as DynamicFormArrayGroupModel).group); expect(formbuilderService.findById).toHaveBeenCalledWith('startDate', (model.parent as DynamicFormArrayGroupModel).group);
}); });
it('should retrieve Value From Field properly', () => {
let field;
expect(compAsAny.retrieveValueFromField(field)).toBeUndefined();
field = new FormFieldMetadataValueObject('test');
expect(compAsAny.retrieveValueFromField(field)).toBe('test');
field = [new FormFieldMetadataValueObject('test')];
expect(compAsAny.retrieveValueFromField(field)).toBe('test');
});
it('should save Bitstream File data properly when form is valid', fakeAsync(() => {
compAsAny.formRef = {formGroup: null};
compAsAny.fileData = fileData;
compAsAny.pathCombiner = pathCombiner;
formService.validateAllFormFields.and.callFake(() => null);
formService.isValid.and.returnValue(of(true));
formService.getFormData.and.returnValue(of(mockFileFormData));
const response = [
Object.assign(mockSubmissionObject, {
sections: {
upload: {
files: mockUploadFiles
}
}
})
];
operationsService.jsonPatchByResourceID.and.returnValue(of(response));
const accessConditionsToSave = [
{ name: 'openaccess' },
{ name: 'lease', endDate: dateToISOFormat('2019-01-16T00:00:00Z') },
{ name: 'embargo', startDate: dateToISOFormat('2019-01-16T00:00:00Z') },
];
comp.saveBitstreamData();
tick();
let path = 'metadata/dc.title';
expect(operationsBuilder.add).toHaveBeenCalledWith(
pathCombiner.getPath(path),
mockFileFormData.metadata['dc.title'],
true
);
path = 'metadata/dc.description';
expect(operationsBuilder.add).toHaveBeenCalledWith(
pathCombiner.getPath(path),
mockFileFormData.metadata['dc.description'],
true
);
path = 'accessConditions';
expect(operationsBuilder.add).toHaveBeenCalledWith(
pathCombiner.getPath(path),
accessConditionsToSave,
true
);
expect(uploadService.updateFileData).toHaveBeenCalledWith(submissionId, sectionId, mockUploadFiles[0].uuid, mockUploadFiles[0]);
}));
it('should not save Bitstream File data properly when form is not valid', fakeAsync(() => {
compAsAny.formRef = {formGroup: null};
compAsAny.pathCombiner = pathCombiner;
formService.validateAllFormFields.and.callFake(() => null);
formService.isValid.and.returnValue(of(false));
comp.saveBitstreamData();
tick();
expect(uploadService.updateFileData).not.toHaveBeenCalled();
}));
}); });
}); });

View File

@@ -1,4 +1,4 @@
import { ChangeDetectorRef, Component, Input, OnChanges, ViewChild } from '@angular/core'; import { ChangeDetectorRef, Component, OnInit, ViewChild } from '@angular/core';
import { FormControl } from '@angular/forms'; import { FormControl } from '@angular/forms';
import { import {
@@ -32,13 +32,23 @@ import {
BITSTREAM_METADATA_FORM_GROUP_LAYOUT BITSTREAM_METADATA_FORM_GROUP_LAYOUT
} from './section-upload-file-edit.model'; } from './section-upload-file-edit.model';
import { POLICY_DEFAULT_WITH_LIST } from '../../section-upload.component'; import { POLICY_DEFAULT_WITH_LIST } from '../../section-upload.component';
import { isNotEmpty } from '../../../../../shared/empty.util'; import { hasNoValue, hasValue, isNotEmpty, isNotNull } from '../../../../../shared/empty.util';
import { SubmissionFormsModel } from '../../../../../core/config/models/config-submission-forms.model'; import { SubmissionFormsModel } from '../../../../../core/config/models/config-submission-forms.model';
import { FormFieldModel } from '../../../../../shared/form/builder/models/form-field.model'; import { FormFieldModel } from '../../../../../shared/form/builder/models/form-field.model';
import { AccessConditionOption } from '../../../../../core/config/models/config-access-condition-option.model'; import { AccessConditionOption } from '../../../../../core/config/models/config-access-condition-option.model';
import { SubmissionService } from '../../../../submission.service'; import { SubmissionService } from '../../../../submission.service';
import { FormService } from '../../../../../shared/form/form.service'; import { FormService } from '../../../../../shared/form/form.service';
import { FormComponent } from '../../../../../shared/form/form.component'; import { FormComponent } from '../../../../../shared/form/form.component';
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
import { filter, mergeMap, take } from 'rxjs/operators';
import { dateToISOFormat } from '../../../../../shared/date.util';
import { SubmissionObject } from '../../../../../core/submission/models/submission-object.model';
import { WorkspaceitemSectionUploadObject } from '../../../../../core/submission/models/workspaceitem-section-upload.model';
import { JsonPatchOperationsBuilder } from '../../../../../core/json-patch/builder/json-patch-operations-builder';
import { SubmissionJsonPatchOperationsService } from '../../../../../core/submission/submission-json-patch-operations.service';
import { JsonPatchOperationPathCombiner } from '../../../../../core/json-patch/builder/json-patch-operation-path-combiner';
import { SectionUploadService } from '../../section-upload.service';
import { Subscription } from 'rxjs';
/** /**
* This component represents the edit form for bitstream * This component represents the edit form for bitstream
@@ -48,105 +58,246 @@ import { FormComponent } from '../../../../../shared/form/form.component';
styleUrls: ['./section-upload-file-edit.component.scss'], styleUrls: ['./section-upload-file-edit.component.scss'],
templateUrl: './section-upload-file-edit.component.html', templateUrl: './section-upload-file-edit.component.html',
}) })
export class SubmissionSectionUploadFileEditComponent implements OnChanges { export class SubmissionSectionUploadFileEditComponent implements OnInit {
/**
* The list of available access condition
* @type {Array}
*/
@Input() availableAccessConditionOptions: any[];
/**
* The submission id
* @type {string}
*/
@Input() collectionId: string;
/**
* Define if collection access conditions policy type :
* POLICY_DEFAULT_NO_LIST : is not possible to define additional access group/s for the single file
* POLICY_DEFAULT_WITH_LIST : is possible to define additional access group/s for the single file
* @type {number}
*/
@Input() collectionPolicyType: number;
/**
* The configuration for the bitstream's metadata form
* @type {SubmissionFormsModel}
*/
@Input() configMetadataForm: SubmissionFormsModel;
/**
* The bitstream's metadata data
* @type {WorkspaceitemSectionUploadFileObject}
*/
@Input() fileData: WorkspaceitemSectionUploadFileObject;
/**
* The bitstream id
* @type {string}
*/
@Input() fileId: string;
/**
* The bitstream array key
* @type {string}
*/
@Input() fileIndex: string;
/**
* The form id
* @type {string}
*/
@Input() formId: string;
/**
* The section id
* @type {string}
*/
@Input() sectionId: string;
/**
* The submission id
* @type {string}
*/
@Input() submissionId: string;
/**
* The form model
* @type {DynamicFormControlModel[]}
*/
public formModel: DynamicFormControlModel[];
/** /**
* The FormComponent reference * The FormComponent reference
*/ */
@ViewChild('formRef') public formRef: FormComponent; @ViewChild('formRef') public formRef: FormComponent;
/**
* The list of available access condition
* @type {Array}
*/
public availableAccessConditionOptions: any[];
/**
* The submission id
* @type {string}
*/
public collectionId: string;
/**
* Define if collection access conditions policy type :
* POLICY_DEFAULT_NO_LIST : is not possible to define additional access group/s for the single file
* POLICY_DEFAULT_WITH_LIST : is possible to define additional access group/s for the single file
* @type {number}
*/
public collectionPolicyType: number;
/**
* The configuration for the bitstream's metadata form
* @type {SubmissionFormsModel}
*/
public configMetadataForm: SubmissionFormsModel;
/**
* The bitstream's metadata data
* @type {WorkspaceitemSectionUploadFileObject}
*/
public fileData: WorkspaceitemSectionUploadFileObject;
/**
* The bitstream id
* @type {string}
*/
public fileId: string;
/**
* The bitstream array key
* @type {string}
*/
public fileIndex: string;
/**
* The form id
* @type {string}
*/
public formId: string;
/**
* The section id
* @type {string}
*/
public sectionId: string;
/**
* The submission id
* @type {string}
*/
public submissionId: string;
/**
* The list of all available metadata
*/
formMetadata: string[] = [];
/**
* The form model
* @type {DynamicFormControlModel[]}
*/
formModel: DynamicFormControlModel[];
/**
* When `true` form controls are deactivated
*/
isSaving = false;
/**
* The [JsonPatchOperationPathCombiner] object
* @type {JsonPatchOperationPathCombiner}
*/
protected pathCombiner: JsonPatchOperationPathCombiner;
protected subscriptions: Subscription[] = [];
/** /**
* Initialize instance variables * Initialize instance variables
* *
* @param activeModal
* @param {ChangeDetectorRef} cdr * @param {ChangeDetectorRef} cdr
* @param {FormBuilderService} formBuilderService * @param {FormBuilderService} formBuilderService
* @param {FormService} formService * @param {FormService} formService
* @param {SubmissionService} submissionService * @param {SubmissionService} submissionService
* @param {JsonPatchOperationsBuilder} operationsBuilder
* @param {SubmissionJsonPatchOperationsService} operationsService
* @param {SectionUploadService} uploadService
*/ */
constructor(private cdr: ChangeDetectorRef, constructor(
private formBuilderService: FormBuilderService, protected activeModal: NgbActiveModal,
private formService: FormService, private cdr: ChangeDetectorRef,
private submissionService: SubmissionService) { private formBuilderService: FormBuilderService,
private formService: FormService,
private submissionService: SubmissionService,
private operationsBuilder: JsonPatchOperationsBuilder,
private operationsService: SubmissionJsonPatchOperationsService,
private uploadService: SectionUploadService,
) {
}
/**
* Initialize form model values
*
* @param formModel
* The form model
*/
public initModelData(formModel: DynamicFormControlModel[]) {
this.fileData.accessConditions.forEach((accessCondition, index) => {
Array.of('name', 'startDate', 'endDate')
.filter((key) => accessCondition.hasOwnProperty(key) && isNotEmpty(accessCondition[key]))
.forEach((key) => {
const metadataModel: any = this.formBuilderService.findById(key, formModel, index);
if (metadataModel) {
if (metadataModel.type === DYNAMIC_FORM_CONTROL_TYPE_DATEPICKER) {
const date = new Date(accessCondition[key]);
metadataModel.value = {
year: date.getUTCFullYear(),
month: date.getUTCMonth() + 1,
day: date.getUTCDate()
};
} else {
metadataModel.value = accessCondition[key];
}
}
});
});
}
/**
* Dispatch form model update when changing an access condition
*
* @param event
* The event emitted
*/
onChange(event: DynamicFormControlEvent) {
if (event.model.id === 'name') {
this.setOptions(event.model, event.control);
}
}
onModalClose() {
this.activeModal.dismiss();
}
onSubmit() {
this.isSaving = true;
this.saveBitstreamData();
}
/**
* Update `startDate`, 'groupUUID' and 'endDate' model
*
* @param model
* The [[DynamicFormControlModel]] object
* @param control
* The [[FormControl]] object
*/
public setOptions(model: DynamicFormControlModel, control: FormControl) {
let accessCondition: AccessConditionOption = null;
this.availableAccessConditionOptions.filter((element) => element.name === control.value)
.forEach((element) => accessCondition = element );
if (isNotEmpty(accessCondition)) {
const showGroups: boolean = accessCondition.hasStartDate === true || accessCondition.hasEndDate === true;
const startDateControl: FormControl = control.parent.get('startDate') as FormControl;
const endDateControl: FormControl = control.parent.get('endDate') as FormControl;
// Clear previous state
startDateControl?.markAsUntouched();
endDateControl?.markAsUntouched();
startDateControl?.setValue(null);
control.parent.markAsDirty();
endDateControl?.setValue(null);
if (showGroups) {
if (accessCondition.hasStartDate) {
const startDateModel = this.formBuilderService.findById(
'startDate',
(model.parent as DynamicFormArrayGroupModel).group) as DynamicDateControlModel;
const min = new Date(accessCondition.maxStartDate);
startDateModel.max = {
year: min.getUTCFullYear(),
month: min.getUTCMonth() + 1,
day: min.getUTCDate()
};
}
if (accessCondition.hasEndDate) {
const endDateModel = this.formBuilderService.findById(
'endDate',
(model.parent as DynamicFormArrayGroupModel).group) as DynamicDateControlModel;
const max = new Date(accessCondition.maxEndDate);
endDateModel.max = {
year: max.getUTCFullYear(),
month: max.getUTCMonth() + 1,
day: max.getUTCDate()
};
}
}
}
} }
/** /**
* Dispatch form model init * Dispatch form model init
*/ */
ngOnChanges() { ngOnInit() {
if (this.fileData && this.formId) { if (this.fileData && this.formId) {
this.formModel = this.buildFileEditForm(); this.formModel = this.buildFileEditForm();
this.cdr.detectChanges(); this.cdr.detectChanges();
} }
} }
ngOnDestroy(): void {
this.unsubscribeAll();
}
protected retrieveValueFromField(field: any) {
const temp = Array.isArray(field) ? field[0] : field;
return (temp) ? temp.value : undefined;
}
/** /**
* Initialize form model * Initialize form model
*/ */
@@ -193,17 +344,17 @@ export class SubmissionSectionUploadFileEditComponent implements OnChanges {
const showEnd: boolean = condition.hasEndDate === true; const showEnd: boolean = condition.hasEndDate === true;
const showGroups: boolean = showStart || showEnd; const showGroups: boolean = showStart || showEnd;
if (showStart) { if (showStart) {
hasStart.push({ id: 'name', value: condition.name }); hasStart.push({id: 'name', value: condition.name});
} }
if (showEnd) { if (showEnd) {
hasEnd.push({ id: 'name', value: condition.name }); hasEnd.push({id: 'name', value: condition.name});
} }
if (showGroups) { if (showGroups) {
hasGroups.push({ id: 'name', value: condition.name }); hasGroups.push({id: 'name', value: condition.name});
} }
}); });
const confStart = { relations: [{ match: MATCH_ENABLED, operator: OR_OPERATOR, when: hasStart }] }; const confStart = {relations: [{match: MATCH_ENABLED, operator: OR_OPERATOR, when: hasStart}]};
const confEnd = { relations: [{ match: MATCH_ENABLED, operator: OR_OPERATOR, when: hasEnd }] }; const confEnd = {relations: [{match: MATCH_ENABLED, operator: OR_OPERATOR, when: hasEnd}]};
accessConditionsArrayConfig.groupFactory = () => { accessConditionsArrayConfig.groupFactory = () => {
const type = new DynamicSelectModel(accessConditionTypeModelConfig, BITSTREAM_FORM_ACCESS_CONDITION_TYPE_LAYOUT); const type = new DynamicSelectModel(accessConditionTypeModelConfig, BITSTREAM_FORM_ACCESS_CONDITION_TYPE_LAYOUT);
@@ -213,7 +364,9 @@ export class SubmissionSectionUploadFileEditComponent implements OnChanges {
const startDate = new DynamicDatePickerModel(startDateConfig, BITSTREAM_FORM_ACCESS_CONDITION_START_DATE_LAYOUT); const startDate = new DynamicDatePickerModel(startDateConfig, BITSTREAM_FORM_ACCESS_CONDITION_START_DATE_LAYOUT);
const endDate = new DynamicDatePickerModel(endDateConfig, BITSTREAM_FORM_ACCESS_CONDITION_END_DATE_LAYOUT); const endDate = new DynamicDatePickerModel(endDateConfig, BITSTREAM_FORM_ACCESS_CONDITION_END_DATE_LAYOUT);
const accessConditionGroupConfig = Object.assign({}, BITSTREAM_ACCESS_CONDITION_GROUP_CONFIG); const accessConditionGroupConfig = Object.assign({}, BITSTREAM_ACCESS_CONDITION_GROUP_CONFIG);
accessConditionGroupConfig.group = [type, startDate, endDate]; accessConditionGroupConfig.group = [type];
if (hasStart.length > 0) { accessConditionGroupConfig.group.push(startDate); }
if (hasEnd.length > 0) { accessConditionGroupConfig.group.push(endDate); }
return [new DynamicFormGroupModel(accessConditionGroupConfig, BITSTREAM_ACCESS_CONDITION_GROUP_LAYOUT)]; return [new DynamicFormGroupModel(accessConditionGroupConfig, BITSTREAM_ACCESS_CONDITION_GROUP_LAYOUT)];
}; };
@@ -229,98 +382,95 @@ export class SubmissionSectionUploadFileEditComponent implements OnChanges {
} }
/** /**
* Initialize form model values * Save bitstream metadata
*
* @param formModel
* The form model
*/ */
public initModelData(formModel: DynamicFormControlModel[]) { saveBitstreamData() {
this.fileData.accessConditions.forEach((accessCondition, index) => { // validate form
Array.of('name', 'startDate', 'endDate') this.formService.validateAllFormFields(this.formRef.formGroup);
.filter((key) => accessCondition.hasOwnProperty(key) && isNotEmpty(accessCondition[key])) const saveBitstreamDataSubscription = this.formService.isValid(this.formId).pipe(
.forEach((key) => { take(1),
const metadataModel: any = this.formBuilderService.findById(key, formModel, index); filter((isValid) => isValid),
if (metadataModel) { mergeMap(() => this.formService.getFormData(this.formId)),
if (metadataModel.type === DYNAMIC_FORM_CONTROL_TYPE_DATEPICKER) { take(1),
const date = new Date(accessCondition[key]); mergeMap((formData: any) => {
metadataModel.value = { // collect bitstream metadata
year: date.getUTCFullYear(), Object.keys((formData.metadata))
month: date.getUTCMonth() + 1, .filter((key) => isNotEmpty(formData.metadata[key]))
day: date.getUTCDate() .forEach((key) => {
}; const metadataKey = key.replace(/_/g, '.');
} else { const path = `metadata/${metadataKey}`;
metadataModel.value = accessCondition[key]; this.operationsBuilder.add(this.pathCombiner.getPath(path), formData.metadata[key], true);
});
Object.keys((this.fileData.metadata))
.filter((key) => isNotEmpty(this.fileData.metadata[key]))
.filter((key) => hasNoValue(formData.metadata[key]))
.filter((key) => this.formMetadata.includes(key))
.forEach((key) => {
const metadataKey = key.replace(/_/g, '.');
const path = `metadata/${metadataKey}`;
this.operationsBuilder.remove(this.pathCombiner.getPath(path));
});
const accessConditionsToSave = [];
formData.accessConditions
.map((accessConditions) => accessConditions.accessConditionGroup)
.filter((accessCondition) => isNotEmpty(accessCondition))
.forEach((accessCondition) => {
let accessConditionOpt;
this.availableAccessConditionOptions
.filter((element) => isNotNull(accessCondition.name) && element.name === accessCondition.name[0].value)
.forEach((element) => accessConditionOpt = element);
if (accessConditionOpt) {
const currentAccessCondition = Object.assign({}, accessCondition);
currentAccessCondition.name = this.retrieveValueFromField(accessCondition.name);
/* When start and end date fields are deactivated, their values may be still present in formData,
therefore it is necessary to delete them if they're not allowed by the current access condition option. */
if (!accessConditionOpt.hasStartDate) {
delete currentAccessCondition.startDate;
} else if (accessCondition.startDate) {
const startDate = this.retrieveValueFromField(accessCondition.startDate);
currentAccessCondition.startDate = dateToISOFormat(startDate);
}
if (!accessConditionOpt.hasEndDate) {
delete currentAccessCondition.endDate;
} else if (accessCondition.endDate) {
const endDate = this.retrieveValueFromField(accessCondition.endDate);
currentAccessCondition.endDate = dateToISOFormat(endDate);
}
accessConditionsToSave.push(currentAccessCondition);
} }
} });
});
});
}
/** if (isNotEmpty(accessConditionsToSave)) {
* Dispatch form model update when changing an access condition this.operationsBuilder.add(this.pathCombiner.getPath('accessConditions'), accessConditionsToSave, true);
*
* @param event
* The event emitted
*/
public onChange(event: DynamicFormControlEvent) {
if (event.model.id === 'name') {
this.setOptions(event.model, event.control);
}
}
/**
* Update `startDate`, 'groupUUID' and 'endDate' model
*
* @param model
* The [[DynamicFormControlModel]] object
* @param control
* The [[FormControl]] object
*/
public setOptions(model: DynamicFormControlModel, control: FormControl) {
let accessCondition: AccessConditionOption = null;
this.availableAccessConditionOptions.filter((element) => element.name === control.value)
.forEach((element) => accessCondition = element);
if (isNotEmpty(accessCondition)) {
const showGroups: boolean = accessCondition.hasStartDate === true || accessCondition.hasEndDate === true;
const startDateControl: FormControl = control.parent.get('startDate') as FormControl;
const endDateControl: FormControl = control.parent.get('endDate') as FormControl;
// Clear previous state
startDateControl.markAsUntouched();
endDateControl.markAsUntouched();
startDateControl.setValue(null);
control.parent.markAsDirty();
endDateControl.setValue(null);
if (showGroups) {
if (accessCondition.hasStartDate) {
const startDateModel = this.formBuilderService.findById(
'startDate',
(model.parent as DynamicFormArrayGroupModel).group) as DynamicDateControlModel;
const min = new Date(accessCondition.maxStartDate);
startDateModel.max = {
year: min.getUTCFullYear(),
month: min.getUTCMonth() + 1,
day: min.getUTCDate()
};
} }
if (accessCondition.hasEndDate) {
const endDateModel = this.formBuilderService.findById(
'endDate',
(model.parent as DynamicFormArrayGroupModel).group) as DynamicDateControlModel;
const max = new Date(accessCondition.maxEndDate); // dispatch a PATCH request to save metadata
endDateModel.max = { return this.operationsService.jsonPatchByResourceID(
year: max.getUTCFullYear(), this.submissionService.getSubmissionObjectLinkName(),
month: max.getUTCMonth() + 1, this.submissionId,
day: max.getUTCDate() this.pathCombiner.rootElement,
}; this.pathCombiner.subRootElement);
} })
).subscribe((result: SubmissionObject[]) => {
if (result[0].sections[this.sectionId]) {
const uploadSection = (result[0].sections[this.sectionId] as WorkspaceitemSectionUploadObject);
Object.keys(uploadSection.files)
.filter((key) => uploadSection.files[key].uuid === this.fileId)
.forEach((key) => this.uploadService.updateFileData(
this.submissionId, this.sectionId, this.fileId, uploadSection.files[key])
);
} }
} this.isSaving = false;
this.activeModal.close();
});
this.subscriptions.push(saveBitstreamDataSubscription);
}
private unsubscribeAll() {
this.subscriptions.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe());
} }
} }

View File

@@ -8,15 +8,15 @@
<div class="float-left w-75"> <div class="float-left w-75">
<h3>{{fileName}} <span class="text-muted">({{fileData?.sizeBytes | dsFileSize}})</span></h3> <h3>{{fileName}} <span class="text-muted">({{fileData?.sizeBytes | dsFileSize}})</span></h3>
</div> </div>
<div class="float-right w-15" [class.sticky-buttons]="!readMode"> <div class="float-right w-15">
<ng-container *ngIf="readMode"> <ng-container>
<ds-file-download-link [cssClasses]="'btn btn-link-focus'" [isBlank]="true" [bitstream]="getBitstream()" [enableRequestACopy]="false"> <ds-file-download-link [cssClasses]="'btn btn-link-focus'" [isBlank]="true" [bitstream]="getBitstream()" [enableRequestACopy]="false">
<i class="fa fa-download fa-2x text-normal" aria-hidden="true"></i> <i class="fa fa-download fa-2x text-normal" aria-hidden="true"></i>
</ds-file-download-link> </ds-file-download-link>
<button class="btn btn-link-focus" <button class="btn btn-link-focus"
[attr.aria-label]="'submission.sections.upload.edit.title' | translate" [attr.aria-label]="'submission.sections.upload.edit.title' | translate"
title="{{ 'submission.sections.upload.edit.title' | translate }}" title="{{ 'submission.sections.upload.edit.title' | translate }}"
(click)="$event.preventDefault();switchMode();"> (click)="$event.preventDefault();editBitstreamData();">
<i class="fa fa-edit fa-2x text-normal"></i> <i class="fa fa-edit fa-2x text-normal"></i>
</button> </button>
<button class="btn btn-link-focus" <button class="btn btn-link-focus"
@@ -28,40 +28,9 @@
<i *ngIf="!(processingDelete$ | async)" class="fa fa-trash fa-2x text-danger"></i> <i *ngIf="!(processingDelete$ | async)" class="fa fa-trash fa-2x text-danger"></i>
</button> </button>
</ng-container> </ng-container>
<ng-container *ngIf="!readMode">
<button class="btn btn-link-focus"
[attr.aria-label]="'submission.sections.upload.save-metadata' | translate"
title="{{ 'submission.sections.upload.save-metadata' | translate }}"
(click)="saveBitstreamData($event);">
<i class="fa fa-save fa-2x text-success"></i>
</button>
<button class="btn btn-link-focus"
[attr.aria-label]="'submission.sections.upload.undo' | translate"
title="{{ 'submission.sections.upload.undo' | translate }}"
(click)="$event.preventDefault();switchMode();"><i class="fa fa-ban fa-2x text-warning"></i></button>
<button class="btn btn-link-focus"
[attr.aria-label]="'submission.sections.upload.delete.confirm.title' | translate"
title="{{ 'submission.sections.upload.delete.confirm.title' | translate }}"
[disabled]="(processingDelete$ | async)"
(click)="$event.preventDefault();confirmDelete(content);">
<i *ngIf="(processingDelete$ | async)" class="fas fa-circle-notch fa-spin fa-2x text-danger"></i>
<i *ngIf="!(processingDelete$ | async)" class="fa fa-trash fa-2x text-danger"></i>
</button>
</ng-container>
</div> </div>
<div class="clearfix"></div> <div class="clearfix"></div>
<ds-submission-section-upload-file-view *ngIf="readMode" <ds-submission-section-upload-file-view [fileData]="fileData"></ds-submission-section-upload-file-view>
[fileData]="fileData"></ds-submission-section-upload-file-view>
<ds-submission-section-upload-file-edit *ngIf="!readMode"
[availableAccessConditionOptions]="availableAccessConditionOptions"
[collectionId]="collectionId"
[collectionPolicyType]="collectionPolicyType"
[configMetadataForm]="configMetadataForm"
[fileData]="fileData"
[fileId]="fileId"
[fileIndex]="fileIndex"
[formId]="formId"
[sectionId]="sectionId"></ds-submission-section-upload-file-edit>
</div> </div>
</div> </div>
</ng-container> </ng-container>

View File

@@ -1,6 +0,0 @@
.sticky-buttons {
position: sticky;
top: calc(var(--bs-dropdown-item-padding-x) * 3);
z-index: var(--ds-submission-footer-z-index);
background-color: rgba(255, 255, 255, .97);
}

View File

@@ -1,9 +1,9 @@
import { ChangeDetectorRef, Component, NO_ERRORS_SCHEMA } from '@angular/core'; import { ChangeDetectorRef, Component, NO_ERRORS_SCHEMA } from '@angular/core';
import { ComponentFixture, fakeAsync, inject, TestBed, tick, waitForAsync } from '@angular/core/testing'; import { ComponentFixture, inject, TestBed, waitForAsync } from '@angular/core/testing';
import { BrowserModule, By } from '@angular/platform-browser'; import { BrowserModule, By } from '@angular/platform-browser';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { of as observableOf } from 'rxjs'; import { of, of as observableOf } from 'rxjs';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { FormService } from '../../../../shared/form/form.service'; import { FormService } from '../../../../shared/form/form.service';
@@ -17,10 +17,8 @@ import { SubmissionJsonPatchOperationsService } from '../../../../core/submissio
import { SubmissionSectionUploadFileComponent } from './section-upload-file.component'; import { SubmissionSectionUploadFileComponent } from './section-upload-file.component';
import { SubmissionServiceStub } from '../../../../shared/testing/submission-service.stub'; import { SubmissionServiceStub } from '../../../../shared/testing/submission-service.stub';
import { import {
mockFileFormData,
mockSubmissionCollectionId, mockSubmissionCollectionId,
mockSubmissionId, mockSubmissionId,
mockSubmissionObject,
mockUploadConfigResponse, mockUploadConfigResponse,
mockUploadFiles mockUploadFiles
} from '../../../../shared/mocks/submission.mock'; } from '../../../../shared/mocks/submission.mock';
@@ -32,10 +30,19 @@ import { FileSizePipe } from '../../../../shared/utils/file-size-pipe';
import { POLICY_DEFAULT_WITH_LIST } from '../section-upload.component'; import { POLICY_DEFAULT_WITH_LIST } from '../section-upload.component';
import { JsonPatchOperationPathCombiner } from '../../../../core/json-patch/builder/json-patch-operation-path-combiner'; import { JsonPatchOperationPathCombiner } from '../../../../core/json-patch/builder/json-patch-operation-path-combiner';
import { getMockSectionUploadService } from '../../../../shared/mocks/section-upload.service.mock'; import { getMockSectionUploadService } from '../../../../shared/mocks/section-upload.service.mock';
import { FormFieldMetadataValueObject } from '../../../../shared/form/builder/models/form-field-metadata-value.model';
import { SubmissionSectionUploadFileEditComponent } from './edit/section-upload-file-edit.component'; import { SubmissionSectionUploadFileEditComponent } from './edit/section-upload-file-edit.component';
import { FormBuilderService } from '../../../../shared/form/builder/form-builder.service'; import { FormBuilderService } from '../../../../shared/form/builder/form-builder.service';
import { dateToISOFormat } from '../../../../shared/date.util';
const configMetadataFormMock = {
rows: [{
fields: [{
selectableMetadata: [
{metadata: 'dc.title', label: null, closed: false},
{metadata: 'dc.description', label: null, closed: false}
]
}]
}]
};
describe('SubmissionSectionUploadFileComponent test suite', () => { describe('SubmissionSectionUploadFileComponent test suite', () => {
@@ -117,6 +124,7 @@ describe('SubmissionSectionUploadFileComponent test suite', () => {
testFixture = createTestComponent(html, TestComponent) as ComponentFixture<TestComponent>; testFixture = createTestComponent(html, TestComponent) as ComponentFixture<TestComponent>;
testComp = testFixture.componentInstance; testComp = testFixture.componentInstance;
}); });
afterEach(() => { afterEach(() => {
@@ -124,9 +132,7 @@ describe('SubmissionSectionUploadFileComponent test suite', () => {
}); });
it('should create SubmissionSectionUploadFileComponent', inject([SubmissionSectionUploadFileComponent], (app: SubmissionSectionUploadFileComponent) => { it('should create SubmissionSectionUploadFileComponent', inject([SubmissionSectionUploadFileComponent], (app: SubmissionSectionUploadFileComponent) => {
expect(app).toBeDefined(); expect(app).toBeDefined();
})); }));
}); });
@@ -135,6 +141,7 @@ describe('SubmissionSectionUploadFileComponent test suite', () => {
fixture = TestBed.createComponent(SubmissionSectionUploadFileComponent); fixture = TestBed.createComponent(SubmissionSectionUploadFileComponent);
comp = fixture.componentInstance; comp = fixture.componentInstance;
compAsAny = comp; compAsAny = comp;
compAsAny.configMetadataForm = configMetadataFormMock;
submissionServiceStub = TestBed.inject(SubmissionService as any); submissionServiceStub = TestBed.inject(SubmissionService as any);
uploadService = TestBed.inject(SectionUploadService); uploadService = TestBed.inject(SectionUploadService);
formService = TestBed.inject(FormService); formService = TestBed.inject(FormService);
@@ -210,96 +217,20 @@ describe('SubmissionSectionUploadFileComponent test suite', () => {
pathCombiner.subRootElement); pathCombiner.subRootElement);
}); });
it('should save Bitstream File data properly when form is valid', fakeAsync(() => { it('should open edit modal when edit button is clicked', () => {
compAsAny.fileEditComp = TestBed.inject(SubmissionSectionUploadFileEditComponent); spyOn(compAsAny, 'editBitstreamData').and.callThrough();
compAsAny.fileEditComp.formRef = {formGroup: null}; comp.fileData = fileData;
compAsAny.pathCombiner = pathCombiner;
const event = new Event('click', null);
spyOn(comp, 'switchMode');
formService.validateAllFormFields.and.callFake(() => null);
formService.isValid.and.returnValue(observableOf(true));
formService.getFormData.and.returnValue(observableOf(mockFileFormData));
const response = [ fixture.detectChanges();
Object.assign(mockSubmissionObject, {
sections: {
upload: {
files: mockUploadFiles
}
}
})
];
operationsService.jsonPatchByResourceID.and.returnValue(observableOf(response));
const accessConditionsToSave = [ const modalBtn = fixture.debugElement.query(By.css('.fa-edit '));
{ name: 'openaccess' },
{ name: 'lease', endDate: dateToISOFormat('2019-01-16T00:00:00Z') },
{ name: 'embargo', startDate: dateToISOFormat('2019-01-16T00:00:00Z') },
];
comp.saveBitstreamData(event);
tick();
let path = 'metadata/dc.title'; modalBtn.nativeElement.click();
expect(operationsBuilder.add).toHaveBeenCalledWith( fixture.detectChanges();
pathCombiner.getPath(path),
mockFileFormData.metadata['dc.title'],
true
);
path = 'metadata/dc.description'; expect(compAsAny.editBitstreamData).toHaveBeenCalled();
expect(operationsBuilder.add).toHaveBeenCalledWith(
pathCombiner.getPath(path),
mockFileFormData.metadata['dc.description'],
true
);
path = 'accessConditions';
expect(operationsBuilder.add).toHaveBeenCalledWith(
pathCombiner.getPath(path),
accessConditionsToSave,
true
);
expect(comp.switchMode).toHaveBeenCalled();
expect(uploadService.updateFileData).toHaveBeenCalledWith(submissionId, sectionId, mockUploadFiles[0].uuid, mockUploadFiles[0]);
}));
it('should not save Bitstream File data properly when form is not valid', fakeAsync(() => {
compAsAny.fileEditComp = TestBed.inject(SubmissionSectionUploadFileEditComponent);
compAsAny.fileEditComp.formRef = {formGroup: null};
compAsAny.pathCombiner = pathCombiner;
const event = new Event('click', null);
spyOn(comp, 'switchMode');
formService.validateAllFormFields.and.callFake(() => null);
formService.isValid.and.returnValue(observableOf(false));
expect(comp.switchMode).not.toHaveBeenCalled();
expect(uploadService.updateFileData).not.toHaveBeenCalled();
}));
it('should retrieve Value From Field properly', () => {
let field;
expect(compAsAny.retrieveValueFromField(field)).toBeUndefined();
field = new FormFieldMetadataValueObject('test');
expect(compAsAny.retrieveValueFromField(field)).toBe('test');
field = [new FormFieldMetadataValueObject('test')];
expect(compAsAny.retrieveValueFromField(field)).toBe('test');
}); });
it('should switch read mode', () => {
comp.readMode = false;
comp.switchMode();
expect(comp.readMode).toBeTruthy();
comp.switchMode();
expect(comp.readMode).toBeFalsy();
});
}); });
}); });
@@ -314,7 +245,7 @@ class TestComponent {
availableAccessConditionOptions; availableAccessConditionOptions;
collectionId = mockSubmissionCollectionId; collectionId = mockSubmissionCollectionId;
collectionPolicyType; collectionPolicyType;
configMetadataForm$; configMetadataForm$ = of(configMetadataFormMock);
fileIndexes = []; fileIndexes = [];
fileList = []; fileList = [];
fileNames = []; fileNames = [];

View File

@@ -1,25 +1,23 @@
import { ChangeDetectorRef, Component, Input, OnChanges, OnInit, ViewChild } from '@angular/core'; import { ChangeDetectorRef, Component, Input, OnChanges, OnInit, ViewChild } from '@angular/core';
import { BehaviorSubject, Subscription } from 'rxjs'; import { BehaviorSubject, Subscription } from 'rxjs';
import { filter, mergeMap, take } from 'rxjs/operators'; import { filter } from 'rxjs/operators';
import { DynamicFormControlModel, } from '@ng-dynamic-forms/core'; import { DynamicFormControlModel, } from '@ng-dynamic-forms/core';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { SectionUploadService } from '../section-upload.service'; import { SectionUploadService } from '../section-upload.service';
import { isNotEmpty, isNotNull, isNotUndefined } from '../../../../shared/empty.util'; import { hasValue, isNotUndefined } from '../../../../shared/empty.util';
import { FormService } from '../../../../shared/form/form.service'; import { FormService } from '../../../../shared/form/form.service';
import { JsonPatchOperationsBuilder } from '../../../../core/json-patch/builder/json-patch-operations-builder'; import { JsonPatchOperationsBuilder } from '../../../../core/json-patch/builder/json-patch-operations-builder';
import { JsonPatchOperationPathCombiner } from '../../../../core/json-patch/builder/json-patch-operation-path-combiner'; import { JsonPatchOperationPathCombiner } from '../../../../core/json-patch/builder/json-patch-operation-path-combiner';
import { WorkspaceitemSectionUploadFileObject } from '../../../../core/submission/models/workspaceitem-section-upload-file.model'; import { WorkspaceitemSectionUploadFileObject } from '../../../../core/submission/models/workspaceitem-section-upload-file.model';
import { SubmissionFormsModel } from '../../../../core/config/models/config-submission-forms.model'; import { SubmissionFormsModel } from '../../../../core/config/models/config-submission-forms.model';
import { dateToISOFormat } from '../../../../shared/date.util';
import { SubmissionService } from '../../../submission.service'; import { SubmissionService } from '../../../submission.service';
import { HALEndpointService } from '../../../../core/shared/hal-endpoint.service'; import { HALEndpointService } from '../../../../core/shared/hal-endpoint.service';
import { SubmissionJsonPatchOperationsService } from '../../../../core/submission/submission-json-patch-operations.service'; import { SubmissionJsonPatchOperationsService } from '../../../../core/submission/submission-json-patch-operations.service';
import { SubmissionObject } from '../../../../core/submission/models/submission-object.model';
import { WorkspaceitemSectionUploadObject } from '../../../../core/submission/models/workspaceitem-section-upload.model';
import { SubmissionSectionUploadFileEditComponent } from './edit/section-upload-file-edit.component'; import { SubmissionSectionUploadFileEditComponent } from './edit/section-upload-file-edit.component';
import { Bitstream } from '../../../../core/shared/bitstream.model'; import { Bitstream } from '../../../../core/shared/bitstream.model';
import { NgbModalOptions } from '@ng-bootstrap/ng-bootstrap/modal/modal-config';
/** /**
* This component represents a single bitstream contained in the submission * This component represents a single bitstream contained in the submission
@@ -87,6 +85,13 @@ export class SubmissionSectionUploadFileComponent implements OnChanges, OnInit {
*/ */
@Input() submissionId: string; @Input() submissionId: string;
/**
* The [[SubmissionSectionUploadFileEditComponent]] reference
* @type {SubmissionSectionUploadFileEditComponent}
*/
@ViewChild(SubmissionSectionUploadFileEditComponent) fileEditComp: SubmissionSectionUploadFileEditComponent;
/** /**
* The bitstream's metadata data * The bitstream's metadata data
* @type {WorkspaceitemSectionUploadFileObject} * @type {WorkspaceitemSectionUploadFileObject}
@@ -130,10 +135,10 @@ export class SubmissionSectionUploadFileComponent implements OnChanges, OnInit {
protected subscriptions: Subscription[] = []; protected subscriptions: Subscription[] = [];
/** /**
* The [[SubmissionSectionUploadFileEditComponent]] reference * Array containing all the form metadata defined in configMetadataForm
* @type {SubmissionSectionUploadFileEditComponent} * @type {Array}
*/ */
@ViewChild(SubmissionSectionUploadFileEditComponent) fileEditComp: SubmissionSectionUploadFileEditComponent; protected formMetadata: string[] = [];
/** /**
* Initialize instance variables * Initialize instance variables
@@ -147,14 +152,16 @@ export class SubmissionSectionUploadFileComponent implements OnChanges, OnInit {
* @param {SubmissionService} submissionService * @param {SubmissionService} submissionService
* @param {SectionUploadService} uploadService * @param {SectionUploadService} uploadService
*/ */
constructor(private cdr: ChangeDetectorRef, constructor(
private formService: FormService, private cdr: ChangeDetectorRef,
private halService: HALEndpointService, private formService: FormService,
private modalService: NgbModal, private halService: HALEndpointService,
private operationsBuilder: JsonPatchOperationsBuilder, private modalService: NgbModal,
private operationsService: SubmissionJsonPatchOperationsService, private operationsBuilder: JsonPatchOperationsBuilder,
private submissionService: SubmissionService, private operationsService: SubmissionJsonPatchOperationsService,
private uploadService: SectionUploadService) { private submissionService: SubmissionService,
private uploadService: SectionUploadService,
) {
this.readMode = true; this.readMode = true;
} }
@@ -182,22 +189,7 @@ export class SubmissionSectionUploadFileComponent implements OnChanges, OnInit {
ngOnInit() { ngOnInit() {
this.formId = this.formService.getUniqueId(this.fileId); this.formId = this.formService.getUniqueId(this.fileId);
this.pathCombiner = new JsonPatchOperationPathCombiner('sections', this.sectionId, 'files', this.fileIndex); this.pathCombiner = new JsonPatchOperationPathCombiner('sections', this.sectionId, 'files', this.fileIndex);
} this.loadFormMetadata();
/**
* Delete bitstream from submission
*/
protected deleteFile() {
this.operationsBuilder.remove(this.pathCombiner.getPath());
this.subscriptions.push(this.operationsService.jsonPatchByResourceID(
this.submissionService.getSubmissionObjectLinkName(),
this.submissionId,
this.pathCombiner.rootElement,
this.pathCombiner.subRootElement)
.subscribe(() => {
this.uploadService.removeUploadedFile(this.submissionId, this.sectionId, this.fileId);
this.processingDelete$.next(false);
}));
} }
/** /**
@@ -225,98 +217,63 @@ export class SubmissionSectionUploadFileComponent implements OnChanges, OnInit {
}); });
} }
/** editBitstreamData() {
* Save bitstream metadata
*
* @param event
* the click event emitted
*/
public saveBitstreamData(event) {
event.preventDefault();
// validate form const options: NgbModalOptions = {
this.formService.validateAllFormFields(this.fileEditComp.formRef.formGroup); size: 'xl',
this.subscriptions.push(this.formService.isValid(this.formId).pipe( backdrop: 'static',
take(1), };
filter((isValid) => isValid),
mergeMap(() => this.formService.getFormData(this.formId)), const activeModal = this.modalService.open(SubmissionSectionUploadFileEditComponent, options);
take(1),
mergeMap((formData: any) => { activeModal.componentInstance.availableAccessConditionOptions = this.availableAccessConditionOptions;
// collect bitstream metadata activeModal.componentInstance.collectionId = this.collectionId;
Object.keys((formData.metadata)) activeModal.componentInstance.collectionPolicyType = this.collectionPolicyType;
.filter((key) => isNotEmpty(formData.metadata[key])) activeModal.componentInstance.configMetadataForm = this.configMetadataForm;
.forEach((key) => { activeModal.componentInstance.fileData = this.fileData;
const metadataKey = key.replace(/_/g, '.'); activeModal.componentInstance.fileId = this.fileId;
const path = `metadata/${metadataKey}`; activeModal.componentInstance.fileIndex = this.fileIndex;
this.operationsBuilder.add(this.pathCombiner.getPath(path), formData.metadata[key], true); activeModal.componentInstance.formId = this.formId;
activeModal.componentInstance.sectionId = this.sectionId;
activeModal.componentInstance.formMetadata = this.formMetadata;
activeModal.componentInstance.pathCombiner = this.pathCombiner;
activeModal.componentInstance.submissionId = this.submissionId;
}
ngOnDestroy(): void {
this.unsubscribeAll();
}
unsubscribeAll() {
this.subscriptions.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe());
}
protected loadFormMetadata() {
this.configMetadataForm.rows.forEach((row) => {
row.fields.forEach((field) => {
field.selectableMetadata.forEach((metadatum) => {
this.formMetadata.push(metadatum.metadata);
}); });
const accessConditionsToSave = []; });
formData.accessConditions
.map((accessConditions) => accessConditions.accessConditionGroup)
.filter((accessCondition) => isNotEmpty(accessCondition))
.forEach((accessCondition) => {
let accessConditionOpt;
this.availableAccessConditionOptions
.filter((element) => isNotNull(accessCondition.name) && element.name === accessCondition.name[0].value)
.forEach((element) => accessConditionOpt = element);
if (accessConditionOpt) {
accessConditionOpt = Object.assign({}, accessCondition);
accessConditionOpt.name = this.retrieveValueFromField(accessCondition.name);
if (accessCondition.startDate) {
const startDate = this.retrieveValueFromField(accessCondition.startDate);
accessConditionOpt.startDate = dateToISOFormat(startDate);
}
if (accessCondition.endDate) {
const endDate = this.retrieveValueFromField(accessCondition.endDate);
accessConditionOpt.endDate = dateToISOFormat(endDate);
}
accessConditionsToSave.push(accessConditionOpt);
}
});
if (isNotEmpty(accessConditionsToSave)) {
this.operationsBuilder.add(this.pathCombiner.getPath('accessConditions'), accessConditionsToSave, true);
}
// dispatch a PATCH request to save metadata
return this.operationsService.jsonPatchByResourceID(
this.submissionService.getSubmissionObjectLinkName(),
this.submissionId,
this.pathCombiner.rootElement,
this.pathCombiner.subRootElement);
})
).subscribe((result: SubmissionObject[]) => {
if (result[0].sections[this.sectionId]) {
const uploadSection = (result[0].sections[this.sectionId] as WorkspaceitemSectionUploadObject);
Object.keys(uploadSection.files)
.filter((key) => uploadSection.files[key].uuid === this.fileId)
.forEach((key) => this.uploadService.updateFileData(
this.submissionId, this.sectionId, this.fileId, uploadSection.files[key])
);
} }
this.switchMode(); );
}));
} }
/** /**
* Retrieve field value * Delete bitstream from submission
*
* @param field
* the specified field object
*/ */
private retrieveValueFromField(field: any) { protected deleteFile() {
const temp = Array.isArray(field) ? field[0] : field; this.operationsBuilder.remove(this.pathCombiner.getPath());
return (temp) ? temp.value : undefined; this.subscriptions.push(this.operationsService.jsonPatchByResourceID(
} this.submissionService.getSubmissionObjectLinkName(),
this.submissionId,
/** this.pathCombiner.rootElement,
* Switch from edit form to metadata view this.pathCombiner.subRootElement)
*/ .subscribe(() => {
public switchMode() { this.uploadService.removeUploadedFile(this.submissionId, this.sectionId, this.fileId);
this.readMode = !this.readMode; this.processingDelete$.next(false);
this.cdr.detectChanges(); }));
} }
} }

View File

@@ -1,4 +1,5 @@
import { InjectionToken } from '@angular/core'; import { InjectionToken } from '@angular/core';
import { makeStateKey } from '@angular/platform-browser';
import { Config } from './config.interface'; import { Config } from './config.interface';
import { ServerConfig } from './server-config.interface'; import { ServerConfig } from './server-config.interface';
import { CacheConfig } from './cache-config.interface'; import { CacheConfig } from './cache-config.interface';
@@ -7,14 +8,13 @@ import { INotificationBoardOptions } from './notifications-config.interfaces';
import { SubmissionConfig } from './submission-config.interface'; import { SubmissionConfig } from './submission-config.interface';
import { FormConfig } from './form-config.interfaces'; import { FormConfig } from './form-config.interfaces';
import { LangConfig } from './lang-config.interface'; import { LangConfig } from './lang-config.interface';
import { BrowseByConfig } from './browse-by-config.interface';
import { ItemPageConfig } from './item-page-config.interface'; import { ItemPageConfig } from './item-page-config.interface';
import { CollectionPageConfig } from './collection-page-config.interface'; import { CollectionPageConfig } from './collection-page-config.interface';
import { ThemeConfig } from './theme.model'; import { ThemeConfig } from './theme.model';
import { AuthConfig } from './auth-config.interfaces'; import { AuthConfig } from './auth-config.interfaces';
import { UIServerConfig } from './ui-server-config.interface'; import { UIServerConfig } from './ui-server-config.interface';
import { MediaViewerConfig } from './media-viewer-config.interface'; import { MediaViewerConfig } from './media-viewer-config.interface';
import { makeStateKey } from '@angular/platform-browser'; import { BrowseByConfig } from './browse-by-config.interface';
interface AppConfig extends Config { interface AppConfig extends Config {
ui: UIServerConfig; ui: UIServerConfig;

View File

@@ -1,5 +1,4 @@
import { Config } from './config.interface'; import { Config } from './config.interface';
import { BrowseByTypeConfig } from './browse-by-type-config.interface';
/** /**
* Config that determines how the dropdown list of years are created for browse-by-date components * Config that determines how the dropdown list of years are created for browse-by-date components
@@ -19,9 +18,4 @@ export interface BrowseByConfig extends Config {
* The absolute lowest year to display in the dropdown when no lowest date can be found for all items * The absolute lowest year to display in the dropdown when no lowest date can be found for all items
*/ */
defaultLowerLimit: number; defaultLowerLimit: number;
/**
* A list of all the active Browse-By pages
*/
types: BrowseByTypeConfig[];
} }

View File

@@ -1,23 +0,0 @@
import { Config } from './config.interface';
import { BrowseByType } from '../app/browse-by/browse-by-switcher/browse-by-decorator';
/**
* Config used for rendering Browse-By pages and links
*/
export interface BrowseByTypeConfig extends Config {
/**
* The browse id used for fetching browse data from the rest api
* e.g. author
*/
id: string;
/**
* The type of Browse-By page to render
*/
type: BrowseByType | string;
/**
* The metadata field to use for rendering starts-with options (only necessary when type is set to BrowseByType.Date)
*/
metadataField?: string;
}

View File

@@ -1,4 +1,3 @@
import { BrowseByType } from '../app/browse-by/browse-by-switcher/browse-by-decorator';
import { RestRequestMethod } from '../app/core/data/rest-request-method'; import { RestRequestMethod } from '../app/core/data/rest-request-method';
import { NotificationAnimationsType } from '../app/shared/notifications/models/notification-animations-type'; import { NotificationAnimationsType } from '../app/shared/notifications/models/notification-animations-type';
import { AppConfig } from './app-config.interface'; import { AppConfig } from './app-config.interface';
@@ -200,33 +199,7 @@ export class DefaultAppConfig implements AppConfig {
// Limit for years to display using jumps of five years (current year - fiveYearLimit) // Limit for years to display using jumps of five years (current year - fiveYearLimit)
fiveYearLimit: 30, fiveYearLimit: 30,
// The absolute lowest year to display in the dropdown (only used when no lowest date can be found for all items) // The absolute lowest year to display in the dropdown (only used when no lowest date can be found for all items)
defaultLowerLimit: 1900, defaultLowerLimit: 1900
// List of all the active Browse-By types
// Adding a type will activate their Browse-By page and add them to the global navigation menu,
// as well as community and collection pages
// Allowed fields and their purpose:
// id: The browse id to use for fetching info from the rest api
// type: The type of Browse-By page to display
// metadataField: The metadata-field used to create starts-with options (only necessary when the type is set to 'date')
types: [
{
id: 'title',
type: BrowseByType.Title,
},
{
id: 'dateissued',
type: BrowseByType.Date,
metadataField: 'dc.date.issued'
},
{
id: 'author',
type: BrowseByType.Metadata
},
{
id: 'subject',
type: BrowseByType.Metadata
}
]
}; };
// Item Page Config // Item Page Config

View File

@@ -1,5 +1,4 @@
// This configuration is only used for unit tests, end-to-end tests use environment.production.ts // This configuration is only used for unit tests, end-to-end tests use environment.production.ts
import { BrowseByType } from '../app/browse-by/browse-by-switcher/browse-by-decorator';
import { RestRequestMethod } from '../app/core/data/rest-request-method'; import { RestRequestMethod } from '../app/core/data/rest-request-method';
import { NotificationAnimationsType } from '../app/shared/notifications/models/notification-animations-type'; import { NotificationAnimationsType } from '../app/shared/notifications/models/notification-animations-type';
import { AppConfig } from '../config/app-config.interface'; import { AppConfig } from '../config/app-config.interface';
@@ -189,32 +188,6 @@ export const environment: AppConfig = {
fiveYearLimit: 30, fiveYearLimit: 30,
// The absolute lowest year to display in the dropdown (only used when no lowest date can be found for all items) // The absolute lowest year to display in the dropdown (only used when no lowest date can be found for all items)
defaultLowerLimit: 1900, defaultLowerLimit: 1900,
// List of all the active Browse-By types
// Adding a type will activate their Browse-By page and add them to the global navigation menu,
// as well as community and collection pages
// Allowed fields and their purpose:
// id: The browse id to use for fetching info from the rest api
// type: The type of Browse-By page to display
// metadataField: The metadata-field used to create starts-with options (only necessary when the type is set to 'date')
types: [
{
id: 'title',
type: BrowseByType.Title,
},
{
id: 'dateissued',
type: BrowseByType.Date,
metadataField: 'dc.date.issued'
},
{
id: 'author',
type: BrowseByType.Metadata
},
{
id: 'subject',
type: BrowseByType.Metadata
}
]
}, },
item: { item: {
edit: { edit: {