diff --git a/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.html b/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.html index cbb587cca48..4d3b948a583 100644 --- a/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.html +++ b/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.html @@ -12,7 +12,7 @@

{{bitstreamRD?.payload?.name}} ({{bitstreamRD?.payl - div > label { + margin-top: 1.75rem; +} + diff --git a/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.spec.ts b/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.spec.ts index 67f8866e6df..44e48182fd9 100644 --- a/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.spec.ts +++ b/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.spec.ts @@ -22,6 +22,8 @@ import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } f import { getEntityEditRoute } from '../../item-page/item-page-routing-paths'; import { createPaginatedList } from '../../shared/testing/utils.test'; import { Item } from '../../core/shared/item.model'; +import { MetadataValueFilter } from '../../core/shared/metadata.models'; +import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; const infoNotification: INotification = new Notification('id', NotificationType.Info, 'info'); const warningNotification: INotification = new Notification('id', NotificationType.Warning, 'warning'); @@ -31,24 +33,27 @@ let notificationsService: NotificationsService; let formService: DynamicFormService; let bitstreamService: BitstreamDataService; let bitstreamFormatService: BitstreamFormatDataService; +let dsoNameService: DSONameService; let bitstream: Bitstream; let selectedFormat: BitstreamFormat; let allFormats: BitstreamFormat[]; let router: Router; +let comp: EditBitstreamPageComponent; +let fixture: ComponentFixture; + describe('EditBitstreamPageComponent', () => { - let comp: EditBitstreamPageComponent; - let fixture: ComponentFixture; - beforeEach(waitForAsync(() => { + beforeEach(() => { allFormats = [ Object.assign({ id: '1', shortDescription: 'Unknown', description: 'Unknown format', supportLevel: BitstreamFormatSupportLevel.Unknown, + mimetype: 'application/octet-stream', _links: { - self: { href: 'format-selflink-1' } + self: {href: 'format-selflink-1'} } }), Object.assign({ @@ -56,8 +61,9 @@ describe('EditBitstreamPageComponent', () => { shortDescription: 'PNG', description: 'Portable Network Graphics', supportLevel: BitstreamFormatSupportLevel.Known, + mimetype: 'image/png', _links: { - self: { href: 'format-selflink-2' } + self: {href: 'format-selflink-2'} } }), Object.assign({ @@ -65,19 +71,14 @@ describe('EditBitstreamPageComponent', () => { shortDescription: 'GIF', description: 'Graphics Interchange Format', supportLevel: BitstreamFormatSupportLevel.Known, + mimetype: 'image/gif', _links: { - self: { href: 'format-selflink-3' } + self: {href: 'format-selflink-3'} } }) ] as BitstreamFormat[]; selectedFormat = allFormats[1]; - notificationsService = jasmine.createSpyObj('notificationsService', - { - info: infoNotification, - warning: warningNotification, - success: successNotification - } - ); + formService = Object.assign({ createFormGroup: (fModel: DynamicFormControlModel[]) => { const controls = {}; @@ -90,156 +91,418 @@ describe('EditBitstreamPageComponent', () => { return undefined; } }); - bitstream = Object.assign(new Bitstream(), { - metadata: { - 'dc.description': [ - { - value: 'Bitstream description' - } - ], - 'dc.title': [ - { - value: 'Bitstream title' - } - ] - }, - format: createSuccessfulRemoteDataObject$(selectedFormat), - _links: { - self: 'bitstream-selflink' - }, - bundle: createSuccessfulRemoteDataObject$({ - item: createSuccessfulRemoteDataObject$(Object.assign(new Item(), { - uuid: 'some-uuid' - })) - }) - }); - bitstreamService = jasmine.createSpyObj('bitstreamService', { - findById: createSuccessfulRemoteDataObject$(bitstream), - update: createSuccessfulRemoteDataObject$(bitstream), - updateFormat: createSuccessfulRemoteDataObject$(bitstream), - commitUpdates: {}, - patch: {} - }); + bitstreamFormatService = jasmine.createSpyObj('bitstreamFormatService', { findAll: createSuccessfulRemoteDataObject$(createPaginatedList(allFormats)) }); - TestBed.configureTestingModule({ - imports: [TranslateModule.forRoot(), RouterTestingModule], - declarations: [EditBitstreamPageComponent, FileSizePipe, VarDirective], - providers: [ - { provide: NotificationsService, useValue: notificationsService }, - { provide: DynamicFormService, useValue: formService }, - { provide: ActivatedRoute, useValue: { data: observableOf({ bitstream: createSuccessfulRemoteDataObject(bitstream) }), snapshot: { queryParams: {} } } }, - { provide: BitstreamDataService, useValue: bitstreamService }, - { provide: BitstreamFormatDataService, useValue: bitstreamFormatService }, - ChangeDetectorRef - ], - schemas: [NO_ERRORS_SCHEMA] - }).compileComponents(); - - })); - - beforeEach(() => { - fixture = TestBed.createComponent(EditBitstreamPageComponent); - comp = fixture.componentInstance; - fixture.detectChanges(); - router = TestBed.inject(Router); - spyOn(router, 'navigate'); + notificationsService = jasmine.createSpyObj('notificationsService', + { + info: infoNotification, + warning: warningNotification, + success: successNotification + } + ); }); - describe('on startup', () => { - let rawForm; + describe('EditBitstreamPageComponent no IIIF fields', () => { - beforeEach(() => { - rawForm = comp.formGroup.getRawValue(); - }); + beforeEach(waitForAsync(() => { - it('should fill in the bitstream\'s title', () => { - expect(rawForm.fileNamePrimaryContainer.fileName).toEqual(bitstream.name); - }); + const bundleName = 'ORIGINAL'; - it('should fill in the bitstream\'s description', () => { - expect(rawForm.descriptionContainer.description).toEqual(bitstream.firstMetadataValue('dc.description')); - }); + bitstream = Object.assign(new Bitstream(), { + metadata: { + 'dc.description': [ + { + value: 'Bitstream description' + } + ], + 'dc.title': [ + { + value: 'Bitstream title' + } + ] + }, + format: createSuccessfulRemoteDataObject$(selectedFormat), + _links: { + self: 'bitstream-selflink' + }, + bundle: createSuccessfulRemoteDataObject$({ + item: createSuccessfulRemoteDataObject$(Object.assign(new Item(), { + uuid: 'some-uuid', + firstMetadataValue(keyOrKeys: string | string[], valueFilter?: MetadataValueFilter): string { + return undefined; + }, + })) + }) + }); + bitstreamService = jasmine.createSpyObj('bitstreamService', { + findById: createSuccessfulRemoteDataObject$(bitstream), + update: createSuccessfulRemoteDataObject$(bitstream), + updateFormat: createSuccessfulRemoteDataObject$(bitstream), + commitUpdates: {}, + patch: {} + }); + bitstreamFormatService = jasmine.createSpyObj('bitstreamFormatService', { + findAll: createSuccessfulRemoteDataObject$(createPaginatedList(allFormats)) + }); + dsoNameService = jasmine.createSpyObj('dsoNameService', { + getName: bundleName + }); - it('should select the correct format', () => { - expect(rawForm.formatContainer.selectedFormat).toEqual(selectedFormat.id); - }); + TestBed.configureTestingModule({ + imports: [TranslateModule.forRoot(), RouterTestingModule], + declarations: [EditBitstreamPageComponent, FileSizePipe, VarDirective], + providers: [ + {provide: NotificationsService, useValue: notificationsService}, + {provide: DynamicFormService, useValue: formService}, + {provide: ActivatedRoute, + useValue: { + data: observableOf({bitstream: createSuccessfulRemoteDataObject(bitstream)}), + snapshot: {queryParams: {}} + } + }, + {provide: BitstreamDataService, useValue: bitstreamService}, + {provide: DSONameService, useValue: dsoNameService}, + {provide: BitstreamFormatDataService, useValue: bitstreamFormatService}, + ChangeDetectorRef + ], + schemas: [NO_ERRORS_SCHEMA] + }).compileComponents(); - it('should put the \"New Format\" input on invisible', () => { - expect(comp.formLayout.newFormat.grid.host).toContain('invisible'); - }); - }); + })); - describe('when an unknown format is selected', () => { beforeEach(() => { - comp.updateNewFormatLayout(allFormats[0].id); + fixture = TestBed.createComponent(EditBitstreamPageComponent); + comp = fixture.componentInstance; + fixture.detectChanges(); + router = TestBed.inject(Router); + spyOn(router, 'navigate'); }); - it('should remove the invisible class from the \"New Format\" input', () => { - expect(comp.formLayout.newFormat.grid.host).not.toContain('invisible'); - }); - }); + describe('on startup', () => { + let rawForm; - describe('onSubmit', () => { - describe('when selected format hasn\'t changed', () => { beforeEach(() => { - comp.onSubmit(); + rawForm = comp.formGroup.getRawValue(); + }); + + it('should fill in the bitstream\'s title', () => { + expect(rawForm.fileNamePrimaryContainer.fileName).toEqual(bitstream.name); }); - it('should call update', () => { - expect(bitstreamService.update).toHaveBeenCalled(); + it('should fill in the bitstream\'s description', () => { + expect(rawForm.descriptionContainer.description).toEqual(bitstream.firstMetadataValue('dc.description')); }); - it('should commit the updates', () => { - expect(bitstreamService.commitUpdates).toHaveBeenCalled(); + it('should select the correct format', () => { + expect(rawForm.formatContainer.selectedFormat).toEqual(selectedFormat.id); + }); + + it('should put the \"New Format\" input on invisible', () => { + expect(comp.formLayout.newFormat.grid.host).toContain('invisible'); }); }); - describe('when selected format has changed', () => { + describe('when an unknown format is selected', () => { beforeEach(() => { - comp.formGroup.patchValue({ - formatContainer: { - selectedFormat: allFormats[2].id - } - }); - fixture.detectChanges(); - comp.onSubmit(); + comp.updateNewFormatLayout(allFormats[0].id); }); - it('should call update', () => { - expect(bitstreamService.update).toHaveBeenCalled(); + it('should remove the invisible class from the \"New Format\" input', () => { + expect(comp.formLayout.newFormat.grid.host).not.toContain('invisible'); }); + }); + + describe('onSubmit', () => { + describe('when selected format hasn\'t changed', () => { + beforeEach(() => { + comp.onSubmit(); + }); - it('should call updateFormat', () => { - expect(bitstreamService.updateFormat).toHaveBeenCalled(); + it('should call update', () => { + expect(bitstreamService.update).toHaveBeenCalled(); + }); + + it('should commit the updates', () => { + expect(bitstreamService.commitUpdates).toHaveBeenCalled(); + }); }); - it('should commit the updates', () => { - expect(bitstreamService.commitUpdates).toHaveBeenCalled(); + describe('when selected format has changed', () => { + beforeEach(() => { + comp.formGroup.patchValue({ + formatContainer: { + selectedFormat: allFormats[2].id + } + }); + fixture.detectChanges(); + comp.onSubmit(); + }); + + it('should call update', () => { + expect(bitstreamService.update).toHaveBeenCalled(); + }); + + it('should call updateFormat', () => { + expect(bitstreamService.updateFormat).toHaveBeenCalled(); + }); + + it('should commit the updates', () => { + expect(bitstreamService.commitUpdates).toHaveBeenCalled(); + }); }); }); - }); - describe('when the cancel button is clicked', () => { - it('should call navigateToItemEditBitstreams method', () => { - spyOn(comp, 'navigateToItemEditBitstreams'); - comp.onCancel(); - expect(comp.navigateToItemEditBitstreams).toHaveBeenCalled(); + describe('when the cancel button is clicked', () => { + it('should call navigateToItemEditBitstreams method', () => { + spyOn(comp, 'navigateToItemEditBitstreams'); + comp.onCancel(); + expect(comp.navigateToItemEditBitstreams).toHaveBeenCalled(); + }); }); - }); - describe('when navigateToItemEditBitstreams is called, and the component has an itemId', () => { - it('should redirect to the item edit page on the bitstreams tab with the itemId from the component', () => { - comp.itemId = 'some-uuid1'; - comp.navigateToItemEditBitstreams(); - expect(router.navigate).toHaveBeenCalledWith([getEntityEditRoute(null, 'some-uuid1'), 'bitstreams']); + describe('when navigateToItemEditBitstreams is called, and the component has an itemId', () => { + it('should redirect to the item edit page on the bitstreams tab with the itemId from the component', () => { + comp.itemId = 'some-uuid1'; + comp.navigateToItemEditBitstreams(); + expect(router.navigate).toHaveBeenCalledWith([getEntityEditRoute(null, 'some-uuid1'), 'bitstreams']); + }); + }); + describe('when navigateToItemEditBitstreams is called, and the component does not have an itemId', () => { + it('should redirect to the item edit page on the bitstreams tab with the itemId from the bundle links ', () => { + comp.itemId = undefined; + comp.navigateToItemEditBitstreams(); + expect(router.navigate).toHaveBeenCalledWith([getEntityEditRoute(null, 'some-uuid'), 'bitstreams']); + }); }); }); - describe('when navigateToItemEditBitstreams is called, and the component does not have an itemId', () => { - it('should redirect to the item edit page on the bitstreams tab with the itemId from the bundle links ', () => { - comp.itemId = undefined; - comp.navigateToItemEditBitstreams(); - expect(router.navigate).toHaveBeenCalledWith([getEntityEditRoute(null, 'some-uuid'), 'bitstreams']); + + describe('EditBitstreamPageComponent with IIIF fields', () => { + + const bundleName = 'ORIGINAL'; + + beforeEach(waitForAsync(() => { + + bitstream = Object.assign(new Bitstream(), { + metadata: { + 'dc.description': [ + { + value: 'Bitstream description' + } + ], + 'dc.title': [ + { + value: 'Bitstream title' + } + ], + 'iiif.label': [ + { + value: 'chapter one' + } + ], + 'iiif.toc': [ + { + value: 'chapter one' + } + ], + 'iiif.image.width': [ + { + value: '2400' + } + ], + 'iiif.image.height': [ + { + value: '2800' + } + ], + }, + format: createSuccessfulRemoteDataObject$(allFormats[1]), + _links: { + self: 'bitstream-selflink' + }, + bundle: createSuccessfulRemoteDataObject$({ + item: createSuccessfulRemoteDataObject$(Object.assign(new Item(), { + uuid: 'some-uuid', + firstMetadataValue(keyOrKeys: string | string[], valueFilter?: MetadataValueFilter): string { + return 'True'; + } + })) + }) + }); + bitstreamService = jasmine.createSpyObj('bitstreamService', { + findById: createSuccessfulRemoteDataObject$(bitstream), + update: createSuccessfulRemoteDataObject$(bitstream), + updateFormat: createSuccessfulRemoteDataObject$(bitstream), + commitUpdates: {}, + patch: {} + }); + + dsoNameService = jasmine.createSpyObj('dsoNameService', { + getName: bundleName + }); + + TestBed.configureTestingModule({ + imports: [TranslateModule.forRoot(), RouterTestingModule], + declarations: [EditBitstreamPageComponent, FileSizePipe, VarDirective], + providers: [ + {provide: NotificationsService, useValue: notificationsService}, + {provide: DynamicFormService, useValue: formService}, + { + provide: ActivatedRoute, + useValue: { + data: observableOf({bitstream: createSuccessfulRemoteDataObject(bitstream)}), + snapshot: {queryParams: {}} + } + }, + {provide: BitstreamDataService, useValue: bitstreamService}, + {provide: DSONameService, useValue: dsoNameService}, + {provide: BitstreamFormatDataService, useValue: bitstreamFormatService}, + ChangeDetectorRef + ], + schemas: [NO_ERRORS_SCHEMA] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(EditBitstreamPageComponent); + comp = fixture.componentInstance; + fixture.detectChanges(); + router = TestBed.inject(Router); + spyOn(router, 'navigate'); }); + + + describe('on startup', () => { + let rawForm; + + beforeEach(() => { + rawForm = comp.formGroup.getRawValue(); + }); + it('should set isIIIF to true', () => { + expect(comp.isIIIF).toBeTrue(); + }); + it('should fill in the iiif label', () => { + expect(rawForm.iiifLabelContainer.iiifLabel).toEqual('chapter one'); + }); + it('should fill in the iiif toc', () => { + expect(rawForm.iiifTocContainer.iiifToc).toEqual('chapter one'); + }); + it('should fill in the iiif width', () => { + expect(rawForm.iiifWidthContainer.iiifWidth).toEqual('2400'); + }); + it('should fill in the iiif height', () => { + expect(rawForm.iiifHeightContainer.iiifHeight).toEqual('2800'); + }); + }); + }); + + describe('ignore OTHERCONTENT bundle', () => { + + const bundleName = 'OTHERCONTENT'; + + beforeEach(waitForAsync(() => { + + bitstream = Object.assign(new Bitstream(), { + metadata: { + 'dc.description': [ + { + value: 'Bitstream description' + } + ], + 'dc.title': [ + { + value: 'Bitstream title' + } + ], + 'iiif.label': [ + { + value: 'chapter one' + } + ], + 'iiif.toc': [ + { + value: 'chapter one' + } + ], + 'iiif.image.width': [ + { + value: '2400' + } + ], + 'iiif.image.height': [ + { + value: '2800' + } + ], + }, + format: createSuccessfulRemoteDataObject$(allFormats[2]), + _links: { + self: 'bitstream-selflink' + }, + bundle: createSuccessfulRemoteDataObject$({ + item: createSuccessfulRemoteDataObject$(Object.assign(new Item(), { + uuid: 'some-uuid', + firstMetadataValue(keyOrKeys: string | string[], valueFilter?: MetadataValueFilter): string { + return 'True'; + } + })) + }) + }); + bitstreamService = jasmine.createSpyObj('bitstreamService', { + findById: createSuccessfulRemoteDataObject$(bitstream), + update: createSuccessfulRemoteDataObject$(bitstream), + updateFormat: createSuccessfulRemoteDataObject$(bitstream), + commitUpdates: {}, + patch: {} + }); + + dsoNameService = jasmine.createSpyObj('dsoNameService', { + getName: bundleName + }); + + TestBed.configureTestingModule({ + imports: [TranslateModule.forRoot(), RouterTestingModule], + declarations: [EditBitstreamPageComponent, FileSizePipe, VarDirective], + providers: [ + {provide: NotificationsService, useValue: notificationsService}, + {provide: DynamicFormService, useValue: formService}, + {provide: ActivatedRoute, + useValue: { + data: observableOf({bitstream: createSuccessfulRemoteDataObject(bitstream)}), + snapshot: {queryParams: {}} + } + }, + {provide: BitstreamDataService, useValue: bitstreamService}, + {provide: DSONameService, useValue: dsoNameService}, + {provide: BitstreamFormatDataService, useValue: bitstreamFormatService}, + ChangeDetectorRef + ], + schemas: [NO_ERRORS_SCHEMA] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(EditBitstreamPageComponent); + comp = fixture.componentInstance; + fixture.detectChanges(); + router = TestBed.inject(Router); + spyOn(router, 'navigate'); + }); + + describe('EditBitstreamPageComponent with IIIF fields', () => { + let rawForm; + + beforeEach(() => { + rawForm = comp.formGroup.getRawValue(); + }); + + it('should NOT set isIIIF to true', () => { + expect(comp.isIIIF).toBeFalse(); + }); + it('should put the \"IIIF Label\" input not to be shown', () => { + expect(rawForm.iiifLabelContainer).toBeFalsy(); + }); + }); }); + }); diff --git a/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.ts b/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.ts index f6ece7f4fae..9a59df4b95a 100644 --- a/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.ts +++ b/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.ts @@ -1,16 +1,27 @@ -import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core'; +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + OnDestroy, + OnInit +} from '@angular/core'; import { Bitstream } from '../../core/shared/bitstream.model'; import { ActivatedRoute, Router } from '@angular/router'; import { map, mergeMap, switchMap } from 'rxjs/operators'; -import { combineLatest as observableCombineLatest, Observable, of as observableOf, Subscription } from 'rxjs'; +import { + combineLatest, + combineLatest as observableCombineLatest, + Observable, + of as observableOf, + Subscription +} from 'rxjs'; import { DynamicFormControlModel, DynamicFormGroupModel, DynamicFormLayout, DynamicFormService, DynamicInputModel, - DynamicSelectModel, - DynamicTextAreaModel + DynamicSelectModel } from '@ng-dynamic-forms/core'; import { FormGroup } from '@angular/forms'; import { TranslateService } from '@ngx-translate/core'; @@ -28,13 +39,19 @@ import { NotificationsService } from '../../shared/notifications/notifications.s import { BitstreamFormatDataService } from '../../core/data/bitstream-format-data.service'; import { BitstreamFormat } from '../../core/shared/bitstream-format.model'; import { BitstreamFormatSupportLevel } from '../../core/shared/bitstream-format-support-level'; -import { hasValue, isNotEmpty } from '../../shared/empty.util'; +import { hasValue, isNotEmpty, isEmpty } from '../../shared/empty.util'; import { Metadata } from '../../core/shared/metadata.utils'; import { Location } from '@angular/common'; import { RemoteData } from '../../core/data/remote-data'; import { PaginatedList } from '../../core/data/paginated-list.model'; import { getEntityEditRoute, getItemEditRoute } from '../../item-page/item-page-routing-paths'; import { Bundle } from '../../core/shared/bundle.model'; +import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; +import { Item } from '../../core/shared/item.model'; +import { + DsDynamicInputModel +} from '../../shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-input.model'; +import { DsDynamicTextAreaModel } from '../../shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-textarea.model'; @Component({ selector: 'ds-edit-bitstream-page', @@ -94,6 +111,26 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { */ NOTIFICATIONS_PREFIX = 'bitstream.edit.notifications.'; + /** + * IIIF image width metadata key + */ + IMAGE_WIDTH_METADATA = 'iiif.image.width'; + + /** + * IIIF image height metadata key + */ + IMAGE_HEIGHT_METADATA = 'iiif.image.height'; + + /** + * IIIF table of contents metadata key + */ + IIIF_TOC_METADATA = 'iiif.toc'; + + /** + * IIIF label metadata key + */ + IIIF_LABEL_METADATA = 'iiif.label'; + /** * Options for fetching all bitstream formats */ @@ -102,7 +139,8 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { /** * The Dynamic Input Model for the file's name */ - fileNameModel = new DynamicInputModel({ + fileNameModel = new DsDynamicInputModel({ + hasSelectableMetadata: false, metadataFields: [], repeatable: false, submissionId: '', id: 'fileName', name: 'fileName', required: true, @@ -118,14 +156,16 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { * The Dynamic Switch Model for the file's name */ primaryBitstreamModel = new DynamicCustomSwitchModel({ - id: 'primaryBitstream', - name: 'primaryBitstream' - }); + id: 'primaryBitstream', + name: 'primaryBitstream' + } + ); /** * The Dynamic TextArea Model for the file's description */ - descriptionModel = new DynamicTextAreaModel({ + descriptionModel = new DsDynamicTextAreaModel({ + hasSelectableMetadata: false, metadataFields: [], repeatable: false, submissionId: '', id: 'description', name: 'description', rows: 10 @@ -147,10 +187,87 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { name: 'newFormat' }); + /** + * The Dynamic Input Model for the iiif label + */ + iiifLabelModel = new DsDynamicInputModel({ + hasSelectableMetadata: false, metadataFields: [], repeatable: false, submissionId: '', + id: 'iiifLabel', + name: 'iiifLabel' + }, + { + grid: { + host: 'col col-lg-6 d-inline-block' + } + }); + iiifLabelContainer = new DynamicFormGroupModel({ + id: 'iiifLabelContainer', + group: [this.iiifLabelModel] + },{ + grid: { + host: 'form-row' + } + }); + + iiifTocModel = new DsDynamicInputModel({ + hasSelectableMetadata: false, metadataFields: [], repeatable: false, submissionId: '', + id: 'iiifToc', + name: 'iiifToc', + },{ + grid: { + host: 'col col-lg-6 d-inline-block' + } + }); + iiifTocContainer = new DynamicFormGroupModel({ + id: 'iiifTocContainer', + group: [this.iiifTocModel] + },{ + grid: { + host: 'form-row' + } + }); + + iiifWidthModel = new DsDynamicInputModel({ + hasSelectableMetadata: false, metadataFields: [], repeatable: false, submissionId: '', + id: 'iiifWidth', + name: 'iiifWidth', + },{ + grid: { + host: 'col col-lg-6 d-inline-block' + } + }); + iiifWidthContainer = new DynamicFormGroupModel({ + id: 'iiifWidthContainer', + group: [this.iiifWidthModel] + },{ + grid: { + host: 'form-row' + } + }); + + iiifHeightModel = new DsDynamicInputModel({ + hasSelectableMetadata: false, metadataFields: [], repeatable: false, submissionId: '', + id: 'iiifHeight', + name: 'iiifHeight' + },{ + grid: { + host: 'col col-lg-6 d-inline-block' + } + }); + iiifHeightContainer = new DynamicFormGroupModel({ + id: 'iiifHeightContainer', + group: [this.iiifHeightModel] + },{ + grid: { + host: 'form-row' + } + }); + /** * All input models in a simple array for easier iterations */ - inputModels = [this.fileNameModel, this.primaryBitstreamModel, this.descriptionModel, this.selectedFormatModel, this.newFormatModel]; + inputModels = [this.fileNameModel, this.primaryBitstreamModel, this.descriptionModel, this.selectedFormatModel, + this.newFormatModel]; /** * The dynamic form fields used for editing the information of a bitstream @@ -163,7 +280,11 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { this.fileNameModel, this.primaryBitstreamModel ] - }), + },{ + grid: { + host: 'form-row' + } + }), new DynamicFormGroupModel({ id: 'descriptionContainer', group: [ @@ -254,18 +375,27 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { */ entityType: string; + /** + * Set to true when the parent item supports IIIF. + */ + isIIIF = false; + + /** * Array to track all subscriptions and unsubscribe them onDestroy * @type {Array} */ protected subs: Subscription[] = []; + constructor(private route: ActivatedRoute, private router: Router, + private changeDetectorRef: ChangeDetectorRef, private location: Location, private formService: DynamicFormService, private translate: TranslateService, private bitstreamService: BitstreamDataService, + private dsoNameService: DSONameService, private notificationsService: NotificationsService, private bitstreamFormatService: BitstreamFormatDataService) { } @@ -277,7 +407,6 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { * - Translate the form labels and hints */ ngOnInit(): void { - this.formGroup = this.formService.createFormGroup(this.formModel); this.itemId = this.route.snapshot.queryParams.itemId; this.entityType = this.route.snapshot.queryParams.entityType; @@ -301,13 +430,10 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { ).subscribe(([bitstream, allFormats]) => { this.bitstream = bitstream as Bitstream; this.formats = allFormats.page; - this.updateFormatModel(); - this.updateForm(this.bitstream); + this.setIiifStatus(this.bitstream); }) ); - this.updateFieldTranslations(); - this.subs.push( this.translate.onLangChange .subscribe(() => { @@ -316,6 +442,16 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { ); } + /** + * Initializes the form. + */ + setForm() { + this.formGroup = this.formService.createFormGroup(this.formModel); + this.updateFormatModel(); + this.updateForm(this.bitstream); + this.updateFieldTranslations(); + } + /** * Update the current form values with bitstream properties * @param bitstream @@ -333,6 +469,22 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { newFormat: hasValue(bitstream.firstMetadata('dc.format')) ? bitstream.firstMetadata('dc.format').value : undefined } }); + if (this.isIIIF) { + this.formGroup.patchValue({ + iiifLabelContainer: { + iiifLabel: bitstream.firstMetadataValue(this.IIIF_LABEL_METADATA) + }, + iiifTocContainer: { + iiifToc: bitstream.firstMetadataValue(this.IIIF_TOC_METADATA) + }, + iiifWidthContainer: { + iiifWidth: bitstream.firstMetadataValue(this.IMAGE_WIDTH_METADATA) + }, + iiifHeightContainer: { + iiifHeight: bitstream.firstMetadataValue(this.IMAGE_HEIGHT_METADATA) + } + }); + } this.bitstream.format.pipe( getAllSucceededRemoteDataPayload() ).subscribe((format: BitstreamFormat) => { @@ -467,6 +619,32 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { const primary = rawForm.fileNamePrimaryContainer.primaryBitstream; Metadata.setFirstValue(newMetadata, 'dc.title', rawForm.fileNamePrimaryContainer.fileName); Metadata.setFirstValue(newMetadata, 'dc.description', rawForm.descriptionContainer.description); + if (this.isIIIF) { + // It's helpful to remove these metadata elements entirely when the form value is empty. + // This avoids potential issues on the REST side and makes it possible to do things like + // remove an existing "table of contents" entry. + if (isEmpty(rawForm.iiifLabelContainer.iiifLabel)) { + + delete newMetadata[this.IIIF_LABEL_METADATA]; + } else { + Metadata.setFirstValue(newMetadata, this.IIIF_LABEL_METADATA, rawForm.iiifLabelContainer.iiifLabel); + } + if (isEmpty(rawForm.iiifTocContainer.iiifToc)) { + delete newMetadata[this.IIIF_TOC_METADATA]; + } else { + Metadata.setFirstValue(newMetadata, this.IIIF_TOC_METADATA, rawForm.iiifTocContainer.iiifToc); + } + if (isEmpty(rawForm.iiifWidthContainer.iiifWidth)) { + delete newMetadata[this.IMAGE_WIDTH_METADATA]; + } else { + Metadata.setFirstValue(newMetadata, this.IMAGE_WIDTH_METADATA, rawForm.iiifWidthContainer.iiifWidth); + } + if (isEmpty(rawForm.iiifHeightContainer.iiifHeight)) { + delete newMetadata[this.IMAGE_HEIGHT_METADATA]; + } else { + Metadata.setFirstValue(newMetadata, this.IMAGE_HEIGHT_METADATA, rawForm.iiifHeightContainer.iiifHeight); + } + } if (isNotEmpty(rawForm.formatContainer.newFormat)) { Metadata.setFirstValue(newMetadata, 'dc.format', rawForm.formatContainer.newFormat); } @@ -497,6 +675,58 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { } } + /** + * Verifies that the parent item is iiif-enabled. Checks bitstream mimetype to be + * sure it's an image, excluding bitstreams in the THUMBNAIL or OTHERCONTENT bundles. + * @param bitstream + */ + setIiifStatus(bitstream: Bitstream) { + + const regexExcludeBundles = /OTHERCONTENT|THUMBNAIL|LICENSE/; + const regexIIIFItem = /true|yes/i; + + const isImage$ = this.bitstream.format.pipe( + getFirstSucceededRemoteData(), + map((format: RemoteData) => format.payload.mimetype.includes('image/'))); + + const isIIIFBundle$ = this.bitstream.bundle.pipe( + getFirstSucceededRemoteData(), + map((bundle: RemoteData) => + this.dsoNameService.getName(bundle.payload).match(regexExcludeBundles) == null)); + + const isEnabled$ = this.bitstream.bundle.pipe( + getFirstSucceededRemoteData(), + map((bundle: RemoteData) => bundle.payload.item.pipe( + getFirstSucceededRemoteData(), + map((item: RemoteData) => + (item.payload.firstMetadataValue('dspace.iiif.enabled') && + item.payload.firstMetadataValue('dspace.iiif.enabled').match(regexIIIFItem) !== null) + )))); + + const iiifSub = combineLatest( + isImage$, + isIIIFBundle$, + isEnabled$ + ).subscribe(([isImage, isIIIFBundle, isEnabled]) => { + if (isImage && isIIIFBundle && isEnabled) { + this.isIIIF = true; + this.inputModels.push(this.iiifLabelModel); + this.formModel.push(this.iiifLabelContainer); + this.inputModels.push(this.iiifTocModel); + this.formModel.push(this.iiifTocContainer); + this.inputModels.push(this.iiifWidthModel); + this.formModel.push(this.iiifWidthContainer); + this.inputModels.push(this.iiifHeightModel); + this.formModel.push(this.iiifHeightContainer); + } + this.setForm(); + this.changeDetectorRef.detectChanges(); + }); + + this.subs.push(iiifSub); + + } + /** * Unsubscribe from open subscriptions */ diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 7753532838f..f742273edbe 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -587,6 +587,23 @@ "bitstream.edit.notifications.error.format.title": "An error occurred saving the bitstream's format", + "bitstream.edit.form.iiifLabel.label": "IIIF Label", + + "bitstream.edit.form.iiifLabel.hint": "Canvas label for this image. If not provided default label will be used.", + + "bitstream.edit.form.iiifToc.label": "IIIF Table of Contents", + + "bitstream.edit.form.iiifToc.hint": "Adding text here makes this the start of a new table of contents range.", + + "bitstream.edit.form.iiifWidth.label": "IIIF Canvas Width", + + "bitstream.edit.form.iiifWidth.hint": "The canvas width should usually match the image width.", + + "bitstream.edit.form.iiifHeight.label": "IIIF Canvas Height", + + "bitstream.edit.form.iiifHeight.hint": "The canvas height should usually match the image height.", + + "bitstream.edit.notifications.saved.content": "Your changes to this bitstream were saved.", "bitstream.edit.notifications.saved.title": "Bitstream saved",