Merge pull request #2631 from vNovski/CST-12044-visualize-the-primary-bitstream

CST-12044 visualize the primary bitstream & CST-12043 primary bitstream flag
This commit is contained in:
Tim Donohue
2024-02-20 17:09:45 -06:00
committed by GitHub
31 changed files with 589 additions and 124 deletions

View File

@@ -1,12 +0,0 @@
:host {
::ng-deep {
.switch {
position: absolute;
top: calc(var(--bs-spacer) * 2.5);
}
}
}
:host ::ng-deep ds-dynamic-form-control-container > div > label {
margin-top: 1.75rem;
}

View File

@@ -2,14 +2,35 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnIni
import { Bitstream } from '../../core/shared/bitstream.model';
import { ActivatedRoute, Router } from '@angular/router';
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 {
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 {
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';
@@ -245,7 +266,7 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
/**
* All input models in a simple array for easier iterations
*/
inputModels = [this.fileNameModel, this.primaryBitstreamModel, this.descriptionModel, this.selectedFormatModel,
inputModels = [this.primaryBitstreamModel, this.fileNameModel, this.descriptionModel, this.selectedFormatModel,
this.newFormatModel];
/**
@@ -256,8 +277,8 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
new DynamicFormGroupModel({
id: 'fileNamePrimaryContainer',
group: [
this.fileNameModel,
this.primaryBitstreamModel
this.primaryBitstreamModel,
this.fileNameModel
]
}, {
grid: {
@@ -295,7 +316,10 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
},
primaryBitstream: {
grid: {
host: 'col col-sm-4 d-inline-block switch border-0'
container: 'col-12'
},
element: {
container: 'text-right'
}
},
description: {

View File

@@ -21,6 +21,11 @@ import { NotificationsService } from '../../shared/notifications/notifications.s
import objectContaining = jasmine.objectContaining;
import { RemoteData } from './remote-data';
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
import { BundleDataService } from './bundle-data.service';
import { ItemMock } from 'src/app/shared/mocks/item.mock';
import { createFailedRemoteDataObject, createSuccessfulRemoteDataObject } from 'src/app/shared/remote-data.utils';
import { Bundle } from '../shared/bundle.model';
import { cold } from 'jasmine-marbles';
describe('BitstreamDataService', () => {
let service: BitstreamDataService;
@@ -29,6 +34,7 @@ describe('BitstreamDataService', () => {
let halService: HALEndpointService;
let bitstreamFormatService: BitstreamFormatDataService;
let rdbService: RemoteDataBuildService;
let bundleDataService: BundleDataService;
const bitstreamFormatHref = 'rest-api/bitstreamformats';
const bitstream1 = Object.assign(new Bitstream(), {
@@ -62,6 +68,7 @@ describe('BitstreamDataService', () => {
bitstreamFormatService = jasmine.createSpyObj('bistreamFormatService', {
getBrowseEndpoint: observableOf(bitstreamFormatHref)
});
rdbService = getMockRemoteDataBuildService();
TestBed.configureTestingModule({
@@ -76,6 +83,7 @@ describe('BitstreamDataService', () => {
],
});
service = TestBed.inject(BitstreamDataService);
bundleDataService = TestBed.inject(BundleDataService);
});
describe('composition', () => {
@@ -118,6 +126,32 @@ describe('BitstreamDataService', () => {
expect(service.invalidateByHref).toHaveBeenCalledWith('fake-bitstream1-self');
});
describe('findPrimaryBitstreamByItemAndName', () => {
it('should return primary bitstream', () => {
const exprected$ = cold('(a|)', { a: bitstream1} );
const bundle = Object.assign(new Bundle(), {
primaryBitstream: observableOf(createSuccessfulRemoteDataObject(bitstream1)),
});
spyOn(bundleDataService, 'findByItemAndName').and.returnValue(observableOf(createSuccessfulRemoteDataObject(bundle)));
expect(service.findPrimaryBitstreamByItemAndName(ItemMock, 'ORIGINAL')).toBeObservable(exprected$);
});
it('should return null if primary bitstream has not be succeeded ', () => {
const exprected$ = cold('(a|)', { a: null} );
const bundle = Object.assign(new Bundle(), {
primaryBitstream: observableOf(createFailedRemoteDataObject()),
});
spyOn(bundleDataService, 'findByItemAndName').and.returnValue(observableOf(createSuccessfulRemoteDataObject(bundle)));
expect(service.findPrimaryBitstreamByItemAndName(ItemMock, 'ORIGINAL')).toBeObservable(exprected$);
});
it('should return EMPTY if nothing where found', () => {
const exprected$ = cold('(|)', {} );
spyOn(bundleDataService, 'findByItemAndName').and.returnValue(observableOf(createFailedRemoteDataObject<Bundle>()));
expect(service.findPrimaryBitstreamByItemAndName(ItemMock, 'ORIGINAL')).toBeObservable(exprected$);
});
});
it('should be able to delete multiple bitstreams', () => {
service.removeMultiple([bitstream1, bitstream2]);

View File

@@ -1,9 +1,9 @@
import { HttpHeaders } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { combineLatest as observableCombineLatest, Observable } from 'rxjs';
import { combineLatest as observableCombineLatest, Observable, EMPTY } from 'rxjs';
import { find, map, switchMap, take } from 'rxjs/operators';
import { hasValue } from '../../shared/empty.util';
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
import { FollowLinkConfig, followLink } from '../../shared/utils/follow-link-config.model';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { ObjectCacheService } from '../cache/object-cache.service';
import { Bitstream } from '../shared/bitstream.model';
@@ -34,6 +34,7 @@ import { NoContent } from '../shared/NoContent.model';
import { IdentifiableDataService } from './base/identifiable-data.service';
import { dataService } from './base/data-service.decorator';
import { Operation, RemoveOperation } from 'fast-json-patch';
import { getFirstCompletedRemoteData } from '../shared/operators';
/**
* A service to retrieve {@link Bitstream}s from the REST API
@@ -201,6 +202,37 @@ export class BitstreamDataService extends IdentifiableDataService<Bitstream> imp
return this.searchData.getSearchByHref(searchMethod, options, ...linksToFollow);
}
/**
*
* Make a request to get primary bitstream
* in all current use cases, and having it simplifies this method
*
* @param item the {@link Item} the {@link Bundle} is a part of
* @param bundleName the name of the {@link Bundle} we want to find
* {@link Bitstream}s for
* @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's
* no valid cached version. Defaults to true
* @param reRequestOnStale Whether or not the request should automatically be re-
* requested after the response becomes stale
* @return {Observable<Bitstream | null>}
* Return an observable that constains primary bitstream information or null
*/
public findPrimaryBitstreamByItemAndName(item: Item, bundleName: string, useCachedVersionIfAvailable = true, reRequestOnStale = true): Observable<Bitstream | null> {
return this.bundleService.findByItemAndName(item, bundleName, useCachedVersionIfAvailable, reRequestOnStale, followLink('primaryBitstream')).pipe(
getFirstCompletedRemoteData(),
switchMap((rd: RemoteData<Bundle>) => {
if (!rd.hasSucceeded) {
return EMPTY;
}
return rd.payload.primaryBitstream.pipe(
getFirstCompletedRemoteData(),
map((rdb: RemoteData<Bitstream>) => rdb.hasSucceeded ? rdb.payload : null)
);
})
);
}
/**
* Make a new FindListRequest with given search method
*

View File

@@ -4,7 +4,10 @@ import { WorkspaceitemSectionUploadFileObject } from './workspaceitem-section-up
* An interface to represent submission's upload section data.
*/
export interface WorkspaceitemSectionUploadObject {
/**
* Primary bitstream flag
*/
primary: string | null;
/**
* A list of [[WorkspaceitemSectionUploadFileObject]]
*/

View File

@@ -2,7 +2,10 @@
<ds-metadata-field-wrapper *ngIf="bitstreams?.length > 0" [label]="label | translate">
<div class="file-section">
<ds-themed-file-download-link *ngFor="let file of bitstreams; let last=last;" [bitstream]="file" [item]="item">
<span>{{ dsoNameService.getName(file) }}</span>
<span>
<span *ngIf="primaryBitsreamId === file.id" class="badge badge-primary">{{ 'item.page.bitstreams.primary' | translate }}</span>
{{ dsoNameService.getName(file) }}
</span>
<span> ({{(file?.sizeBytes) | dsFileSize }})</span>
<span *ngIf="!last" innerHTML="{{separator}}"></span>
</ds-themed-file-download-link>

View File

@@ -25,7 +25,8 @@ describe('FileSectionComponent', () => {
let fixture: ComponentFixture<FileSectionComponent>;
const bitstreamDataService = jasmine.createSpyObj('bitstreamDataService', {
findAllByItemAndBundleName: createSuccessfulRemoteDataObject$(createPaginatedList([]))
findAllByItemAndBundleName: createSuccessfulRemoteDataObject$(createPaginatedList([])),
findPrimaryBitstreamByItemAndName: observableOf(null)
});
const mockBitstream: Bitstream = Object.assign(new Bitstream(),
@@ -81,6 +82,20 @@ describe('FileSectionComponent', () => {
fixture.detectChanges();
}));
it('should set the id of primary bitstream', () => {
comp.primaryBitsreamId = undefined;
bitstreamDataService.findPrimaryBitstreamByItemAndName.and.returnValue(observableOf(mockBitstream));
comp.ngOnInit();
expect(comp.primaryBitsreamId).toBe(mockBitstream.id);
});
it('should not set the id of primary bitstream', () => {
comp.primaryBitsreamId = undefined;
bitstreamDataService.findPrimaryBitstreamByItemAndName.and.returnValue(observableOf(null));
comp.ngOnInit();
expect(comp.primaryBitsreamId).toBeUndefined();
});
describe('when the bitstreams are loading', () => {
beforeEach(() => {
comp.bitstreams$.next([mockBitstream]);

View File

@@ -39,6 +39,8 @@ export class FileSectionComponent implements OnInit {
pageSize: number;
primaryBitsreamId: string;
constructor(
protected bitstreamDataService: BitstreamDataService,
protected notificationsService: NotificationsService,
@@ -50,9 +52,19 @@ export class FileSectionComponent implements OnInit {
}
ngOnInit(): void {
this.getPrimaryBitstreamId();
this.getNextPage();
}
private getPrimaryBitstreamId() {
this.bitstreamDataService.findPrimaryBitstreamByItemAndName(this.item, 'ORIGINAL', true, true).subscribe((primaryBitstream: Bitstream | null) => {
if (!primaryBitstream) {
return;
}
this.primaryBitsreamId = primaryBitstream?.id;
});
}
/**
* This method will retrieve the next page of Bitstreams from the external BitstreamDataService call.
* It'll retrieve the currentPage from the class variables and it'll add the next page of bitstreams with the

View File

@@ -1,4 +1,4 @@
<div [formGroup]="group" class="form-check custom-control custom-switch" [class.disabled]="model.disabled">
<div [formGroup]="group" [ngClass]="getClass('element', 'container')" class="form-check custom-control custom-switch" [class.disabled]="model.disabled">
<input type="checkbox" class="form-check-input custom-control-input"
[checked]="model.checked"
[class.is-invalid]="showErrorMessages"
@@ -14,7 +14,7 @@
(change)="onChange($event)"
(focus)="onFocus($event)"/>
<label class="form-check-label custom-control-label" [for]="bindId && model.id">
<span [innerHTML]="model.label"
<span [innerHTML]="model.label | translate"
[ngClass]="[getClass('element', 'label'), getClass('grid', 'label')]"></span>
</label>
</div>

View File

@@ -0,0 +1,16 @@
div.custom-switch {
&.custom-control-right {
margin-left: 0;
margin-right: 0;
&::after {
right: -1.5rem;
left: auto;
}
&::before {
right: -2.35rem;
left: auto;
}
}
}

View File

@@ -1,11 +1,12 @@
import { DynamicFormsCoreModule, DynamicFormService } from '@ng-dynamic-forms/core';
import { UntypedFormGroup, ReactiveFormsModule } from '@angular/forms';
import { ComponentFixture, inject, TestBed, waitForAsync } from '@angular/core/testing';
import { DebugElement } from '@angular/core';
import { DebugElement} from '@angular/core';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { By } from '@angular/platform-browser';
import { DynamicCustomSwitchModel } from './custom-switch.model';
import { CustomSwitchComponent } from './custom-switch.component';
import { TranslateModule } from '@ngx-translate/core';
describe('CustomSwitchComponent', () => {
@@ -20,9 +21,10 @@ describe('CustomSwitchComponent', () => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
imports: [
TranslateModule.forRoot(),
ReactiveFormsModule,
NoopAnimationsModule,
DynamicFormsCoreModule.forRoot()
DynamicFormsCoreModule.forRoot(),
],
declarations: [CustomSwitchComponent]

View File

@@ -196,6 +196,7 @@ export class FormBuilderService extends DynamicFormService {
return new FormFieldMetadataValueObject((controlValue as any).value, controlLanguage, authority, (controlValue as any).display, place, (controlValue as any).confidence);
}
}
return controlValue;
};
const iterateControlModels = (findGroupModel: DynamicFormControlModel[], controlModelIndex: number = 0): void => {

View File

@@ -5,6 +5,9 @@ import { SubmissionFormsConfigDataService } from '../../core/config/submission-f
*/
export function getMockSectionUploadService(): SubmissionFormsConfigDataService {
return jasmine.createSpyObj('SectionUploadService', {
updatePrimaryBitstreamOperation: jasmine.createSpy('updatePrimaryBitstreamOperation'),
updateFilePrimaryBitstream: jasmine.createSpy('updateFilePrimaryBitstream'),
getUploadedFilesData: jasmine.createSpy('getUploadedFilesData'),
getUploadedFileList: jasmine.createSpy('getUploadedFileList'),
getFileData: jasmine.createSpy('getFileData'),
getDefaultPolicies: jasmine.createSpy('getDefaultPolicies'),

View File

@@ -1612,7 +1612,13 @@ export const mockUploadFiles = [
}
];
export const mockUploadFilesData = {
primary: null,
files: JSON.parse(JSON.stringify(mockUploadFiles))
};
export const mockFileFormData = {
primary: [true],
metadata: {
'dc.title': [
{

View File

@@ -59,6 +59,7 @@ export const SubmissionObjectActionTypes = {
// Upload file types
NEW_FILE: type('dspace/submission/NEW_FILE'),
EDIT_FILE_DATA: type('dspace/submission/EDIT_FILE_DATA'),
EDIT_FILE_PRIMARY_BITSTREAM_DATA: type('dspace/submission/EDIT_FILE_PRIMARY_BITSTREAM_DATA'),
DELETE_FILE: type('dspace/submission/DELETE_FILE'),
// Errors
@@ -760,6 +761,29 @@ export class NewUploadedFileAction implements Action {
}
}
export class EditFilePrimaryBitstreamAction implements Action {
type = SubmissionObjectActionTypes.EDIT_FILE_PRIMARY_BITSTREAM_DATA;
payload: {
submissionId: string;
sectionId: string;
fileId: string | null;
};
/**
* Edit a file data
*
* @param submissionId
* the submission's ID
* @param sectionId
* the section's ID
* @param fileId
* the file's ID
*/
constructor(submissionId: string, sectionId: string, fileId: string | null) {
this.payload = { submissionId, sectionId, fileId: fileId };
}
}
export class EditFileDataAction implements Action {
type = SubmissionObjectActionTypes.EDIT_FILE_DATA;
payload: {
@@ -833,6 +857,7 @@ export type SubmissionObjectAction = DisableSectionAction
| SectionStatusChangeAction
| NewUploadedFileAction
| EditFileDataAction
| EditFilePrimaryBitstreamAction
| DeleteUploadedFileAction
| InertSectionErrorsAction
| DeleteSectionErrorsAction

View File

@@ -1,4 +1,4 @@
import { hasValue, isEmpty, isNotEmpty, isNotNull, isUndefined } from '../../shared/empty.util';
import { hasValue, isEmpty, isNotEmpty, isNotNull, isNull, isUndefined } from '../../shared/empty.util';
import differenceWith from 'lodash/differenceWith';
import findKey from 'lodash/findKey';
import isEqual from 'lodash/isEqual';
@@ -14,6 +14,7 @@ import {
DepositSubmissionSuccessAction,
DisableSectionAction,
EditFileDataAction,
EditFilePrimaryBitstreamAction,
EnableSectionAction,
InertSectionErrorsAction,
InitSectionAction,
@@ -203,6 +204,10 @@ export function submissionObjectReducer(state = initialState, action: Submission
return newFile(state, action as NewUploadedFileAction);
}
case SubmissionObjectActionTypes.EDIT_FILE_PRIMARY_BITSTREAM_DATA: {
return editPrimaryBitstream(state, action as EditFilePrimaryBitstreamAction);
}
case SubmissionObjectActionTypes.EDIT_FILE_DATA: {
return editFileData(state, action as EditFileDataAction);
}
@@ -735,6 +740,46 @@ function newFile(state: SubmissionObjectState, action: NewUploadedFileAction): S
});
}
/**
* Edit primary bitstream.
*
* @param state
* the current state
* @param action
* an EditFilePrimaryBitstreamAction action
* @return SubmissionObjectState
* the new state, with the edited file.
*/
function editPrimaryBitstream(state: SubmissionObjectState, action: EditFilePrimaryBitstreamAction): SubmissionObjectState {
const filesData = state[ action.payload.submissionId ].sections[ action.payload.sectionId ].data as WorkspaceitemSectionUploadObject;
const { submissionId, sectionId, fileId } = action.payload;
const fileIndex = findKey(filesData.files, { uuid: fileId });
if (isNull(fileIndex)) {
return state;
}
const submission = state[submissionId];
return {
...state,
[submissionId]: {
...submission,
sections: {
...submission.sections,
[sectionId]: {
...submission.sections[sectionId],
data: {
...submission.sections[sectionId].data as WorkspaceitemSectionUploadObject,
primary: fileId
}
}
},
isLoading: submission.isLoading,
savePending: submission.savePending,
}
};
}
/**
* Edit a file.
*

View File

@@ -213,6 +213,7 @@ export class SubmissionSectionLicenseComponent extends SectionModelComponent {
} else {
this.operationsBuilder.remove(this.pathCombiner.getPath(path));
}
this.submissionService.dispatchSaveSection(this.submissionId, this.sectionData.id);
}
/**

View File

@@ -6,7 +6,6 @@
</button>
</div>
<div class="modal-body">
<ds-form *ngIf="formModel"
#formRef="formComponent"
[formId]="formId"

View File

@@ -47,6 +47,7 @@ import {
} from '../../../../../core/json-patch/builder/json-patch-operation-path-combiner';
import { dateToISOFormat } from '../../../../../shared/date.util';
import { of } from 'rxjs';
import { DynamicCustomSwitchModel } from '../../../../../shared/form/builder/ds-dynamic-form-ui/models/custom-switch/custom-switch.model';
const jsonPatchOpBuilder: any = jasmine.createSpyObj('jsonPatchOpBuilder', {
add: jasmine.createSpy('add'),
@@ -78,7 +79,7 @@ describe('SubmissionSectionUploadFileEditComponent test suite', () => {
const fileIndex = '0';
const fileId = '123456-test-upload';
const fileData: any = mockUploadFiles[0];
const pathCombiner = new JsonPatchOperationPathCombiner('sections', sectionId, 'files', fileIndex);
const pathCombiner = new JsonPatchOperationPathCombiner('sections', sectionId);
let noAccessConditionsMock = Object.assign({}, mockFileFormData);
delete noAccessConditionsMock.accessConditions;
@@ -186,11 +187,15 @@ describe('SubmissionSectionUploadFileEditComponent test suite', () => {
comp.ngOnInit();
const models = [DynamicCustomSwitchModel, DynamicFormGroupModel, DynamicFormArrayModel];
expect(comp.formModel).toBeDefined();
expect(comp.formModel.length).toBe(2);
expect(comp.formModel[0] instanceof DynamicFormGroupModel).toBeTruthy();
expect(comp.formModel[1] instanceof DynamicFormArrayModel).toBeTruthy();
expect((comp.formModel[1] as DynamicFormArrayModel).groups.length).toBe(2);
expect(comp.formModel.length).toBe(models.length);
models.forEach((model, i) => {
expect(comp.formModel[i] instanceof model).toBeTruthy();
});
expect((comp.formModel[2] as DynamicFormArrayModel).groups.length).toBe(2);
const startDateModel = formbuilderService.findById('startDate', comp.formModel);
expect(startDateModel.max).toEqual(maxStartDate);
const endDateModel = formbuilderService.findById('endDate', comp.formModel);
@@ -254,6 +259,7 @@ describe('SubmissionSectionUploadFileEditComponent test suite', () => {
compAsAny.formRef = {formGroup: null};
compAsAny.fileData = fileData;
compAsAny.pathCombiner = pathCombiner;
compAsAny.isPrimary = null;
formService.validateAllFormFields.and.callFake(() => null);
formService.isValid.and.returnValue(of(true));
formService.getFormData.and.returnValue(of(mockFileFormData));
@@ -262,6 +268,7 @@ describe('SubmissionSectionUploadFileEditComponent test suite', () => {
Object.assign(mockSubmissionObject, {
sections: {
upload: {
primary: true,
files: mockUploadFiles
}
}
@@ -277,23 +284,28 @@ describe('SubmissionSectionUploadFileEditComponent test suite', () => {
comp.saveBitstreamData();
tick();
let path = 'metadata/dc.title';
let path = 'primary';
expect(uploadService.updatePrimaryBitstreamOperation).toHaveBeenCalledWith(pathCombiner.getPath(path), compAsAny.isPrimary, mockFileFormData.primary[0], compAsAny.fileId);
const pathFragment = ['files', fileIndex];
path = 'metadata/dc.title';
expect(operationsBuilder.add).toHaveBeenCalledWith(
pathCombiner.getPath(path),
pathCombiner.getPath([...pathFragment, path]),
mockFileFormData.metadata['dc.title'],
true
);
path = 'metadata/dc.description';
expect(operationsBuilder.add).toHaveBeenCalledWith(
pathCombiner.getPath(path),
pathCombiner.getPath([...pathFragment, path]),
mockFileFormData.metadata['dc.description'],
true
);
path = 'accessConditions';
expect(operationsBuilder.add).toHaveBeenCalledWith(
pathCombiner.getPath(path),
pathCombiner.getPath([...pathFragment, path]),
accessConditionsToSave,
true
);

View File

@@ -28,6 +28,8 @@ import {
BITSTREAM_FORM_ACCESS_CONDITION_START_DATE_LAYOUT,
BITSTREAM_FORM_ACCESS_CONDITION_TYPE_CONFIG,
BITSTREAM_FORM_ACCESS_CONDITION_TYPE_LAYOUT,
BITSTREAM_FORM_PRIMARY,
BITSTREAM_FORM_PRIMARY_LAYOUT,
BITSTREAM_METADATA_FORM_GROUP_CONFIG,
BITSTREAM_METADATA_FORM_GROUP_LAYOUT
} from './section-upload-file-edit.model';
@@ -42,10 +44,8 @@ import { FormComponent } from '../../../../../shared/form/form.component';
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
import { filter, mergeMap, take } from 'rxjs/operators';
import { dateToISOFormat } from '../../../../../shared/date.util';
import { SubmissionObject } from '../../../../../core/submission/models/submission-object.model';
import {
WorkspaceitemSectionUploadObject
} from '../../../../../core/submission/models/workspaceitem-section-upload.model';
import { JsonPatchOperationsBuilder } from '../../../../../core/json-patch/builder/json-patch-operations-builder';
import {
SubmissionJsonPatchOperationsService
@@ -57,6 +57,9 @@ import { SectionUploadService } from '../../section-upload.service';
import { Subscription } from 'rxjs';
import { DynamicFormControlCondition } from '@ng-dynamic-forms/core/lib/model/misc/dynamic-form-control-relation.model';
import { DynamicDateControlValue } from '@ng-dynamic-forms/core/lib/model/dynamic-date-control.model';
import { DynamicCustomSwitchModel } from 'src/app/shared/form/builder/ds-dynamic-form-ui/models/custom-switch/custom-switch.model';
import { SubmissionObject } from 'src/app/core/submission/models/submission-object.model';
import { WorkspaceitemSectionUploadObject } from 'src/app/core/submission/models/workspaceitem-section-upload.model';
/**
* This component represents the edit form for bitstream
@@ -74,6 +77,13 @@ export class SubmissionSectionUploadFileEditComponent
*/
@ViewChild('formRef') public formRef: FormComponent;
/**
* The indicator is the primary bitstream
* it will be null if no primary bitstream is set for the ORIGINAL bundle
* @type {boolean, null}
*/
isPrimary: boolean;
/**
* The list of available access condition
* @type {Array}
@@ -191,6 +201,10 @@ export class SubmissionSectionUploadFileEditComponent
* The form model
*/
public initModelData(formModel: DynamicFormControlModel[]) {
const primaryBitstreamModel: any = this.formBuilderService.findById('primary', formModel, this.fileIndex);
primaryBitstreamModel.value = this.isPrimary || false;
this.fileData.accessConditions.forEach((accessCondition, index) => {
Array.of('name', 'startDate', 'endDate')
.filter((key) => accessCondition.hasOwnProperty(key) && isNotEmpty(accessCondition[key]))
@@ -291,6 +305,9 @@ export class SubmissionSectionUploadFileEditComponent
])
});
const formModel: DynamicFormControlModel[] = [];
formModel.push(new DynamicCustomSwitchModel(BITSTREAM_FORM_PRIMARY, BITSTREAM_FORM_PRIMARY_LAYOUT));
const metadataGroupModelConfig = Object.assign({}, BITSTREAM_METADATA_FORM_GROUP_CONFIG);
metadataGroupModelConfig.group = this.formBuilderService.modelFromConfiguration(
this.submissionId,
@@ -386,10 +403,14 @@ export class SubmissionSectionUploadFileEditComponent
return formModel;
}
/**
* Save bitstream metadata
*/
saveBitstreamData() {
const pathFragment = ['files', this.fileIndex];
// validate form
this.formService.validateAllFormFields(this.formRef.formGroup);
const saveBitstreamDataSubscription = this.formService.isValid(this.formId).pipe(
@@ -398,13 +419,15 @@ export class SubmissionSectionUploadFileEditComponent
mergeMap(() => this.formService.getFormData(this.formId)),
take(1),
mergeMap((formData: any) => {
this.uploadService.updatePrimaryBitstreamOperation(this.pathCombiner.getPath('primary'), this.isPrimary, formData.primary[0], this.fileId);
// collect bitstream metadata
Object.keys((formData.metadata))
.filter((key) => isNotEmpty(formData.metadata[key]))
.forEach((key) => {
const metadataKey = key.replace(/_/g, '.');
const path = `metadata/${metadataKey}`;
this.operationsBuilder.add(this.pathCombiner.getPath(path), formData.metadata[key], true);
this.operationsBuilder.add(this.pathCombiner.getPath([...pathFragment, path]), formData.metadata[key], true);
});
Object.keys((this.fileData.metadata))
.filter((key) => isNotEmpty(this.fileData.metadata[key]))
@@ -413,7 +436,7 @@ export class SubmissionSectionUploadFileEditComponent
.forEach((key) => {
const metadataKey = key.replace(/_/g, '.');
const path = `metadata/${metadataKey}`;
this.operationsBuilder.remove(this.pathCombiner.getPath(path));
this.operationsBuilder.remove(this.pathCombiner.getPath([...pathFragment, path]));
});
const accessConditionsToSave = [];
if (formData.hasOwnProperty('accessConditions')) {
@@ -469,25 +492,29 @@ export class SubmissionSectionUploadFileEditComponent
});
}
if (isNotEmpty(accessConditionsToSave)) {
this.operationsBuilder.add(this.pathCombiner.getPath('accessConditions'), accessConditionsToSave, true);
this.operationsBuilder.add(this.pathCombiner.getPath([...pathFragment, 'accessConditions']), accessConditionsToSave, true);
}
// dispatch a PATCH request to save metadata
return this.operationsService.jsonPatchByResourceID(
this.submissionService.getSubmissionObjectLinkName(),
this.submissionId,
this.pathCombiner.rootElement,
this.pathCombiner.subRootElement);
})
// dispatch a PATCH request to save metadata
return this.operationsService.jsonPatchByResourceID(
this.submissionService.getSubmissionObjectLinkName(),
this.submissionId,
this.pathCombiner.rootElement,
this.pathCombiner.subRootElement);
})
).subscribe((result: SubmissionObject[]) => {
if (result[0].sections[this.sectionId]) {
const uploadSection = (result[0].sections[this.sectionId] as WorkspaceitemSectionUploadObject);
Object.keys(uploadSection.files)
.filter((key) => uploadSection.files[key].uuid === this.fileId)
.forEach((key) => this.uploadService.updateFileData(
this.submissionId, this.sectionId, this.fileId, uploadSection.files[key])
);
const section = result[0].sections[this.sectionId];
if (!section) {
return;
}
const uploadSection = (section as WorkspaceitemSectionUploadObject);
this.uploadService.updateFilePrimaryBitstream(this.submissionId, this.sectionId, uploadSection.primary);
Object.keys(uploadSection.files)
.filter((key) => uploadSection.files[key].uuid === this.fileId)
.forEach((key) => this.uploadService.updateFileData(
this.submissionId, this.sectionId, this.fileId, uploadSection.files[key])
);
this.isSaving = false;
this.activeModal.close();
});

View File

@@ -4,6 +4,7 @@ import {
DynamicFormControlLayout,
DynamicFormGroupModelConfig,
DynamicSelectModelConfig,
DynamicSwitchModelConfig,
MATCH_ENABLED,
OR_OPERATOR,
} from '@ng-dynamic-forms/core';
@@ -56,6 +57,19 @@ export const BITSTREAM_FORM_ACCESS_CONDITION_TYPE_LAYOUT: DynamicFormControlLayo
label: 'col-form-label name-label'
}
};
export const BITSTREAM_FORM_PRIMARY_LAYOUT: DynamicFormControlLayout = {
element: {
host: 'col-12',
container: 'text-right'
},
};
export const BITSTREAM_FORM_PRIMARY: DynamicSwitchModelConfig = {
id: 'primary',
name: 'primary',
label: 'bitstream.edit.form.primaryBitstream.label'
};
export const BITSTREAM_FORM_ACCESS_CONDITION_START_DATE_CONFIG: DynamicDatePickerModelConfig = {
id: 'startDate',

View File

@@ -1,6 +1,22 @@
<ng-container *ngIf="fileData">
<div class="row">
<div class="col-md-12">
<!-- Default switch -->
<div class="col-md-2 d-flex justify-content-center align-items-center" >
<div class="custom-control custom-switch">
<input
type="checkbox"
class="custom-control-input"
id="primaryBitstream{{fileIndex}}"
[disabled]="processingSaveStatus$ | async"
[checked]="isPrimary"
(change)="togglePrimaryBitstream($event)">
<label class="custom-control-label" for="primaryBitstream{{fileIndex}}">
<span class="sr-only" *ngIf="!isPrimary">{{'submission.sections.upload.primary.make' | translate:{ fileName: fileName } }}</span>
<span class="sr-only" *ngIf="isPrimary">{{'submission.sections.upload.primary.remove' | translate:{ fileName: fileName } }}</span>
</label>
</div>
</div>
<div class="col-md-10">
<div class="float-left w-75">
<h3>{{fileName}} <span class="text-muted">({{fileData?.sizeBytes | dsFileSize}})</span></h3>
</div>

View File

@@ -66,7 +66,7 @@ describe('SubmissionSectionUploadFileComponent test suite', () => {
const fileName = '123456-test-upload.jpg';
const fileId = '123456-test-upload';
const fileData: any = mockUploadFiles[0];
const pathCombiner = new JsonPatchOperationPathCombiner('sections', sectionId, 'files', fileIndex);
const pathCombiner = new JsonPatchOperationPathCombiner('sections', sectionId);
const jsonPatchOpBuilder: any = jasmine.createSpyObj('jsonPatchOpBuilder', {
add: jasmine.createSpy('add'),
@@ -201,6 +201,23 @@ describe('SubmissionSectionUploadFileComponent test suite', () => {
});
});
it('should delete primary if file we delete is primary', () => {
compAsAny.isPrimary = true;
compAsAny.pathCombiner = pathCombiner;
operationsService.jsonPatchByResourceID.and.returnValue(observableOf({}));
compAsAny.deleteFile();
expect(operationsBuilder.remove).toHaveBeenCalledWith(pathCombiner.getPath('primary'));
expect(uploadService.updateFilePrimaryBitstream).toHaveBeenCalledWith(submissionId, sectionId, null);
});
it('should NOT delete primary if file we delete is NOT primary', () => {
compAsAny.isPrimary = false;
compAsAny.pathCombiner = pathCombiner;
operationsService.jsonPatchByResourceID.and.returnValue(observableOf({}));
compAsAny.deleteFile();
expect(uploadService.updateFilePrimaryBitstream).not.toHaveBeenCalledTimes(1);
});
it('should delete file properly', () => {
compAsAny.pathCombiner = pathCombiner;
operationsService.jsonPatchByResourceID.and.returnValue(observableOf({}));
@@ -209,7 +226,8 @@ describe('SubmissionSectionUploadFileComponent test suite', () => {
compAsAny.deleteFile();
expect(uploadService.removeUploadedFile).toHaveBeenCalledWith(submissionId, sectionId, fileId);
expect(operationsBuilder.remove).toHaveBeenCalledWith(pathCombiner.getPath());
expect(operationsBuilder.remove).toHaveBeenCalledWith(pathCombiner.getPath(['files', fileIndex]));
expect(operationsService.jsonPatchByResourceID).toHaveBeenCalledWith(
'workspaceitems',
submissionId,

View File

@@ -1,5 +1,4 @@
import {
ChangeDetectorRef,
Component,
Input,
OnChanges,
@@ -9,7 +8,7 @@ import {
ViewChild
} from '@angular/core';
import { BehaviorSubject, Subscription } from 'rxjs';
import { BehaviorSubject, Observable, Subscription } from 'rxjs';
import { filter } from 'rxjs/operators';
import { DynamicFormControlModel, } from '@ng-dynamic-forms/core';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
@@ -22,7 +21,6 @@ import { JsonPatchOperationPathCombiner } from '../../../../core/json-patch/buil
import { WorkspaceitemSectionUploadFileObject } from '../../../../core/submission/models/workspaceitem-section-upload-file.model';
import { SubmissionFormsModel } from '../../../../core/config/models/config-submission-forms.model';
import { SubmissionService } from '../../../submission.service';
import { HALEndpointService } from '../../../../core/shared/hal-endpoint.service';
import { SubmissionJsonPatchOperationsService } from '../../../../core/submission/submission-json-patch-operations.service';
import { SubmissionSectionUploadFileEditComponent } from './edit/section-upload-file-edit.component';
import { Bitstream } from '../../../../core/shared/bitstream.model';
@@ -37,6 +35,12 @@ import { NgbModalOptions } from '@ng-bootstrap/ng-bootstrap/modal/modal-config';
templateUrl: './section-upload-file.component.html',
})
export class SubmissionSectionUploadFileComponent implements OnChanges, OnInit, OnDestroy {
/**
* The indicator is the primary bitstream
* it will be null if no primary bitstream is set for the ORIGINAL bundle
* @type {boolean, null}
*/
@Input() isPrimary: boolean | null;
/**
* The list of available access condition
@@ -100,6 +104,11 @@ export class SubmissionSectionUploadFileComponent implements OnChanges, OnInit,
*/
@ViewChild(SubmissionSectionUploadFileEditComponent) fileEditComp: SubmissionSectionUploadFileEditComponent;
/**
* A boolean representing if a submission save operation is pending
* @type {Observable<boolean>}
*/
public processingSaveStatus$: Observable<boolean>;
/**
* The bitstream's metadata data
@@ -137,6 +146,12 @@ export class SubmissionSectionUploadFileComponent implements OnChanges, OnInit,
*/
protected pathCombiner: JsonPatchOperationPathCombiner;
/**
* The [JsonPatchOperationPathCombiner] object
* @type {JsonPatchOperationPathCombiner}
*/
protected primaryBitstreamPathCombiner: JsonPatchOperationPathCombiner;
/**
* Array to track all subscriptions and unsubscribe them onDestroy
* @type {Array}
@@ -162,9 +177,7 @@ export class SubmissionSectionUploadFileComponent implements OnChanges, OnInit,
* @param {SectionUploadService} uploadService
*/
constructor(
private cdr: ChangeDetectorRef,
private formService: FormService,
private halService: HALEndpointService,
private modalService: NgbModal,
private operationsBuilder: JsonPatchOperationsBuilder,
private operationsService: SubmissionJsonPatchOperationsService,
@@ -197,7 +210,8 @@ export class SubmissionSectionUploadFileComponent implements OnChanges, OnInit,
*/
ngOnInit() {
this.formId = this.formService.getUniqueId(this.fileId);
this.pathCombiner = new JsonPatchOperationPathCombiner('sections', this.sectionId, 'files', this.fileIndex);
this.processingSaveStatus$ = this.submissionService.getSubmissionSaveProcessingStatus(this.submissionId);
this.pathCombiner = new JsonPatchOperationPathCombiner('sections', this.sectionId);
this.loadFormMetadata();
}
@@ -247,7 +261,12 @@ export class SubmissionSectionUploadFileComponent implements OnChanges, OnInit,
activeModal.componentInstance.formMetadata = this.formMetadata;
activeModal.componentInstance.pathCombiner = this.pathCombiner;
activeModal.componentInstance.submissionId = this.submissionId;
activeModal.componentInstance.isPrimary = this.isPrimary;
}
togglePrimaryBitstream(event) {
this.uploadService.updatePrimaryBitstreamOperation(this.pathCombiner.getPath('primary'), this.isPrimary, event.target.checked, this.fileId);
this.submissionService.dispatchSaveSection(this.submissionId, this.sectionId);
}
ngOnDestroy(): void {
@@ -273,13 +292,20 @@ export class SubmissionSectionUploadFileComponent implements OnChanges, OnInit,
* Delete bitstream from submission
*/
protected deleteFile() {
this.operationsBuilder.remove(this.pathCombiner.getPath());
this.operationsBuilder.remove(this.pathCombiner.getPath(['files', this.fileIndex]));
if (this.isPrimary) {
this.operationsBuilder.remove(this.pathCombiner.getPath('primary'));
}
this.subscriptions.push(this.operationsService.jsonPatchByResourceID(
this.submissionService.getSubmissionObjectLinkName(),
this.submissionId,
this.pathCombiner.rootElement,
this.pathCombiner.subRootElement)
.subscribe(() => {
if (this.isPrimary) {
this.uploadService.updateFilePrimaryBitstream(this.submissionId, this.sectionId, null);
}
this.uploadService.removeUploadedFile(this.submissionId, this.sectionId, this.fileId);
this.processingDelete$.next(false);
}));

View File

@@ -17,6 +17,13 @@ export class ThemedSubmissionSectionUploadFileComponent
*/
@Input() availableAccessConditionOptions: any[];
/**
* The indicator is the primary bitstream
* it will be null if no primary bitstream is set for the ORIGINAL bundle
* @type {boolean, null}
*/
@Input() isPrimary: boolean | null;
/**
* The submission id
* @type {string}
@@ -69,6 +76,7 @@ export class ThemedSubmissionSectionUploadFileComponent
protected inAndOutputNames: (keyof SubmissionSectionUploadFileComponent & keyof this)[] = [
'availableAccessConditionOptions',
'isPrimary',
'collectionId',
'collectionPolicyType',
'configMetadataForm',

View File

@@ -2,15 +2,7 @@
[dismissible]="true"
[type]="AlertTypeEnum.Info"></ds-alert>
<ng-container *ngIf="fileList.length == 0">
<div class="row">
<div class="col-md-12">
<h3 class="text-center"><span class="text-muted">{{'submission.sections.upload.no-file-uploaded' | translate}}</span></h3>
</div>
</div>
</ng-container>
<ng-container *ngIf="fileList.length > 0">
<ng-container *ngIf="fileList.length > 0; else noFileUploaded">
<div *ngIf="collectionDefaultAccessConditions.length > 0" class="row">
<div class="col-sm-12" >
@@ -26,16 +18,26 @@
</ds-alert>
</div>
</div>
<ng-container *ngFor="let fileEntry of fileList">
<div class="row">
<div class="col-md-2">
<span class="text-left font-weight-bold">{{ 'bitstream.edit.form.primaryBitstream.label' | translate }}</span>
</div>
</div>
<div class="row">
<div class="col-md-12">
<hr/>
</div>
</div>
<ng-container *ngFor="let fileEntry of fileList; let i = index;">
<ds-themed-submission-upload-section-file
[isPrimary]="primaryBitstreamUUID ? primaryBitstreamUUID === fileEntry.uuid : null"
[availableAccessConditionOptions]="availableAccessConditionOptions"
[collectionId]="collectionId"
[collectionPolicyType]="collectionPolicyType"
[configMetadataForm]="(configMetadataForm$ | async)"
[fileId]="fileIndexes[fileList.indexOf(fileEntry)]"
[fileIndex]="fileList.indexOf(fileEntry)"
[fileName]="fileNames[fileList.indexOf(fileEntry)]"
[fileId]="fileEntry.uuid"
[fileIndex]="i"
[fileName]="fileNames[i]"
[sectionId]="sectionData.id"
[submissionId]="submissionId"></ds-themed-submission-upload-section-file>
<div class="row">
@@ -45,3 +47,11 @@
</div>
</ng-container>
</ng-container>
<ng-template #noFileUploaded>
<div class="row">
<div class="col-md-12">
<h3 class="text-center"><span class="text-muted">{{'submission.sections.upload.no-file-uploaded' | translate}}</span></h3>
</div>
</div>
</ng-template>

View File

@@ -25,6 +25,7 @@ import {
mockUploadConfigResponse,
mockUploadConfigResponseNotRequired,
mockUploadFiles,
mockUploadFilesData,
} from '../../../shared/mocks/submission.mock';
import { SubmissionUploadsConfigDataService } from '../../../core/config/submission-uploads-config-data.service';
import { SectionUploadService } from './section-upload.service';
@@ -160,6 +161,7 @@ describe('SubmissionSectionUploadComponent test suite', () => {
);
bitstreamService.getUploadedFileList.and.returnValue(observableOf([]));
bitstreamService.getUploadedFilesData.and.returnValue(observableOf({ primary: null, files: [] }));
};
TestBed.configureTestingModule({
@@ -230,7 +232,7 @@ describe('SubmissionSectionUploadComponent test suite', () => {
});
it('should init component properly', () => {
bitstreamService.getUploadedFilesData.and.returnValue(observableOf({ primary: null, files: [] }));
submissionServiceStub.getSubmissionObject.and.returnValue(observableOf(submissionState));
collectionDataService.findById.and.returnValue(createSuccessfulRemoteDataObject$(Object.assign(new Collection(), mockCollection, {
@@ -246,15 +248,8 @@ describe('SubmissionSectionUploadComponent test suite', () => {
createSuccessfulRemoteDataObject$(Object.assign(new Group(), mockGroup))
);
bitstreamService.getUploadedFileList.and.returnValue(observableOf([]));
comp.onSectionInit();
const expectedGroupsMap = new Map([
[mockUploadConfigResponse.accessConditionOptions[1].name, [mockGroup as any]],
[mockUploadConfigResponse.accessConditionOptions[2].name, [mockGroup as any]],
]);
expect(comp.collectionId).toBe(collectionId);
expect(comp.collectionName).toBe(mockCollection.name);
expect(comp.availableAccessConditionOptions.length).toBe(4);
@@ -262,12 +257,12 @@ describe('SubmissionSectionUploadComponent test suite', () => {
expect(comp.required$.getValue()).toBe(true);
expect(compAsAny.subs.length).toBe(2);
expect(compAsAny.fileList).toEqual([]);
expect(compAsAny.fileIndexes).toEqual([]);
expect(compAsAny.fileNames).toEqual([]);
expect(compAsAny.primaryBitstreamUUID).toEqual(null);
});
it('should init file list properly', () => {
bitstreamService.getUploadedFilesData.and.returnValue(observableOf({ primary: null, files: [] }));
submissionServiceStub.getSubmissionObject.and.returnValue(observableOf(submissionState));
@@ -282,7 +277,7 @@ describe('SubmissionSectionUploadComponent test suite', () => {
createSuccessfulRemoteDataObject$(Object.assign(new Group(), mockGroup))
);
bitstreamService.getUploadedFileList.and.returnValue(observableOf(mockUploadFiles));
bitstreamService.getUploadedFilesData.and.returnValue(observableOf(mockUploadFilesData));
comp.onSectionInit();
@@ -298,12 +293,14 @@ describe('SubmissionSectionUploadComponent test suite', () => {
expect(comp.required$.getValue()).toBe(true);
expect(compAsAny.subs.length).toBe(2);
expect(compAsAny.fileList).toEqual(mockUploadFiles);
expect(compAsAny.fileIndexes).toEqual(['123456-test-upload']);
expect(compAsAny.primaryBitstreamUUID).toEqual(null);
expect(compAsAny.fileNames).toEqual(['123456-test-upload.jpg']);
});
it('should properly read the section status when required is true', () => {
bitstreamService.getUploadedFilesData.and.returnValue(observableOf({ primary: null, files: [] }));
submissionServiceStub.getSubmissionObject.and.returnValue(observableOf(submissionState));
collectionDataService.findById.and.returnValue(createSuccessfulRemoteDataObject$(mockCollection));
@@ -324,7 +321,7 @@ describe('SubmissionSectionUploadComponent test suite', () => {
comp.onSectionInit();
expect(comp.required$.getValue()).toBe(true);
expect(comp.required$.getValue()).toBe(true);
expect(compAsAny.getSectionStatus()).toBeObservable(cold('-c-d', {
c: false,
@@ -335,6 +332,7 @@ describe('SubmissionSectionUploadComponent test suite', () => {
it('should properly read the section status when required is false', () => {
submissionServiceStub.getSubmissionObject.and.returnValue(observableOf(submissionState));
bitstreamService.getUploadedFilesData.and.returnValue(observableOf({ primary: null, files: [] }));
collectionDataService.findById.and.returnValue(createSuccessfulRemoteDataObject$(mockCollection));
resourcePolicyService.findByHref.and.returnValue(createSuccessfulRemoteDataObject$(mockDefaultAccessCondition));

View File

@@ -4,7 +4,8 @@ import {
BehaviorSubject,
combineLatest as observableCombineLatest,
Observable,
Subscription
Subscription,
combineLatest
} from 'rxjs';
import { distinctUntilChanged, filter, map, mergeMap, switchMap, tap } from 'rxjs/operators';
@@ -31,6 +32,7 @@ import { AccessConditionOption } from '../../../core/config/models/config-access
import { followLink } from '../../../shared/utils/follow-link-config.model';
import { getFirstSucceededRemoteData } from '../../../core/shared/operators';
import { DSONameService } from '../../../core/breadcrumbs/dso-name.service';
import { WorkspaceitemSectionUploadObject } from 'src/app/core/submission/models/workspaceitem-section-upload.model';
export const POLICY_DEFAULT_NO_LIST = 1; // Banner1
export const POLICY_DEFAULT_WITH_LIST = 2; // Banner2
@@ -58,10 +60,10 @@ export class SubmissionSectionUploadComponent extends SectionModelComponent {
public AlertTypeEnum = AlertType;
/**
* The array containing the keys of file list array
* The uuid of primary bitstream file
* @type {Array}
*/
public fileIndexes: string[] = [];
public primaryBitstreamUUID: string | null = null;
/**
* The file list
@@ -194,27 +196,18 @@ export class SubmissionSectionUploadComponent extends SectionModelComponent {
this.changeDetectorRef.detectChanges();
}),
// retrieve submission's bitstreams from state
observableCombineLatest(this.configMetadataForm$,
this.bitstreamService.getUploadedFileList(this.submissionId, this.sectionData.id)).pipe(
filter(([configMetadataForm, fileList]: [SubmissionFormsModel, any[]]) => {
return isNotEmpty(configMetadataForm) && isNotUndefined(fileList);
// retrieve submission's bitstream data from state
combineLatest([this.configMetadataForm$,
this.bitstreamService.getUploadedFilesData(this.submissionId, this.sectionData.id)]).pipe(
filter(([configMetadataForm, { files }]: [SubmissionFormsModel, WorkspaceitemSectionUploadObject]) => {
return isNotEmpty(configMetadataForm) && isNotEmpty(files);
}),
distinctUntilChanged())
.subscribe(([configMetadataForm, fileList]: [SubmissionFormsModel, any[]]) => {
this.fileList = [];
this.fileIndexes = [];
this.fileNames = [];
this.changeDetectorRef.detectChanges();
if (isNotUndefined(fileList) && fileList.length > 0) {
fileList.forEach((file) => {
this.fileList.push(file);
this.fileIndexes.push(file.uuid);
this.fileNames.push(this.getFileName(configMetadataForm, file));
});
}
this.changeDetectorRef.detectChanges();
.subscribe(([configMetadataForm, { primary, files }]: [SubmissionFormsModel, WorkspaceitemSectionUploadObject]) => {
this.primaryBitstreamUUID = primary;
this.fileList = files;
this.fileNames = Array.from(files, file => this.getFileName(configMetadataForm, file));
}
)
);

View File

@@ -0,0 +1,63 @@
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { TestBed, waitForAsync } from '@angular/core/testing';
import { JsonPatchOperationPathCombiner } from 'src/app/core/json-patch/builder/json-patch-operation-path-combiner';
import { JsonPatchOperationsBuilder } from 'src/app/core/json-patch/builder/json-patch-operations-builder';
import { SectionUploadService } from './section-upload.service';
import { Store, StoreModule } from '@ngrx/store';
const jsonPatchOpBuilder: any = jasmine.createSpyObj('jsonPatchOpBuilder', {
add: jasmine.createSpy('add'),
replace: jasmine.createSpy('replace'),
remove: jasmine.createSpy('remove'),
});
describe('SectionUploadService test suite', () => {
let sectionUploadService: SectionUploadService;
let operationsBuilder: any;
const pathCombiner = new JsonPatchOperationPathCombiner('sections', 'upload');
const primaryPath = pathCombiner.getPath('primary');
const fileId = 'test';
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
imports: [StoreModule],
providers: [
{ provide: Store, useValue: {} },
SectionUploadService,
{ provide: JsonPatchOperationsBuilder, useValue: jsonPatchOpBuilder },
],
schemas: [NO_ERRORS_SCHEMA]
});
}));
beforeEach(() => {
sectionUploadService = TestBed.inject(SectionUploadService);
operationsBuilder = TestBed.inject(JsonPatchOperationsBuilder);
});
[
{
initialPrimary: null,
primary: true,
operationName: 'add',
expected: [primaryPath, fileId, false, true]
},
{
initialPrimary: true,
primary: false,
operationName: 'remove',
expected: [primaryPath]
},
{
initialPrimary: false,
primary: true,
operationName: 'replace',
expected: [primaryPath, fileId, true]
}
].forEach(({ initialPrimary, primary, operationName, expected }) => {
it(`updatePrimaryBitstreamOperation should add ${operationName} operation`, () => {
const path = pathCombiner.getPath('primary');
sectionUploadService.updatePrimaryBitstreamOperation(path, initialPrimary, primary, fileId);
expect(operationsBuilder[operationName]).toHaveBeenCalledWith(...expected);
});
});
});

View File

@@ -8,11 +8,15 @@ import { SubmissionState } from '../../submission.reducers';
import {
DeleteUploadedFileAction,
EditFileDataAction,
EditFilePrimaryBitstreamAction,
NewUploadedFileAction
} from '../../objects/submission-objects.actions';
import { submissionUploadedFileFromUuidSelector, submissionUploadedFilesFromIdSelector } from '../../selectors';
import { submissionSectionDataFromIdSelector, submissionUploadedFileFromUuidSelector, submissionUploadedFilesFromIdSelector } from '../../selectors';
import { isUndefined } from '../../../shared/empty.util';
import { WorkspaceitemSectionUploadFileObject } from '../../../core/submission/models/workspaceitem-section-upload-file.model';
import { WorkspaceitemSectionUploadObject } from 'src/app/core/submission/models/workspaceitem-section-upload.model';
import { JsonPatchOperationPathObject } from 'src/app/core/json-patch/builder/json-patch-operation-path-combiner';
import { JsonPatchOperationsBuilder } from 'src/app/core/json-patch/builder/json-patch-operations-builder';
/**
* A service that provides methods to handle submission's bitstream state.
@@ -24,8 +28,53 @@ export class SectionUploadService {
* Initialize service variables
*
* @param {Store<SubmissionState>} store
* @param {JsonPatchOperationsBuilder} operationsBuilder
*/
constructor(private store: Store<SubmissionState>) {}
constructor(private store: Store<SubmissionState>, private operationsBuilder: JsonPatchOperationsBuilder) {}
/**
* Define and add an operation based on a change
*
* @param path
* The path to endpoint
* @param intitialPrimary
* The initial primary indicator
* @param primary
* the new primary indicator
* @param fileId
* The file id
* @returns {void}
*/
public updatePrimaryBitstreamOperation(path: JsonPatchOperationPathObject, intitialPrimary: boolean | null, primary: boolean | null, fileId: string): void {
if (intitialPrimary === null && primary) {
this.operationsBuilder.add(path, fileId, false, true);
return;
}
if (intitialPrimary !== primary) {
if (primary) {
this.operationsBuilder.replace(path, fileId, true);
return;
}
this.operationsBuilder.remove(path);
}
}
/**
* Return submission's bitstream data from state
*
* @param submissionId
* The submission id
* @param sectionId
* The section id
* @returns {WorkspaceitemSectionUploadObject}
* Returns submission's bitstream data
*/
public getUploadedFilesData(submissionId: string, sectionId: string): Observable<WorkspaceitemSectionUploadObject> {
return this.store.select(submissionSectionDataFromIdSelector(submissionId, sectionId)).pipe(
map((state) => state),
distinctUntilChanged());
}
/**
* Return submission's bitstream list from state
@@ -104,6 +153,22 @@ export class SectionUploadService {
);
}
/**
* Update primary bitstream into the state
*
* @param submissionId
* The submission id
* @param sectionId
* The section id
* @param fileUUID
* The bitstream UUID
*/
public updateFilePrimaryBitstream(submissionId: string, sectionId: string, fileUUID: string | null) {
this.store.dispatch(
new EditFilePrimaryBitstreamAction(submissionId, sectionId, fileUUID)
);
}
/**
* Update bitstream metadata into the state
*

View File

@@ -726,7 +726,7 @@
"bitstream.edit.form.newFormat.hint": "The application you used to create the file, and the version number (for example, \"<i>ACMESoft SuperApp version 1.5</i>\").",
"bitstream.edit.form.primaryBitstream.label": "Primary bitstream",
"bitstream.edit.form.primaryBitstream.label": "Primary File",
"bitstream.edit.form.selectedFormat.hint": "If the format is not in the above list, <b>select \"format not in list\" above</b> and describe it under \"Describe new format\".",
@@ -2496,6 +2496,8 @@
"item.page.bitstreams.collapse": "Collapse",
"item.page.bitstreams.primary": "Primary",
"item.page.filesection.original.bundle": "Original bundle",
"item.page.filesection.license.bundle": "License bundle",
@@ -4818,6 +4820,10 @@
"submission.sections.toggle.aria.close": "Collapse {{sectionHeader}} section",
"submission.sections.upload.primary.make": "Make {{fileName}} the primary bitstream",
"submission.sections.upload.primary.remove": "Remove {{fileName}} as the primary bitstream",
"submission.sections.upload.delete.confirm.cancel": "Cancel",
"submission.sections.upload.delete.confirm.info": "This operation can't be undone. Are you sure?",