From 7008afd05f19703e91c92f9f194feb54139c3d92 Mon Sep 17 00:00:00 2001 From: lotte Date: Tue, 25 Apr 2023 16:12:00 +0200 Subject: [PATCH 01/10] 1578: primaryBitstream implementation --- .../edit-bitstream-page.component.spec.ts | 152 ++++++++++++++++-- .../edit-bitstream-page.component.ts | 136 +++++++++++----- src/app/core/shared/bundle.model.ts | 8 +- 3 files changed, 243 insertions(+), 53 deletions(-) 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 44e48182fd..13f3ec7f27 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 @@ -24,6 +24,8 @@ 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'; +import { Bundle } from '../../core/shared/bundle.model'; +import { BundleDataService } from '../../core/data/bundle-data.service'; const infoNotification: INotification = new Notification('id', NotificationType.Info, 'info'); const warningNotification: INotification = new Notification('id', NotificationType.Warning, 'warning'); @@ -35,9 +37,14 @@ let bitstreamService: BitstreamDataService; let bitstreamFormatService: BitstreamFormatDataService; let dsoNameService: DSONameService; let bitstream: Bitstream; +let bitstreamID: string; let selectedFormat: BitstreamFormat; let allFormats: BitstreamFormat[]; let router: Router; +let bundleDataService; +let bundleWithCurrentPrimary: Bundle; +let bundleWithDifferentPrimary: Bundle; +let bundleWithNoPrimary: Bundle; let comp: EditBitstreamPageComponent; let fixture: ComponentFixture; @@ -45,6 +52,12 @@ let fixture: ComponentFixture; describe('EditBitstreamPageComponent', () => { beforeEach(() => { + bitstreamID = 'current-bitstream-id'; + bundleWithCurrentPrimary = Object.assign(new Bundle(), { 'primaryBitstreamUUID': bitstreamID }); + bundleWithDifferentPrimary = Object.assign(new Bundle(), { 'primaryBitstreamUUID': '12345-abcde-54321-edcba' }); + bundleWithNoPrimary = Object.assign(new Bundle(), { 'primaryBitstreamUUID': null }); + bundleDataService = jasmine.createSpyObj('BundleDataService', ['patch']); + bundleDataService.patch.and.callFake((a, b) => createSuccessfulRemoteDataObject$(a)); allFormats = [ Object.assign({ id: '1', @@ -53,7 +66,7 @@ describe('EditBitstreamPageComponent', () => { supportLevel: BitstreamFormatSupportLevel.Unknown, mimetype: 'application/octet-stream', _links: { - self: {href: 'format-selflink-1'} + self: { href: 'format-selflink-1' } } }), Object.assign({ @@ -63,7 +76,7 @@ describe('EditBitstreamPageComponent', () => { supportLevel: BitstreamFormatSupportLevel.Known, mimetype: 'image/png', _links: { - self: {href: 'format-selflink-2'} + self: { href: 'format-selflink-2' } } }), Object.assign({ @@ -73,7 +86,7 @@ describe('EditBitstreamPageComponent', () => { supportLevel: BitstreamFormatSupportLevel.Known, mimetype: 'image/gif', _links: { - self: {href: 'format-selflink-3'} + self: { href: 'format-selflink-3' } } }) ] as BitstreamFormat[]; @@ -112,6 +125,8 @@ describe('EditBitstreamPageComponent', () => { const bundleName = 'ORIGINAL'; bitstream = Object.assign(new Bitstream(), { + uuid: bitstreamID, + id: bitstreamID, metadata: { 'dc.description': [ { @@ -155,17 +170,19 @@ describe('EditBitstreamPageComponent', () => { imports: [TranslateModule.forRoot(), RouterTestingModule], declarations: [EditBitstreamPageComponent, FileSizePipe, VarDirective], providers: [ - {provide: NotificationsService, useValue: notificationsService}, - {provide: DynamicFormService, useValue: formService}, - {provide: ActivatedRoute, + { provide: NotificationsService, useValue: notificationsService }, + { provide: DynamicFormService, useValue: formService }, + { + provide: ActivatedRoute, useValue: { - data: observableOf({bitstream: createSuccessfulRemoteDataObject(bitstream)}), - snapshot: {queryParams: {}} + data: observableOf({ bitstream: createSuccessfulRemoteDataObject(bitstream) }), + snapshot: { queryParams: {} } } }, - {provide: BitstreamDataService, useValue: bitstreamService}, - {provide: DSONameService, useValue: dsoNameService}, - {provide: BitstreamFormatDataService, useValue: bitstreamFormatService}, + { provide: BitstreamDataService, useValue: bitstreamService }, + { provide: DSONameService, useValue: dsoNameService }, + { provide: BitstreamFormatDataService, useValue: bitstreamFormatService }, + { provide: BundleDataService, useValue: bundleDataService }, ChangeDetectorRef ], schemas: [NO_ERRORS_SCHEMA] @@ -203,6 +220,27 @@ describe('EditBitstreamPageComponent', () => { it('should put the \"New Format\" input on invisible', () => { expect(comp.formLayout.newFormat.grid.host).toContain('invisible'); }); + describe('when the bitstream is the primary bitstream on the bundle', () => { + beforeEach(() => { + (comp as any).bundle = bundleWithCurrentPrimary; + comp.setForm(); + rawForm = comp.formGroup.getRawValue(); + + }); + it('should enable the primary bitstream toggle', () => { + expect(rawForm.fileNamePrimaryContainer.primaryBitstream).toEqual(true); + }); + }); + describe('when the bitstream is not the primary bitstream on the bundle', () => { + beforeEach(() => { + (comp as any).bundle = bundleWithDifferentPrimary; + comp.setForm(); + rawForm = comp.formGroup.getRawValue(); + }); + it('should disable the primary bitstream toggle', () => { + expect(rawForm.fileNamePrimaryContainer.primaryBitstream).toEqual(false); + }); + }); }); describe('when an unknown format is selected', () => { @@ -216,6 +254,95 @@ describe('EditBitstreamPageComponent', () => { }); describe('onSubmit', () => { + describe('when the primaryBitstream changed', () => { + describe('to the current bitstream', () => { + beforeEach(() => { + const rawValue = Object.assign(comp.formGroup.getRawValue(), { fileNamePrimaryContainer: { primaryBitstream: true } }); + spyOn(comp.formGroup, 'getRawValue').and.returnValue(rawValue); + }); + + describe('from a different primary bitstream', () => { + beforeEach(() => { + (comp as any).bundle = bundleWithDifferentPrimary; + comp.onSubmit(); + }); + + it('should call patch with a replace operation', () => { + expect(bundleDataService.patch).toHaveBeenCalledWith(bundleWithDifferentPrimary, [jasmine.objectContaining({ + op: 'replace' + })]); + }); + + it('should call patch with the correct bitstream uuid', () => { + expect(bundleDataService.patch).toHaveBeenCalledWith(bundleWithDifferentPrimary, [jasmine.objectContaining({ + value: bitstreamID + })]); + }); + }); + describe('from no primary bitstream', () => { + beforeEach(() => { + (comp as any).bundle = bundleWithNoPrimary; + comp.onSubmit(); + }); + + it('should call patch with an add operation', () => { + expect(bundleDataService.patch).toHaveBeenCalledWith(bundleWithNoPrimary, [jasmine.objectContaining({ + op: 'add' + })]); + }); + + it('should call patch with the correct bitstream uuid', () => { + expect(bundleDataService.patch).toHaveBeenCalledWith(bundleWithNoPrimary, [jasmine.objectContaining({ + value: bitstreamID + })]); + }); + }); + }); + describe('to no primary bitstream', () => { + beforeEach(() => { + const rawValue = Object.assign(comp.formGroup.getRawValue(), { fileNamePrimaryContainer: { primaryBitstream: false } }); + spyOn(comp.formGroup, 'getRawValue').and.returnValue(rawValue); + }); + + describe('from the current bitstream', () => { + beforeEach(() => { + (comp as any).bundle = bundleWithCurrentPrimary; + comp.onSubmit(); + }); + + it('should call patch with a remove operation', () => { + expect(bundleDataService.patch).toHaveBeenCalledWith(bundleWithCurrentPrimary, [jasmine.objectContaining({ + op: 'remove' + })]); + }); + }); + }); + }); + describe('when the primaryBitstream did not changed', () => { + describe('the current bitstream stayed the primary bitstream', () => { + beforeEach(() => { + const rawValue = Object.assign(comp.formGroup.getRawValue(), { fileNamePrimaryContainer: { primaryBitstream: true } }); + spyOn(comp.formGroup, 'getRawValue').and.returnValue(rawValue); + (comp as any).bundle = bundleWithCurrentPrimary; + comp.onSubmit(); + }); + it('should not call patch on the bundle data service', () => { + expect(bundleDataService.patch).not.toHaveBeenCalled(); + }); + }); + describe('the bitstream was not and did not become the primary bitstream', () => { + beforeEach(() => { + const rawValue = Object.assign(comp.formGroup.getRawValue(), { fileNamePrimaryContainer: { primaryBitstream: false } }); + spyOn(comp.formGroup, 'getRawValue').and.returnValue(rawValue); + (comp as any).bundle = bundleWithDifferentPrimary; + comp.onSubmit(); + }); + it('should not call patch on the bundle data service', () => { + expect(bundleDataService.patch).not.toHaveBeenCalled(); + }); + }); + }); + describe('when selected format hasn\'t changed', () => { beforeEach(() => { comp.onSubmit(); @@ -357,6 +484,7 @@ describe('EditBitstreamPageComponent', () => { {provide: BitstreamDataService, useValue: bitstreamService}, {provide: DSONameService, useValue: dsoNameService}, {provide: BitstreamFormatDataService, useValue: bitstreamFormatService}, + { provide: BundleDataService, useValue: bundleDataService }, ChangeDetectorRef ], schemas: [NO_ERRORS_SCHEMA] @@ -371,7 +499,6 @@ describe('EditBitstreamPageComponent', () => { spyOn(router, 'navigate'); }); - describe('on startup', () => { let rawForm; @@ -475,6 +602,7 @@ describe('EditBitstreamPageComponent', () => { {provide: BitstreamDataService, useValue: bitstreamService}, {provide: DSONameService, useValue: dsoNameService}, {provide: BitstreamFormatDataService, useValue: bitstreamFormatService}, + { provide: BundleDataService, useValue: bundleDataService }, ChangeDetectorRef ], schemas: [NO_ERRORS_SCHEMA] 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 8e63ec939f..3c3965a70a 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 @@ -52,6 +52,7 @@ 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'; +import { BundleDataService } from '../../core/data/bundle-data.service'; @Component({ selector: 'ds-edit-bitstream-page', @@ -191,19 +192,19 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { * The Dynamic Input Model for the iiif label */ iiifLabelModel = new DsDynamicInputModel({ - hasSelectableMetadata: false, metadataFields: [], repeatable: false, submissionId: '', - id: 'iiifLabel', - name: 'iiifLabel' - }, + hasSelectableMetadata: false, metadataFields: [], repeatable: false, submissionId: '', + id: 'iiifLabel', + name: 'iiifLabel' + }, { - grid: { - host: 'col col-lg-6 d-inline-block' - } + grid: { + host: 'col col-lg-6 d-inline-block' + } }); iiifLabelContainer = new DynamicFormGroupModel({ id: 'iiifLabelContainer', group: [this.iiifLabelModel] - },{ + }, { grid: { host: 'form-row' } @@ -213,7 +214,7 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { hasSelectableMetadata: false, metadataFields: [], repeatable: false, submissionId: '', id: 'iiifToc', name: 'iiifToc', - },{ + }, { grid: { host: 'col col-lg-6 d-inline-block' } @@ -221,7 +222,7 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { iiifTocContainer = new DynamicFormGroupModel({ id: 'iiifTocContainer', group: [this.iiifTocModel] - },{ + }, { grid: { host: 'form-row' } @@ -231,7 +232,7 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { hasSelectableMetadata: false, metadataFields: [], repeatable: false, submissionId: '', id: 'iiifWidth', name: 'iiifWidth', - },{ + }, { grid: { host: 'col col-lg-6 d-inline-block' } @@ -239,7 +240,7 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { iiifWidthContainer = new DynamicFormGroupModel({ id: 'iiifWidthContainer', group: [this.iiifWidthModel] - },{ + }, { grid: { host: 'form-row' } @@ -249,7 +250,7 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { hasSelectableMetadata: false, metadataFields: [], repeatable: false, submissionId: '', id: 'iiifHeight', name: 'iiifHeight' - },{ + }, { grid: { host: 'col col-lg-6 d-inline-block' } @@ -257,7 +258,7 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { iiifHeightContainer = new DynamicFormGroupModel({ id: 'iiifHeightContainer', group: [this.iiifHeightModel] - },{ + }, { grid: { host: 'form-row' } @@ -280,11 +281,11 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { this.fileNameModel, this.primaryBitstreamModel ] - },{ - grid: { - host: 'form-row' - } - }), + }, { + grid: { + host: 'form-row' + } + }), new DynamicFormGroupModel({ id: 'descriptionContainer', group: [ @@ -380,13 +381,23 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { */ isIIIF = false; - /** * Array to track all subscriptions and unsubscribe them onDestroy * @type {Array} */ protected subs: Subscription[] = []; + /** + * The parent bundle containing the Bitstream + * @private + */ + private bundle: Bundle; + + /** + * Path to patch primary bitstream on the bundle + * @private + */ + private readonly primaryBitstreamPath = '/primarybitstream'; constructor(private route: ActivatedRoute, private router: Router, @@ -397,7 +408,8 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { private bitstreamService: BitstreamDataService, private dsoNameService: DSONameService, private notificationsService: NotificationsService, - private bitstreamFormatService: BitstreamFormatDataService) { + private bitstreamFormatService: BitstreamFormatDataService, + private bundleService: BundleDataService) { } /** @@ -423,13 +435,20 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { getRemoteDataPayload() ); + const bundle$ = bitstream$.pipe( + switchMap((bitstream: Bitstream) => bitstream.bundle), + getFirstSucceededRemoteDataPayload(), + ); + this.subs.push( observableCombineLatest( bitstream$, - allFormats$ - ).subscribe(([bitstream, allFormats]) => { + allFormats$, + bundle$ + ).subscribe(([bitstream, allFormats, bundle]) => { this.bitstream = bitstream as Bitstream; this.formats = allFormats.page; + this.bundle = bundle; this.setIiifStatus(this.bitstream); }) ); @@ -437,8 +456,8 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { this.subs.push( this.translate.onLangChange .subscribe(() => { - this.updateFieldTranslations(); - }) + this.updateFieldTranslations(); + }) ); } @@ -460,7 +479,7 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { this.formGroup.patchValue({ fileNamePrimaryContainer: { fileName: bitstream.name, - primaryBitstream: false + primaryBitstream: this.bundle.primaryBitstreamUUID === bitstream.uuid }, descriptionContainer: { description: bitstream.firstMetadataValue('dc.description') @@ -563,6 +582,7 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { } } + /** * Check for changes against the bitstream and send update requests to the REST API */ @@ -571,9 +591,45 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { const updatedBitstream = this.formToBitstream(updatedValues); const selectedFormat = this.formats.find((f: BitstreamFormat) => f.id === updatedValues.formatContainer.selectedFormat); const isNewFormat = selectedFormat.id !== this.originalFormat.id; + const isPrimary = updatedValues.fileNamePrimaryContainer.primaryBitstream; + const wasPrimary = this.bundle.primaryBitstreamUUID === this.bitstream.uuid; let bitstream$; + let bundle$: Observable; + if (wasPrimary !== isPrimary) { + let patchOperation; + // No longer primary bitstream: remove + if (wasPrimary) { + patchOperation = { + path: this.primaryBitstreamPath, + op: 'remove' + }; + } else { + // Has become primary bitstream + // If it already had a value: replace, otherwise: add + patchOperation = { + path: this.primaryBitstreamPath, + op: hasValue(this.bundle.primaryBitstreamUUID) ? 'replace' : 'add', + value: this.bitstream.uuid + }; + } + bundle$ = this.bundleService.patch(this.bundle, [patchOperation]).pipe( + getFirstCompletedRemoteData(), + map((bundleResponse: RemoteData) => { + if (hasValue(bundleResponse) && bundleResponse.hasFailed) { + this.notificationsService.error( + this.translate.instant(this.NOTIFICATIONS_PREFIX + 'error.primaryBitstream.title'), + bundleResponse.errorMessage + ); + } else { + return bundleResponse.payload; + } + }) + ); + } else { + bundle$ = observableOf(this.bundle); + } if (isNewFormat) { bitstream$ = this.bitstreamService.updateFormat(this.bitstream, selectedFormat).pipe( getFirstCompletedRemoteData(), @@ -592,7 +648,7 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { bitstream$ = observableOf(this.bitstream); } - bitstream$.pipe( + combineLatest([bundle$, bitstream$]).pipe( switchMap(() => { return this.bitstreamService.update(updatedBitstream).pipe( getFirstSucceededRemoteDataPayload() @@ -633,11 +689,11 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { } else { Metadata.setFirstValue(newMetadata, this.IIIF_LABEL_METADATA, rawForm.iiifLabelContainer.iiifLabel); } - if (isEmpty(rawForm.iiifTocContainer.iiifToc)) { - delete newMetadata[this.IIIF_TOC_METADATA]; - } else { + 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 { @@ -672,10 +728,10 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { this.router.navigate([getEntityEditRoute(this.entityType, this.itemId), 'bitstreams']); } else { this.bitstream.bundle.pipe(getFirstSucceededRemoteDataPayload(), - mergeMap((bundle: Bundle) => bundle.item.pipe(getFirstSucceededRemoteDataPayload()))) - .subscribe((item) => { - this.router.navigate(([getItemEditRoute(item), 'bitstreams'])); - }); + mergeMap((bundle: Bundle) => bundle.item.pipe(getFirstSucceededRemoteDataPayload()))) + .subscribe((item) => { + this.router.navigate(([getItemEditRoute(item), 'bitstreams'])); + }); } } @@ -701,11 +757,11 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { 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) - )))); + getFirstSucceededRemoteData(), + map((item: RemoteData) => + (item.payload.firstMetadataValue('dspace.iiif.enabled') && + item.payload.firstMetadataValue('dspace.iiif.enabled').match(regexIIIFItem) !== null) + )))); const iiifSub = combineLatest( isImage$, diff --git a/src/app/core/shared/bundle.model.ts b/src/app/core/shared/bundle.model.ts index 36b7012e47..72c466d2d6 100644 --- a/src/app/core/shared/bundle.model.ts +++ b/src/app/core/shared/bundle.model.ts @@ -1,4 +1,4 @@ -import { deserialize, inheritSerialization } from 'cerialize'; +import { autoserializeAs, deserialize, inheritSerialization } from 'cerialize'; import { Observable } from 'rxjs'; @@ -29,6 +29,12 @@ export class Bundle extends DSpaceObject { item: HALLink; }; + /** + * The ID of the primaryBitstream of this Bundle + */ + @autoserializeAs('primarybitstream') + primaryBitstreamUUID: string; + /** * The primary Bitstream of this Bundle * Will be undefined unless the primaryBitstream {@link HALLink} has been resolved. From 222c220c5667fb76c183ca9eb25273aaf68aa1f8 Mon Sep 17 00:00:00 2001 From: lotte Date: Wed, 26 Apr 2023 10:54:36 +0200 Subject: [PATCH 02/10] 101289: #1578 fixed redirection bug --- .../edit-bitstream-page.component.spec.ts | 9 +- .../edit-bitstream-page.component.ts | 116 +++++++----------- src/assets/i18n/en.json5 | 2 + 3 files changed, 48 insertions(+), 79 deletions(-) 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 13f3ec7f27..258ff2432f 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 @@ -388,20 +388,13 @@ describe('EditBitstreamPageComponent', () => { expect(comp.navigateToItemEditBitstreams).toHaveBeenCalled(); }); }); - describe('when navigateToItemEditBitstreams is called, and the component has an itemId', () => { + describe('when navigateToItemEditBitstreams is called', () => { 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('EditBitstreamPageComponent with IIIF fields', () => { 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 3c3965a70a..5110a5e936 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,58 +1,32 @@ -import { - ChangeDetectionStrategy, - ChangeDetectorRef, - 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, - combineLatest as observableCombineLatest, - Observable, - of as observableOf, - Subscription -} from 'rxjs'; -import { - DynamicFormControlModel, - DynamicFormGroupModel, - DynamicFormLayout, - DynamicFormService, - DynamicInputModel, - DynamicSelectModel -} from '@ng-dynamic-forms/core'; +import { map, switchMap, tap } from 'rxjs/operators'; +import { combineLatest, combineLatest as observableCombineLatest, Observable, of as observableOf, Subscription } from 'rxjs'; +import { DynamicFormControlModel, DynamicFormGroupModel, DynamicFormLayout, DynamicFormService, DynamicInputModel, DynamicSelectModel } from '@ng-dynamic-forms/core'; import { FormGroup } from '@angular/forms'; import { TranslateService } from '@ngx-translate/core'; import { DynamicCustomSwitchModel } from '../../shared/form/builder/ds-dynamic-form-ui/models/custom-switch/custom-switch.model'; import cloneDeep from 'lodash/cloneDeep'; import { BitstreamDataService } from '../../core/data/bitstream-data.service'; -import { - getAllSucceededRemoteDataPayload, - getFirstCompletedRemoteData, - getFirstSucceededRemoteData, - getFirstSucceededRemoteDataPayload, - getRemoteDataPayload -} from '../../core/shared/operators'; +import { getAllSucceededRemoteDataPayload, getFirstCompletedRemoteData, getFirstSucceededRemoteData, getFirstSucceededRemoteDataPayload, getRemoteDataPayload } from '../../core/shared/operators'; import { NotificationsService } from '../../shared/notifications/notifications.service'; 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, isEmpty } from '../../shared/empty.util'; +import { hasValue, isEmpty, isNotEmpty } 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 { getEntityEditRoute } 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 { 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'; import { BundleDataService } from '../../core/data/bundle-data.service'; +import { Operation } from 'fast-json-patch'; @Component({ selector: 'ds-edit-bitstream-page', @@ -440,17 +414,24 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { getFirstSucceededRemoteDataPayload(), ); + const item$ = bundle$.pipe( + switchMap((bundle: Bundle) => bundle.item), + getFirstSucceededRemoteDataPayload() + ); this.subs.push( observableCombineLatest( bitstream$, allFormats$, - bundle$ - ).subscribe(([bitstream, allFormats, bundle]) => { - this.bitstream = bitstream as Bitstream; - this.formats = allFormats.page; - this.bundle = bundle; - this.setIiifStatus(this.bitstream); - }) + bundle$, + item$, + ).pipe() + .subscribe(([bitstream, allFormats, bundle, item]) => { + this.bitstream = bitstream as Bitstream; + this.formats = allFormats.page; + this.bundle = bundle; + this.itemId = item.uuid; + this.setIiifStatus(this.bitstream); + }) ); this.subs.push( @@ -582,7 +563,6 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { } } - /** * Check for changes against the bitstream and send update requests to the REST API */ @@ -598,23 +578,8 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { let bundle$: Observable; if (wasPrimary !== isPrimary) { - let patchOperation; - // No longer primary bitstream: remove - if (wasPrimary) { - patchOperation = { - path: this.primaryBitstreamPath, - op: 'remove' - }; - } else { - // Has become primary bitstream - // If it already had a value: replace, otherwise: add - patchOperation = { - path: this.primaryBitstreamPath, - op: hasValue(this.bundle.primaryBitstreamUUID) ? 'replace' : 'add', - value: this.bitstream.uuid - }; - } - bundle$ = this.bundleService.patch(this.bundle, [patchOperation]).pipe( + const patchOperations: Operation[] = this.retrieveBundlePatch(wasPrimary); + bundle$ = this.bundleService.patch(this.bundle, patchOperations).pipe( getFirstCompletedRemoteData(), map((bundleResponse: RemoteData) => { if (hasValue(bundleResponse) && bundleResponse.hasFailed) { @@ -649,6 +614,7 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { } combineLatest([bundle$, bitstream$]).pipe( + tap(([bundle]) => this.bundle = bundle), switchMap(() => { return this.bitstreamService.update(updatedBitstream).pipe( getFirstSucceededRemoteDataPayload() @@ -664,6 +630,24 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { }); } + private retrieveBundlePatch(wasPrimary: boolean): Operation[] { + // No longer primary bitstream: remove + if (wasPrimary) { + return [{ + path: this.primaryBitstreamPath, + op: 'remove' + }]; + } else { + // Has become primary bitstream + // If it already had a value: replace, otherwise: add + return [{ + path: this.primaryBitstreamPath, + op: hasValue(this.bundle.primaryBitstreamUUID) ? 'replace' : 'add', + value: this.bitstream.uuid + }]; + } + } + /** * Parse form data to an updated bitstream object * @param rawForm Raw form data @@ -671,8 +655,6 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { formToBitstream(rawForm): Bitstream { const updatedBitstream = cloneDeep(this.bitstream); const newMetadata = updatedBitstream.metadata; - // TODO: Set bitstream to primary when supported - const primary = rawForm.fileNamePrimaryContainer.primaryBitstream; Metadata.setFirstValue(newMetadata, 'dc.title', rawForm.fileNamePrimaryContainer.fileName); if (isEmpty(rawForm.descriptionContainer.description)) { delete newMetadata['dc.description']; @@ -724,15 +706,7 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { * otherwise retrieve the item ID based on the owning bundle's link */ navigateToItemEditBitstreams() { - if (hasValue(this.itemId)) { - this.router.navigate([getEntityEditRoute(this.entityType, this.itemId), 'bitstreams']); - } else { - this.bitstream.bundle.pipe(getFirstSucceededRemoteDataPayload(), - mergeMap((bundle: Bundle) => bundle.item.pipe(getFirstSucceededRemoteDataPayload()))) - .subscribe((item) => { - this.router.navigate(([getItemEditRoute(item), 'bitstreams'])); - }); - } + this.router.navigate([getEntityEditRoute(this.entityType, this.itemId), 'bitstreams']); } /** diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 66824e56b3..5f09574ab0 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -705,6 +705,8 @@ "bitstream.edit.notifications.error.format.title": "An error occurred saving the bitstream's format", + "bitstream.edit.notifications.error.primaryBitstream.title": "An error occurred saving the primary bitstream", + "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.", From 94f52721d45159fa9037111ad8063815dc77a0d5 Mon Sep 17 00:00:00 2001 From: lotte Date: Wed, 26 Apr 2023 11:29:17 +0200 Subject: [PATCH 03/10] 101289: #1578 Removed border from primaryBitstream switch --- .../edit-bitstream-page/edit-bitstream-page.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 5110a5e936..df8f285a6a 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 @@ -291,7 +291,7 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { }, primaryBitstream: { grid: { - host: 'col col-sm-4 d-inline-block switch' + host: 'col col-sm-4 d-inline-block switch border-0' } }, description: { From 3ee09834b1d070c11630d1221ccbbb746982c801 Mon Sep 17 00:00:00 2001 From: lotte Date: Wed, 26 Apr 2023 13:47:39 +0200 Subject: [PATCH 04/10] 101289: #1578 Changed primarybitstream field to primaryBitstreamUUID --- .../edit-bitstream-page/edit-bitstream-page.component.ts | 2 +- src/app/core/shared/bundle.model.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) 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 df8f285a6a..d84b8ae4e6 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 @@ -371,7 +371,7 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { * Path to patch primary bitstream on the bundle * @private */ - private readonly primaryBitstreamPath = '/primarybitstream'; + private readonly primaryBitstreamPath = '/primaryBitstreamUUID'; constructor(private route: ActivatedRoute, private router: Router, diff --git a/src/app/core/shared/bundle.model.ts b/src/app/core/shared/bundle.model.ts index 72c466d2d6..bcaa582703 100644 --- a/src/app/core/shared/bundle.model.ts +++ b/src/app/core/shared/bundle.model.ts @@ -1,4 +1,4 @@ -import { autoserializeAs, deserialize, inheritSerialization } from 'cerialize'; +import { autoserialize, deserialize, inheritSerialization } from 'cerialize'; import { Observable } from 'rxjs'; @@ -30,9 +30,9 @@ export class Bundle extends DSpaceObject { }; /** - * The ID of the primaryBitstream of this Bundle + * The UUID of the primaryBitstream of this Bundle */ - @autoserializeAs('primarybitstream') + @autoserialize primaryBitstreamUUID: string; /** From d69adab419cc415295e8240ffa05992701459182 Mon Sep 17 00:00:00 2001 From: Art Lowel Date: Wed, 7 Jun 2023 16:22:57 +0200 Subject: [PATCH 05/10] refactor primarybitstreams to work with DELETE, POST and PUT instead of PATCh --- .../bitstream-page/bitstream-page.resolver.ts | 2 +- .../edit-bitstream-page.component.ts | 96 ++++++++----- .../data/primary-bitstream-data.service.ts | 134 ++++++++++++++++++ src/app/core/shared/bundle.model.ts | 6 - 4 files changed, 197 insertions(+), 41 deletions(-) create mode 100644 src/app/core/data/primary-bitstream-data.service.ts diff --git a/src/app/bitstream-page/bitstream-page.resolver.ts b/src/app/bitstream-page/bitstream-page.resolver.ts index be92041dfc..db2af2d554 100644 --- a/src/app/bitstream-page/bitstream-page.resolver.ts +++ b/src/app/bitstream-page/bitstream-page.resolver.ts @@ -12,7 +12,7 @@ import { getFirstCompletedRemoteData } from '../core/shared/operators'; * Requesting them as embeds will limit the number of requests */ export const BITSTREAM_PAGE_LINKS_TO_FOLLOW: FollowLinkConfig[] = [ - followLink('bundle', {}, followLink('item')), + followLink('bundle', {}, followLink('primaryBitstream'), followLink('item')), followLink('format') ]; 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 10d9652a65..8b3dad3c1c 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,7 +1,7 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; import { Bitstream } from '../../core/shared/bitstream.model'; import { ActivatedRoute, Router } from '@angular/router'; -import { map, switchMap, tap } from 'rxjs/operators'; +import { map, switchMap, tap, filter } from 'rxjs/operators'; import { combineLatest, combineLatest as observableCombineLatest, Observable, of as observableOf, Subscription } from 'rxjs'; import { DynamicFormControlModel, DynamicFormGroupModel, DynamicFormLayout, DynamicFormService, DynamicInputModel, DynamicSelectModel } from '@ng-dynamic-forms/core'; import { UntypedFormGroup } from '@angular/forms'; @@ -14,7 +14,7 @@ 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, isEmpty, isNotEmpty } from '../../shared/empty.util'; +import { hasValue, isEmpty, isNotEmpty, hasValueOperator } from '../../shared/empty.util'; import { Metadata } from '../../core/shared/metadata.utils'; import { Location } from '@angular/common'; import { RemoteData } from '../../core/data/remote-data'; @@ -27,6 +27,8 @@ import { DsDynamicInputModel } from '../../shared/form/builder/ds-dynamic-form-u import { DsDynamicTextAreaModel } from '../../shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-textarea.model'; import { BundleDataService } from '../../core/data/bundle-data.service'; import { Operation } from 'fast-json-patch'; +import { PrimaryBitstreamService } from '../../core/data/primary-bitstream-data.service'; +import { hasSucceeded } from '../../core/data/request-entry-state.model'; @Component({ selector: 'ds-edit-bitstream-page', @@ -51,6 +53,11 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { */ bitstreamFormatsRD$: Observable>>; + /** + * The UUID of the primary bitstream for this bundle + */ + primaryBitstreamUUID: string; + /** * The bitstream to edit */ @@ -383,6 +390,7 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { public dsoNameService: DSONameService, private notificationsService: NotificationsService, private bitstreamFormatService: BitstreamFormatDataService, + private primaryBitstreamService: PrimaryBitstreamService, private bundleService: BundleDataService) { } @@ -414,6 +422,12 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { getFirstSucceededRemoteDataPayload(), ); + const primaryBitstream$ = bundle$.pipe( + hasValueOperator(), + switchMap((bundle: Bundle) => this.bitstreamService.findByHref(bundle._links.primaryBitstream.href)), + getFirstSucceededRemoteDataPayload() + ); + const item$ = bundle$.pipe( switchMap((bundle: Bundle) => bundle.item), getFirstSucceededRemoteDataPayload() @@ -423,12 +437,16 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { bitstream$, allFormats$, bundle$, + primaryBitstream$, item$, ).pipe() - .subscribe(([bitstream, allFormats, bundle, item]) => { + .subscribe(([bitstream, allFormats, bundle, primaryBitstream, item]) => { this.bitstream = bitstream as Bitstream; this.formats = allFormats.page; this.bundle = bundle; + // hasValue(primaryBitstream) because if there's no primaryBitstream on the bundle it will + // be a success response, but empty + this.primaryBitstreamUUID = hasValue(primaryBitstream) ? primaryBitstream.uuid : null; this.itemId = item.uuid; this.setIiifStatus(this.bitstream); }) @@ -460,7 +478,7 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { this.formGroup.patchValue({ fileNamePrimaryContainer: { fileName: bitstream.name, - primaryBitstream: this.bundle.primaryBitstreamUUID === bitstream.uuid + primaryBitstream: this.primaryBitstreamUUID === bitstream.uuid }, descriptionContainer: { description: bitstream.firstMetadataValue('dc.description') @@ -572,26 +590,52 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { const selectedFormat = this.formats.find((f: BitstreamFormat) => f.id === updatedValues.formatContainer.selectedFormat); const isNewFormat = selectedFormat.id !== this.originalFormat.id; const isPrimary = updatedValues.fileNamePrimaryContainer.primaryBitstream; - const wasPrimary = this.bundle.primaryBitstreamUUID === this.bitstream.uuid; + const wasPrimary = this.primaryBitstreamUUID === this.bitstream.uuid; let bitstream$; let bundle$: Observable; + let errorWhileSaving = false; if (wasPrimary !== isPrimary) { - const patchOperations: Operation[] = this.retrieveBundlePatch(wasPrimary); - bundle$ = this.bundleService.patch(this.bundle, patchOperations).pipe( - getFirstCompletedRemoteData(), - map((bundleResponse: RemoteData) => { - if (hasValue(bundleResponse) && bundleResponse.hasFailed) { - this.notificationsService.error( - this.translate.instant(this.NOTIFICATIONS_PREFIX + 'error.primaryBitstream.title'), - bundleResponse.errorMessage - ); + let bundleRd$: Observable>; + if (wasPrimary) { + bundleRd$ = this.primaryBitstreamService.delete(this.bundle); + } else if (hasValue(this.primaryBitstreamUUID)) { + bundleRd$ = this.primaryBitstreamService.put(this.bitstream, this.bundle); + } else { + bundleRd$ = this.primaryBitstreamService.create(this.bitstream, this.bundle); + } + + const completedBundleRd$ = bundleRd$.pipe(getFirstCompletedRemoteData()); + + this.subs.push(completedBundleRd$.pipe( + filter((bundleRd: RemoteData) => bundleRd.hasFailed) + ).subscribe((bundleRd: RemoteData) => { + this.notificationsService.error( + this.translate.instant(this.NOTIFICATIONS_PREFIX + 'error.primaryBitstream.title'), + bundleRd.errorMessage + ); + errorWhileSaving = true; + })); + + bundle$ = completedBundleRd$.pipe( + map((bundleRd: RemoteData) => { + if (bundleRd.hasSucceeded) { + return bundleRd.payload } else { - return bundleResponse.payload; + return this.bundle; } }) ); + + this.subs.push(bundle$.pipe( + hasValueOperator(), + switchMap((bundle: Bundle) => this.bitstreamService.findByHref(bundle._links.primaryBitstream.href, false)), + getFirstSucceededRemoteDataPayload() + ).subscribe((bitstream: Bitstream) => { + this.primaryBitstreamUUID = hasValue(bitstream) ? bitstream.uuid : null; + })); + } else { bundle$ = observableOf(this.bundle); } @@ -626,28 +670,12 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { this.translate.instant(this.NOTIFICATIONS_PREFIX + 'saved.title'), this.translate.instant(this.NOTIFICATIONS_PREFIX + 'saved.content') ); - this.navigateToItemEditBitstreams(); + if (!errorWhileSaving) { + this.navigateToItemEditBitstreams(); + } }); } - private retrieveBundlePatch(wasPrimary: boolean): Operation[] { - // No longer primary bitstream: remove - if (wasPrimary) { - return [{ - path: this.primaryBitstreamPath, - op: 'remove' - }]; - } else { - // Has become primary bitstream - // If it already had a value: replace, otherwise: add - return [{ - path: this.primaryBitstreamPath, - op: hasValue(this.bundle.primaryBitstreamUUID) ? 'replace' : 'add', - value: this.bitstream.uuid - }]; - } - } - /** * Parse form data to an updated bitstream object * @param rawForm Raw form data diff --git a/src/app/core/data/primary-bitstream-data.service.ts b/src/app/core/data/primary-bitstream-data.service.ts new file mode 100644 index 0000000000..6380a6f148 --- /dev/null +++ b/src/app/core/data/primary-bitstream-data.service.ts @@ -0,0 +1,134 @@ +import { Bitstream } from '../shared/bitstream.model'; +import { DeleteDataImpl } from './base/delete-data'; +import { PutDataImpl } from './base/put-data'; +import { CreateDataImpl } from './base/create-data'; +import { Injectable } from '@angular/core'; +import { RequestService } from './request.service'; +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { ObjectCacheService } from '../cache/object-cache.service'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { Observable, switchMap } from 'rxjs'; +import { RemoteData } from './remote-data'; +import { Bundle } from '../shared/bundle.model'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { constructIdEndpointDefault } from './base/identifiable-data.service'; +import { HttpOptions } from '../dspace-rest/dspace-rest.service'; +import { HttpHeaders } from '@angular/common/http'; +import { GenericConstructor } from '../shared/generic-constructor'; +import { PutRequest, PostRequest, DeleteRequest } from './request.models'; +import { getAllCompletedRemoteData } from '../shared/operators'; +import { NoContent } from '../shared/NoContent.model'; +import { BundleDataService } from './bundle-data.service'; + +@Injectable({ + providedIn: 'root', +}) +export class PrimaryBitstreamService { + private createData: CreateDataImpl; + private putData: PutDataImpl; + private deleteData: DeleteDataImpl; + + constructor( + protected requestService: RequestService, + protected rdbService: RemoteDataBuildService, + protected objectCache: ObjectCacheService, + protected halService: HALEndpointService, + protected notificationsService: NotificationsService, + protected bundleDataService: BundleDataService, + ) { + // linkPath can be undefined because we'll only use them to do things "byHref" + this.createData = new CreateDataImpl(undefined, requestService, rdbService, objectCache, halService, notificationsService, undefined); + this.putData = new PutDataImpl(undefined, requestService, rdbService, objectCache, halService, undefined); + this.deleteData = new DeleteDataImpl(undefined, requestService, rdbService, objectCache, halService, notificationsService, undefined, constructIdEndpointDefault); + } + + /** + * Returns the type of HttpOptions object needed from primary bitstream requests. + * i.e. with a Content-Type header set to `text/uri-list` + * @protected + */ + protected getHttpOptions(): HttpOptions { + const options: HttpOptions = Object.create({}); + let headers = new HttpHeaders(); + headers = headers.append('Content-Type', 'text/uri-list'); + options.headers = headers; + return options; + } + + /** + * Send a request of the given type to the endpointURL with an optional primaryBitstreamSelfLink + * as payload, and return the resulting Observable + * + * @param requestType The type of request: PostRequest, PutRequest, or DeleteRequest + * @param endpointURL The endpoint URL + * @param primaryBitstreamSelfLink + * @protected + */ + protected createAndSendRequest( + requestType: GenericConstructor, + endpointURL: string, + primaryBitstreamSelfLink?: string, + ): Observable> { + const requestId = this.requestService.generateRequestId(); + const request = new requestType( + requestId, + endpointURL, + primaryBitstreamSelfLink, + this.getHttpOptions() + ); + + this.requestService.send(request); + + return this.rdbService.buildFromRequestUUID(requestId); + } + + /** + * Create a new primaryBitstream + * + * @param primaryBitstream The object to create + * @param bundle The bundle to create it on + */ + create(primaryBitstream: Bitstream, bundle: Bundle): Observable> { + return this.createAndSendRequest( + PostRequest, + bundle._links.primaryBitstream.href, + primaryBitstream.self + ) as Observable>; + } + + /** + * Update an exiting primaryBitstream + * + * @param primaryBitstream The object to update + * @param bundle The bundle to update it on + */ + put(primaryBitstream: Bitstream, bundle: Bundle): Observable> { + return this.createAndSendRequest( + PutRequest, + bundle._links.primaryBitstream.href, + primaryBitstream.self + ) as Observable>; + } + + /** + * Delete an exiting primaryBitstream + * + * @param bundle The bundle to delete it from + */ + delete(bundle: Bundle): Observable> { + return this.createAndSendRequest( + DeleteRequest, + bundle._links.primaryBitstream.href + ).pipe( + getAllCompletedRemoteData(), + switchMap((rd: RemoteData) => { + if (rd.hasSucceeded) { + return this.bundleDataService.findByHref(bundle.self, false); + } else { + [bundle]; + } + }) + ); + } + +} diff --git a/src/app/core/shared/bundle.model.ts b/src/app/core/shared/bundle.model.ts index bcaa582703..f37cd96545 100644 --- a/src/app/core/shared/bundle.model.ts +++ b/src/app/core/shared/bundle.model.ts @@ -29,12 +29,6 @@ export class Bundle extends DSpaceObject { item: HALLink; }; - /** - * The UUID of the primaryBitstream of this Bundle - */ - @autoserialize - primaryBitstreamUUID: string; - /** * The primary Bitstream of this Bundle * Will be undefined unless the primaryBitstream {@link HALLink} has been resolved. From 56cba82c2df9e2fd2cd9816db7c5fe7a2dfe419a Mon Sep 17 00:00:00 2001 From: lotte Date: Thu, 8 Jun 2023 14:16:04 +0200 Subject: [PATCH 06/10] 101289: intermediate commit for test issues --- .../edit-bitstream-page.component.spec.ts | 140 ++++++++++-------- .../edit-bitstream-page.component.ts | 31 ++-- 2 files changed, 91 insertions(+), 80 deletions(-) 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 04ab4e47b0..3be3297880 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 @@ -24,8 +24,7 @@ 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'; -import { Bundle } from '../../core/shared/bundle.model'; -import { BundleDataService } from '../../core/data/bundle-data.service'; +import { PrimaryBitstreamService } from '../../core/data/primary-bitstream-data.service'; const infoNotification: INotification = new Notification('id', NotificationType.Info, 'info'); const warningNotification: INotification = new Notification('id', NotificationType.Warning, 'warning'); @@ -34,6 +33,7 @@ const successNotification: INotification = new Notification('id', NotificationTy let notificationsService: NotificationsService; let formService: DynamicFormService; let bitstreamService: BitstreamDataService; +let primaryBitstreamService: PrimaryBitstreamService; let bitstreamFormatService: BitstreamFormatDataService; let dsoNameService: DSONameService; let bitstream: Bitstream; @@ -41,23 +41,19 @@ let bitstreamID: string; let selectedFormat: BitstreamFormat; let allFormats: BitstreamFormat[]; let router: Router; -let bundleDataService; -let bundleWithCurrentPrimary: Bundle; -let bundleWithDifferentPrimary: Bundle; -let bundleWithNoPrimary: Bundle; - +let currentPrimary: string; +let differentPrimary: string; +let bundle; let comp: EditBitstreamPageComponent; let fixture: ComponentFixture; -describe('EditBitstreamPageComponent', () => { +fdescribe('EditBitstreamPageComponent', () => { beforeEach(() => { bitstreamID = 'current-bitstream-id'; - bundleWithCurrentPrimary = Object.assign(new Bundle(), { 'primaryBitstreamUUID': bitstreamID }); - bundleWithDifferentPrimary = Object.assign(new Bundle(), { 'primaryBitstreamUUID': '12345-abcde-54321-edcba' }); - bundleWithNoPrimary = Object.assign(new Bundle(), { 'primaryBitstreamUUID': null }); - bundleDataService = jasmine.createSpyObj('BundleDataService', ['patch']); - bundleDataService.patch.and.callFake((a, b) => createSuccessfulRemoteDataObject$(a)); + currentPrimary = bitstreamID; + differentPrimary = '12345-abcde-54321-edcba'; + allFormats = [ Object.assign({ id: '1', @@ -116,12 +112,47 @@ describe('EditBitstreamPageComponent', () => { success: successNotification } ); + + bundle = { + _links: { + primaryBitstream: { + href: 'bundle-selflink' + } + }, + item: createSuccessfulRemoteDataObject$(Object.assign(new Item(), { + uuid: 'some-uuid', + firstMetadataValue(keyOrKeys: string | string[], valueFilter?: MetadataValueFilter): string { + return undefined; + }, + })) + }; + + const result = createSuccessfulRemoteDataObject$(bundle); + primaryBitstreamService = jasmine.createSpyObj('PrimaryBitstreamService', + { + put: result, + create: result, + delete: result, + }); + }); describe('EditBitstreamPageComponent no IIIF fields', () => { beforeEach(waitForAsync(() => { - + bundle = { + _links: { + primaryBitstream: { + href: 'bundle-selflink' + } + }, + item: createSuccessfulRemoteDataObject$(Object.assign(new Item(), { + uuid: 'some-uuid', + firstMetadataValue(keyOrKeys: string | string[], valueFilter?: MetadataValueFilter): string { + return undefined; + }, + })) + }; const bundleName = 'ORIGINAL'; bitstream = Object.assign(new Bitstream(), { @@ -143,17 +174,11 @@ describe('EditBitstreamPageComponent', () => { _links: { self: 'bitstream-selflink' }, - bundle: createSuccessfulRemoteDataObject$({ - item: createSuccessfulRemoteDataObject$(Object.assign(new Item(), { - uuid: 'some-uuid', - firstMetadataValue(keyOrKeys: string | string[], valueFilter?: MetadataValueFilter): string { - return undefined; - }, - })) - }) + bundle: createSuccessfulRemoteDataObject$(bundle) }); bitstreamService = jasmine.createSpyObj('bitstreamService', { findById: createSuccessfulRemoteDataObject$(bitstream), + findByHref: createSuccessfulRemoteDataObject$(bitstream), update: createSuccessfulRemoteDataObject$(bitstream), updateFormat: createSuccessfulRemoteDataObject$(bitstream), commitUpdates: {}, @@ -182,7 +207,7 @@ describe('EditBitstreamPageComponent', () => { { provide: BitstreamDataService, useValue: bitstreamService }, { provide: DSONameService, useValue: dsoNameService }, { provide: BitstreamFormatDataService, useValue: bitstreamFormatService }, - { provide: BundleDataService, useValue: bundleDataService }, + { provide: PrimaryBitstreamService, useValue: primaryBitstreamService }, ChangeDetectorRef ], schemas: [NO_ERRORS_SCHEMA] @@ -198,7 +223,7 @@ describe('EditBitstreamPageComponent', () => { spyOn(router, 'navigate'); }); - describe('on startup', () => { + fdescribe('on startup', () => { let rawForm; beforeEach(() => { @@ -222,7 +247,7 @@ describe('EditBitstreamPageComponent', () => { }); describe('when the bitstream is the primary bitstream on the bundle', () => { beforeEach(() => { - (comp as any).bundle = bundleWithCurrentPrimary; + (comp as any).primaryBitstreamUUID = currentPrimary; comp.setForm(); rawForm = comp.formGroup.getRawValue(); @@ -233,7 +258,7 @@ describe('EditBitstreamPageComponent', () => { }); describe('when the bitstream is not the primary bitstream on the bundle', () => { beforeEach(() => { - (comp as any).bundle = bundleWithDifferentPrimary; + (comp as any).primaryBitstreamUUID = differentPrimary; comp.setForm(); rawForm = comp.formGroup.getRawValue(); }); @@ -263,38 +288,23 @@ describe('EditBitstreamPageComponent', () => { describe('from a different primary bitstream', () => { beforeEach(() => { - (comp as any).bundle = bundleWithDifferentPrimary; + (comp as any).primaryBitstreamUUID = differentPrimary; comp.onSubmit(); }); - it('should call patch with a replace operation', () => { - expect(bundleDataService.patch).toHaveBeenCalledWith(bundleWithDifferentPrimary, [jasmine.objectContaining({ - op: 'replace' - })]); - }); - - it('should call patch with the correct bitstream uuid', () => { - expect(bundleDataService.patch).toHaveBeenCalledWith(bundleWithDifferentPrimary, [jasmine.objectContaining({ - value: bitstreamID - })]); + it('should call put with the correct bitstream on the PrimaryBitstreamService', () => { + expect(primaryBitstreamService.put).toHaveBeenCalledWith(jasmine.objectContaining({uuid: currentPrimary}), bundle); }); }); + describe('from no primary bitstream', () => { beforeEach(() => { - (comp as any).bundle = bundleWithNoPrimary; + (comp as any).primaryBitstreamUUID = null; comp.onSubmit(); }); - it('should call patch with an add operation', () => { - expect(bundleDataService.patch).toHaveBeenCalledWith(bundleWithNoPrimary, [jasmine.objectContaining({ - op: 'add' - })]); - }); - - it('should call patch with the correct bitstream uuid', () => { - expect(bundleDataService.patch).toHaveBeenCalledWith(bundleWithNoPrimary, [jasmine.objectContaining({ - value: bitstreamID - })]); + it('should call create with the correct bitstream on the PrimaryBitstreamService', () => { + expect(primaryBitstreamService.create).toHaveBeenCalledWith(jasmine.objectContaining({uuid: currentPrimary}), bundle); }); }); }); @@ -306,39 +316,42 @@ describe('EditBitstreamPageComponent', () => { describe('from the current bitstream', () => { beforeEach(() => { - (comp as any).bundle = bundleWithCurrentPrimary; + (comp as any).primaryBitstreamUUID = currentPrimary; comp.onSubmit(); }); - it('should call patch with a remove operation', () => { - expect(bundleDataService.patch).toHaveBeenCalledWith(bundleWithCurrentPrimary, [jasmine.objectContaining({ - op: 'remove' - })]); + it('should call delete on the PrimaryBitstreamService', () => { + expect(primaryBitstreamService.delete).toHaveBeenCalledWith(jasmine.objectContaining(bundle)); }); }); }); }); - describe('when the primaryBitstream did not changed', () => { + describe('when the primaryBitstream did not change', () => { describe('the current bitstream stayed the primary bitstream', () => { beforeEach(() => { const rawValue = Object.assign(comp.formGroup.getRawValue(), { fileNamePrimaryContainer: { primaryBitstream: true } }); spyOn(comp.formGroup, 'getRawValue').and.returnValue(rawValue); - (comp as any).bundle = bundleWithCurrentPrimary; + (comp as any).primaryBitstreamUUID = currentPrimary; comp.onSubmit(); }); - it('should not call patch on the bundle data service', () => { - expect(bundleDataService.patch).not.toHaveBeenCalled(); + it('should not call anything on the PrimaryBitstreamService', () => { + expect(primaryBitstreamService.put).not.toHaveBeenCalled(); + expect(primaryBitstreamService.delete).not.toHaveBeenCalled(); + expect(primaryBitstreamService.create).not.toHaveBeenCalled(); }); }); + describe('the bitstream was not and did not become the primary bitstream', () => { beforeEach(() => { const rawValue = Object.assign(comp.formGroup.getRawValue(), { fileNamePrimaryContainer: { primaryBitstream: false } }); spyOn(comp.formGroup, 'getRawValue').and.returnValue(rawValue); - (comp as any).bundle = bundleWithDifferentPrimary; + (comp as any).primaryBitstreamUUID = differentPrimary; comp.onSubmit(); }); - it('should not call patch on the bundle data service', () => { - expect(bundleDataService.patch).not.toHaveBeenCalled(); + it('should not call anything on the PrimaryBitstreamService', () => { + expect(primaryBitstreamService.put).not.toHaveBeenCalled(); + expect(primaryBitstreamService.delete).not.toHaveBeenCalled(); + expect(primaryBitstreamService.create).not.toHaveBeenCalled(); }); }); }); @@ -451,6 +464,7 @@ describe('EditBitstreamPageComponent', () => { }); bitstreamService = jasmine.createSpyObj('bitstreamService', { findById: createSuccessfulRemoteDataObject$(bitstream), + findByHref: createSuccessfulRemoteDataObject$(bitstream), update: createSuccessfulRemoteDataObject$(bitstream), updateFormat: createSuccessfulRemoteDataObject$(bitstream), commitUpdates: {}, @@ -477,7 +491,7 @@ describe('EditBitstreamPageComponent', () => { {provide: BitstreamDataService, useValue: bitstreamService}, {provide: DSONameService, useValue: dsoNameService}, {provide: BitstreamFormatDataService, useValue: bitstreamFormatService}, - { provide: BundleDataService, useValue: bundleDataService }, + { provide: PrimaryBitstreamService, useValue: primaryBitstreamService }, ChangeDetectorRef ], schemas: [NO_ERRORS_SCHEMA] @@ -595,7 +609,7 @@ describe('EditBitstreamPageComponent', () => { {provide: BitstreamDataService, useValue: bitstreamService}, {provide: DSONameService, useValue: dsoNameService}, {provide: BitstreamFormatDataService, useValue: bitstreamFormatService}, - { provide: BundleDataService, useValue: bundleDataService }, + { provide: PrimaryBitstreamService, useValue: primaryBitstreamService }, ChangeDetectorRef ], schemas: [NO_ERRORS_SCHEMA] @@ -617,7 +631,7 @@ describe('EditBitstreamPageComponent', () => { rawForm = comp.formGroup.getRawValue(); }); - it('should NOT set isIIIF to true', () => { + it('should NOT set is IIIF to true', () => { expect(comp.isIIIF).toBeFalse(); }); it('should put the \"IIIF Label\" input not to be shown', () => { 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 8b3dad3c1c..25c7f41868 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,7 +1,7 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; import { Bitstream } from '../../core/shared/bitstream.model'; import { ActivatedRoute, Router } from '@angular/router'; -import { map, switchMap, tap, filter } from 'rxjs/operators'; +import { filter, map, switchMap, tap } from 'rxjs/operators'; import { combineLatest, combineLatest as observableCombineLatest, Observable, of as observableOf, Subscription } from 'rxjs'; import { DynamicFormControlModel, DynamicFormGroupModel, DynamicFormLayout, DynamicFormService, DynamicInputModel, DynamicSelectModel } from '@ng-dynamic-forms/core'; import { UntypedFormGroup } from '@angular/forms'; @@ -14,7 +14,7 @@ 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, isEmpty, isNotEmpty, hasValueOperator } from '../../shared/empty.util'; +import { hasValue, hasValueOperator, isEmpty, isNotEmpty } from '../../shared/empty.util'; import { Metadata } from '../../core/shared/metadata.utils'; import { Location } from '@angular/common'; import { RemoteData } from '../../core/data/remote-data'; @@ -25,10 +25,7 @@ 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'; -import { BundleDataService } from '../../core/data/bundle-data.service'; -import { Operation } from 'fast-json-patch'; import { PrimaryBitstreamService } from '../../core/data/primary-bitstream-data.service'; -import { hasSucceeded } from '../../core/data/request-entry-state.model'; @Component({ selector: 'ds-edit-bitstream-page', @@ -374,12 +371,6 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { */ private bundle: Bundle; - /** - * Path to patch primary bitstream on the bundle - * @private - */ - private readonly primaryBitstreamPath = '/primaryBitstreamUUID'; - constructor(private route: ActivatedRoute, private router: Router, private changeDetectorRef: ChangeDetectorRef, @@ -391,7 +382,7 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { private notificationsService: NotificationsService, private bitstreamFormatService: BitstreamFormatDataService, private primaryBitstreamService: PrimaryBitstreamService, - private bundleService: BundleDataService) { + ) { } /** @@ -404,33 +395,39 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { this.itemId = this.route.snapshot.queryParams.itemId; this.entityType = this.route.snapshot.queryParams.entityType; - this.bitstreamRD$ = this.route.data.pipe(map((data) => data.bitstream)); + this.bitstreamRD$ = this.route.data.pipe(map((data: any) => data.bitstream)); this.bitstreamFormatsRD$ = this.bitstreamFormatService.findAll(this.findAllOptions); const bitstream$ = this.bitstreamRD$.pipe( getFirstSucceededRemoteData(), - getRemoteDataPayload() + getRemoteDataPayload(), + tap(t => console.log(t)), ); const allFormats$ = this.bitstreamFormatsRD$.pipe( getFirstSucceededRemoteData(), - getRemoteDataPayload() + getRemoteDataPayload(), + tap(t => console.log(t)), ); const bundle$ = bitstream$.pipe( switchMap((bitstream: Bitstream) => bitstream.bundle), getFirstSucceededRemoteDataPayload(), + tap(t => console.log(t)), ); const primaryBitstream$ = bundle$.pipe( hasValueOperator(), + tap(t => console.log(t._links.primaryBitstream.href)), switchMap((bundle: Bundle) => this.bitstreamService.findByHref(bundle._links.primaryBitstream.href)), - getFirstSucceededRemoteDataPayload() + getFirstSucceededRemoteDataPayload(), + tap(t => console.log(t)), ); const item$ = bundle$.pipe( switchMap((bundle: Bundle) => bundle.item), - getFirstSucceededRemoteDataPayload() + getFirstSucceededRemoteDataPayload(), + tap(t => console.log(t)), ); this.subs.push( observableCombineLatest( From c9324b0714fdaadd7716e9184d4e0876238b759a Mon Sep 17 00:00:00 2001 From: Art Lowel Date: Thu, 8 Jun 2023 15:44:30 +0200 Subject: [PATCH 07/10] fix issue where iiif tests would fail --- .../edit-bitstream-page.component.spec.ts | 19 +++++++++++++++---- .../edit-bitstream-page.component.ts | 6 ------ 2 files changed, 15 insertions(+), 10 deletions(-) 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 3be3297880..2e8c6368c1 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 @@ -47,7 +47,7 @@ let bundle; let comp: EditBitstreamPageComponent; let fixture: ComponentFixture; -fdescribe('EditBitstreamPageComponent', () => { +describe('EditBitstreamPageComponent', () => { beforeEach(() => { bitstreamID = 'current-bitstream-id'; @@ -223,7 +223,7 @@ fdescribe('EditBitstreamPageComponent', () => { spyOn(router, 'navigate'); }); - fdescribe('on startup', () => { + describe('on startup', () => { let rawForm; beforeEach(() => { @@ -454,13 +454,18 @@ fdescribe('EditBitstreamPageComponent', () => { self: 'bitstream-selflink' }, bundle: createSuccessfulRemoteDataObject$({ + _links: { + primaryBitstream: { + href: 'bundle-selflink' + } + }, 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), @@ -574,16 +579,22 @@ fdescribe('EditBitstreamPageComponent', () => { self: 'bitstream-selflink' }, bundle: createSuccessfulRemoteDataObject$({ + _links: { + primaryBitstream: { + href: 'bundle-selflink' + } + }, 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), + findByHref: createSuccessfulRemoteDataObject$(bitstream), update: createSuccessfulRemoteDataObject$(bitstream), updateFormat: createSuccessfulRemoteDataObject$(bitstream), commitUpdates: {}, 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 25c7f41868..0d5e1eecc1 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 @@ -401,33 +401,27 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { const bitstream$ = this.bitstreamRD$.pipe( getFirstSucceededRemoteData(), getRemoteDataPayload(), - tap(t => console.log(t)), ); const allFormats$ = this.bitstreamFormatsRD$.pipe( getFirstSucceededRemoteData(), getRemoteDataPayload(), - tap(t => console.log(t)), ); const bundle$ = bitstream$.pipe( switchMap((bitstream: Bitstream) => bitstream.bundle), getFirstSucceededRemoteDataPayload(), - tap(t => console.log(t)), ); const primaryBitstream$ = bundle$.pipe( hasValueOperator(), - tap(t => console.log(t._links.primaryBitstream.href)), switchMap((bundle: Bundle) => this.bitstreamService.findByHref(bundle._links.primaryBitstream.href)), getFirstSucceededRemoteDataPayload(), - tap(t => console.log(t)), ); const item$ = bundle$.pipe( switchMap((bundle: Bundle) => bundle.item), getFirstSucceededRemoteDataPayload(), - tap(t => console.log(t)), ); this.subs.push( observableCombineLatest( From a936878c9673dc79d98404939595cf5c6b2f3ded Mon Sep 17 00:00:00 2001 From: lotte Date: Thu, 8 Jun 2023 16:02:33 +0200 Subject: [PATCH 08/10] 101289: tests --- .../edit-bitstream-page.component.spec.ts | 2 +- .../edit-bitstream-page.component.ts | 2 +- .../data/primary-bitstream.service.spec.ts | 158 ++++++++++++++++++ ...ervice.ts => primary-bitstream.service.ts} | 6 +- 4 files changed, 163 insertions(+), 5 deletions(-) create mode 100644 src/app/core/data/primary-bitstream.service.spec.ts rename src/app/core/data/{primary-bitstream-data.service.ts => primary-bitstream.service.ts} (97%) 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 2e8c6368c1..b83f2b9664 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 @@ -24,7 +24,7 @@ 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'; -import { PrimaryBitstreamService } from '../../core/data/primary-bitstream-data.service'; +import { PrimaryBitstreamService } from '../../core/data/primary-bitstream.service'; const infoNotification: INotification = new Notification('id', NotificationType.Info, 'info'); const warningNotification: INotification = new Notification('id', NotificationType.Warning, 'warning'); 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 0d5e1eecc1..cdc905d456 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 @@ -25,7 +25,7 @@ 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'; -import { PrimaryBitstreamService } from '../../core/data/primary-bitstream-data.service'; +import { PrimaryBitstreamService } from '../../core/data/primary-bitstream.service'; @Component({ selector: 'ds-edit-bitstream-page', diff --git a/src/app/core/data/primary-bitstream.service.spec.ts b/src/app/core/data/primary-bitstream.service.spec.ts new file mode 100644 index 0000000000..93e9882d04 --- /dev/null +++ b/src/app/core/data/primary-bitstream.service.spec.ts @@ -0,0 +1,158 @@ +import { ObjectCacheService } from '../cache/object-cache.service'; +import { RequestService } from './request.service'; +import { Bitstream } from '../shared/bitstream.model'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { getMockRequestService } from '../../shared/mocks/request.service.mock'; +import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub'; +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { getMockRemoteDataBuildService } from '../../shared/mocks/remote-data-build.service.mock'; +import { PrimaryBitstreamService } from './primary-bitstream.service'; +import { BundleDataService } from './bundle-data.service'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub'; +import { CreateRequest, DeleteRequest, PostRequest, PutRequest } from './request.models'; +import { createFailedRemoteDataObject, createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; +import { Bundle } from '../shared/bundle.model'; +import { getTestScheduler } from 'jasmine-marbles'; +import { of as observableOf } from 'rxjs'; + +fdescribe('PrimaryBitstreamService', () => { + let service: PrimaryBitstreamService; + let objectCache: ObjectCacheService; + let requestService: RequestService; + let halService: HALEndpointService; + let rdbService: RemoteDataBuildService; + let notificationService: NotificationsService; + let bundleDataService: BundleDataService; + + const bitstream = Object.assign(new Bitstream(), { + uuid: 'fake-bitstream', + _links: { + self: { href: 'fake-bitstream-self' } + } + }); + + const bundle = Object.assign(new Bundle(), { + uuid: 'fake-bundle', + _links: { + self: { href: 'fake-bundle-self' }, + primaryBitstream: { href: 'fake-primary-bitstream-self' }, + } + }); + + const url = 'fake-bitstream-url'; + + beforeEach(() => { + objectCache = jasmine.createSpyObj('objectCache', { + remove: jasmine.createSpy('remove') + }); + requestService = getMockRequestService(); + halService = Object.assign(new HALEndpointServiceStub(url)); + + rdbService = getMockRemoteDataBuildService(); + notificationService = new NotificationsServiceStub() as any; + bundleDataService = jasmine.createSpyObj('bundleDataService', {'findByHref': createSuccessfulRemoteDataObject$(bundle)}); + service = new PrimaryBitstreamService(requestService, rdbService, objectCache, halService, notificationService, bundleDataService); + }); + + describe('getHttpOptions', () => { + it('should return a HttpOptions object with text/url-list Context-Type header', () => { + const result = (service as any).getHttpOptions() + expect(result.headers.get('Content-Type')).toEqual('text/uri-list'); + }); + }); + + describe('createAndSendRequest', () => { + const testId = '12345-12345'; + const options = {}; + const testResult = createSuccessfulRemoteDataObject(new Bundle()); + + beforeEach(() => { + spyOn(service as any, 'getHttpOptions').and.returnValue(options); + (requestService.generateRequestId as jasmine.Spy).and.returnValue(testId); + spyOn(rdbService, 'buildFromRequestUUID').and.returnValue(observableOf(testResult)); + }); + + it('should return a Request object with the given constructor and the given parameters', () => { + const result = (service as any).createAndSendRequest(CreateRequest, url, bitstream.self); + const request = new CreateRequest(testId, url, bitstream.self, options); + getTestScheduler().expectObservable(result).toBe('(a|)', { a: testResult }); + + expect(requestService.send).toHaveBeenCalledWith(request); + expect(rdbService.buildFromRequestUUID).toHaveBeenCalledWith(testId); + }); + }); + + describe('create', () => { + const testResult = createSuccessfulRemoteDataObject(new Bundle()); + beforeEach(() => { + spyOn((service as any), 'createAndSendRequest').and.returnValue(observableOf(testResult)); + }); + + it('should delegate the call to createAndSendRequest', () => { + const result = service.create(bitstream, bundle); + getTestScheduler().expectObservable(result).toBe('(a|)', { a: testResult }); + + expect((service as any).createAndSendRequest).toHaveBeenCalledWith( + PostRequest, + bundle._links.primaryBitstream.href, + bitstream.self + ); + }); + }); + describe('put', () => { + const testResult = createSuccessfulRemoteDataObject(new Bundle()); + beforeEach(() => { + spyOn((service as any), 'createAndSendRequest').and.returnValue(observableOf(testResult)); + }); + + it('should delegate the call to createAndSendRequest and return the requested bundle', () => { + const result = service.put(bitstream, bundle); + getTestScheduler().expectObservable(result).toBe('(a|)', { a: testResult }); + + expect((service as any).createAndSendRequest).toHaveBeenCalledWith( + PutRequest, + bundle._links.primaryBitstream.href, + bitstream.self + ); + }); + }); + describe('delete', () => { + describe('when the delete request succeeds', () => { + const testResult = createSuccessfulRemoteDataObject(new Bundle()); + const bundleServiceResult = createSuccessfulRemoteDataObject(bundle); + + beforeEach(() => { + spyOn((service as any), 'createAndSendRequest').and.returnValue(observableOf(testResult)); + (bundleDataService.findByHref as jasmine.Spy).and.returnValue(observableOf(bundleServiceResult)); + }); + + it('should delegate the call to createAndSendRequest', () => { + const result = service.delete(bundle); + getTestScheduler().expectObservable(result).toBe('(a|)', { a: testResult }); + + expect((service as any).createAndSendRequest).toHaveBeenCalledWith( + DeleteRequest, + bundle._links.primaryBitstream.href, + ); + }); + }); + describe('when the delete request fails', () => { + const testResult = createFailedRemoteDataObject(); + + beforeEach(() => { + spyOn((service as any), 'createAndSendRequest').and.returnValue(observableOf(testResult)); + }); + + it('should delegate the call to createAndSendRequest and retrieve the bundle from the rdbService', () => { + const result = service.delete(bundle); + getTestScheduler().expectObservable(result).toBe('(a|)', { a: bundle }); + + expect((service as any).createAndSendRequest).toHaveBeenCalledWith( + DeleteRequest, + bundle._links.primaryBitstream.href, + ); + }); + }); + }); +}); diff --git a/src/app/core/data/primary-bitstream-data.service.ts b/src/app/core/data/primary-bitstream.service.ts similarity index 97% rename from src/app/core/data/primary-bitstream-data.service.ts rename to src/app/core/data/primary-bitstream.service.ts index 6380a6f148..5c1c3c52ac 100644 --- a/src/app/core/data/primary-bitstream-data.service.ts +++ b/src/app/core/data/primary-bitstream.service.ts @@ -97,7 +97,7 @@ export class PrimaryBitstreamService { } /** - * Update an exiting primaryBitstream + * Update an existing primaryBitstream * * @param primaryBitstream The object to update * @param bundle The bundle to update it on @@ -111,7 +111,7 @@ export class PrimaryBitstreamService { } /** - * Delete an exiting primaryBitstream + * Delete an existing primaryBitstream * * @param bundle The bundle to delete it from */ @@ -125,7 +125,7 @@ export class PrimaryBitstreamService { if (rd.hasSucceeded) { return this.bundleDataService.findByHref(bundle.self, false); } else { - [bundle]; + return this.rdbService.buildSingle(bundle.self); } }) ); From 9d2fed4186186e22fab2b72a07e200c8c6a004de Mon Sep 17 00:00:00 2001 From: lotte Date: Thu, 8 Jun 2023 16:42:25 +0200 Subject: [PATCH 09/10] 101289: Fixed test issues --- .../edit-bitstream-page.component.ts | 2 +- .../data/primary-bitstream.service.spec.ts | 45 ++++++++++++++----- .../core/data/primary-bitstream.service.ts | 6 +-- src/app/core/shared/bundle.model.ts | 2 +- 4 files changed, 37 insertions(+), 18 deletions(-) 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 cdc905d456..b77d2151a9 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 @@ -612,7 +612,7 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { bundle$ = completedBundleRd$.pipe( map((bundleRd: RemoteData) => { if (bundleRd.hasSucceeded) { - return bundleRd.payload + return bundleRd.payload; } else { return this.bundle; } diff --git a/src/app/core/data/primary-bitstream.service.spec.ts b/src/app/core/data/primary-bitstream.service.spec.ts index 93e9882d04..00d6d7f03c 100644 --- a/src/app/core/data/primary-bitstream.service.spec.ts +++ b/src/app/core/data/primary-bitstream.service.spec.ts @@ -16,7 +16,7 @@ import { Bundle } from '../shared/bundle.model'; import { getTestScheduler } from 'jasmine-marbles'; import { of as observableOf } from 'rxjs'; -fdescribe('PrimaryBitstreamService', () => { +describe('PrimaryBitstreamService', () => { let service: PrimaryBitstreamService; let objectCache: ObjectCacheService; let requestService: RequestService; @@ -57,7 +57,7 @@ fdescribe('PrimaryBitstreamService', () => { describe('getHttpOptions', () => { it('should return a HttpOptions object with text/url-list Context-Type header', () => { - const result = (service as any).getHttpOptions() + const result = (service as any).getHttpOptions(); expect(result.headers.get('Content-Type')).toEqual('text/uri-list'); }); }); @@ -118,9 +118,20 @@ fdescribe('PrimaryBitstreamService', () => { }); }); describe('delete', () => { + const testBundle = Object.assign(new Bundle(), { + _links: { + self: { + href: 'test-href' + }, + primaryBitstream: { + href: 'test-primaryBitstream-href' + } + } + }); + describe('when the delete request succeeds', () => { const testResult = createSuccessfulRemoteDataObject(new Bundle()); - const bundleServiceResult = createSuccessfulRemoteDataObject(bundle); + const bundleServiceResult = createSuccessfulRemoteDataObject(testBundle); beforeEach(() => { spyOn((service as any), 'createAndSendRequest').and.returnValue(observableOf(testResult)); @@ -128,30 +139,42 @@ fdescribe('PrimaryBitstreamService', () => { }); it('should delegate the call to createAndSendRequest', () => { - const result = service.delete(bundle); - getTestScheduler().expectObservable(result).toBe('(a|)', { a: testResult }); + const result = service.delete(testBundle); + getTestScheduler().expectObservable(result).toBe('(a|)', { a: bundleServiceResult }); + + result.subscribe(); + + expect(bundleDataService.findByHref).toHaveBeenCalledWith(testBundle.self, false); expect((service as any).createAndSendRequest).toHaveBeenCalledWith( DeleteRequest, - bundle._links.primaryBitstream.href, + testBundle._links.primaryBitstream.href, ); }); }); describe('when the delete request fails', () => { const testResult = createFailedRemoteDataObject(); + const bundleServiceResult = createSuccessfulRemoteDataObject(testBundle); beforeEach(() => { spyOn((service as any), 'createAndSendRequest').and.returnValue(observableOf(testResult)); + (bundleDataService.findByHref as jasmine.Spy).and.returnValue(observableOf(bundleServiceResult)); }); - it('should delegate the call to createAndSendRequest and retrieve the bundle from the rdbService', () => { - const result = service.delete(bundle); - getTestScheduler().expectObservable(result).toBe('(a|)', { a: bundle }); - + it('should delegate the call to createAndSendRequest and request the bundle from the bundleDataService', () => { + const result = service.delete(testBundle); + result.subscribe(); expect((service as any).createAndSendRequest).toHaveBeenCalledWith( DeleteRequest, - bundle._links.primaryBitstream.href, + testBundle._links.primaryBitstream.href, ); + expect(bundleDataService.findByHref).toHaveBeenCalledWith(testBundle.self, true); + + }); + + it('should delegate the call to createAndSendRequest and', () => { + const result = service.delete(bundle); + getTestScheduler().expectObservable(result).toBe('(a|)', { a: bundleServiceResult }); }); }); }); diff --git a/src/app/core/data/primary-bitstream.service.ts b/src/app/core/data/primary-bitstream.service.ts index 5c1c3c52ac..646e8271e8 100644 --- a/src/app/core/data/primary-bitstream.service.ts +++ b/src/app/core/data/primary-bitstream.service.ts @@ -122,11 +122,7 @@ export class PrimaryBitstreamService { ).pipe( getAllCompletedRemoteData(), switchMap((rd: RemoteData) => { - if (rd.hasSucceeded) { - return this.bundleDataService.findByHref(bundle.self, false); - } else { - return this.rdbService.buildSingle(bundle.self); - } + return this.bundleDataService.findByHref(bundle.self, rd.hasFailed); }) ); } diff --git a/src/app/core/shared/bundle.model.ts b/src/app/core/shared/bundle.model.ts index f37cd96545..36b7012e47 100644 --- a/src/app/core/shared/bundle.model.ts +++ b/src/app/core/shared/bundle.model.ts @@ -1,4 +1,4 @@ -import { autoserialize, deserialize, inheritSerialization } from 'cerialize'; +import { deserialize, inheritSerialization } from 'cerialize'; import { Observable } from 'rxjs'; From daf297b94b61556ddb697b440a35f1a941b127e1 Mon Sep 17 00:00:00 2001 From: lotte Date: Fri, 9 Jun 2023 11:17:00 +0200 Subject: [PATCH 10/10] 101289: Removed unnecessary data services from primary bitstream service --- src/app/core/data/primary-bitstream.service.ts | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/app/core/data/primary-bitstream.service.ts b/src/app/core/data/primary-bitstream.service.ts index 646e8271e8..488cb5d22e 100644 --- a/src/app/core/data/primary-bitstream.service.ts +++ b/src/app/core/data/primary-bitstream.service.ts @@ -1,7 +1,4 @@ import { Bitstream } from '../shared/bitstream.model'; -import { DeleteDataImpl } from './base/delete-data'; -import { PutDataImpl } from './base/put-data'; -import { CreateDataImpl } from './base/create-data'; import { Injectable } from '@angular/core'; import { RequestService } from './request.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; @@ -11,7 +8,6 @@ import { Observable, switchMap } from 'rxjs'; import { RemoteData } from './remote-data'; import { Bundle } from '../shared/bundle.model'; import { NotificationsService } from '../../shared/notifications/notifications.service'; -import { constructIdEndpointDefault } from './base/identifiable-data.service'; import { HttpOptions } from '../dspace-rest/dspace-rest.service'; import { HttpHeaders } from '@angular/common/http'; import { GenericConstructor } from '../shared/generic-constructor'; @@ -24,9 +20,6 @@ import { BundleDataService } from './bundle-data.service'; providedIn: 'root', }) export class PrimaryBitstreamService { - private createData: CreateDataImpl; - private putData: PutDataImpl; - private deleteData: DeleteDataImpl; constructor( protected requestService: RequestService, @@ -36,10 +29,6 @@ export class PrimaryBitstreamService { protected notificationsService: NotificationsService, protected bundleDataService: BundleDataService, ) { - // linkPath can be undefined because we'll only use them to do things "byHref" - this.createData = new CreateDataImpl(undefined, requestService, rdbService, objectCache, halService, notificationsService, undefined); - this.putData = new PutDataImpl(undefined, requestService, rdbService, objectCache, halService, undefined); - this.deleteData = new DeleteDataImpl(undefined, requestService, rdbService, objectCache, halService, notificationsService, undefined, constructIdEndpointDefault); } /**