diff --git a/src/app/browse-by/browse-by-date-page/browse-by-date-page.component.ts b/src/app/browse-by/browse-by-date-page/browse-by-date-page.component.ts index 3158c3d7cc..e977b52ad6 100644 --- a/src/app/browse-by/browse-by-date-page/browse-by-date-page.component.ts +++ b/src/app/browse-by/browse-by-date-page/browse-by-date-page.component.ts @@ -12,7 +12,7 @@ import { ActivatedRoute, Params, Router } from '@angular/router'; import { BrowseService } from '../../core/browse/browse.service'; import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service'; 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 { PaginationService } from '../../core/pagination/pagination.service'; 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. * An example would be 'dateissued' for 'dc.date.issued' */ -@rendersBrowseBy(BrowseByType.Date) +@rendersBrowseBy(BrowseByDataType.Date) 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, protected browseService: BrowseService, @@ -59,13 +59,13 @@ export class BrowseByDatePageComponent extends BrowseByMetadataPageComponent { return [Object.assign({}, routeParams, queryParams, data), currentPage, currentSort]; }) ).subscribe(([params, currentPage, currentSort]: [Params, PaginationComponentOptions, SortOptions]) => { - const metadataField = params.metadataField || this.defaultMetadataField; - this.browseId = params.id || this.defaultBrowseId; - this.startsWith = +params.startsWith || params.startsWith; + const metadataKeys = params.browseDefinition ? params.browseDefinition.metadataKeys : this.defaultMetadataKeys; + this.browseId = params.id || this.defaultBrowseId; + this.startsWith = +params.startsWith || params.startsWith; const searchOptions = browseParamsToOptions(params, currentPage, currentSort, this.browseId); this.updatePageWithItems(searchOptions, this.value); 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. * 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 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 */ - updateStartsWithOptions(definition: string, metadataField: string, scope?: string) { + updateStartsWithOptions(definition: string, metadataKeys: string[], scope?: string) { this.subs.push( this.browseService.getFirstItemFor(definition, scope).subscribe((firstItemRD: RemoteData) => { let lowerLimit = environment.browseBy.defaultLowerLimit; if (hasValue(firstItemRD.payload)) { - const date = firstItemRD.payload.firstMetadataValue(metadataField); + const date = firstItemRD.payload.firstMetadataValue(metadataKeys); if (hasValue(date)) { const dateObj = new Date(date); // TODO: it appears that getFullYear (based on local time) is sometimes unreliable. Switching to UTC. @@ -120,5 +120,4 @@ export class BrowseByDatePageComponent extends BrowseByMetadataPageComponent { }) ); } - } diff --git a/src/app/browse-by/browse-by-guard.spec.ts b/src/app/browse-by/browse-by-guard.spec.ts index 4592f47175..fc483d87e2 100644 --- a/src/app/browse-by/browse-by-guard.spec.ts +++ b/src/app/browse-by/browse-by-guard.spec.ts @@ -1,20 +1,25 @@ import { first } from 'rxjs/operators'; import { BrowseByGuard } from './browse-by-guard'; 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('canActivate', () => { let guard: BrowseByGuard; let dsoService: any; let translateService: any; + let browseDefinitionService: any; const name = 'An interesting DSO'; const title = 'Author'; const field = 'Author'; const id = 'author'; - const metadataField = 'dc.contributor'; const scope = '1234-65487-12354-1235'; const value = 'Filter'; + const browseDefinition = Object.assign(new BrowseDefinition(), { type: BrowseByDataType.Metadata, metadataKeys: ['dc.contributor'] }); beforeEach(() => { dsoService = { @@ -24,14 +29,19 @@ describe('BrowseByGuard', () => { translateService = { 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', () => { const scopedRoute = { data: { title: field, - metadataField, + browseDefinition, }, params: { id, @@ -48,7 +58,7 @@ describe('BrowseByGuard', () => { const result = { title, id, - metadataField, + browseDefinition, collection: name, field, value: '"' + value + '"' @@ -63,7 +73,7 @@ describe('BrowseByGuard', () => { const scopedNoValueRoute = { data: { title: field, - metadataField, + browseDefinition, }, params: { id, @@ -80,7 +90,7 @@ describe('BrowseByGuard', () => { const result = { title, id, - metadataField, + browseDefinition, collection: name, field, value: '' @@ -95,7 +105,7 @@ describe('BrowseByGuard', () => { const route = { data: { title: field, - metadataField, + browseDefinition, }, params: { id, @@ -111,7 +121,7 @@ describe('BrowseByGuard', () => { const result = { title, id, - metadataField, + browseDefinition, collection: '', field, value: '"' + value + '"' diff --git a/src/app/browse-by/browse-by-guard.ts b/src/app/browse-by/browse-by-guard.ts index 8ac77bbd64..e4582cb77a 100644 --- a/src/app/browse-by/browse-by-guard.ts +++ b/src/app/browse-by/browse-by-guard.ts @@ -2,11 +2,12 @@ import { ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot } from '@angul import { Injectable } from '@angular/core'; import { DSpaceObjectDataService } from '../core/data/dspace-object-data.service'; import { hasNoValue, hasValue } from '../shared/empty.util'; -import { map } from 'rxjs/operators'; -import { getFirstSucceededRemoteData } from '../core/shared/operators'; +import { map, switchMap } from 'rxjs/operators'; +import { getFirstSucceededRemoteData, getFirstSucceededRemoteDataPayload } from '../core/shared/operators'; import { TranslateService } from '@ngx-translate/core'; -import { of as observableOf } from 'rxjs'; -import { environment } from '../../environments/environment'; +import { Observable, of as observableOf } from 'rxjs'; +import { BrowseDefinitionDataService } from '../core/browse/browse-definition-data.service'; +import { BrowseDefinition } from '../core/shared/browse-definition.model'; @Injectable() /** @@ -15,42 +16,46 @@ import { environment } from '../../environments/environment'; export class BrowseByGuard implements CanActivate { constructor(protected dsoService: DSpaceObjectDataService, - protected translate: TranslateService) { + protected translate: TranslateService, + protected browseDefinitionService: BrowseDefinitionDataService) { } canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) { const title = route.data.title; const id = route.params.id || route.queryParams.id || route.data.id; - let metadataField = route.data.metadataField; - if (hasNoValue(metadataField) && hasValue(id)) { - const config = environment.browseBy.types.find((conf) => conf.id === id); - if (hasValue(config) && hasValue(config.metadataField)) { - metadataField = config.metadataField; - } + let browseDefinition$: Observable; + if (hasNoValue(route.data.browseDefinition) && hasValue(id)) { + browseDefinition$ = this.browseDefinitionService.findById(id).pipe(getFirstSucceededRemoteDataPayload()); + } else { + browseDefinition$ = observableOf(route.data.browseDefinition); } const scope = route.queryParams.scope; const value = route.queryParams.value; const metadataTranslated = this.translate.instant('browse.metadata.' + id); - if (hasValue(scope)) { - const dsoAndMetadata$ = this.dsoService.findById(scope).pipe(getFirstSucceededRemoteData()); - return dsoAndMetadata$.pipe( - map((dsoRD) => { - const name = dsoRD.payload.name; - route.data = this.createData(title, id, metadataField, name, metadataTranslated, value, route); - return true; - }) - ); - } else { - route.data = this.createData(title, id, metadataField, '', metadataTranslated, value, route); - return observableOf(true); - } + return browseDefinition$.pipe( + switchMap((browseDefinition) => { + if (hasValue(scope)) { + const dsoAndMetadata$ = this.dsoService.findById(scope).pipe(getFirstSucceededRemoteData()); + return dsoAndMetadata$.pipe( + map((dsoRD) => { + 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, 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, { title: title, id: id, - metadataField: metadataField, + browseDefinition: browseDefinition, collection: collection, field: field, value: hasValue(value) ? `"${value}"` : '' diff --git a/src/app/browse-by/browse-by-metadata-page/browse-by-metadata-page.component.ts b/src/app/browse-by/browse-by-metadata-page/browse-by-metadata-page.component.ts index 3573ffb264..6655f98392 100644 --- a/src/app/browse-by/browse-by-metadata-page/browse-by-metadata-page.component.ts +++ b/src/app/browse-by/browse-by-metadata-page/browse-by-metadata-page.component.ts @@ -14,7 +14,7 @@ import { getFirstSucceededRemoteData } from '../../core/shared/operators'; import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service'; import { DSpaceObject } from '../../core/shared/dspace-object.model'; 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 { 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. * An example would be 'author' for 'dc.contributor.*' */ -@rendersBrowseBy(BrowseByType.Metadata) +@rendersBrowseBy(BrowseByDataType.Metadata) export class BrowseByMetadataPageComponent implements OnInit { /** diff --git a/src/app/browse-by/browse-by-switcher/browse-by-decorator.spec.ts b/src/app/browse-by/browse-by-switcher/browse-by-decorator.spec.ts index f54efb9378..19a6277151 100644 --- a/src/app/browse-by/browse-by-switcher/browse-by-decorator.spec.ts +++ b/src/app/browse-by/browse-by-switcher/browse-by-decorator.spec.ts @@ -1,9 +1,9 @@ -import { BrowseByType, rendersBrowseBy } from './browse-by-decorator'; +import { BrowseByDataType, rendersBrowseBy } from './browse-by-decorator'; describe('BrowseByDecorator', () => { - const titleDecorator = rendersBrowseBy(BrowseByType.Title); - const dateDecorator = rendersBrowseBy(BrowseByType.Date); - const metadataDecorator = rendersBrowseBy(BrowseByType.Metadata); + const titleDecorator = rendersBrowseBy(BrowseByDataType.Title); + const dateDecorator = rendersBrowseBy(BrowseByDataType.Date); + const metadataDecorator = rendersBrowseBy(BrowseByDataType.Metadata); it('should have a decorator for all types', () => { expect(titleDecorator.length).not.toEqual(0); expect(dateDecorator.length).not.toEqual(0); diff --git a/src/app/browse-by/browse-by-switcher/browse-by-decorator.ts b/src/app/browse-by/browse-by-switcher/browse-by-decorator.ts index efb4a4a9f4..1ebaa7face 100644 --- a/src/app/browse-by/browse-by-switcher/browse-by-decorator.ts +++ b/src/app/browse-by/browse-by-switcher/browse-by-decorator.ts @@ -2,13 +2,13 @@ import { hasNoValue } from '../../shared/empty.util'; import { InjectionToken } from '@angular/core'; import { GenericConstructor } from '../../core/shared/generic-constructor'; -export enum BrowseByType { +export enum BrowseByDataType { Title = 'title', - Metadata = 'metadata', + Metadata = 'text', 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>('getComponentByBrowseByType', { providedIn: 'root', @@ -21,7 +21,7 @@ const map = new Map(); * Decorator used for rendering Browse-By pages by type * @param browseByType The type of page */ -export function rendersBrowseBy(browseByType: BrowseByType) { +export function rendersBrowseBy(browseByType: BrowseByDataType) { return function decorator(component: any) { if (hasNoValue(map.get(browseByType))) { map.set(browseByType, component); diff --git a/src/app/browse-by/browse-by-switcher/browse-by-switcher.component.spec.ts b/src/app/browse-by/browse-by-switcher/browse-by-switcher.component.spec.ts index f340237e26..cb82ddb7c4 100644 --- a/src/app/browse-by/browse-by-switcher/browse-by-switcher.component.spec.ts +++ b/src/app/browse-by/browse-by-switcher/browse-by-switcher.component.spec.ts @@ -2,20 +2,46 @@ import { BrowseBySwitcherComponent } from './browse-by-switcher.component'; import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { NO_ERRORS_SCHEMA } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; -import { BehaviorSubject } from 'rxjs'; -import { environment } from '../../../environments/environment'; -import { BROWSE_BY_COMPONENT_FACTORY } from './browse-by-decorator'; +import { BROWSE_BY_COMPONENT_FACTORY, BrowseByDataType } from './browse-by-decorator'; +import { BrowseDefinition } from '../../core/shared/browse-definition.model'; +import { BehaviorSubject, of as observableOf } from 'rxjs'; describe('BrowseBySwitcherComponent', () => { let comp: BrowseBySwitcherComponent; let fixture: ComponentFixture; - 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 = { - params: params + data }; beforeEach(waitForAsync(() => { @@ -34,20 +60,20 @@ describe('BrowseBySwitcherComponent', () => { comp = fixture.componentInstance; })); - types.forEach((type) => { + types.forEach((type: BrowseDefinition) => { describe(`when switching to a browse-by page for "${type.id}"`, () => { beforeEach(() => { - params.next(createParamsWithId(type.id)); + data.next(createDataWithBrowseDefinition(type)); fixture.detectChanges(); }); - it(`should call getComponentByBrowseByType with type "${type.type}"`, () => { - expect((comp as any).getComponentByBrowseByType).toHaveBeenCalledWith(type.type); + it(`should call getComponentByBrowseByType with type "${type.dataType}"`, () => { + expect((comp as any).getComponentByBrowseByType).toHaveBeenCalledWith(type.dataType); }); }); }); }); -export function createParamsWithId(id) { - return { id: id }; +export function createDataWithBrowseDefinition(browseDefinition) { + return { browseDefinition: browseDefinition }; } diff --git a/src/app/browse-by/browse-by-switcher/browse-by-switcher.component.ts b/src/app/browse-by/browse-by-switcher/browse-by-switcher.component.ts index 043a4ce90a..cf4c1d9856 100644 --- a/src/app/browse-by/browse-by-switcher/browse-by-switcher.component.ts +++ b/src/app/browse-by/browse-by-switcher/browse-by-switcher.component.ts @@ -1,11 +1,10 @@ import { Component, Inject, OnInit } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { Observable } from 'rxjs'; -import { BrowseByTypeConfig } from '../../../config/browse-by-type-config.interface'; import { map } from 'rxjs/operators'; import { BROWSE_BY_COMPONENT_FACTORY } from './browse-by-decorator'; -import { environment } from '../../../environments/environment'; import { GenericConstructor } from '../../core/shared/generic-constructor'; +import { BrowseDefinition } from '../../core/shared/browse-definition.model'; @Component({ 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 { - this.browseByComponent = this.route.params.pipe( - map((params) => { - const id = params.id; - return environment.browseBy.types.find((config: BrowseByTypeConfig) => config.id === id); - }), - map((config: BrowseByTypeConfig) => this.getComponentByBrowseByType(config.type)) + this.browseByComponent = this.route.data.pipe( + map((data: { browseDefinition: BrowseDefinition }) => this.getComponentByBrowseByType(data.browseDefinition.dataType)) ); } diff --git a/src/app/browse-by/browse-by-title-page/browse-by-title-page.component.ts b/src/app/browse-by/browse-by-title-page/browse-by-title-page.component.ts index b3a2ceed00..b4a8331458 100644 --- a/src/app/browse-by/browse-by-title-page/browse-by-title-page.component.ts +++ b/src/app/browse-by/browse-by-title-page/browse-by-title-page.component.ts @@ -10,7 +10,7 @@ import { BrowseEntrySearchOptions } from '../../core/browse/browse-entry-search- import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service'; import { BrowseService } from '../../core/browse/browse.service'; 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 { map } from 'rxjs/operators'; 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) */ -@rendersBrowseBy(BrowseByType.Title) +@rendersBrowseBy(BrowseByDataType.Title) export class BrowseByTitlePageComponent extends BrowseByMetadataPageComponent { public constructor(protected route: ActivatedRoute, diff --git a/src/app/core/browse/browse-definition-data.service.spec.ts b/src/app/core/browse/browse-definition-data.service.spec.ts index 1127748ca9..d6770f80c0 100644 --- a/src/app/core/browse/browse-definition-data.service.spec.ts +++ b/src/app/core/browse/browse-definition-data.service.spec.ts @@ -9,9 +9,11 @@ describe(`BrowseDefinitionDataService`, () => { findAll: EMPTY, findByHref: EMPTY, findAllByHref: EMPTY, + findById: EMPTY, }); const hrefAll = 'https://rest.api/server/api/discover/browses'; const hrefSingle = 'https://rest.api/server/api/discover/browses/author'; + const id = 'author'; const options = new FindListOptions(); const linksToFollow = [ 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); + }); + }); }); diff --git a/src/app/core/browse/browse-definition-data.service.ts b/src/app/core/browse/browse-definition-data.service.ts index 31338417ca..dd66d8fa53 100644 --- a/src/app/core/browse/browse-definition-data.service.ts +++ b/src/app/core/browse/browse-definition-data.service.ts @@ -106,6 +106,21 @@ export class BrowseDefinitionDataService { findAllByHref(href: string, findListOptions: FindListOptions = {}, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig[]): Observable>> { 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[]): Observable> { + return this.dataService.findById(id, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow); + } } /* tslint:enable:max-classes-per-file */ diff --git a/src/app/core/shared/browse-definition.model.ts b/src/app/core/shared/browse-definition.model.ts index 2c08417b6d..68406f3f7d 100644 --- a/src/app/core/shared/browse-definition.model.ts +++ b/src/app/core/shared/browse-definition.model.ts @@ -6,6 +6,7 @@ import { BROWSE_DEFINITION } from './browse-definition.resource-type'; import { HALLink } from './hal-link.model'; import { ResourceType } from './resource-type'; import { SortOption } from './sort-option.model'; +import { BrowseByDataType } from '../../browse-by/browse-by-switcher/browse-by-decorator'; @typedObject export class BrowseDefinition extends CacheableObject { @@ -33,6 +34,9 @@ export class BrowseDefinition extends CacheableObject { @autoserializeAs('metadata') metadataKeys: string[]; + @autoserialize + dataType: BrowseByDataType; + get self(): string { return this._links.self.href; } diff --git a/src/app/navbar/navbar.component.spec.ts b/src/app/navbar/navbar.component.spec.ts index cbe6738241..5aa2bf1786 100644 --- a/src/app/navbar/navbar.component.spec.ts +++ b/src/app/navbar/navbar.component.spec.ts @@ -13,15 +13,48 @@ import { MenuService } from '../shared/menu/menu.service'; import { MenuServiceStub } from '../shared/testing/menu-service.stub'; import { ActivatedRoute } from '@angular/router'; 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 fixture: ComponentFixture; describe('NavbarComponent', () => { const menuService = new MenuServiceStub(); - + let browseDefinitions; // waitForAsync beforeEach 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({ imports: [ TranslateModule.forRoot(), @@ -33,7 +66,8 @@ describe('NavbarComponent', () => { Injector, { provide: MenuService, useValue: menuService }, { provide: HostWindowService, useValue: new HostWindowServiceStub(800) }, - { provide: ActivatedRoute, useValue: {} } + { provide: ActivatedRoute, useValue: {} }, + { provide: BrowseService, useValue: { getBrowseDefinitions: createSuccessfulRemoteDataObject$(buildPaginatedList(undefined, browseDefinitions)) } } ], schemas: [NO_ERRORS_SCHEMA] }) diff --git a/src/app/navbar/navbar.component.ts b/src/app/navbar/navbar.component.ts index e741cea285..c3b34d12ee 100644 --- a/src/app/navbar/navbar.component.ts +++ b/src/app/navbar/navbar.component.ts @@ -6,7 +6,11 @@ import { MenuID, MenuItemType } from '../shared/menu/initial-menus-state'; import { TextMenuItemModel } from '../shared/menu/menu-item/models/text.model'; import { LinkMenuItemModel } from '../shared/menu/menu-item/models/link.model'; 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 @@ -26,7 +30,8 @@ export class NavbarComponent extends MenuComponent { constructor(protected menuService: MenuService, protected injector: Injector, - public windowService: HostWindowService + public windowService: HostWindowService, + public browseService: BrowseService ) { super(menuService, injector); } @@ -52,37 +57,44 @@ export class NavbarComponent extends MenuComponent { text: `menu.section.browse_global_communities_and_collections`, link: `/community-list` } 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 - const types = environment.browseBy.types; - types.forEach((typeConfig) => { - menuList.push({ - id: `browse_global_by_${typeConfig.id}`, - parentID: 'browse_global', - active: false, - visible: true, - model: { - type: MenuItemType.LINK, - text: `menu.section.browse_global_by_${typeConfig.id}`, - link: `/browse/${typeConfig.id}` - } as LinkMenuItemModel + this.browseService.getBrowseDefinitions() + .pipe(getFirstCompletedRemoteData>()) + .subscribe((browseDefListRD: RemoteData>) => { + if (browseDefListRD.hasSucceeded) { + browseDefListRD.payload.page.forEach((browseDef: BrowseDefinition) => { + menuList.push({ + id: `browse_global_by_${browseDef.id}`, + parentID: 'browse_global', + active: false, + visible: true, + model: { + 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 - }))); } } diff --git a/src/app/shared/comcol-page-browse-by/comcol-page-browse-by.component.ts b/src/app/shared/comcol-page-browse-by/comcol-page-browse-by.component.ts index 01912dbcaa..08f7ec67ee 100644 --- a/src/app/shared/comcol-page-browse-by/comcol-page-browse-by.component.ts +++ b/src/app/shared/comcol-page-browse-by/comcol-page-browse-by.component.ts @@ -2,10 +2,13 @@ import { Component, Input, OnInit } from '@angular/core'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; 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 { 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 { id: string; @@ -29,10 +32,6 @@ export class ComcolPageBrowseByComponent implements OnInit { */ @Input() id: string; @Input() contentType: string; - /** - * List of currently active browse configurations - */ - types: BrowseByTypeConfig[]; allOptions: ComColPageNavOption[]; @@ -40,31 +39,39 @@ export class ComcolPageBrowseByComponent implements OnInit { constructor( private route: ActivatedRoute, - private router: Router) { + private router: Router, + private browseService: BrowseService + ) { } ngOnInit(): void { - this.allOptions = environment.browseBy.types - .map((config: BrowseByTypeConfig) => ({ - id: config.id, - label: `browse.comcol.by.${config.id}`, - routerLink: `/browse/${config.id}`, - params: { scope: this.id } - })); + this.browseService.getBrowseDefinitions() + .pipe(getFirstCompletedRemoteData>()) + .subscribe((browseDefListRD: RemoteData>) => { + if (browseDefListRD.hasSucceeded) { + this.allOptions = browseDefListRD.payload.page + .map((config: BrowseDefinition) => ({ + id: config.id, + label: `browse.comcol.by.${config.id}`, + routerLink: `/browse/${config.id}`, + params: { scope: this.id } + })); - if (this.contentType === 'collection') { - this.allOptions = [ { - id: this.id, - label: 'collection.page.browse.recent.head', - routerLink: getCollectionPageRoute(this.id) - }, ...this.allOptions ]; - } else if (this.contentType === 'community') { - this.allOptions = [{ - id: this.id, - label: 'community.all-lists.head', - routerLink: getCommunityPageRoute(this.id) - }, ...this.allOptions ]; - } + if (this.contentType === 'collection') { + this.allOptions = [{ + id: this.id, + label: 'collection.page.browse.recent.head', + routerLink: getCollectionPageRoute(this.id) + }, ...this.allOptions]; + } else if (this.contentType === 'community') { + this.allOptions = [{ + id: this.id, + label: 'community.all-lists.head', + routerLink: getCommunityPageRoute(this.id) + }, ...this.allOptions]; + } + } + }); this.currentOptionId$ = this.route.params.pipe( map((params: Params) => params.id) diff --git a/src/app/submission/sections/upload/file/edit/section-upload-file-edit.component.html b/src/app/submission/sections/upload/file/edit/section-upload-file-edit.component.html index f6e0646974..2baa6c1555 100644 --- a/src/app/submission/sections/upload/file/edit/section-upload-file-edit.component.html +++ b/src/app/submission/sections/upload/file/edit/section-upload-file-edit.component.html @@ -1,9 +1,21 @@
- + +
diff --git a/src/app/submission/sections/upload/file/edit/section-upload-file-edit.component.spec.ts b/src/app/submission/sections/upload/file/edit/section-upload-file-edit.component.spec.ts index a644cf8270..aa03d37eb2 100644 --- a/src/app/submission/sections/upload/file/edit/section-upload-file-edit.component.spec.ts +++ b/src/app/submission/sections/upload/file/edit/section-upload-file-edit.component.spec.ts @@ -1,5 +1,5 @@ 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 { CommonModule } from '@angular/common'; import { TranslateModule } from '@ngx-translate/core'; @@ -17,19 +17,37 @@ import { SubmissionService } from '../../../../submission.service'; import { SubmissionSectionUploadFileEditComponent } from './section-upload-file-edit.component'; import { POLICY_DEFAULT_WITH_LIST } from '../../section-upload.component'; import { - mockGroup, mockSubmissionCollectionId, mockSubmissionId, mockUploadConfigResponse, mockUploadConfigResponseMetadata, - mockUploadFiles + mockUploadFiles, + mockFileFormData, + mockSubmissionObject, } from '../../../../../shared/mocks/submission.mock'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { FormComponent } from '../../../../../shared/form/form.component'; import { FormService } from '../../../../../shared/form/form.service'; import { getMockFormService } from '../../../../../shared/mocks/form-service.mock'; -import { Group } from '../../../../../core/eperson/models/group.model'; 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', () => { @@ -38,7 +56,12 @@ describe('SubmissionSectionUploadFileEditComponent test suite', () => { let fixture: ComponentFixture; let submissionServiceStub: SubmissionServiceStub; let formbuilderService: any; + let operationsBuilder: any; + let operationsService: any; + let formService: any; + let uploadService: any; + const submissionJsonPatchOperationsServiceStub = new SubmissionJsonPatchOperationsServiceStub(); const submissionId = mockSubmissionId; const sectionId = 'upload'; const collectionId = mockSubmissionCollectionId; @@ -48,6 +71,7 @@ describe('SubmissionSectionUploadFileEditComponent test suite', () => { const fileIndex = '0'; const fileId = '123456-test-upload'; const fileData: any = mockUploadFiles[0]; + const pathCombiner = new JsonPatchOperationPathCombiner('sections', sectionId, 'files', fileIndex); beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ @@ -66,9 +90,15 @@ describe('SubmissionSectionUploadFileEditComponent test suite', () => { providers: [ { provide: FormService, useValue: getMockFormService() }, { provide: SubmissionService, useClass: SubmissionServiceStub }, + { provide: SubmissionJsonPatchOperationsService, useValue: submissionJsonPatchOperationsServiceStub }, + { provide: JsonPatchOperationsBuilder, useValue: jsonPatchOpBuilder }, + { provide: SectionUploadService, useValue: getMockSectionUploadService() }, FormBuilderService, ChangeDetectorRef, - SubmissionSectionUploadFileEditComponent + SubmissionSectionUploadFileEditComponent, + NgbModal, + NgbActiveModal, + FormComponent, ], schemas: [NO_ERRORS_SCHEMA] }).compileComponents().then(); @@ -114,6 +144,10 @@ describe('SubmissionSectionUploadFileEditComponent test suite', () => { compAsAny = comp; submissionServiceStub = TestBed.inject(SubmissionService as any); formbuilderService = TestBed.inject(FormBuilderService); + operationsBuilder = TestBed.inject(JsonPatchOperationsBuilder); + operationsService = TestBed.inject(SubmissionJsonPatchOperationsService); + formService = TestBed.inject(FormService); + uploadService = TestBed.inject(SectionUploadService); comp.submissionId = submissionId; comp.collectionId = collectionId; @@ -123,6 +157,9 @@ describe('SubmissionSectionUploadFileEditComponent test suite', () => { comp.fileIndex = fileIndex; comp.fileId = fileId; comp.configMetadataForm = configMetadataForm; + comp.formMetadata = formMetadataMock; + + formService.isValid.and.returnValue(of(true)); }); afterEach(() => { @@ -135,7 +172,7 @@ describe('SubmissionSectionUploadFileEditComponent test suite', () => { comp.fileData = fileData; comp.formId = 'testFileForm'; - comp.ngOnChanges(); + comp.ngOnInit(); expect(comp.formModel).toBeDefined(); expect(comp.formModel.length).toBe(2); @@ -165,7 +202,7 @@ describe('SubmissionSectionUploadFileEditComponent test suite', () => { comp.fileData = fileData; comp.formId = 'testFileForm'; - comp.ngOnChanges(); + comp.ngOnInit(); const model: DynamicSelectModel = formbuilderService.findById('name', comp.formModel, 0); const formGroup = formbuilderService.createFormGroup(comp.formModel); @@ -186,6 +223,82 @@ describe('SubmissionSectionUploadFileEditComponent test suite', () => { comp.setOptions(model, control); 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(); + + })); + }); }); diff --git a/src/app/submission/sections/upload/file/edit/section-upload-file-edit.component.ts b/src/app/submission/sections/upload/file/edit/section-upload-file-edit.component.ts index 96725f151e..3a43e718a0 100644 --- a/src/app/submission/sections/upload/file/edit/section-upload-file-edit.component.ts +++ b/src/app/submission/sections/upload/file/edit/section-upload-file-edit.component.ts @@ -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 { @@ -32,13 +32,23 @@ import { BITSTREAM_METADATA_FORM_GROUP_LAYOUT } from './section-upload-file-edit.model'; 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 { FormFieldModel } from '../../../../../shared/form/builder/models/form-field.model'; import { AccessConditionOption } from '../../../../../core/config/models/config-access-condition-option.model'; import { SubmissionService } from '../../../../submission.service'; import { FormService } from '../../../../../shared/form/form.service'; 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 @@ -48,105 +58,246 @@ import { FormComponent } from '../../../../../shared/form/form.component'; styleUrls: ['./section-upload-file-edit.component.scss'], templateUrl: './section-upload-file-edit.component.html', }) -export class SubmissionSectionUploadFileEditComponent implements OnChanges { - - /** - * 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[]; +export class SubmissionSectionUploadFileEditComponent implements OnInit { /** * The FormComponent reference */ @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 * + * @param activeModal * @param {ChangeDetectorRef} cdr * @param {FormBuilderService} formBuilderService * @param {FormService} formService * @param {SubmissionService} submissionService + * @param {JsonPatchOperationsBuilder} operationsBuilder + * @param {SubmissionJsonPatchOperationsService} operationsService + * @param {SectionUploadService} uploadService */ - constructor(private cdr: ChangeDetectorRef, - private formBuilderService: FormBuilderService, - private formService: FormService, - private submissionService: SubmissionService) { + constructor( + protected activeModal: NgbActiveModal, + private cdr: ChangeDetectorRef, + 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 */ - ngOnChanges() { + ngOnInit() { if (this.fileData && this.formId) { this.formModel = this.buildFileEditForm(); 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 */ @@ -193,17 +344,17 @@ export class SubmissionSectionUploadFileEditComponent implements OnChanges { const showEnd: boolean = condition.hasEndDate === true; const showGroups: boolean = showStart || showEnd; if (showStart) { - hasStart.push({ id: 'name', value: condition.name }); + hasStart.push({id: 'name', value: condition.name}); } if (showEnd) { - hasEnd.push({ id: 'name', value: condition.name }); + hasEnd.push({id: 'name', value: condition.name}); } 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 confEnd = { relations: [{ match: MATCH_ENABLED, operator: OR_OPERATOR, when: hasEnd }] }; + const confStart = {relations: [{match: MATCH_ENABLED, operator: OR_OPERATOR, when: hasStart}]}; + const confEnd = {relations: [{match: MATCH_ENABLED, operator: OR_OPERATOR, when: hasEnd}]}; accessConditionsArrayConfig.groupFactory = () => { 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 endDate = new DynamicDatePickerModel(endDateConfig, BITSTREAM_FORM_ACCESS_CONDITION_END_DATE_LAYOUT); 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)]; }; @@ -229,98 +382,95 @@ export class SubmissionSectionUploadFileEditComponent implements OnChanges { } /** - * Initialize form model values - * - * @param formModel - * The form model + * Save bitstream metadata */ - 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]; + saveBitstreamData() { + // validate form + this.formService.validateAllFormFields(this.formRef.formGroup); + const saveBitstreamDataSubscription = this.formService.isValid(this.formId).pipe( + take(1), + filter((isValid) => isValid), + mergeMap(() => this.formService.getFormData(this.formId)), + take(1), + mergeMap((formData: any) => { + // collect bitstream metadata + Object.keys((formData.metadata)) + .filter((key) => isNotEmpty(formData.metadata[key])) + .forEach((key) => { + const metadataKey = key.replace(/_/g, '.'); + const path = `metadata/${metadataKey}`; + 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); } - } - }); - }); - } + }); - /** - * Dispatch form model update when changing an access condition - * - * @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 (isNotEmpty(accessConditionsToSave)) { + this.operationsBuilder.add(this.pathCombiner.getPath('accessConditions'), accessConditionsToSave, true); } - 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 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.isSaving = false; + this.activeModal.close(); + }); + this.subscriptions.push(saveBitstreamDataSubscription); + } + + private unsubscribeAll() { + this.subscriptions.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe()); } } diff --git a/src/app/submission/sections/upload/file/section-upload-file.component.html b/src/app/submission/sections/upload/file/section-upload-file.component.html index 259418c22c..1bfc52529b 100644 --- a/src/app/submission/sections/upload/file/section-upload-file.component.html +++ b/src/app/submission/sections/upload/file/section-upload-file.component.html @@ -8,15 +8,15 @@

{{fileName}} ({{fileData?.sizeBytes | dsFileSize}})

-
- +
+ - - - - -
- - +
diff --git a/src/app/submission/sections/upload/file/section-upload-file.component.scss b/src/app/submission/sections/upload/file/section-upload-file.component.scss index 256775eb66..e69de29bb2 100644 --- a/src/app/submission/sections/upload/file/section-upload-file.component.scss +++ b/src/app/submission/sections/upload/file/section-upload-file.component.scss @@ -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); -} diff --git a/src/app/submission/sections/upload/file/section-upload-file.component.spec.ts b/src/app/submission/sections/upload/file/section-upload-file.component.spec.ts index 39aebf7413..4fea8d3f25 100644 --- a/src/app/submission/sections/upload/file/section-upload-file.component.spec.ts +++ b/src/app/submission/sections/upload/file/section-upload-file.component.spec.ts @@ -1,9 +1,9 @@ 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 { CommonModule } from '@angular/common'; -import { of as observableOf } from 'rxjs'; +import { of, of as observableOf } from 'rxjs'; import { TranslateModule } from '@ngx-translate/core'; import { FormService } from '../../../../shared/form/form.service'; @@ -17,10 +17,8 @@ import { SubmissionJsonPatchOperationsService } from '../../../../core/submissio import { SubmissionSectionUploadFileComponent } from './section-upload-file.component'; import { SubmissionServiceStub } from '../../../../shared/testing/submission-service.stub'; import { - mockFileFormData, mockSubmissionCollectionId, mockSubmissionId, - mockSubmissionObject, mockUploadConfigResponse, mockUploadFiles } 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 { JsonPatchOperationPathCombiner } from '../../../../core/json-patch/builder/json-patch-operation-path-combiner'; 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 { 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', () => { @@ -117,6 +124,7 @@ describe('SubmissionSectionUploadFileComponent test suite', () => { testFixture = createTestComponent(html, TestComponent) as ComponentFixture; testComp = testFixture.componentInstance; + }); afterEach(() => { @@ -124,9 +132,7 @@ describe('SubmissionSectionUploadFileComponent test suite', () => { }); it('should create SubmissionSectionUploadFileComponent', inject([SubmissionSectionUploadFileComponent], (app: SubmissionSectionUploadFileComponent) => { - expect(app).toBeDefined(); - })); }); @@ -135,6 +141,7 @@ describe('SubmissionSectionUploadFileComponent test suite', () => { fixture = TestBed.createComponent(SubmissionSectionUploadFileComponent); comp = fixture.componentInstance; compAsAny = comp; + compAsAny.configMetadataForm = configMetadataFormMock; submissionServiceStub = TestBed.inject(SubmissionService as any); uploadService = TestBed.inject(SectionUploadService); formService = TestBed.inject(FormService); @@ -210,96 +217,20 @@ describe('SubmissionSectionUploadFileComponent test suite', () => { pathCombiner.subRootElement); }); - it('should save Bitstream File data properly when form is 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(true)); - formService.getFormData.and.returnValue(observableOf(mockFileFormData)); + it('should open edit modal when edit button is clicked', () => { + spyOn(compAsAny, 'editBitstreamData').and.callThrough(); + comp.fileData = fileData; - const response = [ - Object.assign(mockSubmissionObject, { - sections: { - upload: { - files: mockUploadFiles - } - } - }) - ]; - operationsService.jsonPatchByResourceID.and.returnValue(observableOf(response)); + fixture.detectChanges(); - const accessConditionsToSave = [ - { name: 'openaccess' }, - { name: 'lease', endDate: dateToISOFormat('2019-01-16T00:00:00Z') }, - { name: 'embargo', startDate: dateToISOFormat('2019-01-16T00:00:00Z') }, - ]; - comp.saveBitstreamData(event); - tick(); + const modalBtn = fixture.debugElement.query(By.css('.fa-edit ')); - let path = 'metadata/dc.title'; - expect(operationsBuilder.add).toHaveBeenCalledWith( - pathCombiner.getPath(path), - mockFileFormData.metadata['dc.title'], - true - ); + modalBtn.nativeElement.click(); + fixture.detectChanges(); - 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(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'); + expect(compAsAny.editBitstreamData).toHaveBeenCalled(); }); - 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; collectionId = mockSubmissionCollectionId; collectionPolicyType; - configMetadataForm$; + configMetadataForm$ = of(configMetadataFormMock); fileIndexes = []; fileList = []; fileNames = []; diff --git a/src/app/submission/sections/upload/file/section-upload-file.component.ts b/src/app/submission/sections/upload/file/section-upload-file.component.ts index ac6c0d70c4..53358d48e2 100644 --- a/src/app/submission/sections/upload/file/section-upload-file.component.ts +++ b/src/app/submission/sections/upload/file/section-upload-file.component.ts @@ -1,25 +1,23 @@ import { ChangeDetectorRef, Component, Input, OnChanges, OnInit, ViewChild } from '@angular/core'; 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 { NgbModal } from '@ng-bootstrap/ng-bootstrap'; 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 { JsonPatchOperationsBuilder } from '../../../../core/json-patch/builder/json-patch-operations-builder'; import { JsonPatchOperationPathCombiner } from '../../../../core/json-patch/builder/json-patch-operation-path-combiner'; import { WorkspaceitemSectionUploadFileObject } from '../../../../core/submission/models/workspaceitem-section-upload-file.model'; import { SubmissionFormsModel } from '../../../../core/config/models/config-submission-forms.model'; -import { dateToISOFormat } from '../../../../shared/date.util'; import { SubmissionService } from '../../../submission.service'; import { HALEndpointService } from '../../../../core/shared/hal-endpoint.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 { 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 @@ -87,6 +85,13 @@ export class SubmissionSectionUploadFileComponent implements OnChanges, OnInit { */ @Input() submissionId: string; + /** + * The [[SubmissionSectionUploadFileEditComponent]] reference + * @type {SubmissionSectionUploadFileEditComponent} + */ + @ViewChild(SubmissionSectionUploadFileEditComponent) fileEditComp: SubmissionSectionUploadFileEditComponent; + + /** * The bitstream's metadata data * @type {WorkspaceitemSectionUploadFileObject} @@ -130,10 +135,10 @@ export class SubmissionSectionUploadFileComponent implements OnChanges, OnInit { protected subscriptions: Subscription[] = []; /** - * The [[SubmissionSectionUploadFileEditComponent]] reference - * @type {SubmissionSectionUploadFileEditComponent} + * Array containing all the form metadata defined in configMetadataForm + * @type {Array} */ - @ViewChild(SubmissionSectionUploadFileEditComponent) fileEditComp: SubmissionSectionUploadFileEditComponent; + protected formMetadata: string[] = []; /** * Initialize instance variables @@ -147,14 +152,16 @@ export class SubmissionSectionUploadFileComponent implements OnChanges, OnInit { * @param {SubmissionService} submissionService * @param {SectionUploadService} uploadService */ - constructor(private cdr: ChangeDetectorRef, - private formService: FormService, - private halService: HALEndpointService, - private modalService: NgbModal, - private operationsBuilder: JsonPatchOperationsBuilder, - private operationsService: SubmissionJsonPatchOperationsService, - private submissionService: SubmissionService, - private uploadService: SectionUploadService) { + constructor( + private cdr: ChangeDetectorRef, + private formService: FormService, + private halService: HALEndpointService, + private modalService: NgbModal, + private operationsBuilder: JsonPatchOperationsBuilder, + private operationsService: SubmissionJsonPatchOperationsService, + private submissionService: SubmissionService, + private uploadService: SectionUploadService, + ) { this.readMode = true; } @@ -182,22 +189,7 @@ export class SubmissionSectionUploadFileComponent implements OnChanges, OnInit { ngOnInit() { this.formId = this.formService.getUniqueId(this.fileId); this.pathCombiner = new JsonPatchOperationPathCombiner('sections', this.sectionId, 'files', this.fileIndex); - } - - /** - * 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); - })); + this.loadFormMetadata(); } /** @@ -225,98 +217,63 @@ export class SubmissionSectionUploadFileComponent implements OnChanges, OnInit { }); } - /** - * Save bitstream metadata - * - * @param event - * the click event emitted - */ - public saveBitstreamData(event) { - event.preventDefault(); + editBitstreamData() { - // validate form - this.formService.validateAllFormFields(this.fileEditComp.formRef.formGroup); - this.subscriptions.push(this.formService.isValid(this.formId).pipe( - take(1), - filter((isValid) => isValid), - mergeMap(() => this.formService.getFormData(this.formId)), - take(1), - mergeMap((formData: any) => { - // collect bitstream metadata - Object.keys((formData.metadata)) - .filter((key) => isNotEmpty(formData.metadata[key])) - .forEach((key) => { - const metadataKey = key.replace(/_/g, '.'); - const path = `metadata/${metadataKey}`; - this.operationsBuilder.add(this.pathCombiner.getPath(path), formData.metadata[key], true); + const options: NgbModalOptions = { + size: 'xl', + backdrop: 'static', + }; + + const activeModal = this.modalService.open(SubmissionSectionUploadFileEditComponent, options); + + activeModal.componentInstance.availableAccessConditionOptions = this.availableAccessConditionOptions; + activeModal.componentInstance.collectionId = this.collectionId; + activeModal.componentInstance.collectionPolicyType = this.collectionPolicyType; + activeModal.componentInstance.configMetadataForm = this.configMetadataForm; + activeModal.componentInstance.fileData = this.fileData; + activeModal.componentInstance.fileId = this.fileId; + activeModal.componentInstance.fileIndex = this.fileIndex; + 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 - * - * @param field - * the specified field object + * Delete bitstream from submission */ - private retrieveValueFromField(field: any) { - const temp = Array.isArray(field) ? field[0] : field; - return (temp) ? temp.value : undefined; - } - - /** - * Switch from edit form to metadata view - */ - public switchMode() { - this.readMode = !this.readMode; - this.cdr.detectChanges(); + 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); + })); } } diff --git a/src/config/app-config.interface.ts b/src/config/app-config.interface.ts index 4c15e6dbc1..62d0be7216 100644 --- a/src/config/app-config.interface.ts +++ b/src/config/app-config.interface.ts @@ -1,4 +1,5 @@ import { InjectionToken } from '@angular/core'; +import { makeStateKey } from '@angular/platform-browser'; import { Config } from './config.interface'; import { ServerConfig } from './server-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 { FormConfig } from './form-config.interfaces'; import { LangConfig } from './lang-config.interface'; -import { BrowseByConfig } from './browse-by-config.interface'; import { ItemPageConfig } from './item-page-config.interface'; import { CollectionPageConfig } from './collection-page-config.interface'; import { ThemeConfig } from './theme.model'; import { AuthConfig } from './auth-config.interfaces'; import { UIServerConfig } from './ui-server-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 { ui: UIServerConfig; diff --git a/src/config/browse-by-config.interface.ts b/src/config/browse-by-config.interface.ts index 719e127b4b..6adba66b92 100644 --- a/src/config/browse-by-config.interface.ts +++ b/src/config/browse-by-config.interface.ts @@ -1,5 +1,4 @@ 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 @@ -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 */ defaultLowerLimit: number; - - /** - * A list of all the active Browse-By pages - */ - types: BrowseByTypeConfig[]; } diff --git a/src/config/browse-by-type-config.interface.ts b/src/config/browse-by-type-config.interface.ts deleted file mode 100644 index f15846c210..0000000000 --- a/src/config/browse-by-type-config.interface.ts +++ /dev/null @@ -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; -} diff --git a/src/config/default-app-config.ts b/src/config/default-app-config.ts index 77b53cee35..9205df1002 100644 --- a/src/config/default-app-config.ts +++ b/src/config/default-app-config.ts @@ -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 { NotificationAnimationsType } from '../app/shared/notifications/models/notification-animations-type'; 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) fiveYearLimit: 30, // The absolute lowest year to display in the dropdown (only used when no lowest date can be found for all items) - 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 - } - ] + defaultLowerLimit: 1900 }; // Item Page Config diff --git a/src/environments/environment.test.ts b/src/environments/environment.test.ts index 0cf069bd56..f8a3248837 100644 --- a/src/environments/environment.test.ts +++ b/src/environments/environment.test.ts @@ -1,5 +1,4 @@ // 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 { NotificationAnimationsType } from '../app/shared/notifications/models/notification-animations-type'; import { AppConfig } from '../config/app-config.interface'; @@ -189,32 +188,6 @@ export const environment: AppConfig = { fiveYearLimit: 30, // The absolute lowest year to display in the dropdown (only used when no lowest date can be found for all items) 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: { edit: {