diff --git a/src/app/submission/sections/upload/file/edit/section-upload-file-edit.component.html b/src/app/submission/sections/upload/file/edit/section-upload-file-edit.component.html index f6e0646974..2baa6c1555 100644 --- a/src/app/submission/sections/upload/file/edit/section-upload-file-edit.component.html +++ b/src/app/submission/sections/upload/file/edit/section-upload-file-edit.component.html @@ -1,9 +1,21 @@
- + +
diff --git a/src/app/submission/sections/upload/file/edit/section-upload-file-edit.component.spec.ts b/src/app/submission/sections/upload/file/edit/section-upload-file-edit.component.spec.ts index a644cf8270..aa03d37eb2 100644 --- a/src/app/submission/sections/upload/file/edit/section-upload-file-edit.component.spec.ts +++ b/src/app/submission/sections/upload/file/edit/section-upload-file-edit.component.spec.ts @@ -1,5 +1,5 @@ import { ChangeDetectorRef, Component, NO_ERRORS_SCHEMA } from '@angular/core'; -import { waitForAsync, ComponentFixture, inject, TestBed } from '@angular/core/testing'; +import { waitForAsync, ComponentFixture, inject, TestBed, fakeAsync, tick } from '@angular/core/testing'; import { BrowserModule } from '@angular/platform-browser'; import { CommonModule } from '@angular/common'; import { TranslateModule } from '@ngx-translate/core'; @@ -17,19 +17,37 @@ import { SubmissionService } from '../../../../submission.service'; import { SubmissionSectionUploadFileEditComponent } from './section-upload-file-edit.component'; import { POLICY_DEFAULT_WITH_LIST } from '../../section-upload.component'; import { - mockGroup, mockSubmissionCollectionId, mockSubmissionId, mockUploadConfigResponse, mockUploadConfigResponseMetadata, - mockUploadFiles + mockUploadFiles, + mockFileFormData, + mockSubmissionObject, } from '../../../../../shared/mocks/submission.mock'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { FormComponent } from '../../../../../shared/form/form.component'; import { FormService } from '../../../../../shared/form/form.service'; import { getMockFormService } from '../../../../../shared/mocks/form-service.mock'; -import { Group } from '../../../../../core/eperson/models/group.model'; import { createTestComponent } from '../../../../../shared/testing/utils.test'; +import { NgbActiveModal, NgbModal } from '@ng-bootstrap/ng-bootstrap'; +import { JsonPatchOperationsBuilder } from '../../../../../core/json-patch/builder/json-patch-operations-builder'; +import { SubmissionJsonPatchOperationsServiceStub } from '../../../../../shared/testing/submission-json-patch-operations-service.stub'; +import { SubmissionJsonPatchOperationsService } from '../../../../../core/submission/submission-json-patch-operations.service'; +import { SectionUploadService } from '../../section-upload.service'; +import { getMockSectionUploadService } from '../../../../../shared/mocks/section-upload.service.mock'; +import { FormFieldMetadataValueObject } from '../../../../../shared/form/builder/models/form-field-metadata-value.model'; +import { JsonPatchOperationPathCombiner } from '../../../../../core/json-patch/builder/json-patch-operation-path-combiner'; +import { dateToISOFormat } from '../../../../../shared/date.util'; +import { of } from 'rxjs'; + +const jsonPatchOpBuilder: any = jasmine.createSpyObj('jsonPatchOpBuilder', { + add: jasmine.createSpy('add'), + replace: jasmine.createSpy('replace'), + remove: jasmine.createSpy('remove'), +}); + +const formMetadataMock = ['dc.title', 'dc.description']; describe('SubmissionSectionUploadFileEditComponent test suite', () => { @@ -38,7 +56,12 @@ describe('SubmissionSectionUploadFileEditComponent test suite', () => { let fixture: ComponentFixture; let submissionServiceStub: SubmissionServiceStub; let formbuilderService: any; + let operationsBuilder: any; + let operationsService: any; + let formService: any; + let uploadService: any; + const submissionJsonPatchOperationsServiceStub = new SubmissionJsonPatchOperationsServiceStub(); const submissionId = mockSubmissionId; const sectionId = 'upload'; const collectionId = mockSubmissionCollectionId; @@ -48,6 +71,7 @@ describe('SubmissionSectionUploadFileEditComponent test suite', () => { const fileIndex = '0'; const fileId = '123456-test-upload'; const fileData: any = mockUploadFiles[0]; + const pathCombiner = new JsonPatchOperationPathCombiner('sections', sectionId, 'files', fileIndex); beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ @@ -66,9 +90,15 @@ describe('SubmissionSectionUploadFileEditComponent test suite', () => { providers: [ { provide: FormService, useValue: getMockFormService() }, { provide: SubmissionService, useClass: SubmissionServiceStub }, + { provide: SubmissionJsonPatchOperationsService, useValue: submissionJsonPatchOperationsServiceStub }, + { provide: JsonPatchOperationsBuilder, useValue: jsonPatchOpBuilder }, + { provide: SectionUploadService, useValue: getMockSectionUploadService() }, FormBuilderService, ChangeDetectorRef, - SubmissionSectionUploadFileEditComponent + SubmissionSectionUploadFileEditComponent, + NgbModal, + NgbActiveModal, + FormComponent, ], schemas: [NO_ERRORS_SCHEMA] }).compileComponents().then(); @@ -114,6 +144,10 @@ describe('SubmissionSectionUploadFileEditComponent test suite', () => { compAsAny = comp; submissionServiceStub = TestBed.inject(SubmissionService as any); formbuilderService = TestBed.inject(FormBuilderService); + operationsBuilder = TestBed.inject(JsonPatchOperationsBuilder); + operationsService = TestBed.inject(SubmissionJsonPatchOperationsService); + formService = TestBed.inject(FormService); + uploadService = TestBed.inject(SectionUploadService); comp.submissionId = submissionId; comp.collectionId = collectionId; @@ -123,6 +157,9 @@ describe('SubmissionSectionUploadFileEditComponent test suite', () => { comp.fileIndex = fileIndex; comp.fileId = fileId; comp.configMetadataForm = configMetadataForm; + comp.formMetadata = formMetadataMock; + + formService.isValid.and.returnValue(of(true)); }); afterEach(() => { @@ -135,7 +172,7 @@ describe('SubmissionSectionUploadFileEditComponent test suite', () => { comp.fileData = fileData; comp.formId = 'testFileForm'; - comp.ngOnChanges(); + comp.ngOnInit(); expect(comp.formModel).toBeDefined(); expect(comp.formModel.length).toBe(2); @@ -165,7 +202,7 @@ describe('SubmissionSectionUploadFileEditComponent test suite', () => { comp.fileData = fileData; comp.formId = 'testFileForm'; - comp.ngOnChanges(); + comp.ngOnInit(); const model: DynamicSelectModel = formbuilderService.findById('name', comp.formModel, 0); const formGroup = formbuilderService.createFormGroup(comp.formModel); @@ -186,6 +223,82 @@ describe('SubmissionSectionUploadFileEditComponent test suite', () => { comp.setOptions(model, control); expect(formbuilderService.findById).toHaveBeenCalledWith('startDate', (model.parent as DynamicFormArrayGroupModel).group); }); + + it('should retrieve Value From Field properly', () => { + let field; + expect(compAsAny.retrieveValueFromField(field)).toBeUndefined(); + + field = new FormFieldMetadataValueObject('test'); + expect(compAsAny.retrieveValueFromField(field)).toBe('test'); + + field = [new FormFieldMetadataValueObject('test')]; + expect(compAsAny.retrieveValueFromField(field)).toBe('test'); + }); + + it('should save Bitstream File data properly when form is valid', fakeAsync(() => { + compAsAny.formRef = {formGroup: null}; + compAsAny.fileData = fileData; + compAsAny.pathCombiner = pathCombiner; + formService.validateAllFormFields.and.callFake(() => null); + formService.isValid.and.returnValue(of(true)); + formService.getFormData.and.returnValue(of(mockFileFormData)); + + const response = [ + Object.assign(mockSubmissionObject, { + sections: { + upload: { + files: mockUploadFiles + } + } + }) + ]; + operationsService.jsonPatchByResourceID.and.returnValue(of(response)); + + const accessConditionsToSave = [ + { name: 'openaccess' }, + { name: 'lease', endDate: dateToISOFormat('2019-01-16T00:00:00Z') }, + { name: 'embargo', startDate: dateToISOFormat('2019-01-16T00:00:00Z') }, + ]; + comp.saveBitstreamData(); + tick(); + + let path = 'metadata/dc.title'; + expect(operationsBuilder.add).toHaveBeenCalledWith( + pathCombiner.getPath(path), + mockFileFormData.metadata['dc.title'], + true + ); + + path = 'metadata/dc.description'; + expect(operationsBuilder.add).toHaveBeenCalledWith( + pathCombiner.getPath(path), + mockFileFormData.metadata['dc.description'], + true + ); + + path = 'accessConditions'; + expect(operationsBuilder.add).toHaveBeenCalledWith( + pathCombiner.getPath(path), + accessConditionsToSave, + true + ); + + expect(uploadService.updateFileData).toHaveBeenCalledWith(submissionId, sectionId, mockUploadFiles[0].uuid, mockUploadFiles[0]); + + })); + + it('should not save Bitstream File data properly when form is not valid', fakeAsync(() => { + compAsAny.formRef = {formGroup: null}; + compAsAny.pathCombiner = pathCombiner; + formService.validateAllFormFields.and.callFake(() => null); + formService.isValid.and.returnValue(of(false)); + comp.saveBitstreamData(); + tick(); + + expect(uploadService.updateFileData).not.toHaveBeenCalled(); + + })); + }); }); diff --git a/src/app/submission/sections/upload/file/edit/section-upload-file-edit.component.ts b/src/app/submission/sections/upload/file/edit/section-upload-file-edit.component.ts index 96725f151e..3a43e718a0 100644 --- a/src/app/submission/sections/upload/file/edit/section-upload-file-edit.component.ts +++ b/src/app/submission/sections/upload/file/edit/section-upload-file-edit.component.ts @@ -1,4 +1,4 @@ -import { ChangeDetectorRef, Component, Input, OnChanges, ViewChild } from '@angular/core'; +import { ChangeDetectorRef, Component, OnInit, ViewChild } from '@angular/core'; import { FormControl } from '@angular/forms'; import { @@ -32,13 +32,23 @@ import { BITSTREAM_METADATA_FORM_GROUP_LAYOUT } from './section-upload-file-edit.model'; import { POLICY_DEFAULT_WITH_LIST } from '../../section-upload.component'; -import { isNotEmpty } from '../../../../../shared/empty.util'; +import { hasNoValue, hasValue, isNotEmpty, isNotNull } from '../../../../../shared/empty.util'; import { SubmissionFormsModel } from '../../../../../core/config/models/config-submission-forms.model'; import { FormFieldModel } from '../../../../../shared/form/builder/models/form-field.model'; import { AccessConditionOption } from '../../../../../core/config/models/config-access-condition-option.model'; import { SubmissionService } from '../../../../submission.service'; import { FormService } from '../../../../../shared/form/form.service'; import { FormComponent } from '../../../../../shared/form/form.component'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import { filter, mergeMap, take } from 'rxjs/operators'; +import { dateToISOFormat } from '../../../../../shared/date.util'; +import { SubmissionObject } from '../../../../../core/submission/models/submission-object.model'; +import { WorkspaceitemSectionUploadObject } from '../../../../../core/submission/models/workspaceitem-section-upload.model'; +import { JsonPatchOperationsBuilder } from '../../../../../core/json-patch/builder/json-patch-operations-builder'; +import { SubmissionJsonPatchOperationsService } from '../../../../../core/submission/submission-json-patch-operations.service'; +import { JsonPatchOperationPathCombiner } from '../../../../../core/json-patch/builder/json-patch-operation-path-combiner'; +import { SectionUploadService } from '../../section-upload.service'; +import { Subscription } from 'rxjs'; /** * This component represents the edit form for bitstream @@ -48,105 +58,246 @@ import { FormComponent } from '../../../../../shared/form/form.component'; styleUrls: ['./section-upload-file-edit.component.scss'], templateUrl: './section-upload-file-edit.component.html', }) -export class SubmissionSectionUploadFileEditComponent implements OnChanges { - - /** - * The list of available access condition - * @type {Array} - */ - @Input() availableAccessConditionOptions: any[]; - - /** - * The submission id - * @type {string} - */ - @Input() collectionId: string; - - /** - * Define if collection access conditions policy type : - * POLICY_DEFAULT_NO_LIST : is not possible to define additional access group/s for the single file - * POLICY_DEFAULT_WITH_LIST : is possible to define additional access group/s for the single file - * @type {number} - */ - @Input() collectionPolicyType: number; - - /** - * The configuration for the bitstream's metadata form - * @type {SubmissionFormsModel} - */ - @Input() configMetadataForm: SubmissionFormsModel; - - /** - * The bitstream's metadata data - * @type {WorkspaceitemSectionUploadFileObject} - */ - @Input() fileData: WorkspaceitemSectionUploadFileObject; - - /** - * The bitstream id - * @type {string} - */ - @Input() fileId: string; - - /** - * The bitstream array key - * @type {string} - */ - @Input() fileIndex: string; - - /** - * The form id - * @type {string} - */ - @Input() formId: string; - - /** - * The section id - * @type {string} - */ - @Input() sectionId: string; - - /** - * The submission id - * @type {string} - */ - @Input() submissionId: string; - - /** - * The form model - * @type {DynamicFormControlModel[]} - */ - public formModel: DynamicFormControlModel[]; +export class SubmissionSectionUploadFileEditComponent implements OnInit { /** * The FormComponent reference */ @ViewChild('formRef') public formRef: FormComponent; + /** + * The list of available access condition + * @type {Array} + */ + public availableAccessConditionOptions: any[]; + + /** + * The submission id + * @type {string} + */ + public collectionId: string; + + /** + * Define if collection access conditions policy type : + * POLICY_DEFAULT_NO_LIST : is not possible to define additional access group/s for the single file + * POLICY_DEFAULT_WITH_LIST : is possible to define additional access group/s for the single file + * @type {number} + */ + public collectionPolicyType: number; + + /** + * The configuration for the bitstream's metadata form + * @type {SubmissionFormsModel} + */ + public configMetadataForm: SubmissionFormsModel; + + /** + * The bitstream's metadata data + * @type {WorkspaceitemSectionUploadFileObject} + */ + public fileData: WorkspaceitemSectionUploadFileObject; + + /** + * The bitstream id + * @type {string} + */ + public fileId: string; + + /** + * The bitstream array key + * @type {string} + */ + public fileIndex: string; + + /** + * The form id + * @type {string} + */ + public formId: string; + + /** + * The section id + * @type {string} + */ + public sectionId: string; + + /** + * The submission id + * @type {string} + */ + public submissionId: string; + + /** + * The list of all available metadata + */ + formMetadata: string[] = []; + + /** + * The form model + * @type {DynamicFormControlModel[]} + */ + formModel: DynamicFormControlModel[]; + + /** + * When `true` form controls are deactivated + */ + isSaving = false; + + /** + * The [JsonPatchOperationPathCombiner] object + * @type {JsonPatchOperationPathCombiner} + */ + protected pathCombiner: JsonPatchOperationPathCombiner; + + protected subscriptions: Subscription[] = []; + /** * Initialize instance variables * + * @param activeModal * @param {ChangeDetectorRef} cdr * @param {FormBuilderService} formBuilderService * @param {FormService} formService * @param {SubmissionService} submissionService + * @param {JsonPatchOperationsBuilder} operationsBuilder + * @param {SubmissionJsonPatchOperationsService} operationsService + * @param {SectionUploadService} uploadService */ - constructor(private cdr: ChangeDetectorRef, - private formBuilderService: FormBuilderService, - private formService: FormService, - private submissionService: SubmissionService) { + constructor( + protected activeModal: NgbActiveModal, + private cdr: ChangeDetectorRef, + private formBuilderService: FormBuilderService, + private formService: FormService, + private submissionService: SubmissionService, + private operationsBuilder: JsonPatchOperationsBuilder, + private operationsService: SubmissionJsonPatchOperationsService, + private uploadService: SectionUploadService, + ) { + } + + /** + * Initialize form model values + * + * @param formModel + * The form model + */ + public initModelData(formModel: DynamicFormControlModel[]) { + this.fileData.accessConditions.forEach((accessCondition, index) => { + Array.of('name', 'startDate', 'endDate') + .filter((key) => accessCondition.hasOwnProperty(key) && isNotEmpty(accessCondition[key])) + .forEach((key) => { + const metadataModel: any = this.formBuilderService.findById(key, formModel, index); + if (metadataModel) { + if (metadataModel.type === DYNAMIC_FORM_CONTROL_TYPE_DATEPICKER) { + const date = new Date(accessCondition[key]); + metadataModel.value = { + year: date.getUTCFullYear(), + month: date.getUTCMonth() + 1, + day: date.getUTCDate() + }; + } else { + metadataModel.value = accessCondition[key]; + } + } + }); + }); + } + + /** + * Dispatch form model update when changing an access condition + * + * @param event + * The event emitted + */ + onChange(event: DynamicFormControlEvent) { + if (event.model.id === 'name') { + this.setOptions(event.model, event.control); + } + } + + onModalClose() { + this.activeModal.dismiss(); + } + + onSubmit() { + this.isSaving = true; + this.saveBitstreamData(); + } + + /** + * Update `startDate`, 'groupUUID' and 'endDate' model + * + * @param model + * The [[DynamicFormControlModel]] object + * @param control + * The [[FormControl]] object + */ + public setOptions(model: DynamicFormControlModel, control: FormControl) { + let accessCondition: AccessConditionOption = null; + this.availableAccessConditionOptions.filter((element) => element.name === control.value) + .forEach((element) => accessCondition = element ); + if (isNotEmpty(accessCondition)) { + const showGroups: boolean = accessCondition.hasStartDate === true || accessCondition.hasEndDate === true; + + const startDateControl: FormControl = control.parent.get('startDate') as FormControl; + const endDateControl: FormControl = control.parent.get('endDate') as FormControl; + + // Clear previous state + startDateControl?.markAsUntouched(); + endDateControl?.markAsUntouched(); + + startDateControl?.setValue(null); + control.parent.markAsDirty(); + endDateControl?.setValue(null); + + if (showGroups) { + if (accessCondition.hasStartDate) { + const startDateModel = this.formBuilderService.findById( + 'startDate', + (model.parent as DynamicFormArrayGroupModel).group) as DynamicDateControlModel; + + const min = new Date(accessCondition.maxStartDate); + startDateModel.max = { + year: min.getUTCFullYear(), + month: min.getUTCMonth() + 1, + day: min.getUTCDate() + }; + } + if (accessCondition.hasEndDate) { + const endDateModel = this.formBuilderService.findById( + 'endDate', + (model.parent as DynamicFormArrayGroupModel).group) as DynamicDateControlModel; + + const max = new Date(accessCondition.maxEndDate); + endDateModel.max = { + year: max.getUTCFullYear(), + month: max.getUTCMonth() + 1, + day: max.getUTCDate() + }; + } + } + } } /** * Dispatch form model init */ - ngOnChanges() { + ngOnInit() { if (this.fileData && this.formId) { this.formModel = this.buildFileEditForm(); this.cdr.detectChanges(); } } + ngOnDestroy(): void { + this.unsubscribeAll(); + } + + protected retrieveValueFromField(field: any) { + const temp = Array.isArray(field) ? field[0] : field; + return (temp) ? temp.value : undefined; + } + /** * Initialize form model */ @@ -193,17 +344,17 @@ export class SubmissionSectionUploadFileEditComponent implements OnChanges { const showEnd: boolean = condition.hasEndDate === true; const showGroups: boolean = showStart || showEnd; if (showStart) { - hasStart.push({ id: 'name', value: condition.name }); + hasStart.push({id: 'name', value: condition.name}); } if (showEnd) { - hasEnd.push({ id: 'name', value: condition.name }); + hasEnd.push({id: 'name', value: condition.name}); } if (showGroups) { - hasGroups.push({ id: 'name', value: condition.name }); + hasGroups.push({id: 'name', value: condition.name}); } }); - const confStart = { relations: [{ match: MATCH_ENABLED, operator: OR_OPERATOR, when: hasStart }] }; - const confEnd = { relations: [{ match: MATCH_ENABLED, operator: OR_OPERATOR, when: hasEnd }] }; + const confStart = {relations: [{match: MATCH_ENABLED, operator: OR_OPERATOR, when: hasStart}]}; + const confEnd = {relations: [{match: MATCH_ENABLED, operator: OR_OPERATOR, when: hasEnd}]}; accessConditionsArrayConfig.groupFactory = () => { const type = new DynamicSelectModel(accessConditionTypeModelConfig, BITSTREAM_FORM_ACCESS_CONDITION_TYPE_LAYOUT); @@ -213,7 +364,9 @@ export class SubmissionSectionUploadFileEditComponent implements OnChanges { const startDate = new DynamicDatePickerModel(startDateConfig, BITSTREAM_FORM_ACCESS_CONDITION_START_DATE_LAYOUT); const endDate = new DynamicDatePickerModel(endDateConfig, BITSTREAM_FORM_ACCESS_CONDITION_END_DATE_LAYOUT); const accessConditionGroupConfig = Object.assign({}, BITSTREAM_ACCESS_CONDITION_GROUP_CONFIG); - accessConditionGroupConfig.group = [type, startDate, endDate]; + accessConditionGroupConfig.group = [type]; + if (hasStart.length > 0) { accessConditionGroupConfig.group.push(startDate); } + if (hasEnd.length > 0) { accessConditionGroupConfig.group.push(endDate); } return [new DynamicFormGroupModel(accessConditionGroupConfig, BITSTREAM_ACCESS_CONDITION_GROUP_LAYOUT)]; }; @@ -229,98 +382,95 @@ export class SubmissionSectionUploadFileEditComponent implements OnChanges { } /** - * Initialize form model values - * - * @param formModel - * The form model + * Save bitstream metadata */ - public initModelData(formModel: DynamicFormControlModel[]) { - this.fileData.accessConditions.forEach((accessCondition, index) => { - Array.of('name', 'startDate', 'endDate') - .filter((key) => accessCondition.hasOwnProperty(key) && isNotEmpty(accessCondition[key])) - .forEach((key) => { - const metadataModel: any = this.formBuilderService.findById(key, formModel, index); - if (metadataModel) { - if (metadataModel.type === DYNAMIC_FORM_CONTROL_TYPE_DATEPICKER) { - const date = new Date(accessCondition[key]); - metadataModel.value = { - year: date.getUTCFullYear(), - month: date.getUTCMonth() + 1, - day: date.getUTCDate() - }; - } else { - metadataModel.value = accessCondition[key]; + saveBitstreamData() { + // validate form + this.formService.validateAllFormFields(this.formRef.formGroup); + const saveBitstreamDataSubscription = this.formService.isValid(this.formId).pipe( + take(1), + filter((isValid) => isValid), + mergeMap(() => this.formService.getFormData(this.formId)), + take(1), + mergeMap((formData: any) => { + // collect bitstream metadata + Object.keys((formData.metadata)) + .filter((key) => isNotEmpty(formData.metadata[key])) + .forEach((key) => { + const metadataKey = key.replace(/_/g, '.'); + const path = `metadata/${metadataKey}`; + this.operationsBuilder.add(this.pathCombiner.getPath(path), formData.metadata[key], true); + }); + Object.keys((this.fileData.metadata)) + .filter((key) => isNotEmpty(this.fileData.metadata[key])) + .filter((key) => hasNoValue(formData.metadata[key])) + .filter((key) => this.formMetadata.includes(key)) + .forEach((key) => { + const metadataKey = key.replace(/_/g, '.'); + const path = `metadata/${metadataKey}`; + this.operationsBuilder.remove(this.pathCombiner.getPath(path)); + }); + const accessConditionsToSave = []; + formData.accessConditions + .map((accessConditions) => accessConditions.accessConditionGroup) + .filter((accessCondition) => isNotEmpty(accessCondition)) + .forEach((accessCondition) => { + let accessConditionOpt; + + this.availableAccessConditionOptions + .filter((element) => isNotNull(accessCondition.name) && element.name === accessCondition.name[0].value) + .forEach((element) => accessConditionOpt = element); + + if (accessConditionOpt) { + const currentAccessCondition = Object.assign({}, accessCondition); + currentAccessCondition.name = this.retrieveValueFromField(accessCondition.name); + + /* When start and end date fields are deactivated, their values may be still present in formData, + therefore it is necessary to delete them if they're not allowed by the current access condition option. */ + if (!accessConditionOpt.hasStartDate) { + delete currentAccessCondition.startDate; + } else if (accessCondition.startDate) { + const startDate = this.retrieveValueFromField(accessCondition.startDate); + currentAccessCondition.startDate = dateToISOFormat(startDate); + } + if (!accessConditionOpt.hasEndDate) { + delete currentAccessCondition.endDate; + } else if (accessCondition.endDate) { + const endDate = this.retrieveValueFromField(accessCondition.endDate); + currentAccessCondition.endDate = dateToISOFormat(endDate); + } + accessConditionsToSave.push(currentAccessCondition); } - } - }); - }); - } + }); - /** - * Dispatch form model update when changing an access condition - * - * @param event - * The event emitted - */ - public onChange(event: DynamicFormControlEvent) { - if (event.model.id === 'name') { - this.setOptions(event.model, event.control); - } - } - - /** - * Update `startDate`, 'groupUUID' and 'endDate' model - * - * @param model - * The [[DynamicFormControlModel]] object - * @param control - * The [[FormControl]] object - */ - public setOptions(model: DynamicFormControlModel, control: FormControl) { - let accessCondition: AccessConditionOption = null; - this.availableAccessConditionOptions.filter((element) => element.name === control.value) - .forEach((element) => accessCondition = element); - if (isNotEmpty(accessCondition)) { - const showGroups: boolean = accessCondition.hasStartDate === true || accessCondition.hasEndDate === true; - - const startDateControl: FormControl = control.parent.get('startDate') as FormControl; - const endDateControl: FormControl = control.parent.get('endDate') as FormControl; - - // Clear previous state - startDateControl.markAsUntouched(); - endDateControl.markAsUntouched(); - - startDateControl.setValue(null); - control.parent.markAsDirty(); - endDateControl.setValue(null); - - if (showGroups) { - if (accessCondition.hasStartDate) { - const startDateModel = this.formBuilderService.findById( - 'startDate', - (model.parent as DynamicFormArrayGroupModel).group) as DynamicDateControlModel; - - const min = new Date(accessCondition.maxStartDate); - startDateModel.max = { - year: min.getUTCFullYear(), - month: min.getUTCMonth() + 1, - day: min.getUTCDate() - }; + if (isNotEmpty(accessConditionsToSave)) { + this.operationsBuilder.add(this.pathCombiner.getPath('accessConditions'), accessConditionsToSave, true); } - if (accessCondition.hasEndDate) { - const endDateModel = this.formBuilderService.findById( - 'endDate', - (model.parent as DynamicFormArrayGroupModel).group) as DynamicDateControlModel; - const max = new Date(accessCondition.maxEndDate); - endDateModel.max = { - year: max.getUTCFullYear(), - month: max.getUTCMonth() + 1, - day: max.getUTCDate() - }; - } + // dispatch a PATCH request to save metadata + return this.operationsService.jsonPatchByResourceID( + this.submissionService.getSubmissionObjectLinkName(), + this.submissionId, + this.pathCombiner.rootElement, + this.pathCombiner.subRootElement); + }) + ).subscribe((result: SubmissionObject[]) => { + if (result[0].sections[this.sectionId]) { + const uploadSection = (result[0].sections[this.sectionId] as WorkspaceitemSectionUploadObject); + Object.keys(uploadSection.files) + .filter((key) => uploadSection.files[key].uuid === this.fileId) + .forEach((key) => this.uploadService.updateFileData( + this.submissionId, this.sectionId, this.fileId, uploadSection.files[key]) + ); } - } + this.isSaving = false; + this.activeModal.close(); + }); + this.subscriptions.push(saveBitstreamDataSubscription); + } + + private unsubscribeAll() { + this.subscriptions.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe()); } } diff --git a/src/app/submission/sections/upload/file/section-upload-file.component.html b/src/app/submission/sections/upload/file/section-upload-file.component.html index 259418c22c..1bfc52529b 100644 --- a/src/app/submission/sections/upload/file/section-upload-file.component.html +++ b/src/app/submission/sections/upload/file/section-upload-file.component.html @@ -8,15 +8,15 @@

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

-
- +
+ - - - - -
- - +
diff --git a/src/app/submission/sections/upload/file/section-upload-file.component.scss b/src/app/submission/sections/upload/file/section-upload-file.component.scss index 256775eb66..e69de29bb2 100644 --- a/src/app/submission/sections/upload/file/section-upload-file.component.scss +++ b/src/app/submission/sections/upload/file/section-upload-file.component.scss @@ -1,6 +0,0 @@ -.sticky-buttons { - position: sticky; - top: calc(var(--bs-dropdown-item-padding-x) * 3); - z-index: var(--ds-submission-footer-z-index); - background-color: rgba(255, 255, 255, .97); -} diff --git a/src/app/submission/sections/upload/file/section-upload-file.component.spec.ts b/src/app/submission/sections/upload/file/section-upload-file.component.spec.ts index 39aebf7413..4fea8d3f25 100644 --- a/src/app/submission/sections/upload/file/section-upload-file.component.spec.ts +++ b/src/app/submission/sections/upload/file/section-upload-file.component.spec.ts @@ -1,9 +1,9 @@ import { ChangeDetectorRef, Component, NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, fakeAsync, inject, TestBed, tick, waitForAsync } from '@angular/core/testing'; +import { ComponentFixture, inject, TestBed, waitForAsync } from '@angular/core/testing'; import { BrowserModule, By } from '@angular/platform-browser'; import { CommonModule } from '@angular/common'; -import { of as observableOf } from 'rxjs'; +import { of, of as observableOf } from 'rxjs'; import { TranslateModule } from '@ngx-translate/core'; import { FormService } from '../../../../shared/form/form.service'; @@ -17,10 +17,8 @@ import { SubmissionJsonPatchOperationsService } from '../../../../core/submissio import { SubmissionSectionUploadFileComponent } from './section-upload-file.component'; import { SubmissionServiceStub } from '../../../../shared/testing/submission-service.stub'; import { - mockFileFormData, mockSubmissionCollectionId, mockSubmissionId, - mockSubmissionObject, mockUploadConfigResponse, mockUploadFiles } from '../../../../shared/mocks/submission.mock'; @@ -32,10 +30,19 @@ import { FileSizePipe } from '../../../../shared/utils/file-size-pipe'; import { POLICY_DEFAULT_WITH_LIST } from '../section-upload.component'; import { JsonPatchOperationPathCombiner } from '../../../../core/json-patch/builder/json-patch-operation-path-combiner'; import { getMockSectionUploadService } from '../../../../shared/mocks/section-upload.service.mock'; -import { FormFieldMetadataValueObject } from '../../../../shared/form/builder/models/form-field-metadata-value.model'; import { SubmissionSectionUploadFileEditComponent } from './edit/section-upload-file-edit.component'; import { FormBuilderService } from '../../../../shared/form/builder/form-builder.service'; -import { dateToISOFormat } from '../../../../shared/date.util'; + +const configMetadataFormMock = { + rows: [{ + fields: [{ + selectableMetadata: [ + {metadata: 'dc.title', label: null, closed: false}, + {metadata: 'dc.description', label: null, closed: false} + ] + }] + }] +}; describe('SubmissionSectionUploadFileComponent test suite', () => { @@ -117,6 +124,7 @@ describe('SubmissionSectionUploadFileComponent test suite', () => { testFixture = createTestComponent(html, TestComponent) as ComponentFixture; testComp = testFixture.componentInstance; + }); afterEach(() => { @@ -124,9 +132,7 @@ describe('SubmissionSectionUploadFileComponent test suite', () => { }); it('should create SubmissionSectionUploadFileComponent', inject([SubmissionSectionUploadFileComponent], (app: SubmissionSectionUploadFileComponent) => { - expect(app).toBeDefined(); - })); }); @@ -135,6 +141,7 @@ describe('SubmissionSectionUploadFileComponent test suite', () => { fixture = TestBed.createComponent(SubmissionSectionUploadFileComponent); comp = fixture.componentInstance; compAsAny = comp; + compAsAny.configMetadataForm = configMetadataFormMock; submissionServiceStub = TestBed.inject(SubmissionService as any); uploadService = TestBed.inject(SectionUploadService); formService = TestBed.inject(FormService); @@ -210,96 +217,20 @@ describe('SubmissionSectionUploadFileComponent test suite', () => { pathCombiner.subRootElement); }); - it('should save Bitstream File data properly when form is valid', fakeAsync(() => { - compAsAny.fileEditComp = TestBed.inject(SubmissionSectionUploadFileEditComponent); - compAsAny.fileEditComp.formRef = {formGroup: null}; - compAsAny.pathCombiner = pathCombiner; - const event = new Event('click', null); - spyOn(comp, 'switchMode'); - formService.validateAllFormFields.and.callFake(() => null); - formService.isValid.and.returnValue(observableOf(true)); - formService.getFormData.and.returnValue(observableOf(mockFileFormData)); + it('should open edit modal when edit button is clicked', () => { + spyOn(compAsAny, 'editBitstreamData').and.callThrough(); + comp.fileData = fileData; - const response = [ - Object.assign(mockSubmissionObject, { - sections: { - upload: { - files: mockUploadFiles - } - } - }) - ]; - operationsService.jsonPatchByResourceID.and.returnValue(observableOf(response)); + fixture.detectChanges(); - const accessConditionsToSave = [ - { name: 'openaccess' }, - { name: 'lease', endDate: dateToISOFormat('2019-01-16T00:00:00Z') }, - { name: 'embargo', startDate: dateToISOFormat('2019-01-16T00:00:00Z') }, - ]; - comp.saveBitstreamData(event); - tick(); + const modalBtn = fixture.debugElement.query(By.css('.fa-edit ')); - let path = 'metadata/dc.title'; - expect(operationsBuilder.add).toHaveBeenCalledWith( - pathCombiner.getPath(path), - mockFileFormData.metadata['dc.title'], - true - ); + modalBtn.nativeElement.click(); + fixture.detectChanges(); - path = 'metadata/dc.description'; - expect(operationsBuilder.add).toHaveBeenCalledWith( - pathCombiner.getPath(path), - mockFileFormData.metadata['dc.description'], - true - ); - - path = 'accessConditions'; - expect(operationsBuilder.add).toHaveBeenCalledWith( - pathCombiner.getPath(path), - accessConditionsToSave, - true - ); - - expect(comp.switchMode).toHaveBeenCalled(); - expect(uploadService.updateFileData).toHaveBeenCalledWith(submissionId, sectionId, mockUploadFiles[0].uuid, mockUploadFiles[0]); - - })); - - it('should not save Bitstream File data properly when form is not valid', fakeAsync(() => { - compAsAny.fileEditComp = TestBed.inject(SubmissionSectionUploadFileEditComponent); - compAsAny.fileEditComp.formRef = {formGroup: null}; - compAsAny.pathCombiner = pathCombiner; - const event = new Event('click', null); - spyOn(comp, 'switchMode'); - formService.validateAllFormFields.and.callFake(() => null); - formService.isValid.and.returnValue(observableOf(false)); - - expect(comp.switchMode).not.toHaveBeenCalled(); - expect(uploadService.updateFileData).not.toHaveBeenCalled(); - - })); - - it('should retrieve Value From Field properly', () => { - let field; - expect(compAsAny.retrieveValueFromField(field)).toBeUndefined(); - - field = new FormFieldMetadataValueObject('test'); - expect(compAsAny.retrieveValueFromField(field)).toBe('test'); - - field = [new FormFieldMetadataValueObject('test')]; - expect(compAsAny.retrieveValueFromField(field)).toBe('test'); + expect(compAsAny.editBitstreamData).toHaveBeenCalled(); }); - it('should switch read mode', () => { - comp.readMode = false; - - comp.switchMode(); - expect(comp.readMode).toBeTruthy(); - - comp.switchMode(); - - expect(comp.readMode).toBeFalsy(); - }); }); }); @@ -314,7 +245,7 @@ class TestComponent { availableAccessConditionOptions; collectionId = mockSubmissionCollectionId; collectionPolicyType; - configMetadataForm$; + configMetadataForm$ = of(configMetadataFormMock); fileIndexes = []; fileList = []; fileNames = []; diff --git a/src/app/submission/sections/upload/file/section-upload-file.component.ts b/src/app/submission/sections/upload/file/section-upload-file.component.ts index ac6c0d70c4..53358d48e2 100644 --- a/src/app/submission/sections/upload/file/section-upload-file.component.ts +++ b/src/app/submission/sections/upload/file/section-upload-file.component.ts @@ -1,25 +1,23 @@ import { ChangeDetectorRef, Component, Input, OnChanges, OnInit, ViewChild } from '@angular/core'; import { BehaviorSubject, Subscription } from 'rxjs'; -import { filter, mergeMap, take } from 'rxjs/operators'; +import { filter } from 'rxjs/operators'; import { DynamicFormControlModel, } from '@ng-dynamic-forms/core'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { SectionUploadService } from '../section-upload.service'; -import { isNotEmpty, isNotNull, isNotUndefined } from '../../../../shared/empty.util'; +import { hasValue, isNotUndefined } from '../../../../shared/empty.util'; import { FormService } from '../../../../shared/form/form.service'; import { JsonPatchOperationsBuilder } from '../../../../core/json-patch/builder/json-patch-operations-builder'; import { JsonPatchOperationPathCombiner } from '../../../../core/json-patch/builder/json-patch-operation-path-combiner'; import { WorkspaceitemSectionUploadFileObject } from '../../../../core/submission/models/workspaceitem-section-upload-file.model'; import { SubmissionFormsModel } from '../../../../core/config/models/config-submission-forms.model'; -import { dateToISOFormat } from '../../../../shared/date.util'; import { SubmissionService } from '../../../submission.service'; import { HALEndpointService } from '../../../../core/shared/hal-endpoint.service'; import { SubmissionJsonPatchOperationsService } from '../../../../core/submission/submission-json-patch-operations.service'; -import { SubmissionObject } from '../../../../core/submission/models/submission-object.model'; -import { WorkspaceitemSectionUploadObject } from '../../../../core/submission/models/workspaceitem-section-upload.model'; import { SubmissionSectionUploadFileEditComponent } from './edit/section-upload-file-edit.component'; import { Bitstream } from '../../../../core/shared/bitstream.model'; +import { NgbModalOptions } from '@ng-bootstrap/ng-bootstrap/modal/modal-config'; /** * This component represents a single bitstream contained in the submission @@ -87,6 +85,13 @@ export class SubmissionSectionUploadFileComponent implements OnChanges, OnInit { */ @Input() submissionId: string; + /** + * The [[SubmissionSectionUploadFileEditComponent]] reference + * @type {SubmissionSectionUploadFileEditComponent} + */ + @ViewChild(SubmissionSectionUploadFileEditComponent) fileEditComp: SubmissionSectionUploadFileEditComponent; + + /** * The bitstream's metadata data * @type {WorkspaceitemSectionUploadFileObject} @@ -130,10 +135,10 @@ export class SubmissionSectionUploadFileComponent implements OnChanges, OnInit { protected subscriptions: Subscription[] = []; /** - * The [[SubmissionSectionUploadFileEditComponent]] reference - * @type {SubmissionSectionUploadFileEditComponent} + * Array containing all the form metadata defined in configMetadataForm + * @type {Array} */ - @ViewChild(SubmissionSectionUploadFileEditComponent) fileEditComp: SubmissionSectionUploadFileEditComponent; + protected formMetadata: string[] = []; /** * Initialize instance variables @@ -147,14 +152,16 @@ export class SubmissionSectionUploadFileComponent implements OnChanges, OnInit { * @param {SubmissionService} submissionService * @param {SectionUploadService} uploadService */ - constructor(private cdr: ChangeDetectorRef, - private formService: FormService, - private halService: HALEndpointService, - private modalService: NgbModal, - private operationsBuilder: JsonPatchOperationsBuilder, - private operationsService: SubmissionJsonPatchOperationsService, - private submissionService: SubmissionService, - private uploadService: SectionUploadService) { + constructor( + private cdr: ChangeDetectorRef, + private formService: FormService, + private halService: HALEndpointService, + private modalService: NgbModal, + private operationsBuilder: JsonPatchOperationsBuilder, + private operationsService: SubmissionJsonPatchOperationsService, + private submissionService: SubmissionService, + private uploadService: SectionUploadService, + ) { this.readMode = true; } @@ -182,22 +189,7 @@ export class SubmissionSectionUploadFileComponent implements OnChanges, OnInit { ngOnInit() { this.formId = this.formService.getUniqueId(this.fileId); this.pathCombiner = new JsonPatchOperationPathCombiner('sections', this.sectionId, 'files', this.fileIndex); - } - - /** - * Delete bitstream from submission - */ - protected deleteFile() { - this.operationsBuilder.remove(this.pathCombiner.getPath()); - this.subscriptions.push(this.operationsService.jsonPatchByResourceID( - this.submissionService.getSubmissionObjectLinkName(), - this.submissionId, - this.pathCombiner.rootElement, - this.pathCombiner.subRootElement) - .subscribe(() => { - this.uploadService.removeUploadedFile(this.submissionId, this.sectionId, this.fileId); - this.processingDelete$.next(false); - })); + this.loadFormMetadata(); } /** @@ -225,98 +217,63 @@ export class SubmissionSectionUploadFileComponent implements OnChanges, OnInit { }); } - /** - * Save bitstream metadata - * - * @param event - * the click event emitted - */ - public saveBitstreamData(event) { - event.preventDefault(); + editBitstreamData() { - // validate form - this.formService.validateAllFormFields(this.fileEditComp.formRef.formGroup); - this.subscriptions.push(this.formService.isValid(this.formId).pipe( - take(1), - filter((isValid) => isValid), - mergeMap(() => this.formService.getFormData(this.formId)), - take(1), - mergeMap((formData: any) => { - // collect bitstream metadata - Object.keys((formData.metadata)) - .filter((key) => isNotEmpty(formData.metadata[key])) - .forEach((key) => { - const metadataKey = key.replace(/_/g, '.'); - const path = `metadata/${metadataKey}`; - this.operationsBuilder.add(this.pathCombiner.getPath(path), formData.metadata[key], true); + const options: NgbModalOptions = { + size: 'xl', + backdrop: 'static', + }; + + const activeModal = this.modalService.open(SubmissionSectionUploadFileEditComponent, options); + + activeModal.componentInstance.availableAccessConditionOptions = this.availableAccessConditionOptions; + activeModal.componentInstance.collectionId = this.collectionId; + activeModal.componentInstance.collectionPolicyType = this.collectionPolicyType; + activeModal.componentInstance.configMetadataForm = this.configMetadataForm; + activeModal.componentInstance.fileData = this.fileData; + activeModal.componentInstance.fileId = this.fileId; + activeModal.componentInstance.fileIndex = this.fileIndex; + activeModal.componentInstance.formId = this.formId; + activeModal.componentInstance.sectionId = this.sectionId; + activeModal.componentInstance.formMetadata = this.formMetadata; + activeModal.componentInstance.pathCombiner = this.pathCombiner; + activeModal.componentInstance.submissionId = this.submissionId; + + } + + ngOnDestroy(): void { + this.unsubscribeAll(); + } + + unsubscribeAll() { + this.subscriptions.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe()); + } + + protected loadFormMetadata() { + this.configMetadataForm.rows.forEach((row) => { + row.fields.forEach((field) => { + field.selectableMetadata.forEach((metadatum) => { + this.formMetadata.push(metadatum.metadata); }); - const accessConditionsToSave = []; - formData.accessConditions - .map((accessConditions) => accessConditions.accessConditionGroup) - .filter((accessCondition) => isNotEmpty(accessCondition)) - .forEach((accessCondition) => { - let accessConditionOpt; - - this.availableAccessConditionOptions - .filter((element) => isNotNull(accessCondition.name) && element.name === accessCondition.name[0].value) - .forEach((element) => accessConditionOpt = element); - - if (accessConditionOpt) { - accessConditionOpt = Object.assign({}, accessCondition); - accessConditionOpt.name = this.retrieveValueFromField(accessCondition.name); - if (accessCondition.startDate) { - const startDate = this.retrieveValueFromField(accessCondition.startDate); - accessConditionOpt.startDate = dateToISOFormat(startDate); - } - if (accessCondition.endDate) { - const endDate = this.retrieveValueFromField(accessCondition.endDate); - accessConditionOpt.endDate = dateToISOFormat(endDate); - } - accessConditionsToSave.push(accessConditionOpt); - } - }); - - if (isNotEmpty(accessConditionsToSave)) { - this.operationsBuilder.add(this.pathCombiner.getPath('accessConditions'), accessConditionsToSave, true); - } - - // dispatch a PATCH request to save metadata - return this.operationsService.jsonPatchByResourceID( - this.submissionService.getSubmissionObjectLinkName(), - this.submissionId, - this.pathCombiner.rootElement, - this.pathCombiner.subRootElement); - }) - ).subscribe((result: SubmissionObject[]) => { - if (result[0].sections[this.sectionId]) { - const uploadSection = (result[0].sections[this.sectionId] as WorkspaceitemSectionUploadObject); - Object.keys(uploadSection.files) - .filter((key) => uploadSection.files[key].uuid === this.fileId) - .forEach((key) => this.uploadService.updateFileData( - this.submissionId, this.sectionId, this.fileId, uploadSection.files[key]) - ); + }); } - this.switchMode(); - })); + ); } /** - * Retrieve field value - * - * @param field - * the specified field object + * Delete bitstream from submission */ - private retrieveValueFromField(field: any) { - const temp = Array.isArray(field) ? field[0] : field; - return (temp) ? temp.value : undefined; - } - - /** - * Switch from edit form to metadata view - */ - public switchMode() { - this.readMode = !this.readMode; - this.cdr.detectChanges(); + protected deleteFile() { + this.operationsBuilder.remove(this.pathCombiner.getPath()); + this.subscriptions.push(this.operationsService.jsonPatchByResourceID( + this.submissionService.getSubmissionObjectLinkName(), + this.submissionId, + this.pathCombiner.rootElement, + this.pathCombiner.subRootElement) + .subscribe(() => { + this.uploadService.removeUploadedFile(this.submissionId, this.sectionId, this.fileId); + this.processingDelete$.next(false); + })); } }