diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts index f1b0dc4f37..e248685ac3 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -137,6 +137,7 @@ import { SiteAdministratorGuard } from './data/feature-authorization/feature-aut import { Registration } from './shared/registration.model'; import { MetadataSchemaDataService } from './data/metadata-schema-data.service'; import { MetadataFieldDataService } from './data/metadata-field-data.service'; +import { DsDynamicTypeBindRelationService } from '../shared/form/builder/ds-dynamic-form-ui/ds-dynamic-type-bind-relation.service'; import { TokenResponseParsingService } from './auth/token-response-parsing.service'; import { SubmissionCcLicenseDataService } from './submission/submission-cc-license-data.service'; import { SubmissionCcLicence } from './submission/models/submission-cc-license.model'; @@ -253,6 +254,7 @@ const PROVIDERS = [ ClaimedTaskDataService, PoolTaskDataService, BitstreamDataService, + DsDynamicTypeBindRelationService, EntityTypeService, ContentSourceResponseParsingService, ItemTemplateDataService, diff --git a/src/app/shared/empty.util.spec.ts b/src/app/shared/empty.util.spec.ts index 1112883c2a..94122990ae 100644 --- a/src/app/shared/empty.util.spec.ts +++ b/src/app/shared/empty.util.spec.ts @@ -9,7 +9,7 @@ import { isNotEmptyOperator, isNotNull, isNotUndefined, - isNull, + isNull, isObjectEmpty, isUndefined } from './empty.util'; @@ -444,6 +444,43 @@ describe('Empty Utils', () => { }); }); + describe('isObjectEmpty', () => { + /* + isObjectEmpty(); // true + isObjectEmpty(null); // true + isObjectEmpty(undefined); // true + isObjectEmpty(''); // true + isObjectEmpty([]); // true + isObjectEmpty({}); // true + isObjectEmpty({name: null}); // true + isObjectEmpty({ name: 'Adam Hawkins', surname : null}); // false + */ + it('should be empty if no parameter passed', () => { + expect(isObjectEmpty()).toBeTrue(); + }); + it('should be empty if null parameter passed', () => { + expect(isObjectEmpty(null)).toBeTrue(); + }); + it('should be empty if undefined parameter passed', () => { + expect(isObjectEmpty(undefined)).toBeTrue(); + }); + it('should be empty if empty string passed', () => { + expect(isObjectEmpty('')).toBeTrue(); + }); + it('should be empty if empty array passed', () => { + expect(isObjectEmpty([])).toBeTrue(); + }); + it('should be empty if empty object passed', () => { + expect(isObjectEmpty({})).toBeTrue(); + }); + it('should be empty if single key with null value passed', () => { + expect(isObjectEmpty({ name: null })).toBeTrue(); + }); + it('should NOT be empty if object with at least one non-null value passed', () => { + expect(isObjectEmpty({ name: 'Adam Hawkins', surname : null })).toBeFalse(); + }); + }); + describe('ensureArrayHasValue', () => { it('should let all arrays pass unchanged, and turn everything else in to empty arrays', () => { const sourceData = { diff --git a/src/app/shared/empty.util.ts b/src/app/shared/empty.util.ts index d79c520fda..355314550a 100644 --- a/src/app/shared/empty.util.ts +++ b/src/app/shared/empty.util.ts @@ -177,3 +177,29 @@ export const isNotEmptyOperator = () => export const ensureArrayHasValue = () => (source: Observable): Observable => source.pipe(map((arr: T[]): T[] => Array.isArray(arr) ? arr : [])); + +/** + * Verifies that a object keys are all empty or not. + * isObjectEmpty(); // true + * isObjectEmpty(null); // true + * isObjectEmpty(undefined); // true + * isObjectEmpty(''); // true + * isObjectEmpty([]); // true + * isObjectEmpty({}); // true + * isObjectEmpty({name: null}); // true + * isObjectEmpty({ name: 'Adam Hawkins', surname : null}); // false + */ +export function isObjectEmpty(obj?: any): boolean { + + if (typeof(obj) !== 'object') { + return true; + } + + for (const key in obj) { + if (obj.hasOwnProperty(key) && isNotEmpty(obj[key])) { + return false; + } + } + return true; +} + diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.html index 55e354ea7a..7eef1d8655 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.html +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.html @@ -1,4 +1,5 @@
- +
diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.spec.ts index b67e6f9e46..ffc4df244e 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.spec.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.spec.ts @@ -25,7 +25,7 @@ import { DynamicSliderModel, DynamicSwitchModel, DynamicTextAreaModel, - DynamicTimePickerModel + DynamicTimePickerModel, MATCH_VISIBLE, OR_OPERATOR } from '@ng-dynamic-forms/core'; import { DynamicNGBootstrapCalendarComponent, @@ -65,6 +65,7 @@ import { DsDynamicFormArrayComponent } from './models/array-group/dynamic-form-a import { DsDynamicFormGroupComponent } from './models/form-group/dynamic-form-group.component'; import { DsDynamicRelationGroupComponent } from './models/relation-group/dynamic-relation-group.components'; import { DsDatePickerInlineComponent } from './models/date-picker-inline/dynamic-date-picker-inline.component'; +import { DsDynamicTypeBindRelationService } from './ds-dynamic-type-bind-relation.service'; import { RelationshipService } from '../../../../core/data/relationship.service'; import { SelectableListService } from '../../../object-list/selectable-list/selectable-list.service'; import { ItemDataService } from '../../../../core/data/item-data.service'; @@ -79,6 +80,14 @@ import { SubmissionService } from '../../../../submission/submission.service'; import { FormBuilderService } from '../form-builder.service'; import { NgxMaskModule } from 'ngx-mask'; +function getMockDsDynamicTypeBindRelationService(): DsDynamicTypeBindRelationService { + return jasmine.createSpyObj('DsDynamicTypeBindRelationService', { + getRelatedFormModel: jasmine.createSpy('getRelatedFormModel'), + matchesCondition: jasmine.createSpy('matchesCondition'), + subscribeRelations: jasmine.createSpy('subscribeRelations') + }); +} + describe('DsDynamicFormControlContainerComponent test suite', () => { const vocabularyOptions: VocabularyOptions = { @@ -111,7 +120,12 @@ describe('DsDynamicFormControlContainerComponent test suite', () => { metadataFields: [], repeatable: false, submissionId: '1234', - hasSelectableMetadata: false + hasSelectableMetadata: false, + typeBindRelations: [{ + match: MATCH_VISIBLE, + operator: OR_OPERATOR, + when: [{id: 'dc.type', value: 'Book'}] + }] }), new DynamicScrollableDropdownModel({ id: 'scrollableDropdown', @@ -200,6 +214,7 @@ describe('DsDynamicFormControlContainerComponent test suite', () => { providers: [ DsDynamicFormControlContainerComponent, DynamicFormService, + { provide: DsDynamicTypeBindRelationService, useValue: getMockDsDynamicTypeBindRelationService() }, { provide: RelationshipService, useValue: {} }, { provide: SelectableListService, useValue: {} }, { provide: ItemDataService, useValue: {} }, @@ -231,7 +246,7 @@ describe('DsDynamicFormControlContainerComponent test suite', () => { }); })); - beforeEach(inject([DynamicFormService], (service: DynamicFormService) => { + beforeEach(inject([DynamicFormService, FormBuilderService], (service: DynamicFormService, formBuilderService: FormBuilderService) => { formGroup = service.createFormGroup(formModel); diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts index c3359fd65a..25f650ea7e 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts @@ -81,6 +81,7 @@ import { DYNAMIC_FORM_CONTROL_TYPE_CUSTOM_SWITCH } from './models/custom-switch/ import { CustomSwitchComponent } from './models/custom-switch/custom-switch.component'; import { find, map, startWith, switchMap, take } from 'rxjs/operators'; import { combineLatest as observableCombineLatest, Observable, Subscription } from 'rxjs'; +import { DsDynamicTypeBindRelationService } from './ds-dynamic-type-bind-relation.service'; import { SearchResult } from '../../../search/models/search-result.model'; import { DSpaceObject } from '../../../../core/shared/dspace-object.model'; import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; @@ -194,8 +195,10 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo @ContentChildren(DynamicTemplateDirective) contentTemplateList: QueryList; // eslint-disable-next-line @angular-eslint/no-input-rename @Input('templates') inputTemplateList: QueryList; - + @Input() hasMetadataModel: any; @Input() formId: string; + @Input() formGroup: FormGroup; + @Input() formModel: DynamicFormControlModel[]; @Input() asBootstrapFormGroup = false; @Input() bindId = true; @Input() context: any | null = null; @@ -237,6 +240,7 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo protected dynamicFormComponentService: DynamicFormComponentService, protected layoutService: DynamicFormLayoutService, protected validationService: DynamicFormValidationService, + protected typeBindRelationService: DsDynamicTypeBindRelationService, protected translateService: TranslateService, protected relationService: DynamicFormRelationService, private modalService: NgbModal, @@ -343,6 +347,9 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo if (this.model && this.model.placeholder) { this.model.placeholder = this.translateService.instant(this.model.placeholder); } + if (this.model.typeBindRelations && this.model.typeBindRelations.length > 0) { + this.subscriptions.push(...this.typeBindRelationService.subscribeRelations(this.model, this.control)); + } } } @@ -357,6 +364,22 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo this.showErrorMessagesPreviousStage = this.showErrorMessages; } + protected createFormControlComponent(): void { + super.createFormControlComponent(); + if (this.componentType !== null) { + let index; + + if (this.context && this.context instanceof DynamicFormArrayGroupModel) { + index = this.context.index; + } + const instance = this.dynamicFormComponentService.getFormControlRef(this.model, index); + if (instance) { + (instance as any).formModel = this.formModel; + (instance as any).formGroup = this.formGroup; + } + } + } + /** * Since Form Control Components created dynamically have 'OnPush' change detection strategy, * changes are not propagated. So use this method to force an update diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form.component.html index 2a18565178..4c1ea2dd96 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form.component.html +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form.component.html @@ -3,6 +3,7 @@ [group]="formGroup" [hasErrorMessaging]="model.hasErrorMessages" [hidden]="model.hidden" + [class.d-none]="model.hidden" [layout]="formLayout" [model]="model" [templates]="templates" diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-type-bind-relation.service.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-type-bind-relation.service.spec.ts new file mode 100644 index 0000000000..f8bc7ea886 --- /dev/null +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-type-bind-relation.service.spec.ts @@ -0,0 +1,143 @@ +import {inject, TestBed} from '@angular/core/testing'; + +import { + DynamicFormControlRelation, + DynamicFormRelationService, + MATCH_VISIBLE, + OR_OPERATOR, + HIDDEN_MATCHER, + HIDDEN_MATCHER_PROVIDER, REQUIRED_MATCHER_PROVIDER, DISABLED_MATCHER_PROVIDER, +} from '@ng-dynamic-forms/core'; + +import { + mockInputWithTypeBindModel, MockRelationModel, mockDcTypeInputModel +} from '../../../mocks/form-models.mock'; +import {DsDynamicTypeBindRelationService} from './ds-dynamic-type-bind-relation.service'; +import {FormFieldMetadataValueObject} from '../models/form-field-metadata-value.model'; +import {FormControl, ReactiveFormsModule} from '@angular/forms'; +import {FormBuilderService} from '../form-builder.service'; +import {getMockFormBuilderService} from '../../../mocks/form-builder-service.mock'; +import {Injector} from '@angular/core'; + +describe('DSDynamicTypeBindRelationService test suite', () => { + let service: DsDynamicTypeBindRelationService; + let dynamicFormRelationService: DynamicFormRelationService; + let injector: Injector; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ReactiveFormsModule], + providers: [ + { provide: FormBuilderService, useValue: getMockFormBuilderService() }, + { provide: DsDynamicTypeBindRelationService, useClass: DsDynamicTypeBindRelationService }, + { provide: DynamicFormRelationService }, + DISABLED_MATCHER_PROVIDER, HIDDEN_MATCHER_PROVIDER, REQUIRED_MATCHER_PROVIDER + ] + }).compileComponents().then(); + }); + + beforeEach(inject([DsDynamicTypeBindRelationService, DynamicFormRelationService], + (relationService: DsDynamicTypeBindRelationService, + formRelationService: DynamicFormRelationService, + ) => { + service = relationService; + dynamicFormRelationService = formRelationService; + })); + + describe('Test getTypeBindValue method', () => { + it('Should get type bind "boundType" from the given metadata object value', () => { + const mockMetadataValueObject: FormFieldMetadataValueObject = new FormFieldMetadataValueObject( + 'boundType', null, null, 'Bound Type' + ); + const bindType = service.getTypeBindValue(mockMetadataValueObject); + expect(bindType).toBe('boundType'); + }); + it('Should get type authority key "bound-auth-key" from the given metadata object value', () => { + const mockMetadataValueObject: FormFieldMetadataValueObject = new FormFieldMetadataValueObject( + 'boundType', null, 'bound-auth-key', 'Bound Type' + ); + const bindType = service.getTypeBindValue(mockMetadataValueObject); + expect(bindType).toBe('bound-auth-key'); + }); + it('Should get passed string returned directly as string passed instead of metadata', () => { + const bindType = service.getTypeBindValue('rawString'); + expect(bindType).toBe('rawString'); + }); + it('Should get "undefined" returned directly as no object given', () => { + const bindType = service.getTypeBindValue(undefined); + expect(bindType).toBeUndefined(); + }); + }); + + describe('Test getRelatedFormModel method', () => { + it('Should get 0 related form models for simple type bind mock data', () => { + const testModel = MockRelationModel; + const relatedModels = service.getRelatedFormModel(testModel); + expect(relatedModels).toHaveSize(0); + }); + it('Should get 1 related form models for mock relation model data', () => { + const testModel = mockInputWithTypeBindModel; + testModel.typeBindRelations = getTypeBindRelations(['boundType']); + const relatedModels = service.getRelatedFormModel(testModel); + expect(relatedModels).toHaveSize(1); + }); + }); + + describe('Test matchesCondition method', () => { + it('Should receive one subscription to dc.type type binding"', () => { + const testModel = mockInputWithTypeBindModel; + testModel.typeBindRelations = getTypeBindRelations(['boundType']); + const dcTypeControl = new FormControl(); + dcTypeControl.setValue('boundType'); + let subscriptions = service.subscribeRelations(testModel, dcTypeControl); + expect(subscriptions).toHaveSize(1); + }); + + it('Expect hasMatch to be true (ie. this should be hidden)', () => { + const testModel = mockInputWithTypeBindModel; + testModel.typeBindRelations = getTypeBindRelations(['boundType']); + const dcTypeControl = new FormControl(); + dcTypeControl.setValue('boundType'); + testModel.typeBindRelations[0].when[0].value = 'anotherType'; + const relation = dynamicFormRelationService.findRelationByMatcher((testModel as any).typeBindRelations, HIDDEN_MATCHER); + const matcher = HIDDEN_MATCHER; + if (relation !== undefined) { + const hasMatch = service.matchesCondition(relation, matcher); + matcher.onChange(hasMatch, testModel, dcTypeControl, injector); + expect(hasMatch).toBeTruthy(); + } + }); + + it('Expect hasMatch to be false (ie. this should NOT be hidden)', () => { + const testModel = mockInputWithTypeBindModel; + testModel.typeBindRelations = getTypeBindRelations(['boundType']); + const dcTypeControl = new FormControl(); + dcTypeControl.setValue('boundType'); + testModel.typeBindRelations[0].when[0].value = 'boundType'; + const relation = dynamicFormRelationService.findRelationByMatcher((testModel as any).typeBindRelations, HIDDEN_MATCHER); + const matcher = HIDDEN_MATCHER; + if (relation !== undefined) { + const hasMatch = service.matchesCondition(relation, matcher); + matcher.onChange(hasMatch, testModel, dcTypeControl, injector); + expect(hasMatch).toBeFalsy(); + } + }); + + }); + +}); + +function getTypeBindRelations(configuredTypeBindValues: string[]): DynamicFormControlRelation[] { + const bindValues = []; + configuredTypeBindValues.forEach((value) => { + bindValues.push({ + id: 'dc.type', + value: value + }); + }); + return [{ + match: MATCH_VISIBLE, + operator: OR_OPERATOR, + when: bindValues + }]; +} diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-type-bind-relation.service.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-type-bind-relation.service.ts new file mode 100644 index 0000000000..5dd4a6627d --- /dev/null +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-type-bind-relation.service.ts @@ -0,0 +1,230 @@ +import { Inject, Injectable, Injector, Optional } from '@angular/core'; +import { FormControl } from '@angular/forms'; + +import { Subscription } from 'rxjs'; +import { startWith } from 'rxjs/operators'; + +import { + AND_OPERATOR, + DYNAMIC_MATCHERS, + DynamicFormControlCondition, + DynamicFormControlMatcher, + DynamicFormControlModel, + DynamicFormControlRelation, + DynamicFormRelationService, MATCH_VISIBLE, + OR_OPERATOR +} from '@ng-dynamic-forms/core'; + +import {hasNoValue, hasValue} from '../../../empty.util'; +import { FormBuilderService } from '../form-builder.service'; +import { FormFieldMetadataValueObject } from '../models/form-field-metadata-value.model'; +import { DYNAMIC_FORM_CONTROL_TYPE_RELATION_GROUP } from './ds-dynamic-form-constants'; + +/** + * Service to manage type binding for submission input fields + * Any form component with the typeBindRelations DynamicFormControlRelation property can be controlled this way + */ +@Injectable() +export class DsDynamicTypeBindRelationService { + + constructor(@Optional() @Inject(DYNAMIC_MATCHERS) private dynamicMatchers: DynamicFormControlMatcher[], + protected dynamicFormRelationService: DynamicFormRelationService, + protected formBuilderService: FormBuilderService, + protected injector: Injector) { + } + + /** + * Return the string value of the type bind model + * @param bindModelValue + * @private + */ + public getTypeBindValue(bindModelValue: string | FormFieldMetadataValueObject): string { + let value; + if (hasNoValue(bindModelValue) || typeof bindModelValue === 'string') { + value = bindModelValue; + } else if (bindModelValue instanceof FormFieldMetadataValueObject + && bindModelValue.hasAuthority()) { + value = bindModelValue.authority; + } else { + value = bindModelValue.value; + } + + return value; + } + + + /** + * Get models for this bind type + * @param model + */ + public getRelatedFormModel(model: DynamicFormControlModel): DynamicFormControlModel[] { + + const models: DynamicFormControlModel[] = []; + + (model as any).typeBindRelations.forEach((relGroup) => relGroup.when.forEach((rel) => { + + if (model.id === rel.id) { + throw new Error(`FormControl ${model.id} cannot depend on itself`); + } + + const bindModel: DynamicFormControlModel = this.formBuilderService.getTypeBindModel(); + + if (model && !models.some((modelElement) => modelElement === bindModel)) { + models.push(bindModel); + } + })); + + return models; + } + + /** + * Return false if the type bind relation (eg. {MATCH_VISIBLE, OR, ['book', 'book part']}) matches the value in + * matcher.match or true if the opposite match. Since this is called with regard to actively *hiding* a form + * component, the negation of the comparison is returned. + * @param relation type bind relation (eg. {MATCH_VISIBLE, OR, ['book', 'book part']}) + * @param matcher contains 'match' value and an onChange() event listener + */ + public matchesCondition(relation: DynamicFormControlRelation, matcher: DynamicFormControlMatcher): boolean { + + // Default to OR for operator (OR is explicitly set in field-parser.ts anyway) + const operator = relation.operator || OR_OPERATOR; + + + return relation.when.reduce((hasAlreadyMatched: boolean, condition: DynamicFormControlCondition, index: number) => { + // Get the DynamicFormControlModel (typeBindModel) from the form builder service, set in the form builder + // in the form model at init time in formBuilderService.modelFromConfiguration (called by other form components + // like relation group component and submission section form component). + // This model (DynamicRelationGroupModel) contains eg. mandatory field, formConfiguration, relationFields, + // submission scope, form/section type and other high level properties + const bindModel: any = this.formBuilderService.getTypeBindModel(); + + let values: string[]; + let bindModelValue = bindModel.value; + + // If the form type is RELATION, set bindModelValue to the mandatory field for this model, otherwise leave + // as plain value + if (bindModel.type === DYNAMIC_FORM_CONTROL_TYPE_RELATION_GROUP) { + bindModelValue = bindModel.value.map((entry) => entry[bindModel.mandatoryField]); + } + // Support multiple bind models + if (Array.isArray(bindModelValue)) { + values = [...bindModelValue.map((entry) => this.getTypeBindValue(entry))]; + } else { + values = [this.getTypeBindValue(bindModelValue)]; + } + + // If bind model evaluates to 'true' (is not undefined, is not null, is not false etc, + // AND the relation match (type bind) is equal to the matcher match (item publication type), then the return + // value is initialised as false. + let returnValue = (!(bindModel && relation.match === matcher.match)); + + // Iterate the type bind values parsed and mapped from our form/relation group model + for (const value of values) { + if (bindModel && relation.match === matcher.match) { + // If we're not at the first array element, and we're using the AND operator, and we have not + // yet matched anything, return false. + if (index > 0 && operator === AND_OPERATOR && !hasAlreadyMatched) { + return false; + } + // If we're not at the first array element, and we're using the OR operator (almost always the case) + // and we've already matched then there is no need to continue, just return true. + if (index > 0 && operator === OR_OPERATOR && hasAlreadyMatched) { + return true; + } + + // Do the actual match. Does condition.value (the item publication type) match the field model + // type bind currently being inspected? + returnValue = condition.value === value; + + // If return value is already true, break. + if (returnValue) { + break; + } + } + + // Test opposingMatch (eg. if match is VISIBLE, opposingMatch will be HIDDEN) + if (bindModel && relation.match === matcher.opposingMatch) { + // If we're not at the first element, using AND, and already matched, just return true here + if (index > 0 && operator === AND_OPERATOR && hasAlreadyMatched) { + return true; + } + + // If we're not at the first element, using OR, and we have NOT already matched, return false + if (index > 0 && operator === OR_OPERATOR && !hasAlreadyMatched) { + return false; + } + + // Negated comparison for return value since this is expected to be in the context of a HIDDEN_MATCHER + returnValue = !(condition.value === value); + + // Break if already false + if (!returnValue) { + break; + } + } + } + return returnValue; + }, false); + } + + /** + * Return an array of subscriptions to a calling component + * @param model + * @param control + */ + subscribeRelations(model: DynamicFormControlModel, control: FormControl): Subscription[] { + + const relatedModels = this.getRelatedFormModel(model); + const subscriptions: Subscription[] = []; + + Object.values(relatedModels).forEach((relatedModel: any) => { + + if (hasValue(relatedModel)) { + const initValue = (hasNoValue(relatedModel.value) || typeof relatedModel.value === 'string') ? relatedModel.value : + (Array.isArray(relatedModel.value) ? relatedModel.value : relatedModel.value.value); + + const valueChanges = relatedModel.valueChanges.pipe( + startWith(initValue) + ); + + // Build up the subscriptions to watch for changes; + subscriptions.push(valueChanges.subscribe(() => { + // Iterate each matcher + if (hasValue(this.dynamicMatchers)) { + this.dynamicMatchers.forEach((matcher) => { + // Find the relation + const relation = this.dynamicFormRelationService.findRelationByMatcher((model as any).typeBindRelations, matcher); + // If the relation is defined, get matchesCondition result and pass it to the onChange event listener + if (relation !== undefined) { + const hasMatch = this.matchesCondition(relation, matcher); + matcher.onChange(hasMatch, model, control, this.injector); + } + }); + } + })); + } + }); + + return subscriptions; + } + + /** + * Helper function to construct a typeBindRelations array + * @param configuredTypeBindValues + */ + public getTypeBindRelations(configuredTypeBindValues: string[]): DynamicFormControlRelation[] { + const bindValues = []; + configuredTypeBindValues.forEach((value) => { + bindValues.push({ + id: 'dc.type', + value: value + }); + }); + return [{ + match: MATCH_VISIBLE, + operator: OR_OPERATOR, + when: bindValues + }]; + } + +} diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/array-group/dynamic-form-array.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/models/array-group/dynamic-form-array.component.html index bc41ade088..d518d59da2 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/array-group/dynamic-form-array.component.html +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/array-group/dynamic-form-array.component.html @@ -1,6 +1,7 @@
@@ -13,7 +14,8 @@ cdkDrag cdkDragHandle [cdkDragDisabled]="dragDisabled" - [cdkDragPreviewClass]="'ds-submission-reorder-dragging'"> + [cdkDragPreviewClass]="'ds-submission-reorder-dragging'" + [class.grey-background]="model.isInlineGroupArray">
@@ -22,9 +24,11 @@
- - -
diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/array-group/dynamic-form-array.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/array-group/dynamic-form-array.component.ts index 921b159718..01bba74cc8 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/array-group/dynamic-form-array.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/array-group/dynamic-form-array.component.ts @@ -6,6 +6,7 @@ import { DynamicFormControlCustomEvent, DynamicFormControlEvent, DynamicFormControlLayout, + DynamicFormControlModel, DynamicFormLayout, DynamicFormLayoutService, DynamicFormValidationService, @@ -22,6 +23,8 @@ import { DynamicRowArrayModel } from '../ds-dynamic-row-array-model'; }) export class DsDynamicFormArrayComponent extends DynamicFormArrayComponent { + @Input() bindId = true; + @Input() formModel: DynamicFormControlModel[]; @Input() formLayout: DynamicFormLayout; @Input() group: FormGroup; @Input() layout: DynamicFormControlLayout; diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/date-picker/date-picker.model.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/date-picker/date-picker.model.ts index 1c053ffc80..5af9b2bd32 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/date-picker/date-picker.model.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/date-picker/date-picker.model.ts @@ -2,20 +2,29 @@ import { DynamicDateControlModel, DynamicDatePickerModelConfig, DynamicFormControlLayout, + DynamicFormControlModel, + DynamicFormControlRelation, serializable } from '@ng-dynamic-forms/core'; +import {BehaviorSubject, Subject} from 'rxjs'; +import {isEmpty, isNotUndefined} from '../../../../../empty.util'; +import {MetadataValue} from '../../../../../../core/shared/metadata.models'; export const DYNAMIC_FORM_CONTROL_TYPE_DSDATEPICKER = 'DATE'; export interface DynamicDsDateControlModelConfig extends DynamicDatePickerModelConfig { legend?: string; + typeBindRelations?: DynamicFormControlRelation[]; } /** * Dynamic Date Picker Model class */ export class DynamicDsDatePickerModel extends DynamicDateControlModel { + @serializable() hiddenUpdates: Subject; + @serializable() typeBindRelations: DynamicFormControlRelation[]; @serializable() readonly type: string = DYNAMIC_FORM_CONTROL_TYPE_DSDATEPICKER; + @serializable() metadataValue: MetadataValue; malformedDate: boolean; legend: string; hasLanguages = false; @@ -25,6 +34,23 @@ export class DynamicDsDatePickerModel extends DynamicDateControlModel { super(config, layout); this.malformedDate = false; this.legend = config.legend; + this.metadataValue = (config as any).metadataValue; + this.typeBindRelations = config.typeBindRelations ? config.typeBindRelations : []; + this.hiddenUpdates = new BehaviorSubject(this.hidden); + + // This was a subscription, then an async setTimeout, but it seems unnecessary + const parentModel = this.getRootParent(this); + if (parentModel && isNotUndefined(parentModel.hidden)) { + parentModel.hidden = this.hidden; + } + } + + private getRootParent(model: any): DynamicFormControlModel { + if (isEmpty(model) || isEmpty(model.parent)) { + return model; + } else { + return this.getRootParent(model.parent); + } } } diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-input.model.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-input.model.ts index 290e29dc65..ed99acb34e 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-input.model.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-input.model.ts @@ -1,14 +1,15 @@ import { DynamicFormControlLayout, + DynamicFormControlRelation, DynamicInputModel, DynamicInputModelConfig, serializable } from '@ng-dynamic-forms/core'; -import { Subject } from 'rxjs'; +import {Subject} from 'rxjs'; import { LanguageCode } from '../../models/form-field-language-value.model'; import { VocabularyOptions } from '../../../../../core/submission/vocabularies/models/vocabulary-options.model'; -import { hasValue } from '../../../../empty.util'; +import {hasValue} from '../../../../empty.util'; import { FormFieldMetadataValueObject } from '../../models/form-field-metadata-value.model'; import { RelationshipOptions } from '../../models/relationship-options.model'; @@ -18,12 +19,14 @@ export interface DsDynamicInputModelConfig extends DynamicInputModelConfig { language?: string; place?: number; value?: any; + typeBindRelations?: DynamicFormControlRelation[]; relationship?: RelationshipOptions; repeatable: boolean; metadataFields: string[]; submissionId: string; hasSelectableMetadata: boolean; metadataValue?: FormFieldMetadataValueObject; + isModelOfInnerForm?: boolean; } @@ -33,12 +36,17 @@ export class DsDynamicInputModel extends DynamicInputModel { @serializable() private _languageCodes: LanguageCode[]; @serializable() private _language: string; @serializable() languageUpdates: Subject; + @serializable() place: number; + @serializable() typeBindRelations: DynamicFormControlRelation[]; + @serializable() typeBindHidden = false; @serializable() relationship?: RelationshipOptions; @serializable() repeatable?: boolean; @serializable() metadataFields: string[]; @serializable() submissionId: string; @serializable() hasSelectableMetadata: boolean; @serializable() metadataValue: FormFieldMetadataValueObject; + @serializable() isModelOfInnerForm: boolean; + constructor(config: DsDynamicInputModelConfig, layout?: DynamicFormControlLayout) { super(config, layout); @@ -51,6 +59,8 @@ export class DsDynamicInputModel extends DynamicInputModel { this.submissionId = config.submissionId; this.hasSelectableMetadata = config.hasSelectableMetadata; this.metadataValue = config.metadataValue; + this.place = config.place; + this.isModelOfInnerForm = (hasValue(config.isModelOfInnerForm) ? config.isModelOfInnerForm : false); this.language = config.language; if (!this.language) { @@ -71,6 +81,8 @@ export class DsDynamicInputModel extends DynamicInputModel { this.language = lang; }); + this.typeBindRelations = config.typeBindRelations ? config.typeBindRelations : []; + this.vocabularyOptions = config.vocabularyOptions; } diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-row-array-model.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-row-array-model.ts index d0b07de885..52364df45e 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-row-array-model.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-row-array-model.ts @@ -1,5 +1,12 @@ -import { DynamicFormArrayModel, DynamicFormArrayModelConfig, DynamicFormControlLayout, serializable } from '@ng-dynamic-forms/core'; +import { + DynamicFormArrayModel, + DynamicFormArrayModelConfig, + DynamicFormControlLayout, + DynamicFormControlRelation, + serializable +} from '@ng-dynamic-forms/core'; import { RelationshipOptions } from '../../models/relationship-options.model'; +import { hasValue } from '../../../../empty.util'; export interface DynamicRowArrayModelConfig extends DynamicFormArrayModelConfig { notRepeatable: boolean; @@ -10,6 +17,9 @@ export interface DynamicRowArrayModelConfig extends DynamicFormArrayModelConfig metadataFields: string[]; hasSelectableMetadata: boolean; isDraggable: boolean; + showButtons: boolean; + typeBindRelations?: DynamicFormControlRelation[]; + isInlineGroupArray?: boolean; } export class DynamicRowArrayModel extends DynamicFormArrayModel { @@ -21,17 +31,29 @@ export class DynamicRowArrayModel extends DynamicFormArrayModel { @serializable() metadataFields: string[]; @serializable() hasSelectableMetadata: boolean; @serializable() isDraggable: boolean; + @serializable() showButtons = true; + @serializable() typeBindRelations: DynamicFormControlRelation[]; isRowArray = true; + isInlineGroupArray = false; constructor(config: DynamicRowArrayModelConfig, layout?: DynamicFormControlLayout) { super(config, layout); - this.notRepeatable = config.notRepeatable; - this.required = config.required; + if (hasValue(config.notRepeatable)) { + this.notRepeatable = config.notRepeatable; + } + if (hasValue(config.required)) { + this.required = config.required; + } + if (hasValue(config.showButtons)) { + this.showButtons = config.showButtons; + } this.submissionId = config.submissionId; this.relationshipConfig = config.relationshipConfig; this.metadataKey = config.metadataKey; this.metadataFields = config.metadataFields; this.hasSelectableMetadata = config.hasSelectableMetadata; this.isDraggable = config.isDraggable; + this.typeBindRelations = config.typeBindRelations ? config.typeBindRelations : []; + this.isInlineGroupArray = config.isInlineGroupArray ? config.isInlineGroupArray : false; } } diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/form-group/dynamic-form-group.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/models/form-group/dynamic-form-group.component.html index 843ed95530..8e1645633e 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/form-group/dynamic-form-group.component.html +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/form-group/dynamic-form-group.component.html @@ -1,11 +1,17 @@ +
-
- - +
+
diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/form-group/dynamic-form-group.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/form-group/dynamic-form-group.component.ts index 789d5eb87c..9d8d73eab5 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/form-group/dynamic-form-group.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/form-group/dynamic-form-group.component.ts @@ -5,7 +5,9 @@ import { DynamicFormControlCustomEvent, DynamicFormControlEvent, DynamicFormControlLayout, - DynamicFormGroupModel, DynamicFormLayout, + DynamicFormControlModel, + DynamicFormGroupModel, + DynamicFormLayout, DynamicFormLayoutService, DynamicFormValidationService, DynamicTemplateDirective @@ -18,6 +20,7 @@ import { }) export class DsDynamicFormGroupComponent extends DynamicFormControlComponent { + @Input() formModel: DynamicFormControlModel[]; @Input() formLayout: DynamicFormLayout; @Input() group: FormGroup; @Input() layout: DynamicFormControlLayout; diff --git a/src/app/shared/form/builder/form-builder.service.spec.ts b/src/app/shared/form/builder/form-builder.service.spec.ts index 4055c84921..ac9c2b652b 100644 --- a/src/app/shared/form/builder/form-builder.service.spec.ts +++ b/src/app/shared/form/builder/form-builder.service.spec.ts @@ -26,7 +26,7 @@ import { DynamicSliderModel, DynamicSwitchModel, DynamicTextAreaModel, - DynamicTimePickerModel + DynamicTimePickerModel, } from '@ng-dynamic-forms/core'; import { DynamicTagModel } from './ds-dynamic-form-ui/models/tag/dynamic-tag.model'; import { DynamicListCheckboxGroupModel } from './ds-dynamic-form-ui/models/list/dynamic-list-checkbox-group.model'; @@ -48,12 +48,18 @@ import { DynamicConcatModel } from './ds-dynamic-form-ui/models/ds-dynamic-conca import { DynamicLookupNameModel } from './ds-dynamic-form-ui/models/lookup/dynamic-lookup-name.model'; import { DynamicRowArrayModel } from './ds-dynamic-form-ui/models/ds-dynamic-row-array-model'; import { FormRowModel } from '../../../core/config/models/config-submission-form.model'; +import {ConfigurationDataService} from '../../../core/data/configuration-data.service'; +import {createSuccessfulRemoteDataObject$} from '../../remote-data.utils'; +import {ConfigurationProperty} from '../../../core/shared/configuration-property.model'; describe('FormBuilderService test suite', () => { let testModel: DynamicFormControlModel[]; let testFormConfiguration: SubmissionFormsModel; let service: FormBuilderService; + let configSpy: ConfigurationDataService; + const typeFieldProp = 'submit.type-bind.field'; + const typeFieldTestValue = 'dc.type'; const submissionId = '1234'; @@ -65,15 +71,24 @@ describe('FormBuilderService test suite', () => { return new Promise((resolve) => setTimeout(() => resolve(true), 0)); } - beforeEach(() => { + const createConfigSuccessSpy = (...values: string[]) => jasmine.createSpyObj('configurationDataService', { + findByPropertyName: createSuccessfulRemoteDataObject$({ + ... new ConfigurationProperty(), + name: typeFieldProp, + values: values, + }), + }); + beforeEach(() => { + configSpy = createConfigSuccessSpy(typeFieldTestValue); TestBed.configureTestingModule({ imports: [ReactiveFormsModule], providers: [ { provide: FormBuilderService, useClass: FormBuilderService }, { provide: DynamicFormValidationService, useValue: {} }, { provide: NG_VALIDATORS, useValue: testValidator, multi: true }, - { provide: NG_ASYNC_VALIDATORS, useValue: testAsyncValidator, multi: true } + { provide: NG_ASYNC_VALIDATORS, useValue: testAsyncValidator, multi: true }, + { provide: ConfigurationDataService, useValue: configSpy } ] }); @@ -197,7 +212,7 @@ describe('FormBuilderService test suite', () => { repeatable: false, metadataFields: [], submissionId: '1234', - hasSelectableMetadata: false + hasSelectableMetadata: false, }), new DynamicScrollableDropdownModel({ @@ -233,6 +248,7 @@ describe('FormBuilderService test suite', () => { hints: 'Enter the name of the author.', input: { type: 'onebox' }, label: 'Authors', + typeBind: [], languageCodes: [], mandatory: 'true', mandatoryMessage: 'Required field!', @@ -304,7 +320,9 @@ describe('FormBuilderService test suite', () => { required: false, metadataKey: 'dc.contributor.author', metadataFields: ['dc.contributor.author'], - hasSelectableMetadata: true + hasSelectableMetadata: true, + showButtons: true, + typeBindRelations: [{ match: 'VISIBLE', operator: 'OR', when: [{id: 'dc.type', value: 'Book' }]}] }, ), ]; @@ -424,7 +442,9 @@ describe('FormBuilderService test suite', () => { } as any; }); - beforeEach(inject([FormBuilderService], (formService: FormBuilderService) => service = formService)); + beforeEach(inject([FormBuilderService], (formService: FormBuilderService) => { + service = formService; + })); it('should find a dynamic form control model by id', () => { @@ -875,4 +895,12 @@ describe('FormBuilderService test suite', () => { expect(formArray.length === 0).toBe(true); }); + + it(`should request the ${typeFieldProp} property and set value "dc_type"`, () => { + const typeValue = service.getTypeField(); + expect(configSpy.findByPropertyName).toHaveBeenCalledTimes(1); + expect(configSpy.findByPropertyName).toHaveBeenCalledWith(typeFieldProp); + expect(typeValue).toEqual('dc_type'); + }); + }); diff --git a/src/app/shared/form/builder/form-builder.service.ts b/src/app/shared/form/builder/form-builder.service.ts index 85d70f20dc..93a398a59d 100644 --- a/src/app/shared/form/builder/form-builder.service.ts +++ b/src/app/shared/form/builder/form-builder.service.ts @@ -1,5 +1,5 @@ -import { Injectable } from '@angular/core'; -import { AbstractControl, FormGroup } from '@angular/forms'; +import {Injectable, Optional} from '@angular/core'; +import { AbstractControl, FormControl, FormGroup } from '@angular/forms'; import { DYNAMIC_FORM_CONTROL_TYPE_ARRAY, @@ -7,6 +7,7 @@ import { DYNAMIC_FORM_CONTROL_TYPE_GROUP, DYNAMIC_FORM_CONTROL_TYPE_INPUT, DYNAMIC_FORM_CONTROL_TYPE_RADIO_GROUP, + DynamicFormArrayGroupModel, DynamicFormArrayModel, DynamicFormComponentService, DynamicFormControlEvent, @@ -19,7 +20,15 @@ import { } from '@ng-dynamic-forms/core'; import { isObject, isString, mergeWith } from 'lodash'; -import { hasValue, isEmpty, isNotEmpty, isNotNull, isNotUndefined, isNull } from '../../empty.util'; +import { + hasNoValue, + hasValue, + isEmpty, + isNotEmpty, + isNotNull, + isNotUndefined, + isNull +} from '../../empty.util'; import { DynamicQualdropModel } from './ds-dynamic-form-ui/models/ds-dynamic-qualdrop.model'; import { SubmissionFormsModel } from '../../../core/config/models/config-submission-forms.model'; import { DYNAMIC_FORM_CONTROL_TYPE_TAG } from './ds-dynamic-form-ui/models/tag/dynamic-tag.model'; @@ -32,16 +41,61 @@ import { dateToString, isNgbDateStruct } from '../../date.util'; import { DYNAMIC_FORM_CONTROL_TYPE_RELATION_GROUP } from './ds-dynamic-form-ui/ds-dynamic-form-constants'; import { CONCAT_GROUP_SUFFIX, DynamicConcatModel } from './ds-dynamic-form-ui/models/ds-dynamic-concat.model'; import { VIRTUAL_METADATA_PREFIX } from '../../../core/shared/metadata.models'; +import { ConfigurationDataService } from '../../../core/data/configuration-data.service'; +import { getFirstCompletedRemoteData } from '../../../core/shared/operators'; @Injectable() export class FormBuilderService extends DynamicFormService { + private typeBindModel: DynamicFormControlModel; + + /** + * This map contains the active forms model + */ + private formModels: Map; + + /** + * This map contains the active forms control groups + */ + private formGroups: Map; + + /** + * This is the field to use for type binding + */ + private typeField: string; + constructor( componentService: DynamicFormComponentService, validationService: DynamicFormValidationService, - protected rowParser: RowParser + protected rowParser: RowParser, + @Optional() protected configService: ConfigurationDataService, ) { super(componentService, validationService); + this.formModels = new Map(); + this.formGroups = new Map(); + // If optional config service was passed, perform an initial set of type field (default dc_type) for type binds + if (hasValue(this.configService)) { + this.setTypeBindFieldFromConfig(); + } + + + } + + createDynamicFormControlEvent(control: FormControl, group: FormGroup, model: DynamicFormControlModel, type: string): DynamicFormControlEvent { + const $event = { + value: (model as any).value, + autoSave: false + }; + const context: DynamicFormArrayGroupModel = (model?.parent instanceof DynamicFormArrayGroupModel) ? model?.parent : null; + return {$event, context, control: control, group: group, model: model, type}; + } + + getTypeBindModel() { + return this.typeBindModel; + } + + setTypeBindModel(model: DynamicFormControlModel) { + this.typeBindModel = model; } findById(id: string, groupModel: DynamicFormControlModel[], arrayIndex = null): DynamicFormControlModel | null { @@ -223,13 +277,15 @@ export class FormBuilderService extends DynamicFormService { return result; } - modelFromConfiguration(submissionId: string, json: string | SubmissionFormsModel, scopeUUID: string, sectionData: any = {}, submissionScope?: string, readOnly = false): DynamicFormControlModel[] | never { - let rows: DynamicFormControlModel[] = []; - const rawData = typeof json === 'string' ? JSON.parse(json, parseReviver) : json; - + modelFromConfiguration(submissionId: string, json: string | SubmissionFormsModel, scopeUUID: string, sectionData: any = {}, + submissionScope?: string, readOnly = false, typeBindModel = null, + isInnerForm = false): DynamicFormControlModel[] | never { + let rows: DynamicFormControlModel[] = []; + const rawData = typeof json === 'string' ? JSON.parse(json, parseReviver) : json; if (rawData.rows && !isEmpty(rawData.rows)) { rawData.rows.forEach((currentRow) => { - const rowParsed = this.rowParser.parse(submissionId, currentRow, scopeUUID, sectionData, submissionScope, readOnly); + const rowParsed = this.rowParser.parse(submissionId, currentRow, scopeUUID, sectionData, submissionScope, + readOnly, this.getTypeField()); if (isNotNull(rowParsed)) { if (Array.isArray(rowParsed)) { rows = rows.concat(rowParsed); @@ -240,6 +296,13 @@ export class FormBuilderService extends DynamicFormService { }); } + if (hasNoValue(typeBindModel)) { + typeBindModel = this.findById(this.typeField, rows); + } + + if (hasValue(typeBindModel)) { + this.setTypeBindModel(typeBindModel); + } return rows; } @@ -309,6 +372,10 @@ export class FormBuilderService extends DynamicFormService { return isNotEmpty(fieldModel) ? formGroup.get(this.getPath(fieldModel)) : null; } + getFormControlByModel(formGroup: FormGroup, fieldModel: DynamicFormControlModel): AbstractControl { + return isNotEmpty(fieldModel) ? formGroup.get(this.getPath(fieldModel)) : null; + } + /** * Note (discovered while debugging) this is not the ID as used in the form, * but the first part of the path needed in a patch operation: @@ -328,6 +395,35 @@ export class FormBuilderService extends DynamicFormService { return (tempModel.id !== tempModel.name) ? tempModel.name : tempModel.id; } + /** + * If present, remove form model from formModels map + * @param id id of model + */ + removeFormModel(id: string): void { + if (this.formModels.has(id)) { + this.formModels.delete(id); + } + } + + /** + * Add new form model to formModels map + * @param id id of model + * @param formGroup FormGroup + */ + addFormGroups(id: string, formGroup: FormGroup): void { + this.formGroups.set(id, formGroup); + } + + /** + * If present, remove form model from formModels map + * @param id id of model + */ + removeFormGroup(id: string): void { + if (this.formGroups.has(id)) { + this.formGroups.delete(id); + } + } + /** * Calculate the metadata list related to the event. * @param event @@ -400,4 +496,39 @@ export class FormBuilderService extends DynamicFormService { return Object.keys(result); } + /** + * Get the type bind field from config + */ + setTypeBindFieldFromConfig(): void { + this.configService.findByPropertyName('submit.type-bind.field').pipe( + getFirstCompletedRemoteData(), + ).subscribe((remoteData: any) => { + // make sure we got a success response from the backend + if (!remoteData.hasSucceeded) { + this.typeField = 'dc_type'; + return; + } + // Read type bind value from response and set if non-empty + const typeFieldConfig = remoteData.payload.values[0]; + if (isEmpty(typeFieldConfig)) { + this.typeField = 'dc_type'; + } else { + this.typeField = typeFieldConfig.replace(/\./g, '_'); + } + }); + } + + /** + * Get type field. If the type isn't already set, and a ConfigurationDataService is provided, set (with subscribe) + * from back end. Otherwise, get/set a default "dc_type" value + */ + getTypeField(): string { + if (hasValue(this.configService) && hasNoValue(this.typeField)) { + this.setTypeBindFieldFromConfig(); + } else if (hasNoValue(this.typeField)) { + this.typeField = 'dc_type'; + } + return this.typeField; + } + } diff --git a/src/app/shared/form/builder/models/form-field.model.ts b/src/app/shared/form/builder/models/form-field.model.ts index 95ee980aeb..be3150bae3 100644 --- a/src/app/shared/form/builder/models/form-field.model.ts +++ b/src/app/shared/form/builder/models/form-field.model.ts @@ -113,6 +113,12 @@ export class FormFieldModel { @autoserialize style: string; + /** + * Containing types to bind for this field + */ + @autoserialize + typeBind: string[]; + /** * Containing the value for this metadata field */ diff --git a/src/app/shared/form/builder/parsers/concat-field-parser.ts b/src/app/shared/form/builder/parsers/concat-field-parser.ts index 8085924422..2de5f256dc 100644 --- a/src/app/shared/form/builder/parsers/concat-field-parser.ts +++ b/src/app/shared/form/builder/parsers/concat-field-parser.ts @@ -1,4 +1,4 @@ -import { Inject } from '@angular/core'; +import {Inject} from '@angular/core'; import { FormFieldModel } from '../models/form-field.model'; import { FormFieldMetadataValueObject } from '../models/form-field-metadata-value.model'; import { diff --git a/src/app/shared/form/builder/parsers/date-field-parser.spec.ts b/src/app/shared/form/builder/parsers/date-field-parser.spec.ts index b9adf3ed65..9ab43709ad 100644 --- a/src/app/shared/form/builder/parsers/date-field-parser.spec.ts +++ b/src/app/shared/form/builder/parsers/date-field-parser.spec.ts @@ -12,7 +12,8 @@ describe('DateFieldParser test suite', () => { const parserOptions: ParserOptions = { readOnly: false, submissionScope: null, - collectionUUID: null + collectionUUID: null, + typeField: 'dc_type' }; beforeEach(() => { diff --git a/src/app/shared/form/builder/parsers/date-field-parser.ts b/src/app/shared/form/builder/parsers/date-field-parser.ts index aef0219579..c67c2c7695 100644 --- a/src/app/shared/form/builder/parsers/date-field-parser.ts +++ b/src/app/shared/form/builder/parsers/date-field-parser.ts @@ -1,7 +1,7 @@ import { FieldParser } from './field-parser'; import { - DynamicDsDateControlModelConfig, - DynamicDsDatePickerModel + DynamicDsDatePickerModel, + DynamicDsDateControlModelConfig } from '../ds-dynamic-form-ui/models/date-picker/date-picker.model'; import { isNotEmpty } from '../../../empty.util'; import { DS_DATE_PICKER_SEPARATOR } from '../ds-dynamic-form-ui/models/date-picker/date-picker.component'; @@ -13,7 +13,7 @@ export class DateFieldParser extends FieldParser { let malformedDate = false; const inputDateModelConfig: DynamicDsDateControlModelConfig = this.initModel(null, false, true); inputDateModelConfig.legend = this.configData.label; - + inputDateModelConfig.disabled = inputDateModelConfig.readOnly; inputDateModelConfig.toggleIcon = 'fas fa-calendar'; this.setValues(inputDateModelConfig as any, fieldValue); // Init Data and validity check diff --git a/src/app/shared/form/builder/parsers/disabled-field-parser.spec.ts b/src/app/shared/form/builder/parsers/disabled-field-parser.spec.ts index e3e86d7051..d69f0e48e9 100644 --- a/src/app/shared/form/builder/parsers/disabled-field-parser.spec.ts +++ b/src/app/shared/form/builder/parsers/disabled-field-parser.spec.ts @@ -11,7 +11,8 @@ describe('DisabledFieldParser test suite', () => { const parserOptions: ParserOptions = { readOnly: false, submissionScope: null, - collectionUUID: null + collectionUUID: null, + typeField: 'dc_type' }; beforeEach(() => { diff --git a/src/app/shared/form/builder/parsers/dropdown-field-parser.spec.ts b/src/app/shared/form/builder/parsers/dropdown-field-parser.spec.ts index 82d2aeac63..3dca7558b3 100644 --- a/src/app/shared/form/builder/parsers/dropdown-field-parser.spec.ts +++ b/src/app/shared/form/builder/parsers/dropdown-field-parser.spec.ts @@ -11,7 +11,8 @@ describe('DropdownFieldParser test suite', () => { const parserOptions: ParserOptions = { readOnly: false, submissionScope: 'testScopeUUID', - collectionUUID: null + collectionUUID: null, + typeField: 'dc_type' }; beforeEach(() => { diff --git a/src/app/shared/form/builder/parsers/dropdown-field-parser.ts b/src/app/shared/form/builder/parsers/dropdown-field-parser.ts index 760fc63482..3e5ec0b9da 100644 --- a/src/app/shared/form/builder/parsers/dropdown-field-parser.ts +++ b/src/app/shared/form/builder/parsers/dropdown-field-parser.ts @@ -1,4 +1,4 @@ -import { Inject } from '@angular/core'; +import {Inject} from '@angular/core'; import { FormFieldModel } from '../models/form-field.model'; import { CONFIG_DATA, @@ -22,7 +22,7 @@ export class DropdownFieldParser extends FieldParser { @Inject(SUBMISSION_ID) submissionId: string, @Inject(CONFIG_DATA) configData: FormFieldModel, @Inject(INIT_FORM_VALUES) initFormValues, - @Inject(PARSER_OPTIONS) parserOptions: ParserOptions + @Inject(PARSER_OPTIONS) parserOptions: ParserOptions, ) { super(submissionId, configData, initFormValues, parserOptions); } diff --git a/src/app/shared/form/builder/parsers/field-parser.ts b/src/app/shared/form/builder/parsers/field-parser.ts index da304ca267..bd6820d4b3 100644 --- a/src/app/shared/form/builder/parsers/field-parser.ts +++ b/src/app/shared/form/builder/parsers/field-parser.ts @@ -1,7 +1,7 @@ -import { Inject, InjectionToken } from '@angular/core'; +import {Inject, InjectionToken} from '@angular/core'; import { uniqueId } from 'lodash'; -import { DynamicFormControlLayout } from '@ng-dynamic-forms/core'; +import {DynamicFormControlLayout, DynamicFormControlRelation, MATCH_VISIBLE, OR_OPERATOR} from '@ng-dynamic-forms/core'; import { hasValue, isNotEmpty, isNotNull, isNotUndefined } from '../../../empty.util'; import { FormFieldModel } from '../models/form-field.model'; @@ -26,6 +26,11 @@ export const PARSER_OPTIONS: InjectionToken = new InjectionToken< export abstract class FieldParser { protected fieldId: string; + /** + * This is the field to use for type binding + * @protected + */ + protected typeField: string; constructor( @Inject(SUBMISSION_ID) protected submissionId: string, @@ -67,6 +72,8 @@ export abstract class FieldParser { metadataFields: this.getAllFieldIds(), hasSelectableMetadata: isNotEmpty(this.configData.selectableMetadata), isDraggable, + typeBindRelations: isNotEmpty(this.configData.typeBind) ? this.getTypeBindRelations(this.configData.typeBind, + this.parserOptions.typeField) : null, groupFactory: () => { let model; if ((arrayCounter === 0)) { @@ -275,7 +282,7 @@ export abstract class FieldParser { // Set label this.setLabel(controlModel, label); if (hint) { - controlModel.hint = this.configData.hints; + controlModel.hint = this.configData.hints || ' '; } controlModel.placeholder = this.configData.label; @@ -292,9 +299,46 @@ export abstract class FieldParser { (controlModel as DsDynamicInputModel).languageCodes = this.configData.languageCodes; } + // If typeBind is configured + if (isNotEmpty(this.configData.typeBind)) { + (controlModel as DsDynamicInputModel).typeBindRelations = this.getTypeBindRelations(this.configData.typeBind, + this.parserOptions.typeField); + } + return controlModel; } + /** + * Get the type bind values from the REST data for a specific field + * The return value is any[] in the method signature but in reality it's + * returning the 'relation' that'll be used for a dynamic matcher when filtering + * fields in type bind, made up of a 'match' outcome (make this field visible), an 'operator' + * (OR) and a 'when' condition (the bindValues array). + * @param configuredTypeBindValues array of types from the submission definition (CONFIG_DATA) + * @private + * @return DynamicFormControlRelation[] array with one relation in it, for type bind matching to show a field + */ + private getTypeBindRelations(configuredTypeBindValues: string[], typeField: string): DynamicFormControlRelation[] { + const bindValues = []; + configuredTypeBindValues.forEach((value) => { + bindValues.push({ + id: typeField, + value: value + }); + }); + // match: MATCH_VISIBLE means that if true, the field / component will be visible + // operator: OR means that all the values in the 'when' condition will be compared with OR, not AND + // when: the list of values to match against, in this case the list of strings from ... + // Example: Field [x] will be VISIBLE if item type = book OR item type = book_part + // + // The opposing match value will be the dc.type for the workspace item + return [{ + match: MATCH_VISIBLE, + operator: OR_OPERATOR, + when: bindValues + }]; + } + protected hasRegex() { return hasValue(this.configData.input.regex); } diff --git a/src/app/shared/form/builder/parsers/list-field-parser.spec.ts b/src/app/shared/form/builder/parsers/list-field-parser.spec.ts index 8a05b169fd..30d1913a51 100644 --- a/src/app/shared/form/builder/parsers/list-field-parser.spec.ts +++ b/src/app/shared/form/builder/parsers/list-field-parser.spec.ts @@ -13,7 +13,8 @@ describe('ListFieldParser test suite', () => { const parserOptions: ParserOptions = { readOnly: false, submissionScope: 'testScopeUUID', - collectionUUID: null + collectionUUID: null, + typeField: 'dc_type' }; beforeEach(() => { diff --git a/src/app/shared/form/builder/parsers/lookup-field-parser.spec.ts b/src/app/shared/form/builder/parsers/lookup-field-parser.spec.ts index 87cee9d950..24efcf3462 100644 --- a/src/app/shared/form/builder/parsers/lookup-field-parser.spec.ts +++ b/src/app/shared/form/builder/parsers/lookup-field-parser.spec.ts @@ -12,7 +12,8 @@ describe('LookupFieldParser test suite', () => { const parserOptions: ParserOptions = { readOnly: false, submissionScope: 'testScopeUUID', - collectionUUID: null + collectionUUID: null, + typeField: 'dc_type' }; beforeEach(() => { diff --git a/src/app/shared/form/builder/parsers/lookup-name-field-parser.spec.ts b/src/app/shared/form/builder/parsers/lookup-name-field-parser.spec.ts index 3d02b6952e..d0281681ef 100644 --- a/src/app/shared/form/builder/parsers/lookup-name-field-parser.spec.ts +++ b/src/app/shared/form/builder/parsers/lookup-name-field-parser.spec.ts @@ -12,7 +12,8 @@ describe('LookupNameFieldParser test suite', () => { const parserOptions: ParserOptions = { readOnly: false, submissionScope: 'testScopeUUID', - collectionUUID: null + collectionUUID: null, + typeField: 'dc_type' }; beforeEach(() => { diff --git a/src/app/shared/form/builder/parsers/name-field-parser.spec.ts b/src/app/shared/form/builder/parsers/name-field-parser.spec.ts index 514585f03f..6b520142cc 100644 --- a/src/app/shared/form/builder/parsers/name-field-parser.spec.ts +++ b/src/app/shared/form/builder/parsers/name-field-parser.spec.ts @@ -14,7 +14,8 @@ describe('NameFieldParser test suite', () => { const parserOptions: ParserOptions = { readOnly: false, submissionScope: 'testScopeUUID', - collectionUUID: null + collectionUUID: null, + typeField: 'dc_type' }; beforeEach(() => { diff --git a/src/app/shared/form/builder/parsers/onebox-field-parser.spec.ts b/src/app/shared/form/builder/parsers/onebox-field-parser.spec.ts index 8ecce24194..e7e68a6461 100644 --- a/src/app/shared/form/builder/parsers/onebox-field-parser.spec.ts +++ b/src/app/shared/form/builder/parsers/onebox-field-parser.spec.ts @@ -15,7 +15,8 @@ describe('OneboxFieldParser test suite', () => { const parserOptions: ParserOptions = { readOnly: false, submissionScope: 'testScopeUUID', - collectionUUID: null + collectionUUID: null, + typeField: 'dc_type' }; beforeEach(() => { diff --git a/src/app/shared/form/builder/parsers/parser-options.ts b/src/app/shared/form/builder/parsers/parser-options.ts index 8b0b42008e..f7aac3449d 100644 --- a/src/app/shared/form/builder/parsers/parser-options.ts +++ b/src/app/shared/form/builder/parsers/parser-options.ts @@ -2,4 +2,5 @@ export interface ParserOptions { readOnly: boolean; submissionScope: string; collectionUUID: string; + typeField: string; } diff --git a/src/app/shared/form/builder/parsers/relation-group-field-parser.spec.ts b/src/app/shared/form/builder/parsers/relation-group-field-parser.spec.ts index 111193a637..7d48ad2d00 100644 --- a/src/app/shared/form/builder/parsers/relation-group-field-parser.spec.ts +++ b/src/app/shared/form/builder/parsers/relation-group-field-parser.spec.ts @@ -12,7 +12,8 @@ describe('RelationGroupFieldParser test suite', () => { const parserOptions: ParserOptions = { readOnly: false, submissionScope: 'testScopeUUID', - collectionUUID: 'WORKSPACE' + collectionUUID: 'WORKSPACE', + typeField: 'dc_type' }; beforeEach(() => { diff --git a/src/app/shared/form/builder/parsers/row-parser.spec.ts b/src/app/shared/form/builder/parsers/row-parser.spec.ts index e612534d55..1f9bde8a7f 100644 --- a/src/app/shared/form/builder/parsers/row-parser.spec.ts +++ b/src/app/shared/form/builder/parsers/row-parser.spec.ts @@ -22,6 +22,7 @@ describe('RowParser test suite', () => { const initFormValues = {}; const submissionScope = 'WORKSPACE'; const readOnly = false; + const typeField = 'dc_type'; beforeEach(() => { row1 = { @@ -338,7 +339,7 @@ describe('RowParser test suite', () => { it('should return a DynamicRowGroupModel object', () => { const parser = new RowParser(undefined); - const rowModel = parser.parse(submissionId, row1, scopeUUID, initFormValues, submissionScope, readOnly); + const rowModel = parser.parse(submissionId, row1, scopeUUID, initFormValues, submissionScope, readOnly, typeField); expect(rowModel instanceof DynamicRowGroupModel).toBe(true); }); @@ -346,7 +347,7 @@ describe('RowParser test suite', () => { it('should return a row with three fields', () => { const parser = new RowParser(undefined); - const rowModel = parser.parse(submissionId, row1, scopeUUID, initFormValues, submissionScope, readOnly); + const rowModel = parser.parse(submissionId, row1, scopeUUID, initFormValues, submissionScope, readOnly, typeField); expect((rowModel as DynamicRowGroupModel).group.length).toBe(3); }); @@ -354,7 +355,7 @@ describe('RowParser test suite', () => { it('should return a DynamicRowArrayModel object', () => { const parser = new RowParser(undefined); - const rowModel = parser.parse(submissionId, row2, scopeUUID, initFormValues, submissionScope, readOnly); + const rowModel = parser.parse(submissionId, row2, scopeUUID, initFormValues, submissionScope, readOnly, typeField); expect(rowModel instanceof DynamicRowArrayModel).toBe(true); }); @@ -362,7 +363,7 @@ describe('RowParser test suite', () => { it('should return a row that contains only scoped fields', () => { const parser = new RowParser(undefined); - const rowModel = parser.parse(submissionId, row3, scopeUUID, initFormValues, submissionScope, readOnly); + const rowModel = parser.parse(submissionId, row3, scopeUUID, initFormValues, submissionScope, readOnly, typeField); expect((rowModel as DynamicRowGroupModel).group.length).toBe(1); }); @@ -370,7 +371,7 @@ describe('RowParser test suite', () => { it('should be able to parse a dropdown combo field', () => { const parser = new RowParser(undefined); - const rowModel = parser.parse(submissionId, row4, scopeUUID, initFormValues, submissionScope, readOnly); + const rowModel = parser.parse(submissionId, row4, scopeUUID, initFormValues, submissionScope, readOnly, typeField); expect(rowModel).toBeDefined(); }); @@ -378,7 +379,7 @@ describe('RowParser test suite', () => { it('should be able to parse a lookup-name field', () => { const parser = new RowParser(undefined); - const rowModel = parser.parse(submissionId, row5, scopeUUID, initFormValues, submissionScope, readOnly); + const rowModel = parser.parse(submissionId, row5, scopeUUID, initFormValues, submissionScope, readOnly, typeField); expect(rowModel).toBeDefined(); }); @@ -386,7 +387,7 @@ describe('RowParser test suite', () => { it('should be able to parse a list field', () => { const parser = new RowParser(undefined); - const rowModel = parser.parse(submissionId, row6, scopeUUID, initFormValues, submissionScope, readOnly); + const rowModel = parser.parse(submissionId, row6, scopeUUID, initFormValues, submissionScope, readOnly, typeField); expect(rowModel).toBeDefined(); }); @@ -394,7 +395,7 @@ describe('RowParser test suite', () => { it('should be able to parse a date field', () => { const parser = new RowParser(undefined); - const rowModel = parser.parse(submissionId, row7, scopeUUID, initFormValues, submissionScope, readOnly); + const rowModel = parser.parse(submissionId, row7, scopeUUID, initFormValues, submissionScope, readOnly, typeField); expect(rowModel).toBeDefined(); }); @@ -402,7 +403,7 @@ describe('RowParser test suite', () => { it('should be able to parse a tag field', () => { const parser = new RowParser(undefined); - const rowModel = parser.parse(submissionId, row8, scopeUUID, initFormValues, submissionScope, readOnly); + const rowModel = parser.parse(submissionId, row8, scopeUUID, initFormValues, submissionScope, readOnly, typeField); expect(rowModel).toBeDefined(); }); @@ -410,7 +411,7 @@ describe('RowParser test suite', () => { it('should be able to parse a textarea field', () => { const parser = new RowParser(undefined); - const rowModel = parser.parse(submissionId, row9, scopeUUID, initFormValues, submissionScope, readOnly); + const rowModel = parser.parse(submissionId, row9, scopeUUID, initFormValues, submissionScope, readOnly, typeField); expect(rowModel).toBeDefined(); }); @@ -418,7 +419,7 @@ describe('RowParser test suite', () => { it('should be able to parse a group field', () => { const parser = new RowParser(undefined); - const rowModel = parser.parse(submissionId, row10, scopeUUID, initFormValues, submissionScope, readOnly); + const rowModel = parser.parse(submissionId, row10, scopeUUID, initFormValues, submissionScope, readOnly, typeField); expect(rowModel).toBeDefined(); }); diff --git a/src/app/shared/form/builder/parsers/row-parser.ts b/src/app/shared/form/builder/parsers/row-parser.ts index fe664305b0..764f52ffdf 100644 --- a/src/app/shared/form/builder/parsers/row-parser.ts +++ b/src/app/shared/form/builder/parsers/row-parser.ts @@ -31,7 +31,8 @@ export class RowParser { scopeUUID, initFormValues: any, submissionScope, - readOnly: boolean): DynamicRowGroupModel { + readOnly: boolean, + typeField: string): DynamicRowGroupModel { let fieldModel: any = null; let parsedResult = null; const config: DynamicFormGroupModelConfig = { @@ -47,7 +48,8 @@ export class RowParser { const parserOptions: ParserOptions = { readOnly: readOnly, submissionScope: submissionScope, - collectionUUID: scopeUUID + collectionUUID: scopeUUID, + typeField: typeField }; // Iterate over row's fields diff --git a/src/app/shared/form/builder/parsers/series-field-parser.spec.ts b/src/app/shared/form/builder/parsers/series-field-parser.spec.ts index b044f43833..0761cfe60e 100644 --- a/src/app/shared/form/builder/parsers/series-field-parser.spec.ts +++ b/src/app/shared/form/builder/parsers/series-field-parser.spec.ts @@ -12,7 +12,8 @@ describe('SeriesFieldParser test suite', () => { const parserOptions: ParserOptions = { readOnly: false, submissionScope: 'testScopeUUID', - collectionUUID: null + collectionUUID: null, + typeField: 'dc_type' }; beforeEach(() => { diff --git a/src/app/shared/form/builder/parsers/tag-field-parser.spec.ts b/src/app/shared/form/builder/parsers/tag-field-parser.spec.ts index 7c63235f67..115829f8d3 100644 --- a/src/app/shared/form/builder/parsers/tag-field-parser.spec.ts +++ b/src/app/shared/form/builder/parsers/tag-field-parser.spec.ts @@ -12,7 +12,8 @@ describe('TagFieldParser test suite', () => { const parserOptions: ParserOptions = { readOnly: false, submissionScope: 'testScopeUUID', - collectionUUID: null + collectionUUID: null, + typeField: 'dc_type' }; beforeEach(() => { diff --git a/src/app/shared/form/builder/parsers/textarea-field-parser.spec.ts b/src/app/shared/form/builder/parsers/textarea-field-parser.spec.ts index a81907aa13..855e464f21 100644 --- a/src/app/shared/form/builder/parsers/textarea-field-parser.spec.ts +++ b/src/app/shared/form/builder/parsers/textarea-field-parser.spec.ts @@ -12,7 +12,8 @@ describe('TextareaFieldParser test suite', () => { const parserOptions: ParserOptions = { readOnly: false, submissionScope: null, - collectionUUID: null + collectionUUID: null, + typeField: 'dc_type' }; beforeEach(() => { diff --git a/src/app/shared/mocks/find-id-config-data.service.mock.ts b/src/app/shared/mocks/find-id-config-data.service.mock.ts new file mode 100644 index 0000000000..c94fa6c0d6 --- /dev/null +++ b/src/app/shared/mocks/find-id-config-data.service.mock.ts @@ -0,0 +1,14 @@ +import { createSuccessfulRemoteDataObject$ } from '../remote-data.utils'; +import { ConfigurationProperty } from '../../core/shared/configuration-property.model'; + +export function getMockFindByIdDataService(propertyKey: string, ...values: string[]) { + return jasmine.createSpyObj('findByIdDataService', { + findByPropertyName: createSuccessfulRemoteDataObject$({ + ... new ConfigurationProperty(), + name: propertyKey, + values: values, + }) + }); +} + + diff --git a/src/app/shared/mocks/form-builder-service.mock.ts b/src/app/shared/mocks/form-builder-service.mock.ts index e37df20e13..6344ac6a6f 100644 --- a/src/app/shared/mocks/form-builder-service.mock.ts +++ b/src/app/shared/mocks/form-builder-service.mock.ts @@ -1,7 +1,9 @@ import { FormBuilderService } from '../form/builder/form-builder.service'; import { FormControl, FormGroup } from '@angular/forms'; +import {DsDynamicInputModel} from '../form/builder/ds-dynamic-form-ui/models/ds-dynamic-input.model'; export function getMockFormBuilderService(): FormBuilderService { + return jasmine.createSpyObj('FormBuilderService', { modelFromConfiguration: [], createFormGroup: new FormGroup({}), @@ -17,8 +19,26 @@ export function getMockFormBuilderService(): FormBuilderService { isQualdropGroup: false, isModelInCustomGroup: true, isRelationGroup: true, - hasArrayGroupValue: true - + hasArrayGroupValue: true, + getTypeBindModel: new DsDynamicInputModel({ + name: 'dc.type', + id: 'dc_type', + readOnly: false, + disabled: false, + repeatable: false, + value: { + value: 'boundType', + display: 'Bound Type', + authority: 'bound-auth-key' + }, + submissionId: '1234', + metadataFields: ['dc.type'], + hasSelectableMetadata: false, + typeBindRelations: [ + {match: 'VISIBLE', operator: 'OR', when: [{id: 'dc.type', value: 'boundType'}]} + ] + } + ) }); } diff --git a/src/app/shared/mocks/form-models.mock.ts b/src/app/shared/mocks/form-models.mock.ts index c43138fa25..3529f9e81b 100644 --- a/src/app/shared/mocks/form-models.mock.ts +++ b/src/app/shared/mocks/form-models.mock.ts @@ -89,7 +89,8 @@ const rowArrayQualdropConfig = { submissionId: '1234', metadataKey: 'dc.some.key', metadataFields: ['dc.some.key'], - hasSelectableMetadata: false + hasSelectableMetadata: false, + showButtons: true } as DynamicRowArrayModelConfig; export const MockRowArrayQualdropModel: DynamicRowArrayModel = new DynamicRowArrayModel(rowArrayQualdropConfig); @@ -305,3 +306,57 @@ export const mockFileFormEditRowGroupModel = new DynamicRowGroupModel({ id: 'mockRowGroupModel', group: [mockFileFormEditInputModel] }); + +// Mock configuration and model for an input with type binding +export const inputWithTypeBindConfig = { + name: 'testWithTypeBind', + id: 'testWithTypeBind', + readOnly: false, + disabled: false, + repeatable: false, + value: { + value: 'testWithTypeBind', + display: 'testWithTypeBind', + authority: 'bound-auth-key' + }, + submissionId: '1234', + metadataFields: [], + hasSelectableMetadata: false, + getTypeBindModel: new DsDynamicInputModel({ + name: 'testWithTypeBind', + id: 'testWithTypeBind', + readOnly: false, + disabled: false, + repeatable: false, + value: { + value: 'testWithTypeBind', + display: 'testWithTypeBind', + authority: 'bound-auth-key' + }, + submissionId: '1234', + metadataFields: [], + hasSelectableMetadata: false, + typeBindRelations: [ + {match: 'VISIBLE', operator: 'OR', when: [{'id': 'dc.type', 'value': 'boundType'}]} + ] + } + ) +}; + +export const mockInputWithTypeBindModel = new DsDynamicInputModel(inputWithAuthorityValueConfig); + +export const dcTypeInputConfig = { + name: 'dc.type', + id: 'dc_type', + readOnly: false, + disabled: false, + repeatable: false, + submissionId: '1234', + metadataFields: [], + hasSelectableMetadata: false, + value: { + value: 'boundType' + } +}; + +export const mockDcTypeInputModel = new DsDynamicInputModel(dcTypeInputConfig); diff --git a/src/app/submission/sections/form/section-form-operations.service.spec.ts b/src/app/submission/sections/form/section-form-operations.service.spec.ts index d5798b82c8..65ddbe0cb0 100644 --- a/src/app/submission/sections/form/section-form-operations.service.spec.ts +++ b/src/app/submission/sections/form/section-form-operations.service.spec.ts @@ -814,7 +814,9 @@ describe('SectionFormOperationsService test suite', () => { required: false, metadataKey: 'dc.contributor.author', metadataFields: ['dc.contributor.author'], - hasSelectableMetadata: true + hasSelectableMetadata: true, + showButtons: true, + typeBindRelations: [] } ); spyOn(serviceAsAny, 'getFieldPathSegmentedFromChangeEvent').and.returnValue('path'); diff --git a/src/app/submission/submission.module.ts b/src/app/submission/submission.module.ts index 939d1bff29..05aa765054 100644 --- a/src/app/submission/submission.module.ts +++ b/src/app/submission/submission.module.ts @@ -97,7 +97,7 @@ const DECLARATIONS = [ SectionsService, SubmissionUploadsConfigService, SubmissionAccessesConfigService, - SectionAccessesService + SectionAccessesService, ] }) diff --git a/src/config/default-app-config.ts b/src/config/default-app-config.ts index 4c68a38081..476351b403 100644 --- a/src/config/default-app-config.ts +++ b/src/config/default-app-config.ts @@ -111,6 +111,9 @@ export class DefaultAppConfig implements AppConfig { */ timer: 0 }, + typeBind: { + field: 'dc.type' + }, icons: { metadata: [ /** diff --git a/src/config/submission-config.interface.ts b/src/config/submission-config.interface.ts index ce275b9bf8..a63af45e38 100644 --- a/src/config/submission-config.interface.ts +++ b/src/config/submission-config.interface.ts @@ -5,6 +5,10 @@ interface AutosaveConfig extends Config { timer: number; } +interface TypeBindConfig extends Config { + field: string; +} + interface IconsConfig extends Config { metadata: MetadataIconConfig[]; authority: { @@ -24,5 +28,6 @@ export interface ConfidenceIconConfig extends Config { export interface SubmissionConfig extends Config { autosave: AutosaveConfig; + typeBind: TypeBindConfig; icons: IconsConfig; } diff --git a/src/environments/environment.test.ts b/src/environments/environment.test.ts index b2eb0081da..4fb68521fc 100644 --- a/src/environments/environment.test.ts +++ b/src/environments/environment.test.ts @@ -100,6 +100,9 @@ export const environment: BuildConfig = { // NOTE: every how many minutes submission is saved automatically timer: 5 }, + typeBind: { + field: 'dc.type' + }, icons: { metadata: [ { diff --git a/src/styles/_global-styles.scss b/src/styles/_global-styles.scss index 4afd2eb05e..4a855a635e 100644 --- a/src/styles/_global-styles.scss +++ b/src/styles/_global-styles.scss @@ -108,3 +108,10 @@ ngb-modal-backdrop { .ml-gap { margin-left: var(--ds-gap); } + +ds-dynamic-form-control-container.d-none { + /* Ensures that form-control containers hidden and disabled by type binding collapse and let other fields in + the same row expand accordingly + */ + visibility: collapse; +}