Merge pull request #2213 from atmire/w2p-101289_1578_primaryBitstream-edit-implementation

Primary bitstream toggle implementation
This commit is contained in:
Tim Donohue
2023-06-14 11:45:17 -05:00
committed by GitHub
6 changed files with 622 additions and 125 deletions

View File

@@ -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<Bitstream>[] = [
followLink('bundle', {}, followLink('item')),
followLink('bundle', {}, followLink('primaryBitstream'), followLink('item')),
followLink('format')
];

View File

@@ -24,6 +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.service';
const infoNotification: INotification = new Notification('id', NotificationType.Info, 'info');
const warningNotification: INotification = new Notification('id', NotificationType.Warning, 'warning');
@@ -32,19 +33,27 @@ 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;
let bitstreamID: string;
let selectedFormat: BitstreamFormat;
let allFormats: BitstreamFormat[];
let router: Router;
let currentPrimary: string;
let differentPrimary: string;
let bundle;
let comp: EditBitstreamPageComponent;
let fixture: ComponentFixture<EditBitstreamPageComponent>;
describe('EditBitstreamPageComponent', () => {
beforeEach(() => {
bitstreamID = 'current-bitstream-id';
currentPrimary = bitstreamID;
differentPrimary = '12345-abcde-54321-edcba';
allFormats = [
Object.assign({
id: '1',
@@ -53,7 +62,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 +72,7 @@ describe('EditBitstreamPageComponent', () => {
supportLevel: BitstreamFormatSupportLevel.Known,
mimetype: 'image/png',
_links: {
self: {href: 'format-selflink-2'}
self: { href: 'format-selflink-2' }
}
}),
Object.assign({
@@ -73,7 +82,7 @@ describe('EditBitstreamPageComponent', () => {
supportLevel: BitstreamFormatSupportLevel.Known,
mimetype: 'image/gif',
_links: {
self: {href: 'format-selflink-3'}
self: { href: 'format-selflink-3' }
}
})
] as BitstreamFormat[];
@@ -103,15 +112,52 @@ 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(), {
uuid: bitstreamID,
id: bitstreamID,
metadata: {
'dc.description': [
{
@@ -128,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: {},
@@ -155,17 +195,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: PrimaryBitstreamService, useValue: primaryBitstreamService },
ChangeDetectorRef
],
schemas: [NO_ERRORS_SCHEMA]
@@ -203,6 +245,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).primaryBitstreamUUID = currentPrimary;
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).primaryBitstreamUUID = differentPrimary;
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 +279,83 @@ 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).primaryBitstreamUUID = differentPrimary;
comp.onSubmit();
});
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).primaryBitstreamUUID = null;
comp.onSubmit();
});
it('should call create with the correct bitstream on the PrimaryBitstreamService', () => {
expect(primaryBitstreamService.create).toHaveBeenCalledWith(jasmine.objectContaining({uuid: currentPrimary}), bundle);
});
});
});
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).primaryBitstreamUUID = currentPrimary;
comp.onSubmit();
});
it('should call delete on the PrimaryBitstreamService', () => {
expect(primaryBitstreamService.delete).toHaveBeenCalledWith(jasmine.objectContaining(bundle));
});
});
});
});
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).primaryBitstreamUUID = currentPrimary;
comp.onSubmit();
});
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).primaryBitstreamUUID = differentPrimary;
comp.onSubmit();
});
it('should not call anything on the PrimaryBitstreamService', () => {
expect(primaryBitstreamService.put).not.toHaveBeenCalled();
expect(primaryBitstreamService.delete).not.toHaveBeenCalled();
expect(primaryBitstreamService.create).not.toHaveBeenCalled();
});
});
});
describe('when selected format hasn\'t changed', () => {
beforeEach(() => {
comp.onSubmit();
@@ -261,20 +401,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', () => {
@@ -321,16 +454,22 @@ describe('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: {},
@@ -357,6 +496,7 @@ describe('EditBitstreamPageComponent', () => {
{provide: BitstreamDataService, useValue: bitstreamService},
{provide: DSONameService, useValue: dsoNameService},
{provide: BitstreamFormatDataService, useValue: bitstreamFormatService},
{ provide: PrimaryBitstreamService, useValue: primaryBitstreamService },
ChangeDetectorRef
],
schemas: [NO_ERRORS_SCHEMA]
@@ -371,7 +511,6 @@ describe('EditBitstreamPageComponent', () => {
spyOn(router, 'navigate');
});
describe('on startup', () => {
let rawForm;
@@ -440,16 +579,22 @@ describe('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: {},
@@ -475,6 +620,7 @@ describe('EditBitstreamPageComponent', () => {
{provide: BitstreamDataService, useValue: bitstreamService},
{provide: DSONameService, useValue: dsoNameService},
{provide: BitstreamFormatDataService, useValue: bitstreamFormatService},
{ provide: PrimaryBitstreamService, useValue: primaryBitstreamService },
ChangeDetectorRef
],
schemas: [NO_ERRORS_SCHEMA]
@@ -496,7 +642,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', () => {

View File

@@ -1,57 +1,31 @@
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 { 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';
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, 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';
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 { PrimaryBitstreamService } from '../../core/data/primary-bitstream.service';
@Component({
selector: 'ds-edit-bitstream-page',
@@ -76,6 +50,11 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
*/
bitstreamFormatsRD$: Observable<RemoteData<PaginatedList<BitstreamFormat>>>;
/**
* The UUID of the primary bitstream for this bundle
*/
primaryBitstreamUUID: string;
/**
* The bitstream to edit
*/
@@ -203,7 +182,7 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
iiifLabelContainer = new DynamicFormGroupModel({
id: 'iiifLabelContainer',
group: [this.iiifLabelModel]
},{
}, {
grid: {
host: 'form-row'
}
@@ -213,7 +192,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 +200,7 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
iiifTocContainer = new DynamicFormGroupModel({
id: 'iiifTocContainer',
group: [this.iiifTocModel]
},{
}, {
grid: {
host: 'form-row'
}
@@ -231,7 +210,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 +218,7 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
iiifWidthContainer = new DynamicFormGroupModel({
id: 'iiifWidthContainer',
group: [this.iiifWidthModel]
},{
}, {
grid: {
host: 'form-row'
}
@@ -249,7 +228,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 +236,7 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
iiifHeightContainer = new DynamicFormGroupModel({
id: 'iiifHeightContainer',
group: [this.iiifHeightModel]
},{
}, {
grid: {
host: 'form-row'
}
@@ -280,7 +259,7 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
this.fileNameModel,
this.primaryBitstreamModel
]
},{
}, {
grid: {
host: 'form-row'
}
@@ -316,7 +295,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: {
@@ -380,13 +359,17 @@ 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;
constructor(private route: ActivatedRoute,
private router: Router,
@@ -397,7 +380,9 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
private bitstreamService: BitstreamDataService,
public dsoNameService: DSONameService,
private notificationsService: NotificationsService,
private bitstreamFormatService: BitstreamFormatDataService) {
private bitstreamFormatService: BitstreamFormatDataService,
private primaryBitstreamService: PrimaryBitstreamService,
) {
}
/**
@@ -410,26 +395,50 @@ 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(),
);
const allFormats$ = this.bitstreamFormatsRD$.pipe(
getFirstSucceededRemoteData(),
getRemoteDataPayload()
getRemoteDataPayload(),
);
const bundle$ = bitstream$.pipe(
switchMap((bitstream: Bitstream) => bitstream.bundle),
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(),
);
this.subs.push(
observableCombineLatest(
bitstream$,
allFormats$
).subscribe(([bitstream, allFormats]) => {
allFormats$,
bundle$,
primaryBitstream$,
item$,
).pipe()
.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 +469,7 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
this.formGroup.patchValue({
fileNamePrimaryContainer: {
fileName: bitstream.name,
primaryBitstream: false
primaryBitstream: this.primaryBitstreamUUID === bitstream.uuid
},
descriptionContainer: {
description: bitstream.firstMetadataValue('dc.description')
@@ -571,9 +580,56 @@ 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.primaryBitstreamUUID === this.bitstream.uuid;
let bitstream$;
let bundle$: Observable<Bundle>;
let errorWhileSaving = false;
if (wasPrimary !== isPrimary) {
let bundleRd$: Observable<RemoteData<Bundle>>;
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<Bundle>) => bundleRd.hasFailed)
).subscribe((bundleRd: RemoteData<Bundle>) => {
this.notificationsService.error(
this.translate.instant(this.NOTIFICATIONS_PREFIX + 'error.primaryBitstream.title'),
bundleRd.errorMessage
);
errorWhileSaving = true;
}));
bundle$ = completedBundleRd$.pipe(
map((bundleRd: RemoteData<Bundle>) => {
if (bundleRd.hasSucceeded) {
return bundleRd.payload;
} else {
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);
}
if (isNewFormat) {
bitstream$ = this.bitstreamService.updateFormat(this.bitstream, selectedFormat).pipe(
getFirstCompletedRemoteData(),
@@ -592,7 +648,8 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
bitstream$ = observableOf(this.bitstream);
}
bitstream$.pipe(
combineLatest([bundle$, bitstream$]).pipe(
tap(([bundle]) => this.bundle = bundle),
switchMap(() => {
return this.bitstreamService.update(updatedBitstream).pipe(
getFirstSucceededRemoteDataPayload()
@@ -604,7 +661,9 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
this.translate.instant(this.NOTIFICATIONS_PREFIX + 'saved.title'),
this.translate.instant(this.NOTIFICATIONS_PREFIX + 'saved.content')
);
if (!errorWhileSaving) {
this.navigateToItemEditBitstreams();
}
});
}
@@ -615,8 +674,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'];
@@ -668,15 +725,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']));
});
}
}
/**

View File

@@ -0,0 +1,181 @@
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';
describe('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<any>).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', () => {
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(testBundle);
beforeEach(() => {
spyOn((service as any), 'createAndSendRequest').and.returnValue(observableOf(testResult));
(bundleDataService.findByHref as jasmine.Spy<any>).and.returnValue(observableOf(bundleServiceResult));
});
it('should delegate the call to createAndSendRequest', () => {
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,
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<any>).and.returnValue(observableOf(bundleServiceResult));
});
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,
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 });
});
});
});
});

View File

@@ -0,0 +1,119 @@
import { Bitstream } from '../shared/bitstream.model';
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 { 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 {
constructor(
protected requestService: RequestService,
protected rdbService: RemoteDataBuildService,
protected objectCache: ObjectCacheService,
protected halService: HALEndpointService,
protected notificationsService: NotificationsService,
protected bundleDataService: BundleDataService,
) {
}
/**
* 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<RemoteData>
*
* @param requestType The type of request: PostRequest, PutRequest, or DeleteRequest
* @param endpointURL The endpoint URL
* @param primaryBitstreamSelfLink
* @protected
*/
protected createAndSendRequest(
requestType: GenericConstructor<PostRequest | PutRequest | DeleteRequest>,
endpointURL: string,
primaryBitstreamSelfLink?: string,
): Observable<RemoteData<Bundle | NoContent>> {
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<RemoteData<Bundle>> {
return this.createAndSendRequest(
PostRequest,
bundle._links.primaryBitstream.href,
primaryBitstream.self
) as Observable<RemoteData<Bundle>>;
}
/**
* Update an existing primaryBitstream
*
* @param primaryBitstream The object to update
* @param bundle The bundle to update it on
*/
put(primaryBitstream: Bitstream, bundle: Bundle): Observable<RemoteData<Bundle>> {
return this.createAndSendRequest(
PutRequest,
bundle._links.primaryBitstream.href,
primaryBitstream.self
) as Observable<RemoteData<Bundle>>;
}
/**
* Delete an existing primaryBitstream
*
* @param bundle The bundle to delete it from
*/
delete(bundle: Bundle): Observable<RemoteData<Bundle>> {
return this.createAndSendRequest(
DeleteRequest,
bundle._links.primaryBitstream.href
).pipe(
getAllCompletedRemoteData(),
switchMap((rd: RemoteData<NoContent>) => {
return this.bundleDataService.findByHref(bundle.self, rd.hasFailed);
})
);
}
}

View File

@@ -694,6 +694,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.",