From df957fc31b04be2b8c8590100382c9e1e2cf1dc6 Mon Sep 17 00:00:00 2001 From: Davide Negretti Date: Fri, 19 Nov 2021 16:22:52 +0100 Subject: [PATCH 01/15] [CST-4878] Remove start and end date when embargo policy is changed to 'open access' --- .../file/section-upload-file.component.ts | 29 ++++++++++++------- 1 file changed, 18 insertions(+), 11 deletions(-) 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..64d229e3a7 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 @@ -262,17 +262,24 @@ export class SubmissionSectionUploadFileComponent implements OnChanges, OnInit { .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); + 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); } }); From 9363b0fb35f3b756f8ad6603e89c178108bf1d7f Mon Sep 17 00:00:00 2001 From: Davide Negretti Date: Fri, 19 Nov 2021 19:58:07 +0100 Subject: [PATCH 02/15] [CST-4947] File description on bitstream can now be deleted (test TBD) --- .../file/section-upload-file.component.ts | 29 ++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) 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 64d229e3a7..9807aecda3 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 @@ -6,7 +6,7 @@ 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 { hasNoValue, isNotEmpty, isNotNull, 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'; @@ -129,6 +129,12 @@ export class SubmissionSectionUploadFileComponent implements OnChanges, OnInit { */ protected subscriptions: Subscription[] = []; + /** + * Array containing all the form metadata defined in configMetadataForm + * @type {Array} + */ + protected formMetadata: string[] = []; + /** * The [[SubmissionSectionUploadFileEditComponent]] reference * @type {SubmissionSectionUploadFileEditComponent} @@ -158,6 +164,17 @@ export class SubmissionSectionUploadFileComponent implements OnChanges, OnInit { this.readMode = true; } + protected loadFormMetadata() { + this.configMetadataForm.rows.forEach((row) => { + row.fields.forEach((field) => { + field.selectableMetadata.forEach((metadatum) => { + this.formMetadata.push(metadatum.metadata); + }); + }); + } + ); + } + /** * Retrieve bitstream's metadata */ @@ -182,6 +199,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); + this.loadFormMetadata(); } /** @@ -250,6 +268,15 @@ export class SubmissionSectionUploadFileComponent implements OnChanges, OnInit { 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) From 25795772257a2ec05e39da2d7f7199481f08e138 Mon Sep 17 00:00:00 2001 From: Davide Negretti Date: Fri, 19 Nov 2021 19:58:07 +0100 Subject: [PATCH 03/15] [CST-4947] Test WIP --- .../section-upload-file.component.spec.ts | 25 ++++++++++++---- .../file/section-upload-file.component.ts | 29 ++++++++++++++++++- 2 files changed, 48 insertions(+), 6 deletions(-) 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..d16f648e68 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 @@ -3,7 +3,7 @@ import { ComponentFixture, fakeAsync, inject, TestBed, tick, waitForAsync } from 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'; @@ -36,8 +36,20 @@ import { FormFieldMetadataValueObject } from '../../../../shared/form/builder/mo import { SubmissionSectionUploadFileEditComponent } from './edit/section-upload-file-edit.component'; import { FormBuilderService } from '../../../../shared/form/builder/form-builder.service'; import { dateToISOFormat } from '../../../../shared/date.util'; +import { SubmissionFormModel } from '../../../../core/config/models/config-submission-form.model'; -describe('SubmissionSectionUploadFileComponent test suite', () => { +const configMetadataFormMock = { + rows: [{ + fields: [{ + selectableMetadata: [ + {metadata: 'dc.title', label: null, closed: false}, + {metadata: 'dc.description', label: null, closed: false} + ] + }] + }] +}; + +fdescribe('SubmissionSectionUploadFileComponent test suite', () => { let comp: SubmissionSectionUploadFileComponent; let compAsAny: any; @@ -117,6 +129,9 @@ describe('SubmissionSectionUploadFileComponent test suite', () => { testFixture = createTestComponent(html, TestComponent) as ComponentFixture; testComp = testFixture.componentInstance; + + // testComp.configMetadataForm = configMetadataFormMock; + // testFixture.detectChanges(); }); afterEach(() => { @@ -124,9 +139,8 @@ describe('SubmissionSectionUploadFileComponent test suite', () => { }); it('should create SubmissionSectionUploadFileComponent', inject([SubmissionSectionUploadFileComponent], (app: SubmissionSectionUploadFileComponent) => { - + app.configMetadataForm = Object.assign(new SubmissionFormModel(), configMetadataFormMock); expect(app).toBeDefined(); - })); }); @@ -135,6 +149,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); @@ -314,7 +329,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 64d229e3a7..9807aecda3 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 @@ -6,7 +6,7 @@ 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 { hasNoValue, isNotEmpty, isNotNull, 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'; @@ -129,6 +129,12 @@ export class SubmissionSectionUploadFileComponent implements OnChanges, OnInit { */ protected subscriptions: Subscription[] = []; + /** + * Array containing all the form metadata defined in configMetadataForm + * @type {Array} + */ + protected formMetadata: string[] = []; + /** * The [[SubmissionSectionUploadFileEditComponent]] reference * @type {SubmissionSectionUploadFileEditComponent} @@ -158,6 +164,17 @@ export class SubmissionSectionUploadFileComponent implements OnChanges, OnInit { this.readMode = true; } + protected loadFormMetadata() { + this.configMetadataForm.rows.forEach((row) => { + row.fields.forEach((field) => { + field.selectableMetadata.forEach((metadatum) => { + this.formMetadata.push(metadatum.metadata); + }); + }); + } + ); + } + /** * Retrieve bitstream's metadata */ @@ -182,6 +199,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); + this.loadFormMetadata(); } /** @@ -250,6 +268,15 @@ export class SubmissionSectionUploadFileComponent implements OnChanges, OnInit { 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) From 5df2f6f8d5c4b4f697bb3bbd0bbe55279d222704 Mon Sep 17 00:00:00 2001 From: Davide Negretti Date: Mon, 22 Nov 2021 11:15:11 +0100 Subject: [PATCH 04/15] [CST-4947] File description on bitstream can now be deleted --- .../upload/file/section-upload-file.component.spec.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) 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 d16f648e68..cf02dadea7 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 @@ -36,7 +36,6 @@ import { FormFieldMetadataValueObject } from '../../../../shared/form/builder/mo import { SubmissionSectionUploadFileEditComponent } from './edit/section-upload-file-edit.component'; import { FormBuilderService } from '../../../../shared/form/builder/form-builder.service'; import { dateToISOFormat } from '../../../../shared/date.util'; -import { SubmissionFormModel } from '../../../../core/config/models/config-submission-form.model'; const configMetadataFormMock = { rows: [{ @@ -49,7 +48,7 @@ const configMetadataFormMock = { }] }; -fdescribe('SubmissionSectionUploadFileComponent test suite', () => { +describe('SubmissionSectionUploadFileComponent test suite', () => { let comp: SubmissionSectionUploadFileComponent; let compAsAny: any; @@ -130,8 +129,6 @@ fdescribe('SubmissionSectionUploadFileComponent test suite', () => { testFixture = createTestComponent(html, TestComponent) as ComponentFixture; testComp = testFixture.componentInstance; - // testComp.configMetadataForm = configMetadataFormMock; - // testFixture.detectChanges(); }); afterEach(() => { @@ -139,7 +136,6 @@ fdescribe('SubmissionSectionUploadFileComponent test suite', () => { }); it('should create SubmissionSectionUploadFileComponent', inject([SubmissionSectionUploadFileComponent], (app: SubmissionSectionUploadFileComponent) => { - app.configMetadataForm = Object.assign(new SubmissionFormModel(), configMetadataFormMock); expect(app).toBeDefined(); })); }); @@ -228,6 +224,7 @@ fdescribe('SubmissionSectionUploadFileComponent test suite', () => { it('should save Bitstream File data properly when form is valid', fakeAsync(() => { compAsAny.fileEditComp = TestBed.inject(SubmissionSectionUploadFileEditComponent); compAsAny.fileEditComp.formRef = {formGroup: null}; + compAsAny.fileData = fileData; compAsAny.pathCombiner = pathCombiner; const event = new Event('click', null); spyOn(comp, 'switchMode'); From 624f39df1e4cadba6358b1bc4e094627bf27f2e8 Mon Sep 17 00:00:00 2001 From: Davide Negretti Date: Mon, 22 Nov 2021 18:14:45 +0100 Subject: [PATCH 05/15] [CST-4884] Bitstream edit form moved inside modal (test TBD) --- .../section-upload-file-edit.component.html | 26 +- .../section-upload-file-edit.component.ts | 355 ++++++++++++------ .../file/section-upload-file.component.html | 37 +- .../file/section-upload-file.component.ts | 231 ++++-------- 4 files changed, 355 insertions(+), 294 deletions(-) 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.ts b/src/app/submission/sections/upload/file/edit/section-upload-file-edit.component.ts index 96725f151e..78bcca876f 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, Input, 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,7 +58,31 @@ 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 { +export class SubmissionSectionUploadFileEditComponent implements OnInit { + + /** + * 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( + protected activeModal: NgbActiveModal, + private cdr: ChangeDetectorRef, + private formBuilderService: FormBuilderService, + private formService: FormService, + private submissionService: SubmissionService, + private operationsBuilder: JsonPatchOperationsBuilder, + private operationsService: SubmissionJsonPatchOperationsService, + private uploadService: SectionUploadService, + ) { + } /** * The list of available access condition @@ -113,10 +147,15 @@ export class SubmissionSectionUploadFileEditComponent implements OnChanges { @Input() submissionId: string; /** - * The form model - * @type {DynamicFormControlModel[]} + * The list of all available metadata */ - public formModel: DynamicFormControlModel[]; + @Input() formMetadata: string[] = []; + + /** + * The [JsonPatchOperationPathCombiner] object + * @type {JsonPatchOperationPathCombiner} + */ + @Input() pathCombiner: JsonPatchOperationPathCombiner; /** * The FormComponent reference @@ -124,108 +163,18 @@ export class SubmissionSectionUploadFileEditComponent implements OnChanges { @ViewChild('formRef') public formRef: FormComponent; /** - * Initialize instance variables - * - * @param {ChangeDetectorRef} cdr - * @param {FormBuilderService} formBuilderService - * @param {FormService} formService - * @param {SubmissionService} submissionService + * The form model + * @type {DynamicFormControlModel[]} */ - constructor(private cdr: ChangeDetectorRef, - private formBuilderService: FormBuilderService, - private formService: FormService, - private submissionService: SubmissionService) { - } + formModel: DynamicFormControlModel[]; - /** - * Dispatch form model init - */ - ngOnChanges() { - if (this.fileData && this.formId) { - this.formModel = this.buildFileEditForm(); - this.cdr.detectChanges(); - } - } + isSaving = false; - /** - * Initialize form model - */ - protected buildFileEditForm() { - const configDescr: FormFieldModel = Object.assign({}, this.configMetadataForm.rows[0].fields[0]); - configDescr.repeatable = false; - const configForm = Object.assign({}, this.configMetadataForm, { - fields: Object.assign([], this.configMetadataForm.rows[0].fields[0], [ - this.configMetadataForm.rows[0].fields[0], - configDescr - ]) - }); - const formModel: DynamicFormControlModel[] = []; - const metadataGroupModelConfig = Object.assign({}, BITSTREAM_METADATA_FORM_GROUP_CONFIG); - metadataGroupModelConfig.group = this.formBuilderService.modelFromConfiguration( - this.submissionId, - configForm, - this.collectionId, - this.fileData.metadata, - this.submissionService.getSubmissionScope() - ); - formModel.push(new DynamicFormGroupModel(metadataGroupModelConfig, BITSTREAM_METADATA_FORM_GROUP_LAYOUT)); - const accessConditionTypeModelConfig = Object.assign({}, BITSTREAM_FORM_ACCESS_CONDITION_TYPE_CONFIG); - const accessConditionsArrayConfig = Object.assign({}, BITSTREAM_ACCESS_CONDITIONS_FORM_ARRAY_CONFIG); - const accessConditionTypeOptions = []; + protected subscriptions: Subscription[] = []; - if (this.collectionPolicyType === POLICY_DEFAULT_WITH_LIST) { - for (const accessCondition of this.availableAccessConditionOptions) { - accessConditionTypeOptions.push( - { - label: accessCondition.name, - value: accessCondition.name - } - ); - } - accessConditionTypeModelConfig.options = accessConditionTypeOptions; - - // Dynamically assign of relation in config. For startdate, endDate, groups. - const hasStart = []; - const hasEnd = []; - const hasGroups = []; - this.availableAccessConditionOptions.forEach((condition) => { - const showStart: boolean = condition.hasStartDate === true; - const showEnd: boolean = condition.hasEndDate === true; - const showGroups: boolean = showStart || showEnd; - if (showStart) { - hasStart.push({ id: 'name', value: condition.name }); - } - if (showEnd) { - hasEnd.push({ id: 'name', value: condition.name }); - } - if (showGroups) { - 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 }] }; - - accessConditionsArrayConfig.groupFactory = () => { - const type = new DynamicSelectModel(accessConditionTypeModelConfig, BITSTREAM_FORM_ACCESS_CONDITION_TYPE_LAYOUT); - const startDateConfig = Object.assign({}, BITSTREAM_FORM_ACCESS_CONDITION_START_DATE_CONFIG, confStart); - const endDateConfig = Object.assign({}, BITSTREAM_FORM_ACCESS_CONDITION_END_DATE_CONFIG, confEnd); - - 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]; - return [new DynamicFormGroupModel(accessConditionGroupConfig, BITSTREAM_ACCESS_CONDITION_GROUP_LAYOUT)]; - }; - - // Number of access conditions blocks in form - accessConditionsArrayConfig.initialCount = isNotEmpty(this.fileData.accessConditions) ? this.fileData.accessConditions.length : 1; - formModel.push( - new DynamicFormArrayModel(accessConditionsArrayConfig, BITSTREAM_ACCESS_CONDITIONS_FORM_ARRAY_LAYOUT) - ); - - } - this.initModelData(formModel); - return formModel; + private static retrieveValueFromField(field: any) { + const temp = Array.isArray(field) ? field[0] : field; + return (temp) ? temp.value : undefined; } /** @@ -262,12 +211,21 @@ export class SubmissionSectionUploadFileEditComponent implements OnChanges { * @param event * The event emitted */ - public onChange(event: DynamicFormControlEvent) { + 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 * @@ -323,4 +281,191 @@ export class SubmissionSectionUploadFileEditComponent implements OnChanges { } } + /** + * Dispatch form model init + */ + ngOnInit() { + if (this.fileData && this.formId) { + this.formModel = this.buildFileEditForm(); + this.cdr.detectChanges(); + } + } + + ngOnDestroy(): void { + this.unsubscribeAll(); + } + + /** + * Initialize form model + */ + protected buildFileEditForm() { + const configDescr: FormFieldModel = Object.assign({}, this.configMetadataForm.rows[0].fields[0]); + configDescr.repeatable = false; + const configForm = Object.assign({}, this.configMetadataForm, { + fields: Object.assign([], this.configMetadataForm.rows[0].fields[0], [ + this.configMetadataForm.rows[0].fields[0], + configDescr + ]) + }); + const formModel: DynamicFormControlModel[] = []; + const metadataGroupModelConfig = Object.assign({}, BITSTREAM_METADATA_FORM_GROUP_CONFIG); + metadataGroupModelConfig.group = this.formBuilderService.modelFromConfiguration( + this.submissionId, + configForm, + this.collectionId, + this.fileData.metadata, + this.submissionService.getSubmissionScope() + ); + formModel.push(new DynamicFormGroupModel(metadataGroupModelConfig, BITSTREAM_METADATA_FORM_GROUP_LAYOUT)); + const accessConditionTypeModelConfig = Object.assign({}, BITSTREAM_FORM_ACCESS_CONDITION_TYPE_CONFIG); + const accessConditionsArrayConfig = Object.assign({}, BITSTREAM_ACCESS_CONDITIONS_FORM_ARRAY_CONFIG); + const accessConditionTypeOptions = []; + + if (this.collectionPolicyType === POLICY_DEFAULT_WITH_LIST) { + for (const accessCondition of this.availableAccessConditionOptions) { + accessConditionTypeOptions.push( + { + label: accessCondition.name, + value: accessCondition.name + } + ); + } + accessConditionTypeModelConfig.options = accessConditionTypeOptions; + + // Dynamically assign of relation in config. For startdate, endDate, groups. + const hasStart = []; + const hasEnd = []; + const hasGroups = []; + this.availableAccessConditionOptions.forEach((condition) => { + const showStart: boolean = condition.hasStartDate === true; + const showEnd: boolean = condition.hasEndDate === true; + const showGroups: boolean = showStart || showEnd; + if (showStart) { + hasStart.push({id: 'name', value: condition.name}); + } + if (showEnd) { + hasEnd.push({id: 'name', value: condition.name}); + } + if (showGroups) { + 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}]}; + + accessConditionsArrayConfig.groupFactory = () => { + const type = new DynamicSelectModel(accessConditionTypeModelConfig, BITSTREAM_FORM_ACCESS_CONDITION_TYPE_LAYOUT); + const startDateConfig = Object.assign({}, BITSTREAM_FORM_ACCESS_CONDITION_START_DATE_CONFIG, confStart); + const endDateConfig = Object.assign({}, BITSTREAM_FORM_ACCESS_CONDITION_END_DATE_CONFIG, confEnd); + + 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]; + return [new DynamicFormGroupModel(accessConditionGroupConfig, BITSTREAM_ACCESS_CONDITION_GROUP_LAYOUT)]; + }; + + // Number of access conditions blocks in form + accessConditionsArrayConfig.initialCount = isNotEmpty(this.fileData.accessConditions) ? this.fileData.accessConditions.length : 1; + formModel.push( + new DynamicFormArrayModel(accessConditionsArrayConfig, BITSTREAM_ACCESS_CONDITIONS_FORM_ARRAY_LAYOUT) + ); + + } + this.initModelData(formModel); + return formModel; + } + + /** + * Save bitstream metadata + */ + protected 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 = SubmissionSectionUploadFileEditComponent.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 = SubmissionSectionUploadFileEditComponent.retrieveValueFromField(accessCondition.startDate); + currentAccessCondition.startDate = dateToISOFormat(startDate); + } + if (!accessConditionOpt.hasEndDate) { + delete currentAccessCondition.endDate; + } else if (accessCondition.endDate) { + const endDate = SubmissionSectionUploadFileEditComponent.retrieveValueFromField(accessCondition.endDate); + currentAccessCondition.endDate = dateToISOFormat(endDate); + } + accessConditionsToSave.push(currentAccessCondition); + } + }); + + 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.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..8749c8dcf2 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 @@ -9,14 +9,14 @@

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

- + - - - - -
- - + 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 9807aecda3..e152d2f651 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 { hasNoValue, 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} @@ -135,12 +140,6 @@ export class SubmissionSectionUploadFileComponent implements OnChanges, OnInit { */ protected formMetadata: string[] = []; - /** - * The [[SubmissionSectionUploadFileEditComponent]] reference - * @type {SubmissionSectionUploadFileEditComponent} - */ - @ViewChild(SubmissionSectionUploadFileEditComponent) fileEditComp: SubmissionSectionUploadFileEditComponent; - /** * Initialize instance variables * @@ -153,28 +152,19 @@ 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; } - protected loadFormMetadata() { - this.configMetadataForm.rows.forEach((row) => { - row.fields.forEach((field) => { - field.selectableMetadata.forEach((metadatum) => { - this.formMetadata.push(metadatum.metadata); - }); - }); - } - ); - } - /** * Retrieve bitstream's metadata */ @@ -202,22 +192,6 @@ export class SubmissionSectionUploadFileComponent implements OnChanges, OnInit { this.loadFormMetadata(); } - /** - * 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); - })); - } - /** * Show confirmation dialog for delete */ @@ -243,108 +217,6 @@ export class SubmissionSectionUploadFileComponent implements OnChanges, OnInit { }); } - /** - * Save bitstream metadata - * - * @param event - * the click event emitted - */ - public saveBitstreamData(event) { - event.preventDefault(); - - // 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); - }); - 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); - } - }); - - 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 - */ - private retrieveValueFromField(field: any) { - const temp = Array.isArray(field) ? field[0] : field; - return (temp) ? temp.value : undefined; - } - /** * Switch from edit form to metadata view */ @@ -353,4 +225,67 @@ export class SubmissionSectionUploadFileComponent implements OnChanges, OnInit { this.cdr.detectChanges(); } + editBitstreamData() { + + 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; + + /*activeModal.componentInstance.saveBitstreamDataEvent.subscribe((res) => { + console.log(JSON.stringify(res)); + });*/ + + } + + 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); + }); + }); + } + ); + } + + /** + * 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); + })); + } + } From 31442f36a3fbe9101de5b03a45f672722f48d48b Mon Sep 17 00:00:00 2001 From: Davide Negretti Date: Mon, 22 Nov 2021 19:11:58 +0100 Subject: [PATCH 06/15] [CST-4884] Bitstream edit form moved inside modal (test WIP) --- ...section-upload-file-edit.component.spec.ts | 43 +++++++++++++++++-- .../section-upload-file-edit.component.ts | 16 +++---- .../section-upload-file.component.spec.ts | 21 +++------ 3 files changed, 52 insertions(+), 28 deletions(-) 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..9203930ef5 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 @@ -30,15 +30,31 @@ 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'; -describe('SubmissionSectionUploadFileEditComponent test suite', () => { +const jsonPatchOpBuilder: any = jasmine.createSpyObj('jsonPatchOpBuilder', { + add: jasmine.createSpy('add'), + replace: jasmine.createSpy('replace'), + remove: jasmine.createSpy('remove'), +}); + +fdescribe('SubmissionSectionUploadFileEditComponent test suite', () => { let comp: SubmissionSectionUploadFileEditComponent; let compAsAny: any; let fixture: ComponentFixture; let submissionServiceStub: SubmissionServiceStub; let formbuilderService: any; + let operationsBuilder: any; + let operationsService: any; + const submissionJsonPatchOperationsServiceStub = new SubmissionJsonPatchOperationsServiceStub(); const submissionId = mockSubmissionId; const sectionId = 'upload'; const collectionId = mockSubmissionCollectionId; @@ -66,9 +82,14 @@ 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, ], schemas: [NO_ERRORS_SCHEMA] }).compileComponents().then(); @@ -114,6 +135,8 @@ describe('SubmissionSectionUploadFileEditComponent test suite', () => { compAsAny = comp; submissionServiceStub = TestBed.inject(SubmissionService as any); formbuilderService = TestBed.inject(FormBuilderService); + operationsBuilder = TestBed.inject(JsonPatchOperationsBuilder); + operationsService = TestBed.inject(SubmissionJsonPatchOperationsService); comp.submissionId = submissionId; comp.collectionId = collectionId; @@ -135,7 +158,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 +188,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 +209,18 @@ 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'); + }); + }); }); 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 78bcca876f..eb8fbf95d0 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 @@ -172,11 +172,6 @@ export class SubmissionSectionUploadFileEditComponent implements OnInit { protected subscriptions: Subscription[] = []; - private static retrieveValueFromField(field: any) { - const temp = Array.isArray(field) ? field[0] : field; - return (temp) ? temp.value : undefined; - } - /** * Initialize form model values * @@ -295,6 +290,11 @@ export class SubmissionSectionUploadFileEditComponent implements OnInit { this.unsubscribeAll(); } + protected retrieveValueFromField(field: any) { + const temp = Array.isArray(field) ? field[0] : field; + return (temp) ? temp.value : undefined; + } + /** * Initialize form model */ @@ -418,20 +418,20 @@ export class SubmissionSectionUploadFileEditComponent implements OnInit { if (accessConditionOpt) { const currentAccessCondition = Object.assign({}, accessCondition); - currentAccessCondition.name = SubmissionSectionUploadFileEditComponent.retrieveValueFromField(accessCondition.name); + 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 = SubmissionSectionUploadFileEditComponent.retrieveValueFromField(accessCondition.startDate); + const startDate = this.retrieveValueFromField(accessCondition.startDate); currentAccessCondition.startDate = dateToISOFormat(startDate); } if (!accessConditionOpt.hasEndDate) { delete currentAccessCondition.endDate; } else if (accessCondition.endDate) { - const endDate = SubmissionSectionUploadFileEditComponent.retrieveValueFromField(accessCondition.endDate); + const endDate = this.retrieveValueFromField(accessCondition.endDate); currentAccessCondition.endDate = dateToISOFormat(endDate); } accessConditionsToSave.push(currentAccessCondition); 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 cf02dadea7..a8ab34d866 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 @@ -48,7 +48,7 @@ const configMetadataFormMock = { }] }; -describe('SubmissionSectionUploadFileComponent test suite', () => { +fdescribe('SubmissionSectionUploadFileComponent test suite', () => { let comp: SubmissionSectionUploadFileComponent; let compAsAny: any; @@ -221,7 +221,7 @@ describe('SubmissionSectionUploadFileComponent test suite', () => { pathCombiner.subRootElement); }); - it('should save Bitstream File data properly when form is valid', fakeAsync(() => { + /*it('should save Bitstream File data properly when form is valid', fakeAsync(() => { compAsAny.fileEditComp = TestBed.inject(SubmissionSectionUploadFileEditComponent); compAsAny.fileEditComp.formRef = {formGroup: null}; compAsAny.fileData = fileData; @@ -275,9 +275,9 @@ describe('SubmissionSectionUploadFileComponent test suite', () => { 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(() => { + /*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; @@ -289,18 +289,7 @@ describe('SubmissionSectionUploadFileComponent test suite', () => { 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'); - }); + }));*/ it('should switch read mode', () => { comp.readMode = false; From 5e8813f5b6c811ceb611eebdcd128dc3d67a4d8b Mon Sep 17 00:00:00 2001 From: Davide Negretti Date: Wed, 24 Nov 2021 13:12:01 +0100 Subject: [PATCH 07/15] [CST-4884] Bitstream edit form moved inside modal (test WIP) --- ...section-upload-file-edit.component.spec.ts | 89 ++++++++- .../section-upload-file-edit.component.ts | 183 +++++++++--------- .../section-upload-file.component.spec.ts | 6 +- 3 files changed, 179 insertions(+), 99 deletions(-) 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 9203930ef5..866277108f 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,18 +17,18 @@ 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'; @@ -37,6 +37,9 @@ import { SubmissionJsonPatchOperationsService } from '../../../../../core/submis 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'), @@ -44,6 +47,8 @@ const jsonPatchOpBuilder: any = jasmine.createSpyObj('jsonPatchOpBuilder', { remove: jasmine.createSpy('remove'), }); +const formMetadataMock = ['dc.title', 'dc.description']; + fdescribe('SubmissionSectionUploadFileEditComponent test suite', () => { let comp: SubmissionSectionUploadFileEditComponent; @@ -53,6 +58,8 @@ fdescribe('SubmissionSectionUploadFileEditComponent test suite', () => { let formbuilderService: any; let operationsBuilder: any; let operationsService: any; + let formService: any; + let uploadService: any; const submissionJsonPatchOperationsServiceStub = new SubmissionJsonPatchOperationsServiceStub(); const submissionId = mockSubmissionId; @@ -64,6 +71,7 @@ fdescribe('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({ @@ -137,6 +145,8 @@ fdescribe('SubmissionSectionUploadFileEditComponent test suite', () => { 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; @@ -146,6 +156,7 @@ fdescribe('SubmissionSectionUploadFileEditComponent test suite', () => { comp.fileIndex = fileIndex; comp.fileId = fileId; comp.configMetadataForm = configMetadataForm; + comp.formMetadata = formMetadataMock; }); afterEach(() => { @@ -221,6 +232,76 @@ fdescribe('SubmissionSectionUploadFileEditComponent test suite', () => { expect(compAsAny.retrieveValueFromField(field)).toBe('test'); }); + it('should save Bitstream File data properly when form is valid', fakeAsync(() => { + compAsAny.fileEditComp = TestBed.inject(SubmissionSectionUploadFileEditComponent); + compAsAny.fileEditComp.formRef = {formGroup: null}; + compAsAny.fileData = fileData; + compAsAny.pathCombiner = pathCombiner; + // const event = new Event('click', null); + // spyOn(comp, 'switchMode'); + 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(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(of(false)); + + // expect(comp.switchMode).not.toHaveBeenCalled(); + 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 eb8fbf95d0..32b82bec45 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, OnInit, ViewChild } from '@angular/core'; +import { ChangeDetectorRef, Component, OnInit, ViewChild } from '@angular/core'; import { FormControl } from '@angular/forms'; import { @@ -60,6 +60,97 @@ import { Subscription } from 'rxjs'; }) 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 * @@ -84,94 +175,6 @@ export class SubmissionSectionUploadFileEditComponent implements OnInit { ) { } - /** - * 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 list of all available metadata - */ - @Input() formMetadata: string[] = []; - - /** - * The [JsonPatchOperationPathCombiner] object - * @type {JsonPatchOperationPathCombiner} - */ - @Input() pathCombiner: JsonPatchOperationPathCombiner; - - /** - * The FormComponent reference - */ - @ViewChild('formRef') public formRef: FormComponent; - - /** - * The form model - * @type {DynamicFormControlModel[]} - */ - formModel: DynamicFormControlModel[]; - - isSaving = false; - - protected subscriptions: Subscription[] = []; - /** * Initialize form model values * @@ -379,7 +382,7 @@ export class SubmissionSectionUploadFileEditComponent implements OnInit { /** * Save bitstream metadata */ - protected saveBitstreamData() { + saveBitstreamData() { // validate form this.formService.validateAllFormFields(this.formRef.formGroup); const saveBitstreamDataSubscription = this.formService.isValid(this.formId).pipe( 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 a8ab34d866..2fd7be96d0 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,5 +1,5 @@ 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'; @@ -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,8 @@ 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: [{ From 6752acbf128e611a9593679ca64aa52b5fe1d88f Mon Sep 17 00:00:00 2001 From: Davide Negretti Date: Wed, 24 Nov 2021 20:00:21 +0100 Subject: [PATCH 08/15] [CST-4884] Test --- ...section-upload-file-edit.component.spec.ts | 19 ++++++++----------- .../section-upload-file.component.spec.ts | 2 +- 2 files changed, 9 insertions(+), 12 deletions(-) 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 866277108f..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 @@ -49,7 +49,7 @@ const jsonPatchOpBuilder: any = jasmine.createSpyObj('jsonPatchOpBuilder', { const formMetadataMock = ['dc.title', 'dc.description']; -fdescribe('SubmissionSectionUploadFileEditComponent test suite', () => { +describe('SubmissionSectionUploadFileEditComponent test suite', () => { let comp: SubmissionSectionUploadFileEditComponent; let compAsAny: any; @@ -98,6 +98,7 @@ fdescribe('SubmissionSectionUploadFileEditComponent test suite', () => { SubmissionSectionUploadFileEditComponent, NgbModal, NgbActiveModal, + FormComponent, ], schemas: [NO_ERRORS_SCHEMA] }).compileComponents().then(); @@ -157,6 +158,8 @@ fdescribe('SubmissionSectionUploadFileEditComponent test suite', () => { comp.fileId = fileId; comp.configMetadataForm = configMetadataForm; comp.formMetadata = formMetadataMock; + + formService.isValid.and.returnValue(of(true)); }); afterEach(() => { @@ -233,12 +236,9 @@ fdescribe('SubmissionSectionUploadFileEditComponent test suite', () => { }); it('should save Bitstream File data properly when form is valid', fakeAsync(() => { - compAsAny.fileEditComp = TestBed.inject(SubmissionSectionUploadFileEditComponent); - compAsAny.fileEditComp.formRef = {formGroup: null}; + compAsAny.formRef = {formGroup: null}; compAsAny.fileData = fileData; compAsAny.pathCombiner = pathCombiner; - // const event = new Event('click', null); - // spyOn(comp, 'switchMode'); formService.validateAllFormFields.and.callFake(() => null); formService.isValid.and.returnValue(of(true)); formService.getFormData.and.returnValue(of(mockFileFormData)); @@ -283,21 +283,18 @@ fdescribe('SubmissionSectionUploadFileEditComponent test suite', () => { 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.formRef = {formGroup: null}; compAsAny.pathCombiner = pathCombiner; - // const event = new Event('click', null); - // spyOn(comp, 'switchMode'); formService.validateAllFormFields.and.callFake(() => null); formService.isValid.and.returnValue(of(false)); + comp.saveBitstreamData(); + tick(); - // expect(comp.switchMode).not.toHaveBeenCalled(); expect(uploadService.updateFileData).not.toHaveBeenCalled(); })); 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 2fd7be96d0..50b245e481 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 @@ -44,7 +44,7 @@ const configMetadataFormMock = { }] }; -fdescribe('SubmissionSectionUploadFileComponent test suite', () => { +describe('SubmissionSectionUploadFileComponent test suite', () => { let comp: SubmissionSectionUploadFileComponent; let compAsAny: any; From 27bce0e5bb550c1d02d7023eb7ffaf72243ca7cf Mon Sep 17 00:00:00 2001 From: Davide Negretti Date: Wed, 24 Nov 2021 20:17:16 +0100 Subject: [PATCH 09/15] [CST-4879] After changing options in access-conditions.xml, "grant access" fields are displayed incorrectly --- .../upload/file/edit/section-upload-file-edit.component.ts | 6 ++++-- .../sections/upload/file/section-upload-file.component.ts | 4 ---- 2 files changed, 4 insertions(+), 6 deletions(-) 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 32b82bec45..fda86dd629 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 @@ -235,7 +235,7 @@ export class SubmissionSectionUploadFileEditComponent implements OnInit { public setOptions(model: DynamicFormControlModel, control: FormControl) { let accessCondition: AccessConditionOption = null; this.availableAccessConditionOptions.filter((element) => element.name === control.value) - .forEach((element) => accessCondition = element); + .forEach((element) => accessCondition = element ); if (isNotEmpty(accessCondition)) { const showGroups: boolean = accessCondition.hasStartDate === true || accessCondition.hasEndDate === true; @@ -364,7 +364,9 @@ export class SubmissionSectionUploadFileEditComponent implements OnInit { 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)]; }; 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 e152d2f651..0841e1ca3a 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 @@ -247,10 +247,6 @@ export class SubmissionSectionUploadFileComponent implements OnChanges, OnInit { activeModal.componentInstance.pathCombiner = this.pathCombiner; activeModal.componentInstance.submissionId = this.submissionId; - /*activeModal.componentInstance.saveBitstreamDataEvent.subscribe((res) => { - console.log(JSON.stringify(res)); - });*/ - } ngOnDestroy(): void { From 7218c450e6993a4b50f6e169a45e41532affe701 Mon Sep 17 00:00:00 2001 From: Davide Negretti Date: Wed, 24 Nov 2021 23:39:59 +0100 Subject: [PATCH 10/15] [CST-4884] Code cleanup and test improvement --- .../file/section-upload-file.component.html | 2 +- .../file/section-upload-file.component.scss | 6 -- .../section-upload-file.component.spec.ts | 84 ++----------------- .../file/section-upload-file.component.ts | 8 -- 4 files changed, 10 insertions(+), 90 deletions(-) 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 8749c8dcf2..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,7 +8,7 @@

{{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 50b245e481..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 @@ -217,86 +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.fileData = fileData; - 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 switch read mode', () => { - comp.readMode = false; - - comp.switchMode(); - expect(comp.readMode).toBeTruthy(); - - comp.switchMode(); - - expect(comp.readMode).toBeFalsy(); + expect(compAsAny.editBitstreamData).toHaveBeenCalled(); }); + }); }); 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 0841e1ca3a..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 @@ -217,14 +217,6 @@ export class SubmissionSectionUploadFileComponent implements OnChanges, OnInit { }); } - /** - * Switch from edit form to metadata view - */ - public switchMode() { - this.readMode = !this.readMode; - this.cdr.detectChanges(); - } - editBitstreamData() { const options: NgbModalOptions = { From a1578303fa6f2939ecb26f3799b21781461a5164 Mon Sep 17 00:00:00 2001 From: lotte Date: Wed, 15 Dec 2021 16:15:06 +0100 Subject: [PATCH 11/15] 85993: Browse by from rest for menu and comcol page browse by --- src/app/navbar/navbar.component.ts | 72 +++++++++++-------- .../comcol-page-browse-by.component.ts | 41 +++++++---- 2 files changed, 69 insertions(+), 44 deletions(-) diff --git a/src/app/navbar/navbar.component.ts b/src/app/navbar/navbar.component.ts index e741cea285..29541c77a0 100644 --- a/src/app/navbar/navbar.component.ts +++ b/src/app/navbar/navbar.component.ts @@ -6,7 +6,11 @@ import { MenuID, MenuItemType } from '../shared/menu/initial-menus-state'; import { TextMenuItemModel } from '../shared/menu/menu-item/models/text.model'; import { LinkMenuItemModel } from '../shared/menu/menu-item/models/link.model'; import { HostWindowService } from '../shared/host-window.service'; -import { environment } from '../../environments/environment'; +import { BrowseService } from '../core/browse/browse.service'; +import { getFirstCompletedRemoteData } from '../core/shared/operators'; +import { PaginatedList } from '../core/data/paginated-list.model'; +import { BrowseDefinition } from '../core/shared/browse-definition.model'; +import { RemoteData } from '../core/data/remote-data'; /** * Component representing the public navbar @@ -26,7 +30,8 @@ export class NavbarComponent extends MenuComponent { constructor(protected menuService: MenuService, protected injector: Injector, - public windowService: HostWindowService + public windowService: HostWindowService, + public browseService: BrowseService ) { super(menuService, injector); } @@ -52,37 +57,44 @@ export class NavbarComponent extends MenuComponent { text: `menu.section.browse_global_communities_and_collections`, link: `/community-list` } as LinkMenuItemModel - }, - /* News */ - { - id: 'browse_global', - active: false, - visible: true, - index: 1, - model: { - type: MenuItemType.TEXT, - text: 'menu.section.browse_global' - } as TextMenuItemModel, - }, + } ]; // Read the different Browse-By types from config and add them to the browse menu - const types = environment.browseBy.types; - types.forEach((typeConfig) => { - menuList.push({ - id: `browse_global_by_${typeConfig.id}`, - parentID: 'browse_global', - active: false, - visible: true, - model: { - type: MenuItemType.LINK, - text: `menu.section.browse_global_by_${typeConfig.id}`, - link: `/browse/${typeConfig.id}` - } as LinkMenuItemModel + this.browseService.getBrowseDefinitions() + .pipe(getFirstCompletedRemoteData>()) + .subscribe((browseDefListRD: RemoteData>) => { + if (browseDefListRD.hasSucceeded) { + browseDefListRD.payload.page.forEach((browseDef: BrowseDefinition) => { + menuList.push({ + id: `browse_global_by_${browseDef.id}`, + parentID: 'browse_global', + active: false, + visible: true, + model: { + type: MenuItemType.LINK, + text: `menu.section.browse_global_by_${browseDef.id}`, + link: `/browse/${browseDef.id}` + } as LinkMenuItemModel + }); + }); + menuList.push( + /* Browse */ + { + id: 'browse_global', + active: false, + visible: true, + index: 1, + model: { + type: MenuItemType.TEXT, + text: 'menu.section.browse_global' + } as TextMenuItemModel, + },) + } + menuList.forEach((menuSection) => this.menuService.addSection(this.menuID, Object.assign(menuSection, { + shouldPersistOnRouteChange: true + }))); }); - }); - menuList.forEach((menuSection) => this.menuService.addSection(this.menuID, Object.assign(menuSection, { - shouldPersistOnRouteChange: true - }))); + } } diff --git a/src/app/shared/comcol-page-browse-by/comcol-page-browse-by.component.ts b/src/app/shared/comcol-page-browse-by/comcol-page-browse-by.component.ts index 01912dbcaa..5dec1e27f8 100644 --- a/src/app/shared/comcol-page-browse-by/comcol-page-browse-by.component.ts +++ b/src/app/shared/comcol-page-browse-by/comcol-page-browse-by.component.ts @@ -6,6 +6,11 @@ import { BrowseByTypeConfig } from '../../../config/browse-by-type-config.interf import { environment } from '../../../environments/environment'; import { getCommunityPageRoute } from '../../community-page/community-page-routing-paths'; import { getCollectionPageRoute } from '../../collection-page/collection-page-routing-paths'; +import { getFirstCompletedRemoteData } from '../../core/shared/operators'; +import { PaginatedList } from '../../core/data/paginated-list.model'; +import { BrowseDefinition } from '../../core/shared/browse-definition.model'; +import { RemoteData } from '../../core/data/remote-data'; +import { BrowseService } from '../../core/browse/browse.service'; export interface ComColPageNavOption { id: string; @@ -40,30 +45,38 @@ export class ComcolPageBrowseByComponent implements OnInit { constructor( private route: ActivatedRoute, - private router: Router) { + private router: Router, + public browseService: BrowseService + ) { } ngOnInit(): void { - this.allOptions = environment.browseBy.types - .map((config: BrowseByTypeConfig) => ({ - id: config.id, - label: `browse.comcol.by.${config.id}`, - routerLink: `/browse/${config.id}`, - params: { scope: this.id } - })); + this.browseService.getBrowseDefinitions() + .pipe(getFirstCompletedRemoteData>()) + .subscribe((browseDefListRD: RemoteData>) => { + if (browseDefListRD.hasSucceeded) { + this.allOptions = browseDefListRD.payload.page + .map((config: BrowseDefinition) => ({ + id: config.id, + label: `browse.comcol.by.${config.id}`, + routerLink: `/browse/${config.id}`, + params: { scope: this.id } + })); + } + }); if (this.contentType === 'collection') { - this.allOptions = [ { + this.allOptions = [{ id: this.id, label: 'collection.page.browse.recent.head', routerLink: getCollectionPageRoute(this.id) - }, ...this.allOptions ]; + }, ...this.allOptions]; } else if (this.contentType === 'community') { this.allOptions = [{ - id: this.id, - label: 'community.all-lists.head', - routerLink: getCommunityPageRoute(this.id) - }, ...this.allOptions ]; + id: this.id, + label: 'community.all-lists.head', + routerLink: getCommunityPageRoute(this.id) + }, ...this.allOptions]; } this.currentOptionId$ = this.route.params.pipe( From d246965cfb20c60b78b9d3b9ec5b0117875e833a Mon Sep 17 00:00:00 2001 From: Davide Negretti Date: Tue, 21 Dec 2021 11:17:12 +0100 Subject: [PATCH 12/15] [CST-4879] TypeError fixed --- .../file/edit/section-upload-file-edit.component.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 fda86dd629..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 @@ -243,12 +243,12 @@ export class SubmissionSectionUploadFileEditComponent implements OnInit { const endDateControl: FormControl = control.parent.get('endDate') as FormControl; // Clear previous state - startDateControl.markAsUntouched(); - endDateControl.markAsUntouched(); + startDateControl?.markAsUntouched(); + endDateControl?.markAsUntouched(); - startDateControl.setValue(null); + startDateControl?.setValue(null); control.parent.markAsDirty(); - endDateControl.setValue(null); + endDateControl?.setValue(null); if (showGroups) { if (accessCondition.hasStartDate) { From 15dfa3cd82304f4611b866501c18ed9b22abac3c Mon Sep 17 00:00:00 2001 From: lotte Date: Tue, 21 Dec 2021 15:38:50 +0100 Subject: [PATCH 13/15] 85993: comcol/switcher/guard browse by changes + test updates --- .../browse-by-date-page.component.ts | 15 ++--- src/app/browse-by/browse-by-guard.spec.ts | 26 ++++++--- src/app/browse-by/browse-by-guard.ts | 57 +++++++++++-------- .../browse-by-metadata-page.component.ts | 4 +- .../browse-by-decorator.spec.ts | 8 +-- .../browse-by-switcher/browse-by-decorator.ts | 8 +-- .../browse-by-switcher.component.spec.ts | 50 ++++++++++++---- .../browse-by-switcher.component.ts | 12 ++-- .../browse-by-title-page.component.ts | 4 +- .../browse/browse-definition-data.service.ts | 15 +++++ .../core/shared/browse-definition.model.ts | 4 ++ src/app/navbar/navbar.component.spec.ts | 38 ++++++++++++- .../comcol-page-browse-by.component.ts | 36 +++++------- src/config/browse-by-config.interface.ts | 6 -- src/config/browse-by-type-config.interface.ts | 23 -------- src/config/global-config.interface.ts | 2 +- src/environments/environment.common.ts | 27 --------- src/environments/mock-environment.ts | 28 +-------- 18 files changed, 184 insertions(+), 179 deletions(-) delete mode 100644 src/config/browse-by-type-config.interface.ts diff --git a/src/app/browse-by/browse-by-date-page/browse-by-date-page.component.ts b/src/app/browse-by/browse-by-date-page/browse-by-date-page.component.ts index 3158c3d7cc..d0b63a00f9 100644 --- a/src/app/browse-by/browse-by-date-page/browse-by-date-page.component.ts +++ b/src/app/browse-by/browse-by-date-page/browse-by-date-page.component.ts @@ -12,12 +12,13 @@ import { ActivatedRoute, Params, Router } from '@angular/router'; import { BrowseService } from '../../core/browse/browse.service'; import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service'; import { StartsWithType } from '../../shared/starts-with/starts-with-decorator'; -import { BrowseByType, rendersBrowseBy } from '../browse-by-switcher/browse-by-decorator'; +import { BrowseByDataType, rendersBrowseBy } from '../browse-by-switcher/browse-by-decorator'; import { environment } from '../../../environments/environment'; import { PaginationService } from '../../core/pagination/pagination.service'; import { map } from 'rxjs/operators'; import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model'; +import { BrowseDefinition } from '../../core/shared/browse-definition.model'; @Component({ selector: 'ds-browse-by-date-page', @@ -29,13 +30,13 @@ import { SortDirection, SortOptions } from '../../core/cache/models/sort-options * A metadata definition (a.k.a. browse id) is a short term used to describe one or multiple metadata fields. * An example would be 'dateissued' for 'dc.date.issued' */ -@rendersBrowseBy(BrowseByType.Date) +@rendersBrowseBy(BrowseByDataType.Date) export class BrowseByDatePageComponent extends BrowseByMetadataPageComponent { /** * The default metadata-field to use for determining the lower limit of the StartsWith dropdown options */ - defaultMetadataField = 'dc.date.issued'; + defaultBrowseDefinition = Object.assign(new BrowseDefinition(), {metadataKeys: ['dc.date.issued']}); public constructor(protected route: ActivatedRoute, protected browseService: BrowseService, @@ -59,13 +60,13 @@ export class BrowseByDatePageComponent extends BrowseByMetadataPageComponent { return [Object.assign({}, routeParams, queryParams, data), currentPage, currentSort]; }) ).subscribe(([params, currentPage, currentSort]: [Params, PaginationComponentOptions, SortOptions]) => { - const metadataField = params.metadataField || this.defaultMetadataField; + const browseDefinition = params.browseDefinition || this.defaultBrowseDefinition; this.browseId = params.id || this.defaultBrowseId; this.startsWith = +params.startsWith || params.startsWith; const searchOptions = browseParamsToOptions(params, currentPage, currentSort, this.browseId); this.updatePageWithItems(searchOptions, this.value); this.updateParent(params.scope); - this.updateStartsWithOptions(this.browseId, metadataField, params.scope); + this.updateStartsWithOptions(this.browseId, browseDefinition, params.scope); })); } @@ -79,12 +80,12 @@ export class BrowseByDatePageComponent extends BrowseByMetadataPageComponent { * @param metadataField The metadata field to fetch the earliest date from (expects a date field) * @param scope The scope under which to fetch the earliest item for */ - updateStartsWithOptions(definition: string, metadataField: string, scope?: string) { + updateStartsWithOptions(definition: string, browseDefinition: BrowseDefinition, scope?: string) { this.subs.push( this.browseService.getFirstItemFor(definition, scope).subscribe((firstItemRD: RemoteData) => { let lowerLimit = environment.browseBy.defaultLowerLimit; if (hasValue(firstItemRD.payload)) { - const date = firstItemRD.payload.firstMetadataValue(metadataField); + const date = firstItemRD.payload.firstMetadataValue(browseDefinition.metadataKeys); if (hasValue(date)) { const dateObj = new Date(date); // TODO: it appears that getFullYear (based on local time) is sometimes unreliable. Switching to UTC. diff --git a/src/app/browse-by/browse-by-guard.spec.ts b/src/app/browse-by/browse-by-guard.spec.ts index 4592f47175..fc483d87e2 100644 --- a/src/app/browse-by/browse-by-guard.spec.ts +++ b/src/app/browse-by/browse-by-guard.spec.ts @@ -1,20 +1,25 @@ import { first } from 'rxjs/operators'; import { BrowseByGuard } from './browse-by-guard'; import { of as observableOf } from 'rxjs'; +import { BrowseDefinitionDataService } from '../core/browse/browse-definition-data.service'; +import { createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils'; +import { BrowseDefinition } from '../core/shared/browse-definition.model'; +import { BrowseByDataType } from './browse-by-switcher/browse-by-decorator'; describe('BrowseByGuard', () => { describe('canActivate', () => { let guard: BrowseByGuard; let dsoService: any; let translateService: any; + let browseDefinitionService: any; const name = 'An interesting DSO'; const title = 'Author'; const field = 'Author'; const id = 'author'; - const metadataField = 'dc.contributor'; const scope = '1234-65487-12354-1235'; const value = 'Filter'; + const browseDefinition = Object.assign(new BrowseDefinition(), { type: BrowseByDataType.Metadata, metadataKeys: ['dc.contributor'] }); beforeEach(() => { dsoService = { @@ -24,14 +29,19 @@ describe('BrowseByGuard', () => { translateService = { instant: () => field }; - guard = new BrowseByGuard(dsoService, translateService); + + browseDefinitionService = { + findById: () => createSuccessfulRemoteDataObject$(browseDefinition) + }; + + guard = new BrowseByGuard(dsoService, translateService, browseDefinitionService); }); it('should return true, and sets up the data correctly, with a scope and value', () => { const scopedRoute = { data: { title: field, - metadataField, + browseDefinition, }, params: { id, @@ -48,7 +58,7 @@ describe('BrowseByGuard', () => { const result = { title, id, - metadataField, + browseDefinition, collection: name, field, value: '"' + value + '"' @@ -63,7 +73,7 @@ describe('BrowseByGuard', () => { const scopedNoValueRoute = { data: { title: field, - metadataField, + browseDefinition, }, params: { id, @@ -80,7 +90,7 @@ describe('BrowseByGuard', () => { const result = { title, id, - metadataField, + browseDefinition, collection: name, field, value: '' @@ -95,7 +105,7 @@ describe('BrowseByGuard', () => { const route = { data: { title: field, - metadataField, + browseDefinition, }, params: { id, @@ -111,7 +121,7 @@ describe('BrowseByGuard', () => { const result = { title, id, - metadataField, + browseDefinition, collection: '', field, value: '"' + value + '"' diff --git a/src/app/browse-by/browse-by-guard.ts b/src/app/browse-by/browse-by-guard.ts index 8ac77bbd64..0a46d59613 100644 --- a/src/app/browse-by/browse-by-guard.ts +++ b/src/app/browse-by/browse-by-guard.ts @@ -2,11 +2,14 @@ import { ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot } from '@angul import { Injectable } from '@angular/core'; import { DSpaceObjectDataService } from '../core/data/dspace-object-data.service'; import { hasNoValue, hasValue } from '../shared/empty.util'; -import { map } from 'rxjs/operators'; -import { getFirstSucceededRemoteData } from '../core/shared/operators'; +import { map, switchMap } from 'rxjs/operators'; +import { getFirstSucceededRemoteData, getFirstSucceededRemoteDataPayload } from '../core/shared/operators'; import { TranslateService } from '@ngx-translate/core'; -import { of as observableOf } from 'rxjs'; +import { Observable, of as observableOf } from 'rxjs'; import { environment } from '../../environments/environment'; +import { BrowseDefinitionDataService } from '../core/browse/browse-definition-data.service'; +import { RemoteData } from '../core/data/remote-data'; +import { BrowseDefinition } from '../core/shared/browse-definition.model'; @Injectable() /** @@ -15,42 +18,46 @@ import { environment } from '../../environments/environment'; export class BrowseByGuard implements CanActivate { constructor(protected dsoService: DSpaceObjectDataService, - protected translate: TranslateService) { + protected translate: TranslateService, + protected browseDefinitionService: BrowseDefinitionDataService) { } canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) { const title = route.data.title; const id = route.params.id || route.queryParams.id || route.data.id; - let metadataField = route.data.metadataField; - if (hasNoValue(metadataField) && hasValue(id)) { - const config = environment.browseBy.types.find((conf) => conf.id === id); - if (hasValue(config) && hasValue(config.metadataField)) { - metadataField = config.metadataField; - } + let browseDefinition$: Observable; + if (hasNoValue(route.data.browseDefinition) && hasValue(id)) { + browseDefinition$ = this.browseDefinitionService.findById(id).pipe(getFirstSucceededRemoteDataPayload()); + } else { + browseDefinition$ = observableOf(route.data.browseDefinition); } const scope = route.queryParams.scope; const value = route.queryParams.value; const metadataTranslated = this.translate.instant('browse.metadata.' + id); - if (hasValue(scope)) { - const dsoAndMetadata$ = this.dsoService.findById(scope).pipe(getFirstSucceededRemoteData()); - return dsoAndMetadata$.pipe( - map((dsoRD) => { - const name = dsoRD.payload.name; - route.data = this.createData(title, id, metadataField, name, metadataTranslated, value, route); - return true; - }) - ); - } else { - route.data = this.createData(title, id, metadataField, '', metadataTranslated, value, route); - return observableOf(true); - } + return browseDefinition$.pipe( + switchMap((browseDefinition) => { + if (hasValue(scope)) { + const dsoAndMetadata$ = this.dsoService.findById(scope).pipe(getFirstSucceededRemoteData()); + return dsoAndMetadata$.pipe( + map((dsoRD) => { + const name = dsoRD.payload.name; + route.data = this.createData(title, id, browseDefinition, name, metadataTranslated, value, route); + return true; + }) + ); + } else { + route.data = this.createData(title, id, browseDefinition, '', metadataTranslated, value, route); + return observableOf(true); + } + }) + ) } - private createData(title, id, metadataField, collection, field, value, route) { + private createData(title, id, browseDefinition, collection, field, value, route) { return Object.assign({}, route.data, { title: title, id: id, - metadataField: metadataField, + browseDefinition: browseDefinition, collection: collection, field: field, value: hasValue(value) ? `"${value}"` : '' diff --git a/src/app/browse-by/browse-by-metadata-page/browse-by-metadata-page.component.ts b/src/app/browse-by/browse-by-metadata-page/browse-by-metadata-page.component.ts index 3573ffb264..6655f98392 100644 --- a/src/app/browse-by/browse-by-metadata-page/browse-by-metadata-page.component.ts +++ b/src/app/browse-by/browse-by-metadata-page/browse-by-metadata-page.component.ts @@ -14,7 +14,7 @@ import { getFirstSucceededRemoteData } from '../../core/shared/operators'; import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service'; import { DSpaceObject } from '../../core/shared/dspace-object.model'; import { StartsWithType } from '../../shared/starts-with/starts-with-decorator'; -import { BrowseByType, rendersBrowseBy } from '../browse-by-switcher/browse-by-decorator'; +import { BrowseByDataType, rendersBrowseBy } from '../browse-by-switcher/browse-by-decorator'; import { PaginationService } from '../../core/pagination/pagination.service'; import { map } from 'rxjs/operators'; @@ -28,7 +28,7 @@ import { map } from 'rxjs/operators'; * A metadata definition (a.k.a. browse id) is a short term used to describe one or multiple metadata fields. * An example would be 'author' for 'dc.contributor.*' */ -@rendersBrowseBy(BrowseByType.Metadata) +@rendersBrowseBy(BrowseByDataType.Metadata) export class BrowseByMetadataPageComponent implements OnInit { /** diff --git a/src/app/browse-by/browse-by-switcher/browse-by-decorator.spec.ts b/src/app/browse-by/browse-by-switcher/browse-by-decorator.spec.ts index f54efb9378..19a6277151 100644 --- a/src/app/browse-by/browse-by-switcher/browse-by-decorator.spec.ts +++ b/src/app/browse-by/browse-by-switcher/browse-by-decorator.spec.ts @@ -1,9 +1,9 @@ -import { BrowseByType, rendersBrowseBy } from './browse-by-decorator'; +import { BrowseByDataType, rendersBrowseBy } from './browse-by-decorator'; describe('BrowseByDecorator', () => { - const titleDecorator = rendersBrowseBy(BrowseByType.Title); - const dateDecorator = rendersBrowseBy(BrowseByType.Date); - const metadataDecorator = rendersBrowseBy(BrowseByType.Metadata); + const titleDecorator = rendersBrowseBy(BrowseByDataType.Title); + const dateDecorator = rendersBrowseBy(BrowseByDataType.Date); + const metadataDecorator = rendersBrowseBy(BrowseByDataType.Metadata); it('should have a decorator for all types', () => { expect(titleDecorator.length).not.toEqual(0); expect(dateDecorator.length).not.toEqual(0); diff --git a/src/app/browse-by/browse-by-switcher/browse-by-decorator.ts b/src/app/browse-by/browse-by-switcher/browse-by-decorator.ts index efb4a4a9f4..1ebaa7face 100644 --- a/src/app/browse-by/browse-by-switcher/browse-by-decorator.ts +++ b/src/app/browse-by/browse-by-switcher/browse-by-decorator.ts @@ -2,13 +2,13 @@ import { hasNoValue } from '../../shared/empty.util'; import { InjectionToken } from '@angular/core'; import { GenericConstructor } from '../../core/shared/generic-constructor'; -export enum BrowseByType { +export enum BrowseByDataType { Title = 'title', - Metadata = 'metadata', + Metadata = 'text', Date = 'date' } -export const DEFAULT_BROWSE_BY_TYPE = BrowseByType.Metadata; +export const DEFAULT_BROWSE_BY_TYPE = BrowseByDataType.Metadata; export const BROWSE_BY_COMPONENT_FACTORY = new InjectionToken<(browseByType) => GenericConstructor>('getComponentByBrowseByType', { providedIn: 'root', @@ -21,7 +21,7 @@ const map = new Map(); * Decorator used for rendering Browse-By pages by type * @param browseByType The type of page */ -export function rendersBrowseBy(browseByType: BrowseByType) { +export function rendersBrowseBy(browseByType: BrowseByDataType) { return function decorator(component: any) { if (hasNoValue(map.get(browseByType))) { map.set(browseByType, component); diff --git a/src/app/browse-by/browse-by-switcher/browse-by-switcher.component.spec.ts b/src/app/browse-by/browse-by-switcher/browse-by-switcher.component.spec.ts index f340237e26..cb82ddb7c4 100644 --- a/src/app/browse-by/browse-by-switcher/browse-by-switcher.component.spec.ts +++ b/src/app/browse-by/browse-by-switcher/browse-by-switcher.component.spec.ts @@ -2,20 +2,46 @@ import { BrowseBySwitcherComponent } from './browse-by-switcher.component'; import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { NO_ERRORS_SCHEMA } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; -import { BehaviorSubject } from 'rxjs'; -import { environment } from '../../../environments/environment'; -import { BROWSE_BY_COMPONENT_FACTORY } from './browse-by-decorator'; +import { BROWSE_BY_COMPONENT_FACTORY, BrowseByDataType } from './browse-by-decorator'; +import { BrowseDefinition } from '../../core/shared/browse-definition.model'; +import { BehaviorSubject, of as observableOf } from 'rxjs'; describe('BrowseBySwitcherComponent', () => { let comp: BrowseBySwitcherComponent; let fixture: ComponentFixture; - const types = environment.browseBy.types; + const types = [ + Object.assign( + new BrowseDefinition(), { + id: 'title', + dataType: BrowseByDataType.Title, + } + ), + Object.assign( + new BrowseDefinition(), { + id: 'dateissued', + dataType: BrowseByDataType.Date, + metadataKeys: ['dc.date.issued'] + } + ), + Object.assign( + new BrowseDefinition(), { + id: 'author', + dataType: BrowseByDataType.Metadata, + } + ), + Object.assign( + new BrowseDefinition(), { + id: 'subject', + dataType: BrowseByDataType.Metadata, + } + ), + ]; - const params = new BehaviorSubject(createParamsWithId('initialValue')); + const data = new BehaviorSubject(createDataWithBrowseDefinition(new BrowseDefinition())); const activatedRouteStub = { - params: params + data }; beforeEach(waitForAsync(() => { @@ -34,20 +60,20 @@ describe('BrowseBySwitcherComponent', () => { comp = fixture.componentInstance; })); - types.forEach((type) => { + types.forEach((type: BrowseDefinition) => { describe(`when switching to a browse-by page for "${type.id}"`, () => { beforeEach(() => { - params.next(createParamsWithId(type.id)); + data.next(createDataWithBrowseDefinition(type)); fixture.detectChanges(); }); - it(`should call getComponentByBrowseByType with type "${type.type}"`, () => { - expect((comp as any).getComponentByBrowseByType).toHaveBeenCalledWith(type.type); + it(`should call getComponentByBrowseByType with type "${type.dataType}"`, () => { + expect((comp as any).getComponentByBrowseByType).toHaveBeenCalledWith(type.dataType); }); }); }); }); -export function createParamsWithId(id) { - return { id: id }; +export function createDataWithBrowseDefinition(browseDefinition) { + return { browseDefinition: browseDefinition }; } diff --git a/src/app/browse-by/browse-by-switcher/browse-by-switcher.component.ts b/src/app/browse-by/browse-by-switcher/browse-by-switcher.component.ts index 043a4ce90a..55fcebf477 100644 --- a/src/app/browse-by/browse-by-switcher/browse-by-switcher.component.ts +++ b/src/app/browse-by/browse-by-switcher/browse-by-switcher.component.ts @@ -1,11 +1,11 @@ import { Component, Inject, OnInit } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { Observable } from 'rxjs'; -import { BrowseByTypeConfig } from '../../../config/browse-by-type-config.interface'; import { map } from 'rxjs/operators'; import { BROWSE_BY_COMPONENT_FACTORY } from './browse-by-decorator'; import { environment } from '../../../environments/environment'; import { GenericConstructor } from '../../core/shared/generic-constructor'; +import { BrowseDefinition } from '../../core/shared/browse-definition.model'; @Component({ selector: 'ds-browse-by-switcher', @@ -26,15 +26,11 @@ export class BrowseBySwitcherComponent implements OnInit { } /** - * Fetch the correct browse-by component by using the relevant config from environment.js + * Fetch the correct browse-by component by using the relevant config from the route data */ ngOnInit(): void { - this.browseByComponent = this.route.params.pipe( - map((params) => { - const id = params.id; - return environment.browseBy.types.find((config: BrowseByTypeConfig) => config.id === id); - }), - map((config: BrowseByTypeConfig) => this.getComponentByBrowseByType(config.type)) + this.browseByComponent = this.route.data.pipe( + map((data: { browseDefinition: BrowseDefinition }) => this.getComponentByBrowseByType(data.browseDefinition.dataType)) ); } diff --git a/src/app/browse-by/browse-by-title-page/browse-by-title-page.component.ts b/src/app/browse-by/browse-by-title-page/browse-by-title-page.component.ts index b3a2ceed00..b4a8331458 100644 --- a/src/app/browse-by/browse-by-title-page/browse-by-title-page.component.ts +++ b/src/app/browse-by/browse-by-title-page/browse-by-title-page.component.ts @@ -10,7 +10,7 @@ import { BrowseEntrySearchOptions } from '../../core/browse/browse-entry-search- import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service'; import { BrowseService } from '../../core/browse/browse.service'; import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model'; -import { BrowseByType, rendersBrowseBy } from '../browse-by-switcher/browse-by-decorator'; +import { BrowseByDataType, rendersBrowseBy } from '../browse-by-switcher/browse-by-decorator'; import { PaginationService } from '../../core/pagination/pagination.service'; import { map } from 'rxjs/operators'; import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; @@ -23,7 +23,7 @@ import { PaginationComponentOptions } from '../../shared/pagination/pagination-c /** * Component for browsing items by title (dc.title) */ -@rendersBrowseBy(BrowseByType.Title) +@rendersBrowseBy(BrowseByDataType.Title) export class BrowseByTitlePageComponent extends BrowseByMetadataPageComponent { public constructor(protected route: ActivatedRoute, diff --git a/src/app/core/browse/browse-definition-data.service.ts b/src/app/core/browse/browse-definition-data.service.ts index 31338417ca..dd66d8fa53 100644 --- a/src/app/core/browse/browse-definition-data.service.ts +++ b/src/app/core/browse/browse-definition-data.service.ts @@ -106,6 +106,21 @@ export class BrowseDefinitionDataService { findAllByHref(href: string, findListOptions: FindListOptions = {}, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig[]): Observable>> { return this.dataService.findAllByHref(href, findListOptions, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow); } + + /** + * Returns an observable of {@link RemoteData} of an object, based on its ID, with a list of + * {@link FollowLinkConfig}, to automatically resolve {@link HALLink}s of the object + * @param id ID of object we want to retrieve + * @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 + * @param linksToFollow List of {@link FollowLinkConfig} that indicate which + * {@link HALLink}s should be automatically resolved + */ + findById(id: string, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig[]): Observable> { + return this.dataService.findById(id, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow); + } } /* tslint:enable:max-classes-per-file */ diff --git a/src/app/core/shared/browse-definition.model.ts b/src/app/core/shared/browse-definition.model.ts index 2c08417b6d..b5dc47cd3e 100644 --- a/src/app/core/shared/browse-definition.model.ts +++ b/src/app/core/shared/browse-definition.model.ts @@ -6,6 +6,7 @@ import { BROWSE_DEFINITION } from './browse-definition.resource-type'; import { HALLink } from './hal-link.model'; import { ResourceType } from './resource-type'; import { SortOption } from './sort-option.model'; +import { BrowseByDataType } from '../../browse-by/browse-by-switcher/browse-by-decorator'; @typedObject export class BrowseDefinition extends CacheableObject { @@ -33,6 +34,9 @@ export class BrowseDefinition extends CacheableObject { @autoserializeAs('metadata') metadataKeys: string[]; + @autoserializeAs('dataType') + dataType: BrowseByDataType; + get self(): string { return this._links.self.href; } diff --git a/src/app/navbar/navbar.component.spec.ts b/src/app/navbar/navbar.component.spec.ts index cbe6738241..5aa2bf1786 100644 --- a/src/app/navbar/navbar.component.spec.ts +++ b/src/app/navbar/navbar.component.spec.ts @@ -13,15 +13,48 @@ import { MenuService } from '../shared/menu/menu.service'; import { MenuServiceStub } from '../shared/testing/menu-service.stub'; import { ActivatedRoute } from '@angular/router'; import { RouterTestingModule } from '@angular/router/testing'; +import { BrowseService } from '../core/browse/browse.service'; +import { createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils'; +import { buildPaginatedList } from '../core/data/paginated-list.model'; +import { BrowseDefinition } from '../core/shared/browse-definition.model'; +import { BrowseByDataType } from '../browse-by/browse-by-switcher/browse-by-decorator'; let comp: NavbarComponent; let fixture: ComponentFixture; describe('NavbarComponent', () => { const menuService = new MenuServiceStub(); - + let browseDefinitions; // waitForAsync beforeEach beforeEach(waitForAsync(() => { + browseDefinitions = [ + Object.assign( + new BrowseDefinition(), { + id: 'title', + dataType: BrowseByDataType.Title, + } + ), + Object.assign( + new BrowseDefinition(), { + id: 'dateissued', + dataType: BrowseByDataType.Date, + metadataKeys: ['dc.date.issued'] + } + ), + Object.assign( + new BrowseDefinition(), { + id: 'author', + dataType: BrowseByDataType.Metadata, + } + ), + Object.assign( + new BrowseDefinition(), { + id: 'subject', + dataType: BrowseByDataType.Metadata, + } + ), + ]; + TestBed.configureTestingModule({ imports: [ TranslateModule.forRoot(), @@ -33,7 +66,8 @@ describe('NavbarComponent', () => { Injector, { provide: MenuService, useValue: menuService }, { provide: HostWindowService, useValue: new HostWindowServiceStub(800) }, - { provide: ActivatedRoute, useValue: {} } + { provide: ActivatedRoute, useValue: {} }, + { provide: BrowseService, useValue: { getBrowseDefinitions: createSuccessfulRemoteDataObject$(buildPaginatedList(undefined, browseDefinitions)) } } ], schemas: [NO_ERRORS_SCHEMA] }) diff --git a/src/app/shared/comcol-page-browse-by/comcol-page-browse-by.component.ts b/src/app/shared/comcol-page-browse-by/comcol-page-browse-by.component.ts index 5dec1e27f8..08f7ec67ee 100644 --- a/src/app/shared/comcol-page-browse-by/comcol-page-browse-by.component.ts +++ b/src/app/shared/comcol-page-browse-by/comcol-page-browse-by.component.ts @@ -2,8 +2,6 @@ import { Component, Input, OnInit } from '@angular/core'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; import { ActivatedRoute, Params, Router } from '@angular/router'; -import { BrowseByTypeConfig } from '../../../config/browse-by-type-config.interface'; -import { environment } from '../../../environments/environment'; import { getCommunityPageRoute } from '../../community-page/community-page-routing-paths'; import { getCollectionPageRoute } from '../../collection-page/collection-page-routing-paths'; import { getFirstCompletedRemoteData } from '../../core/shared/operators'; @@ -34,10 +32,6 @@ export class ComcolPageBrowseByComponent implements OnInit { */ @Input() id: string; @Input() contentType: string; - /** - * List of currently active browse configurations - */ - types: BrowseByTypeConfig[]; allOptions: ComColPageNavOption[]; @@ -46,7 +40,7 @@ export class ComcolPageBrowseByComponent implements OnInit { constructor( private route: ActivatedRoute, private router: Router, - public browseService: BrowseService + private browseService: BrowseService ) { } @@ -62,23 +56,23 @@ export class ComcolPageBrowseByComponent implements OnInit { routerLink: `/browse/${config.id}`, params: { scope: this.id } })); + + if (this.contentType === 'collection') { + this.allOptions = [{ + id: this.id, + label: 'collection.page.browse.recent.head', + routerLink: getCollectionPageRoute(this.id) + }, ...this.allOptions]; + } else if (this.contentType === 'community') { + this.allOptions = [{ + id: this.id, + label: 'community.all-lists.head', + routerLink: getCommunityPageRoute(this.id) + }, ...this.allOptions]; + } } }); - if (this.contentType === 'collection') { - this.allOptions = [{ - id: this.id, - label: 'collection.page.browse.recent.head', - routerLink: getCollectionPageRoute(this.id) - }, ...this.allOptions]; - } else if (this.contentType === 'community') { - this.allOptions = [{ - id: this.id, - label: 'community.all-lists.head', - routerLink: getCommunityPageRoute(this.id) - }, ...this.allOptions]; - } - this.currentOptionId$ = this.route.params.pipe( map((params: Params) => params.id) ); diff --git a/src/config/browse-by-config.interface.ts b/src/config/browse-by-config.interface.ts index 719e127b4b..6adba66b92 100644 --- a/src/config/browse-by-config.interface.ts +++ b/src/config/browse-by-config.interface.ts @@ -1,5 +1,4 @@ import { Config } from './config.interface'; -import { BrowseByTypeConfig } from './browse-by-type-config.interface'; /** * Config that determines how the dropdown list of years are created for browse-by-date components @@ -19,9 +18,4 @@ export interface BrowseByConfig extends Config { * The absolute lowest year to display in the dropdown when no lowest date can be found for all items */ defaultLowerLimit: number; - - /** - * A list of all the active Browse-By pages - */ - types: BrowseByTypeConfig[]; } diff --git a/src/config/browse-by-type-config.interface.ts b/src/config/browse-by-type-config.interface.ts deleted file mode 100644 index f15846c210..0000000000 --- a/src/config/browse-by-type-config.interface.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { Config } from './config.interface'; -import { BrowseByType } from '../app/browse-by/browse-by-switcher/browse-by-decorator'; - -/** - * Config used for rendering Browse-By pages and links - */ -export interface BrowseByTypeConfig extends Config { - /** - * The browse id used for fetching browse data from the rest api - * e.g. author - */ - id: string; - - /** - * The type of Browse-By page to render - */ - type: BrowseByType | string; - - /** - * The metadata field to use for rendering starts-with options (only necessary when type is set to BrowseByType.Date) - */ - metadataField?: string; -} diff --git a/src/config/global-config.interface.ts b/src/config/global-config.interface.ts index d46822eb61..f8c226bdb1 100644 --- a/src/config/global-config.interface.ts +++ b/src/config/global-config.interface.ts @@ -6,13 +6,13 @@ import { INotificationBoardOptions } from './notifications-config.interfaces'; import { SubmissionConfig } from './submission-config.interface'; import { FormConfig } from './form-config.interfaces'; import { LangConfig } from './lang-config.interface'; -import { BrowseByConfig } from './browse-by-config.interface'; import { ItemPageConfig } from './item-page-config.interface'; import { CollectionPageConfig } from './collection-page-config.interface'; import { ThemeConfig } from './theme.model'; import { AuthConfig } from './auth-config.interfaces'; import { UIServerConfig } from './ui-server-config.interface'; import { MediaViewerConfig } from './media-viewer-config.interface'; +import { BrowseByConfig } from './browse-by-config.interface'; export interface GlobalConfig extends Config { ui: UIServerConfig; diff --git a/src/environments/environment.common.ts b/src/environments/environment.common.ts index b1cbd699a3..1441accf04 100644 --- a/src/environments/environment.common.ts +++ b/src/environments/environment.common.ts @@ -1,6 +1,5 @@ import { GlobalConfig } from '../config/global-config.interface'; import { NotificationAnimationsType } from '../app/shared/notifications/models/notification-animations-type'; -import { BrowseByType } from '../app/browse-by/browse-by-switcher/browse-by-decorator'; import { RestRequestMethod } from '../app/core/data/rest-request-method'; export const environment: GlobalConfig = { @@ -207,32 +206,6 @@ export const environment: GlobalConfig = { fiveYearLimit: 30, // The absolute lowest year to display in the dropdown (only used when no lowest date can be found for all items) defaultLowerLimit: 1900, - // List of all the active Browse-By types - // Adding a type will activate their Browse-By page and add them to the global navigation menu, - // as well as community and collection pages - // Allowed fields and their purpose: - // id: The browse id to use for fetching info from the rest api - // type: The type of Browse-By page to display - // metadataField: The metadata-field used to create starts-with options (only necessary when the type is set to 'date') - types: [ - { - id: 'title', - type: BrowseByType.Title, - }, - { - id: 'dateissued', - type: BrowseByType.Date, - metadataField: 'dc.date.issued' - }, - { - id: 'author', - type: BrowseByType.Metadata - }, - { - id: 'subject', - type: BrowseByType.Metadata - } - ] }, item: { edit: { diff --git a/src/environments/mock-environment.ts b/src/environments/mock-environment.ts index 824c8c8a83..f21de7fae3 100644 --- a/src/environments/mock-environment.ts +++ b/src/environments/mock-environment.ts @@ -1,5 +1,5 @@ // This configuration is only used for unit tests, end-to-end tests use environment.prod.ts -import { BrowseByType } from '../app/browse-by/browse-by-switcher/browse-by-decorator'; +import { BrowseByDataType } from '../app/browse-by/browse-by-switcher/browse-by-decorator'; import { RestRequestMethod } from '../app/core/data/rest-request-method'; import { NotificationAnimationsType } from '../app/shared/notifications/models/notification-animations-type'; import { GlobalConfig } from '../config/global-config.interface'; @@ -169,32 +169,6 @@ export const environment: Partial = { fiveYearLimit: 30, // The absolute lowest year to display in the dropdown (only used when no lowest date can be found for all items) defaultLowerLimit: 1900, - // List of all the active Browse-By types - // Adding a type will activate their Browse-By page and add them to the global navigation menu, - // as well as community and collection pages - // Allowed fields and their purpose: - // id: The browse id to use for fetching info from the rest api - // type: The type of Browse-By page to display - // metadataField: The metadata-field used to create starts-with options (only necessary when the type is set to 'date') - types: [ - { - id: 'title', - type: BrowseByType.Title, - }, - { - id: 'dateissued', - type: BrowseByType.Date, - metadataField: 'dc.date.issued' - }, - { - id: 'author', - type: BrowseByType.Metadata - }, - { - id: 'subject', - type: BrowseByType.Metadata - } - ] }, item: { edit: { From 0e6c3a3a9dd99b976671b372e5bac8ae6e3286fe Mon Sep 17 00:00:00 2001 From: lotte Date: Tue, 21 Dec 2021 16:48:31 +0100 Subject: [PATCH 14/15] 85993: small changes and lint fixes --- .../browse-by-date-page.component.ts | 20 +++++++++---------- src/app/browse-by/browse-by-guard.ts | 2 +- .../core/shared/browse-definition.model.ts | 2 +- src/app/navbar/navbar.component.ts | 4 ++-- 4 files changed, 13 insertions(+), 15 deletions(-) diff --git a/src/app/browse-by/browse-by-date-page/browse-by-date-page.component.ts b/src/app/browse-by/browse-by-date-page/browse-by-date-page.component.ts index d0b63a00f9..e977b52ad6 100644 --- a/src/app/browse-by/browse-by-date-page/browse-by-date-page.component.ts +++ b/src/app/browse-by/browse-by-date-page/browse-by-date-page.component.ts @@ -18,7 +18,6 @@ import { PaginationService } from '../../core/pagination/pagination.service'; import { map } from 'rxjs/operators'; import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model'; -import { BrowseDefinition } from '../../core/shared/browse-definition.model'; @Component({ selector: 'ds-browse-by-date-page', @@ -34,9 +33,9 @@ import { BrowseDefinition } from '../../core/shared/browse-definition.model'; export class BrowseByDatePageComponent extends BrowseByMetadataPageComponent { /** - * The default metadata-field to use for determining the lower limit of the StartsWith dropdown options + * The default metadata keys to use for determining the lower limit of the StartsWith dropdown options */ - defaultBrowseDefinition = Object.assign(new BrowseDefinition(), {metadataKeys: ['dc.date.issued']}); + defaultMetadataKeys = ['dc.date.issued']; public constructor(protected route: ActivatedRoute, protected browseService: BrowseService, @@ -60,13 +59,13 @@ export class BrowseByDatePageComponent extends BrowseByMetadataPageComponent { return [Object.assign({}, routeParams, queryParams, data), currentPage, currentSort]; }) ).subscribe(([params, currentPage, currentSort]: [Params, PaginationComponentOptions, SortOptions]) => { - const browseDefinition = params.browseDefinition || this.defaultBrowseDefinition; - this.browseId = params.id || this.defaultBrowseId; - this.startsWith = +params.startsWith || params.startsWith; + const metadataKeys = params.browseDefinition ? params.browseDefinition.metadataKeys : this.defaultMetadataKeys; + this.browseId = params.id || this.defaultBrowseId; + this.startsWith = +params.startsWith || params.startsWith; const searchOptions = browseParamsToOptions(params, currentPage, currentSort, this.browseId); this.updatePageWithItems(searchOptions, this.value); this.updateParent(params.scope); - this.updateStartsWithOptions(this.browseId, browseDefinition, params.scope); + this.updateStartsWithOptions(this.browseId, metadataKeys, params.scope); })); } @@ -77,15 +76,15 @@ export class BrowseByDatePageComponent extends BrowseByMetadataPageComponent { * extremely long lists with a one-year difference. * To determine the change in years, the config found under GlobalConfig.BrowseBy is used for this. * @param definition The metadata definition to fetch the first item for - * @param metadataField The metadata field to fetch the earliest date from (expects a date field) + * @param metadataKeys The metadata fields to fetch the earliest date from (expects a date field) * @param scope The scope under which to fetch the earliest item for */ - updateStartsWithOptions(definition: string, browseDefinition: BrowseDefinition, scope?: string) { + updateStartsWithOptions(definition: string, metadataKeys: string[], scope?: string) { this.subs.push( this.browseService.getFirstItemFor(definition, scope).subscribe((firstItemRD: RemoteData) => { let lowerLimit = environment.browseBy.defaultLowerLimit; if (hasValue(firstItemRD.payload)) { - const date = firstItemRD.payload.firstMetadataValue(browseDefinition.metadataKeys); + const date = firstItemRD.payload.firstMetadataValue(metadataKeys); if (hasValue(date)) { const dateObj = new Date(date); // TODO: it appears that getFullYear (based on local time) is sometimes unreliable. Switching to UTC. @@ -121,5 +120,4 @@ export class BrowseByDatePageComponent extends BrowseByMetadataPageComponent { }) ); } - } diff --git a/src/app/browse-by/browse-by-guard.ts b/src/app/browse-by/browse-by-guard.ts index 0a46d59613..689d33f0ce 100644 --- a/src/app/browse-by/browse-by-guard.ts +++ b/src/app/browse-by/browse-by-guard.ts @@ -50,7 +50,7 @@ export class BrowseByGuard implements CanActivate { return observableOf(true); } }) - ) + ); } private createData(title, id, browseDefinition, collection, field, value, route) { diff --git a/src/app/core/shared/browse-definition.model.ts b/src/app/core/shared/browse-definition.model.ts index b5dc47cd3e..68406f3f7d 100644 --- a/src/app/core/shared/browse-definition.model.ts +++ b/src/app/core/shared/browse-definition.model.ts @@ -34,7 +34,7 @@ export class BrowseDefinition extends CacheableObject { @autoserializeAs('metadata') metadataKeys: string[]; - @autoserializeAs('dataType') + @autoserialize dataType: BrowseByDataType; get self(): string { diff --git a/src/app/navbar/navbar.component.ts b/src/app/navbar/navbar.component.ts index 29541c77a0..c3b34d12ee 100644 --- a/src/app/navbar/navbar.component.ts +++ b/src/app/navbar/navbar.component.ts @@ -88,13 +88,13 @@ export class NavbarComponent extends MenuComponent { type: MenuItemType.TEXT, text: 'menu.section.browse_global' } as TextMenuItemModel, - },) + } + ); } menuList.forEach((menuSection) => this.menuService.addSection(this.menuID, Object.assign(menuSection, { shouldPersistOnRouteChange: true }))); }); - } } From 39e0f1a65b71b7bd82a52b5da427009909206cdd Mon Sep 17 00:00:00 2001 From: lotte Date: Wed, 22 Dec 2021 07:58:13 +0100 Subject: [PATCH 15/15] Added test and fixed lgtm errors --- src/app/browse-by/browse-by-guard.ts | 2 -- .../browse-by-switcher/browse-by-switcher.component.ts | 1 - .../core/browse/browse-definition-data.service.spec.ts | 8 ++++++++ src/environments/mock-environment.ts | 1 - 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/app/browse-by/browse-by-guard.ts b/src/app/browse-by/browse-by-guard.ts index 689d33f0ce..e4582cb77a 100644 --- a/src/app/browse-by/browse-by-guard.ts +++ b/src/app/browse-by/browse-by-guard.ts @@ -6,9 +6,7 @@ import { map, switchMap } from 'rxjs/operators'; import { getFirstSucceededRemoteData, getFirstSucceededRemoteDataPayload } from '../core/shared/operators'; import { TranslateService } from '@ngx-translate/core'; import { Observable, of as observableOf } from 'rxjs'; -import { environment } from '../../environments/environment'; import { BrowseDefinitionDataService } from '../core/browse/browse-definition-data.service'; -import { RemoteData } from '../core/data/remote-data'; import { BrowseDefinition } from '../core/shared/browse-definition.model'; @Injectable() diff --git a/src/app/browse-by/browse-by-switcher/browse-by-switcher.component.ts b/src/app/browse-by/browse-by-switcher/browse-by-switcher.component.ts index 55fcebf477..cf4c1d9856 100644 --- a/src/app/browse-by/browse-by-switcher/browse-by-switcher.component.ts +++ b/src/app/browse-by/browse-by-switcher/browse-by-switcher.component.ts @@ -3,7 +3,6 @@ import { ActivatedRoute } from '@angular/router'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; import { BROWSE_BY_COMPONENT_FACTORY } from './browse-by-decorator'; -import { environment } from '../../../environments/environment'; import { GenericConstructor } from '../../core/shared/generic-constructor'; import { BrowseDefinition } from '../../core/shared/browse-definition.model'; diff --git a/src/app/core/browse/browse-definition-data.service.spec.ts b/src/app/core/browse/browse-definition-data.service.spec.ts index 1127748ca9..d6770f80c0 100644 --- a/src/app/core/browse/browse-definition-data.service.spec.ts +++ b/src/app/core/browse/browse-definition-data.service.spec.ts @@ -9,9 +9,11 @@ describe(`BrowseDefinitionDataService`, () => { findAll: EMPTY, findByHref: EMPTY, findAllByHref: EMPTY, + findById: EMPTY, }); const hrefAll = 'https://rest.api/server/api/discover/browses'; const hrefSingle = 'https://rest.api/server/api/discover/browses/author'; + const id = 'author'; const options = new FindListOptions(); const linksToFollow = [ followLink('entries'), @@ -44,4 +46,10 @@ describe(`BrowseDefinitionDataService`, () => { }); }); + describe(`findById`, () => { + it(`should call findById on DataServiceImpl`, () => { + service.findAllByHref(id, options, true, false, ...linksToFollow); + expect(dataServiceImplSpy.findAllByHref).toHaveBeenCalledWith(id, options, true, false, ...linksToFollow); + }); + }); }); diff --git a/src/environments/mock-environment.ts b/src/environments/mock-environment.ts index f21de7fae3..02c12d9992 100644 --- a/src/environments/mock-environment.ts +++ b/src/environments/mock-environment.ts @@ -1,5 +1,4 @@ // This configuration is only used for unit tests, end-to-end tests use environment.prod.ts -import { BrowseByDataType } from '../app/browse-by/browse-by-switcher/browse-by-decorator'; import { RestRequestMethod } from '../app/core/data/rest-request-method'; import { NotificationAnimationsType } from '../app/shared/notifications/models/notification-animations-type'; import { GlobalConfig } from '../config/global-config.interface';