Merge pull request #1201 from 4Science/#1110

Fixes for submission forms bugs
This commit is contained in:
Tim Donohue
2021-06-25 14:44:13 -05:00
committed by GitHub
12 changed files with 275 additions and 114 deletions

View File

@@ -9,7 +9,7 @@ import {
import { JsonPatchOperationPathObject } from './json-patch-operation-path-combiner';
import { Injectable } from '@angular/core';
import { hasNoValue, hasValue, isEmpty, isNotEmpty } from '../../../shared/empty.util';
import { dateToISOFormat } from '../../../shared/date.util';
import { dateToISOFormat, dateToString, isNgbDateStruct } from '../../../shared/date.util';
import { VocabularyEntry } from '../../submission/vocabularies/models/vocabulary-entry.model';
import { FormFieldMetadataValueObject } from '../../../shared/form/builder/models/form-field-metadata-value.model';
import { FormFieldLanguageValueObject } from '../../../shared/form/builder/models/form-field-language-value.model';
@@ -136,6 +136,8 @@ export class JsonPatchOperationsBuilder {
operationValue = new FormFieldMetadataValueObject(value.value, value.language);
} else if (value.hasOwnProperty('authority')) {
operationValue = new FormFieldMetadataValueObject(value.value, value.language, value.authority);
} else if (isNgbDateStruct(value)) {
operationValue = new FormFieldMetadataValueObject(dateToString(value));
} else if (value.hasOwnProperty('value')) {
operationValue = new FormFieldMetadataValueObject(value.value);
} else {

View File

@@ -3,7 +3,7 @@ import { NgbDateStruct } from '@ng-bootstrap/ng-bootstrap';
import { isObject } from 'lodash';
import * as moment from 'moment';
import { isNull } from './empty.util';
import { isNull, isUndefined } from './empty.util';
/**
* Returns true if the passed value is a NgbDateStruct.
@@ -27,8 +27,9 @@ export function isNgbDateStruct(value: object): boolean {
* @return string
* the formatted date
*/
export function dateToISOFormat(date: Date | NgbDateStruct): string {
const dateObj: Date = (date instanceof Date) ? date : ngbDateStructToDate(date);
export function dateToISOFormat(date: Date | NgbDateStruct | string): string {
const dateObj: Date = (date instanceof Date) ? date :
((typeof date === 'string') ? ngbDateStructToDate(stringToNgbDateStruct(date)) : ngbDateStructToDate(date));
let year = dateObj.getFullYear().toString();
let month = (dateObj.getMonth() + 1).toString();
@@ -80,7 +81,7 @@ export function stringToNgbDateStruct(date: string): NgbDateStruct {
* the NgbDateStruct object
*/
export function dateToNgbDateStruct(date?: Date): NgbDateStruct {
if (isNull(date)) {
if (isNull(date) || isUndefined(date)) {
date = new Date();
}

View File

@@ -19,6 +19,7 @@
[startDate]="model.focusedDate"
(blur)="onBlur($event)"
(dateSelect)="onChange($event)"
(change)="onChange($event)"
(focus)="onFocus($event)">
<div class="input-group-append">

View File

@@ -309,9 +309,16 @@ export class FormComponent implements OnDestroy, OnInit {
removeItem($event, arrayContext: DynamicFormArrayModel, index: number): void {
const formArrayControl = this.formGroup.get(this.formBuilderService.getPath(arrayContext)) as FormArray;
const event = this.getEvent($event, arrayContext, index, 'remove');
if (this.formBuilderService.isQualdropGroup(event.model as DynamicFormControlModel)) {
// In case of qualdrop value remove event must be dispatched before removing the control from array
this.removeArrayItem.emit(event);
}
this.formBuilderService.removeFormArrayGroup(index, formArrayControl, arrayContext);
this.formService.changeForm(this.formId, this.formModel);
this.removeArrayItem.emit(event);
if (!this.formBuilderService.isQualdropGroup(event.model as DynamicFormControlModel)) {
// dispatch remove event for any field type except for qualdrop value
this.removeArrayItem.emit(event);
}
}
insertItem($event, arrayContext: DynamicFormArrayModel, index: number): void {

View File

@@ -1519,83 +1519,87 @@ export const mockFileFormData = {
},
accessConditions: [
{
name: [
{
value: 'openaccess',
language: null,
authority: null,
display: 'openaccess',
confidence: -1,
place: 0,
otherInformation: null
}
],
}
,
accessConditionGroup: {
name: [
{
value: 'openaccess',
language: null,
authority: null,
display: 'openaccess',
confidence: -1,
place: 0,
otherInformation: null
}
],
},
},
{
name: [
{
value: 'lease',
language: null,
authority: null,
display: 'lease',
confidence: -1,
place: 0,
otherInformation: null
}
],
endDate: [
{
value: {
year: 2019,
month: 1,
day: 16
},
language: null,
authority: null,
display: {
year: 2019,
month: 1,
day: 16
},
confidence: -1,
place: 0,
otherInformation: null
}
],
}
,
accessConditionGroup:{
name: [
{
value: 'lease',
language: null,
authority: null,
display: 'lease',
confidence: -1,
place: 0,
otherInformation: null
}
],
endDate: [
{
value: {
year: 2019,
month: 1,
day: 16
},
language: null,
authority: null,
display: {
year: 2019,
month: 1,
day: 16
},
confidence: -1,
place: 0,
otherInformation: null
}
],
}
},
{
name: [
{
value: 'embargo',
language: null,
authority: null,
display: 'lease',
confidence: -1,
place: 0,
otherInformation: null
}
],
startDate: [
{
value: {
year: 2019,
month: 1,
day: 16
},
language: null,
authority: null,
display: {
year: 2019,
month: 1,
day: 16
},
confidence: -1,
place: 0,
otherInformation: null
}
],
accessConditionGroup: {
name: [
{
value: 'embargo',
language: null,
authority: null,
display: 'lease',
confidence: -1,
place: 0,
otherInformation: null
}
],
startDate: [
{
value: {
year: 2019,
month: 1,
day: 16
},
language: null,
authority: null,
display: {
year: 2019,
month: 1,
day: 16
},
confidence: -1,
place: 0,
otherInformation: null
}
],
}
}
]
};

View File

@@ -122,7 +122,7 @@ export class SubmissionFormComponent implements OnChanges, OnDestroy {
* Initialize all instance variables and retrieve form configuration
*/
ngOnChanges(changes: SimpleChanges) {
if (this.collectionId && this.submissionId) {
if ((changes.collectionId && this.collectionId) && (changes.submissionId && this.submissionId)) {
this.isActive = true;
// retrieve submission's section list

View File

@@ -4,7 +4,8 @@ import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
import {
DYNAMIC_FORM_CONTROL_TYPE_ARRAY,
DYNAMIC_FORM_CONTROL_TYPE_GROUP,
DynamicFormControlEvent
DynamicFormControlEvent,
DynamicInputModel
} from '@ng-dynamic-forms/core';
import { FormBuilderService } from '../../../shared/form/builder/form-builder.service';
@@ -28,6 +29,7 @@ import {
} from '../../../shared/mocks/form-models.mock';
import { FormFieldMetadataValueObject } from '../../../shared/form/builder/models/form-field-metadata-value.model';
import { VocabularyEntry } from '../../../core/submission/vocabularies/models/vocabulary-entry.model';
import { DynamicRowArrayModel } from '../../../shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-row-array-model';
describe('SectionFormOperationsService test suite', () => {
let formBuilderService: any;
@@ -83,6 +85,11 @@ describe('SectionFormOperationsService test suite', () => {
formBuilderService = TestBed.inject(FormBuilderService);
});
afterEach(() => {
jsonPatchOpBuilder.add.calls.reset();
jsonPatchOpBuilder.remove.calls.reset();
});
describe('dispatchOperationsFromEvent', () => {
it('should call dispatchOperationsFromRemoveEvent on remove event', () => {
const previousValue = new FormFieldPreviousValueObject(['path', 'test'], 'value');
@@ -567,7 +574,7 @@ describe('SectionFormOperationsService test suite', () => {
});
it('should dispatch a json-path remove operation when has a stored value', () => {
const previousValue = new FormFieldPreviousValueObject(['path', 'test'], 'value');
let previousValue = new FormFieldPreviousValueObject(['path', 'test'], 'value');
const event = Object.assign({}, dynamicFormControlChangeEvent, {
model: {
parent: mockRowGroupModel
@@ -590,6 +597,7 @@ describe('SectionFormOperationsService test suite', () => {
spyIndex.and.returnValue(1);
spyPath.and.returnValue('path/1');
previousValue = new FormFieldPreviousValueObject(['path', 'test'], 'value');
serviceAsAny.dispatchOperationsFromChangeEvent(pathCombiner, event, previousValue, true);
expect(jsonPatchOpBuilder.remove).toHaveBeenCalledWith(pathCombiner.getPath('path/1'));
@@ -620,6 +628,32 @@ describe('SectionFormOperationsService test suite', () => {
new FormFieldMetadataValueObject('test'));
});
it('should dispatch a json-path add operation when has a stored value but previous value is empty', () => {
const previousValue = new FormFieldPreviousValueObject(['path', 'test'], null);
const event = Object.assign({}, dynamicFormControlChangeEvent, {
model: {
parent: mockRowGroupModel
}
});
spyOn(service, 'getFieldPathFromEvent').and.returnValue('path/0');
spyOn(service, 'getFieldPathSegmentedFromChangeEvent').and.returnValue('path');
spyOn(service, 'getFieldValueFromChangeEvent').and.returnValue(new FormFieldMetadataValueObject('test'));
spyOn(service, 'getArrayIndexFromEvent').and.returnValue(0);
spyOn(serviceAsAny, 'getValueMap');
spyOn(serviceAsAny, 'dispatchOperationsFromMap');
formBuilderService.isQualdropGroup.and.returnValue(false);
formBuilderService.isRelationGroup.and.returnValue(false);
formBuilderService.hasArrayGroupValue.and.returnValue(false);
spyOn(previousValue, 'isPathEqual').and.returnValue(false);
serviceAsAny.dispatchOperationsFromChangeEvent(pathCombiner, event, previousValue, true);
expect(jsonPatchOpBuilder.add).toHaveBeenCalledWith(
pathCombiner.getPath('path'),
new FormFieldMetadataValueObject('test'),
true);
});
it('should dispatch a json-path add operation when has a value and field index is zero or undefined', () => {
const previousValue = new FormFieldPreviousValueObject(['path', 'test'], 'value');
const event = Object.assign({}, dynamicFormControlChangeEvent, {
@@ -760,4 +794,86 @@ describe('SectionFormOperationsService test suite', () => {
});
});
describe('handleArrayGroupPatch', () => {
let arrayModel;
let previousValue;
beforeEach(() => {
arrayModel = new DynamicRowArrayModel(
{
id: 'testFormRowArray',
initialCount: 5,
notRepeatable: false,
relationshipConfig: undefined,
submissionId: '1234',
isDraggable: true,
groupFactory: () => {
return [
new DynamicInputModel({ id: 'testFormRowArrayGroupInput' })
];
},
required: false,
metadataKey: 'dc.contributor.author',
metadataFields: ['dc.contributor.author'],
hasSelectableMetadata: true
}
);
spyOn(serviceAsAny, 'getFieldPathSegmentedFromChangeEvent').and.returnValue('path');
previousValue = new FormFieldPreviousValueObject(['path'], null);
});
it('should not dispatch a json-path operation when a array value is empty', () => {
formBuilderService.getValueFromModel.and.returnValue({});
spyOn(previousValue, 'isPathEqual').and.returnValue(false);
serviceAsAny.handleArrayGroupPatch(
pathCombiner,
dynamicFormControlChangeEvent,
arrayModel,
previousValue
);
expect(jsonPatchOpBuilder.add).not.toHaveBeenCalled();
expect(jsonPatchOpBuilder.remove).not.toHaveBeenCalled();
});
it('should dispatch a json-path add operation when a array value is not empty', () => {
const pathValue = [
new FormFieldMetadataValueObject('test'),
new FormFieldMetadataValueObject('test two')
];
formBuilderService.getValueFromModel.and.returnValue({
path:pathValue
});
spyOn(previousValue, 'isPathEqual').and.returnValue(false);
serviceAsAny.handleArrayGroupPatch(
pathCombiner,
dynamicFormControlChangeEvent,
arrayModel,
previousValue
);
expect(jsonPatchOpBuilder.add).toHaveBeenCalledWith(
pathCombiner.getPath('path'),
pathValue,
false
);
expect(jsonPatchOpBuilder.remove).not.toHaveBeenCalled();
});
it('should dispatch a json-path remove operation when a array value is empty and has previous value', () => {
formBuilderService.getValueFromModel.and.returnValue({});
spyOn(previousValue, 'isPathEqual').and.returnValue(true);
serviceAsAny.handleArrayGroupPatch(
pathCombiner,
dynamicFormControlChangeEvent,
arrayModel,
previousValue
);
expect(jsonPatchOpBuilder.add).not.toHaveBeenCalled();
expect(jsonPatchOpBuilder.remove).toHaveBeenCalledWith(pathCombiner.getPath('path'));
});
});
});

View File

@@ -6,7 +6,8 @@ import {
DYNAMIC_FORM_CONTROL_TYPE_GROUP,
DynamicFormArrayGroupModel,
DynamicFormControlEvent,
DynamicFormControlModel, isDynamicFormControlEvent
DynamicFormControlModel,
isDynamicFormControlEvent
} from '@ng-dynamic-forms/core';
import { hasValue, isNotEmpty, isNotNull, isNotUndefined, isNull, isUndefined } from '../../../shared/empty.util';
@@ -297,17 +298,14 @@ export class SectionFormOperationsService {
event: DynamicFormControlEvent,
previousValue: FormFieldPreviousValueObject): void {
if (event.context && event.context instanceof DynamicFormArrayGroupModel) {
// Model is a DynamicRowArrayModel
this.handleArrayGroupPatch(pathCombiner, event, (event as any).context.context);
return;
}
const path = this.getFieldPathFromEvent(event);
const value = this.getFieldValueFromChangeEvent(event);
console.log(value);
if (this.formBuilder.isQualdropGroup(event.model as DynamicFormControlModel)) {
this.dispatchOperationsFromMap(this.getQualdropValueMap(event), pathCombiner, event, previousValue);
} else if (event.context && event.context instanceof DynamicFormArrayGroupModel) {
// Model is a DynamicRowArrayModel
this.handleArrayGroupPatch(pathCombiner, event, (event as any).context.context, previousValue);
} else if ((isNotEmpty(value) && typeof value === 'string') || (isNotEmpty(value) && value instanceof FormFieldMetadataValueObject && value.hasValue())) {
this.operationsBuilder.remove(pathCombiner.getPath(path));
}
@@ -368,7 +366,7 @@ export class SectionFormOperationsService {
if (event.context && event.context instanceof DynamicFormArrayGroupModel) {
// Model is a DynamicRowArrayModel
this.handleArrayGroupPatch(pathCombiner, event, (event as any).context.context);
this.handleArrayGroupPatch(pathCombiner, event, (event as any).context.context, previousValue);
return;
}
@@ -388,7 +386,7 @@ export class SectionFormOperationsService {
this.operationsBuilder.add(
pathCombiner.getPath(segmentedPath),
value, true);
} else if (previousValue.isPathEqual(this.formBuilder.getPath(event.model)) || hasStoredValue) {
} else if (previousValue.isPathEqual(this.formBuilder.getPath(event.model)) || (hasStoredValue && isNotEmpty(previousValue.value)) ) {
// Here model has a previous value changed or stored in the server
if (hasValue(event.$event) && hasValue(event.$event.previousIndex)) {
if (event.$event.previousIndex < 0) {
@@ -421,7 +419,7 @@ export class SectionFormOperationsService {
previousValue.delete();
} else if (value.hasValue()) {
// Here model has no previous value but a new one
if (isUndefined(this.getArrayIndexFromEvent(event)) || this.getArrayIndexFromEvent(event) === 0) {
if (isUndefined(this.getArrayIndexFromEvent(event)) || this.getArrayIndexFromEvent(event) === 0) {
// Model is single field or is part of an array model but is the first item,
// so dispatch an add operation that initialize the values of a specific metadata
this.operationsBuilder.add(
@@ -498,23 +496,37 @@ export class SectionFormOperationsService {
event: DynamicFormControlEvent,
previousValue: FormFieldPreviousValueObject) {
return this.handleArrayGroupPatch(pathCombiner, event.$event, (event as any).$event.arrayModel);
return this.handleArrayGroupPatch(pathCombiner, event.$event, (event as any).$event.arrayModel, previousValue);
}
/**
* Specific patch handler for a DynamicRowArrayModel.
* Configure a Patch ADD with the current array value.
* @param pathCombiner
* the [[JsonPatchOperationPathCombiner]] object for the specified operation
* @param event
* the [[DynamicFormControlEvent]] for the specified operation
* @param model
* the [[DynamicRowArrayModel]] model
* @param previousValue
* the [[FormFieldPreviousValueObject]] for the specified operation
*/
private handleArrayGroupPatch(pathCombiner: JsonPatchOperationPathCombiner,
event,
model: DynamicRowArrayModel) {
model: DynamicRowArrayModel,
previousValue: FormFieldPreviousValueObject) {
const arrayValue = this.formBuilder.getValueFromModel([model]);
const segmentedPath2 = this.getFieldPathSegmentedFromChangeEvent(event);
this.operationsBuilder.add(
pathCombiner.getPath(segmentedPath2),
arrayValue[segmentedPath2], false);
const segmentedPath = this.getFieldPathSegmentedFromChangeEvent(event);
if (isNotEmpty(arrayValue)) {
this.operationsBuilder.add(
pathCombiner.getPath(segmentedPath),
arrayValue[segmentedPath],
false
);
} else if (previousValue.isPathEqual(this.formBuilder.getPath(event.model))) {
this.operationsBuilder.remove(pathCombiner.getPath(segmentedPath));
}
}
}

View File

@@ -0,0 +1,6 @@
::ng-deep .access-condition-group {
position: relative;
top: -2.3rem;
margin-bottom: -2.3rem;
}

View File

@@ -18,6 +18,8 @@ import {
import { WorkspaceitemSectionUploadFileObject } from '../../../../../core/submission/models/workspaceitem-section-upload-file.model';
import { FormBuilderService } from '../../../../../shared/form/builder/form-builder.service';
import {
BITSTREAM_ACCESS_CONDITION_GROUP_CONFIG,
BITSTREAM_ACCESS_CONDITION_GROUP_LAYOUT,
BITSTREAM_ACCESS_CONDITIONS_FORM_ARRAY_CONFIG,
BITSTREAM_ACCESS_CONDITIONS_FORM_ARRAY_LAYOUT,
BITSTREAM_FORM_ACCESS_CONDITION_END_DATE_CONFIG,
@@ -43,6 +45,7 @@ import { FormComponent } from '../../../../../shared/form/form.component';
*/
@Component({
selector: 'ds-submission-section-upload-file-edit',
styleUrls: ['./section-upload-file-edit.component.scss'],
templateUrl: './section-upload-file-edit.component.html',
})
export class SubmissionSectionUploadFileEditComponent implements OnChanges {
@@ -209,8 +212,9 @@ export class SubmissionSectionUploadFileEditComponent implements OnChanges {
const startDate = new DynamicDatePickerModel(startDateConfig, BITSTREAM_FORM_ACCESS_CONDITION_START_DATE_LAYOUT);
const endDate = new DynamicDatePickerModel(endDateConfig, BITSTREAM_FORM_ACCESS_CONDITION_END_DATE_LAYOUT);
return [type, startDate, endDate];
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
@@ -233,7 +237,7 @@ export class SubmissionSectionUploadFileEditComponent implements OnChanges {
public initModelData(formModel: DynamicFormControlModel[]) {
this.fileData.accessConditions.forEach((accessCondition, index) => {
Array.of('name', 'startDate', 'endDate')
.filter((key) => accessCondition.hasOwnProperty(key))
.filter((key) => accessCondition.hasOwnProperty(key) && isNotEmpty(accessCondition[key]))
.forEach((key) => {
const metadataModel: any = this.formBuilderService.findById(key, formModel, index);
if (metadataModel) {

View File

@@ -15,12 +15,24 @@ export const BITSTREAM_METADATA_FORM_GROUP_CONFIG: DynamicFormGroupModelConfig =
export const BITSTREAM_METADATA_FORM_GROUP_LAYOUT: DynamicFormControlLayout = {
element: {
container: 'form-group',
label: 'col-form-label'
label: 'col-form-label'
},
grid: {
label: 'col-sm-3'
}
};
export const BITSTREAM_ACCESS_CONDITION_GROUP_CONFIG: DynamicFormGroupModelConfig = {
id: 'accessConditionGroup',
group: []
};
export const BITSTREAM_ACCESS_CONDITION_GROUP_LAYOUT: DynamicFormControlLayout = {
element: {
host: 'form-group flex-fill access-condition-group',
container: 'pl-1 pr-1',
control: 'form-row '
}
};
export const BITSTREAM_ACCESS_CONDITIONS_FORM_ARRAY_CONFIG: DynamicFormArrayModelConfig = {
id: 'accessConditions',
@@ -28,7 +40,7 @@ export const BITSTREAM_ACCESS_CONDITIONS_FORM_ARRAY_CONFIG: DynamicFormArrayMode
};
export const BITSTREAM_ACCESS_CONDITIONS_FORM_ARRAY_LAYOUT: DynamicFormControlLayout = {
grid: {
group: 'form-row'
group: 'form-row pt-4',
}
};
@@ -39,11 +51,8 @@ export const BITSTREAM_FORM_ACCESS_CONDITION_TYPE_CONFIG: DynamicSelectModelConf
};
export const BITSTREAM_FORM_ACCESS_CONDITION_TYPE_LAYOUT: DynamicFormControlLayout = {
element: {
container: 'p-0',
label: 'col-form-label'
},
grid: {
host: 'col-md-10'
host: 'col-12',
label: 'col-form-label name-label'
}
};
@@ -70,11 +79,10 @@ export const BITSTREAM_FORM_ACCESS_CONDITION_START_DATE_CONFIG: DynamicDatePicke
};
export const BITSTREAM_FORM_ACCESS_CONDITION_START_DATE_LAYOUT: DynamicFormControlLayout = {
element: {
container: 'p-0',
label: 'col-form-label'
},
grid: {
host: 'col-md-4'
host: 'col-6'
}
};
@@ -101,10 +109,9 @@ export const BITSTREAM_FORM_ACCESS_CONDITION_END_DATE_CONFIG: DynamicDatePickerM
};
export const BITSTREAM_FORM_ACCESS_CONDITION_END_DATE_LAYOUT: DynamicFormControlLayout = {
element: {
container: 'p-0',
label: 'col-form-label'
},
grid: {
host: 'col-md-4'
host: 'col-6'
}
};

View File

@@ -255,6 +255,7 @@ export class SubmissionSectionUploadFileComponent implements OnChanges, OnInit {
});
const accessConditionsToSave = [];
formData.accessConditions
.map((accessConditions) => accessConditions.accessConditionGroup)
.filter((accessCondition) => isNotEmpty(accessCondition))
.forEach((accessCondition) => {
let accessConditionOpt;