diff --git a/src/app/app.module.ts b/src/app/app.module.ts index a8014b78ac..7aeb0f7f3f 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -72,6 +72,7 @@ import { ResultsComponent } from './workspace/results/results.component'; import { environment } from '../environments/environment'; import { ExternalLinksDirective } from './main/directive/external-links.directive'; +import { InvalidControlScrollDirective } from './main/directive/invalid-control-scroll.directive'; import { SelectProjectComponent } from './workspace/resource/resource-instance-form/select-project/select-project.component'; import { SelectOntologyComponent } from './workspace/resource/resource-instance-form/select-ontology/select-ontology.component'; import { SelectResourceClassComponent } from './workspace/resource/resource-instance-form/select-resource-class/select-resource-class.component'; @@ -145,6 +146,7 @@ export function HttpLoaderFactory(httpClient: HttpClient) { HelpComponent, FooterComponent, ExternalLinksDirective, + InvalidControlScrollDirective, ResourceInstanceFormComponent, SelectProjectComponent, SelectOntologyComponent, diff --git a/src/app/main/directive/invalid-control-scroll.directive.spec.ts b/src/app/main/directive/invalid-control-scroll.directive.spec.ts new file mode 100644 index 0000000000..f86c835ef0 --- /dev/null +++ b/src/app/main/directive/invalid-control-scroll.directive.spec.ts @@ -0,0 +1,71 @@ +import { Component, OnInit } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms'; +import { InvalidControlScrollDirective } from './invalid-control-scroll.directive'; + +@Component({ + template: ` +
+
+ + +
+
+ + +
+
+ + +
+ +
+ ` +}) +class TestLinkHostComponent implements OnInit { + + form: FormGroup; + + constructor() { } + + ngOnInit() { + this.form = new FormGroup({ + control1: new FormControl(), + control2: new FormControl(), + control3: new FormControl() + }); + } + + onSubmit() { + console.log('form submitted'); + } +} + +describe('InvalidControlScrollDirective', () => { + + let testHostComponent: TestLinkHostComponent; + let testHostFixture: ComponentFixture; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [ + InvalidControlScrollDirective, + TestLinkHostComponent + ], + imports: [ + ReactiveFormsModule + ] + }); + + testHostFixture = TestBed.createComponent(TestLinkHostComponent); + testHostComponent = testHostFixture.componentInstance; + testHostFixture.detectChanges(); + + expect(testHostComponent).toBeTruthy(); + + }); + + it('should create an instance', () => { + expect(testHostComponent).toBeTruthy(); + }); +}); diff --git a/src/app/main/directive/invalid-control-scroll.directive.ts b/src/app/main/directive/invalid-control-scroll.directive.ts new file mode 100644 index 0000000000..22bc93d3e3 --- /dev/null +++ b/src/app/main/directive/invalid-control-scroll.directive.ts @@ -0,0 +1,37 @@ +import { Directive, ElementRef, HostListener } from '@angular/core'; +import { FormGroupDirective } from '@angular/forms'; + +@Directive({ + selector: '[appInvalidControlScroll]' +}) +export class InvalidControlScrollDirective { + + constructor( + private _el: ElementRef, + private _formGroupDir: FormGroupDirective + ) { } + + @HostListener("ngSubmit") submitData() { + if (this._formGroupDir.control.invalid) { + this._scrollToFirstInvalidControl(); + } + } + + /** + * Target the first invalid element of the resource-instance form (2nd panel property) and scroll to it + */ + private _scrollToFirstInvalidControl() { + // target the first invalid form field + const firstInvalidControl: HTMLElement = this._el.nativeElement.querySelector( + "form .ng-invalid" + ); + + // scroll to the first invalid element in a smooth way + firstInvalidControl.scrollIntoView({ + behavior: "smooth", + block: "nearest", + inline: "nearest" + }); + } + +} diff --git a/src/app/workspace/resource/resource-instance-form/resource-instance-form.component.html b/src/app/workspace/resource/resource-instance-form/resource-instance-form.component.html index adf906f806..9e95e1c35a 100644 --- a/src/app/workspace/resource/resource-instance-form/resource-instance-form.component.html +++ b/src/app/workspace/resource/resource-instance-form/resource-instance-form.component.html @@ -50,7 +50,11 @@ - @@ -60,7 +64,7 @@ -
+ - diff --git a/src/app/workspace/resource/resource-instance-form/resource-instance-form.component.ts b/src/app/workspace/resource/resource-instance-form/resource-instance-form.component.ts index 940c7d1bb1..e5fff9c68f 100644 --- a/src/app/workspace/resource/resource-instance-form/resource-instance-form.component.ts +++ b/src/app/workspace/resource/resource-instance-form/resource-instance-form.component.ts @@ -133,47 +133,52 @@ export class ResourceInstanceFormComponent implements OnInit, OnDestroy { submitData() { - const createResource = new CreateResource(); + if (this.propertiesParentForm.valid) { - createResource.label = this.resourceLabel; + const createResource = new CreateResource(); - createResource.type = this.selectedResourceClass.id; + createResource.label = this.resourceLabel; - createResource.attachedToProject = this.selectedProject; + createResource.type = this.selectedResourceClass.id; - this.selectPropertiesComponent.switchPropertiesComponent.forEach((child) => { - const createVal = child.createValueComponent.getNewValue(); - const iri = child.property.id; - if (createVal instanceof CreateValue) { - if (this.propertiesObj[iri]) { - // if a key already exists, add the createVal to the array - this.propertiesObj[iri].push(createVal); - } else { - // if no key exists, add one and add the createVal as the first value of the array - this.propertiesObj[iri] = [createVal]; + createResource.attachedToProject = this.selectedProject; + + this.selectPropertiesComponent.switchPropertiesComponent.forEach((child) => { + const createVal = child.createValueComponent.getNewValue(); + const iri = child.property.id; + if (createVal instanceof CreateValue) { + if (this.propertiesObj[iri]) { + // if a key already exists, add the createVal to the array + this.propertiesObj[iri].push(createVal); + } else { + // if no key exists, add one and add the createVal as the first value of the array + this.propertiesObj[iri] = [createVal]; + } } - } - }); + }); - createResource.properties = this.propertiesObj; + createResource.properties = this.propertiesObj; - this._dspApiConnection.v2.res.createResource(createResource).subscribe( - (res: ReadResource) => { - this.resource = res; + this._dspApiConnection.v2.res.createResource(createResource).subscribe( + (res: ReadResource) => { + this.resource = res; - // navigate to the resource viewer page - this._router.navigateByUrl('/resource', { skipLocationChange: true }).then(() => - this._router.navigate(['/resource/' + encodeURIComponent(this.resource.id)]) - ); + // navigate to the resource viewer page + this._router.navigateByUrl('/resource', { skipLocationChange: true }).then(() => + this._router.navigate(['/resource/' + encodeURIComponent(this.resource.id)]) + ); - this.closeDialog.emit(); - }, - (error: ApiResponseError) => { - this._errorHandler.showMessage(error); - } - ); + this.closeDialog.emit(); + }, + (error: ApiResponseError) => { + this._errorHandler.showMessage(error); + } + ); + } else { + this.propertiesParentForm.markAllAsTouched(); + } } /** diff --git a/src/app/workspace/resource/resource-instance-form/select-project/select-project.component.spec.ts b/src/app/workspace/resource/resource-instance-form/select-project/select-project.component.spec.ts index 350314b5ce..a017642761 100644 --- a/src/app/workspace/resource/resource-instance-form/select-project/select-project.component.spec.ts +++ b/src/app/workspace/resource/resource-instance-form/select-project/select-project.component.spec.ts @@ -60,13 +60,7 @@ describe('SelectProjectComponent', () => { FormsModule, BrowserAnimationsModule, MatFormFieldModule, - MatSelectModule ], - providers: [ - { - provide: DspApiConnectionToken, - useValue: new KnoraApiConnection(TestConfig.ApiConfig) - } - ] + MatSelectModule ] }) .compileComponents(); })); diff --git a/src/app/workspace/resource/resource-instance-form/select-project/select-project.component.ts b/src/app/workspace/resource/resource-instance-form/select-project/select-project.component.ts index 3020a5d9ad..4ec0f4c1c0 100644 --- a/src/app/workspace/resource/resource-instance-form/select-project/select-project.component.ts +++ b/src/app/workspace/resource/resource-instance-form/select-project/select-project.component.ts @@ -36,7 +36,6 @@ export class SelectProjectComponent implements OnInit, OnDestroy, AfterViewInit projectChangesSubscription: Subscription; constructor( - @Inject(DspApiConnectionToken) private _dspApiConnection: KnoraApiConnection, @Inject(FormBuilder) private _fb: FormBuilder) { } ngOnInit(): void { diff --git a/src/app/workspace/resource/resource-instance-form/select-properties/select-properties.component.html b/src/app/workspace/resource/resource-instance-form/select-properties/select-properties.component.html index 9984fab463..ec75ca9a63 100644 --- a/src/app/workspace/resource/resource-instance-form/select-properties/select-properties.component.html +++ b/src/app/workspace/resource/resource-instance-form/select-properties/select-properties.component.html @@ -3,12 +3,17 @@
-

+

{{prop.label}}

+ + + * +
@@ -19,7 +24,8 @@ [property]="prop" [parentResource]="parentResource" [parentForm]="parentForm" - [formName]="prop.label + '_' + i"> + [formName]="prop.label + '_' + i" + [isRequiredProp]="propertyValuesKeyValuePair[prop.id + '-cardinality']">
diff --git a/src/app/workspace/resource/resource-instance-form/select-properties/select-properties.component.scss b/src/app/workspace/resource/resource-instance-form/select-properties/select-properties.component.scss index 53a0e43cd7..d236932b33 100644 --- a/src/app/workspace/resource/resource-instance-form/select-properties/select-properties.component.scss +++ b/src/app/workspace/resource/resource-instance-form/select-properties/select-properties.component.scss @@ -40,11 +40,21 @@ .label { text-align: right; + display: block; + float: left; + width: 95%; } .label-info { cursor: help; } + + .propIsRequired { + color: red; + display: block; + float: right; + width: 5%; + } } .property-value { diff --git a/src/app/workspace/resource/resource-instance-form/select-properties/select-properties.component.spec.ts b/src/app/workspace/resource/resource-instance-form/select-properties/select-properties.component.spec.ts index dd1595560a..793aef53f7 100644 --- a/src/app/workspace/resource/resource-instance-form/select-properties/select-properties.component.spec.ts +++ b/src/app/workspace/resource/resource-instance-form/select-properties/select-properties.component.spec.ts @@ -97,8 +97,8 @@ describe('SelectPropertiesComponent', () => { } } - // each property has two entries in the keyValuePair object - expect(propsArray.length).toEqual(18 * 2); + // each property has three entries in the keyValuePair object + expect(propsArray.length).toEqual(18 * 3); }); describe('Add/Delete functionality', () => { diff --git a/src/app/workspace/resource/resource-instance-form/select-properties/select-properties.component.ts b/src/app/workspace/resource/resource-instance-form/select-properties/select-properties.component.ts index d1020ad5ec..cd58e271b2 100644 --- a/src/app/workspace/resource/resource-instance-form/select-properties/select-properties.component.ts +++ b/src/app/workspace/resource/resource-instance-form/select-properties/select-properties.component.ts @@ -1,6 +1,6 @@ -import { Component, Input, OnInit, QueryList, ViewChildren } from '@angular/core'; +import { AfterViewInit, Component, Input, OnInit, QueryList, ViewChildren } from '@angular/core'; import { FormGroup } from '@angular/forms'; -import { CardinalityUtil, ReadResource, ResourceClassAndPropertyDefinitions, ResourceClassDefinition, ResourcePropertyDefinition } from '@dasch-swiss/dsp-js'; +import { Cardinality, CardinalityUtil, IHasProperty, ReadResource, ResourceClassAndPropertyDefinitions, ResourceClassDefinition, ResourcePropertyDefinition } from '@dasch-swiss/dsp-js'; import { ValueService } from '@dasch-swiss/dsp-ui'; import { SwitchPropertiesComponent } from './switch-properties/switch-properties.component'; @@ -29,6 +29,8 @@ export class SelectPropertiesComponent implements OnInit { addButtonIsVisible: boolean; + isRequiredProp: boolean; + constructor(private _valueService: ValueService) { } ngOnInit() { @@ -45,6 +47,11 @@ export class SelectPropertiesComponent implements OnInit { // each property will also have a filtered array to be used when deleting a value. // see the deleteValue method below for more info this.propertyValuesKeyValuePair[prop.id + '-filtered'] = [0]; + + // each property will also have a cardinality array to be used when marking a field as required + // see the isPropRequired method below for more info + this.isPropRequired(prop.id); + this.propertyValuesKeyValuePair[prop.id + '-cardinality'] = [this.isRequiredProp ? 1 : 0]; } } } @@ -52,6 +59,7 @@ export class SelectPropertiesComponent implements OnInit { this.parentResource.entityInfo = this.ontologyInfo; } + /** * Given a resource property, check if an add button should be displayed under the property values * @@ -65,6 +73,31 @@ export class SelectPropertiesComponent implements OnInit { ); } + /** + * Check the cardinality of a property + * If the cardinality is 1 or 1-N, the property will be marked as required + * If the cardinality is 0-1 or 0-N, the property will not be required + * + * @param propId property id + */ + isPropRequired(propId: string): boolean { + if (this.resourceClass !== undefined && propId) { + this.resourceClass.propertiesList.filter( + (card: IHasProperty) => { + if (card.propertyIndex === propId) { + // cardinality 1 or 1-N + if (card.cardinality === Cardinality._1 || card.cardinality === Cardinality._1_n) { + this.isRequiredProp = true; + } else { // cardinality 0-1 or 0-N + this.isRequiredProp = false; + } + } + } + ); + return this.isRequiredProp; + } + } + /** * Called from the template when the user clicks on the add button */ diff --git a/src/app/workspace/resource/resource-instance-form/select-properties/switch-properties/switch-properties.component.html b/src/app/workspace/resource/resource-instance-form/select-properties/switch-properties/switch-properties.component.html index e915b03a5c..4e841e88b3 100644 --- a/src/app/workspace/resource/resource-instance-form/select-properties/switch-properties/switch-properties.component.html +++ b/src/app/workspace/resource/resource-instance-form/select-properties/switch-properties/switch-properties.component.html @@ -1,18 +1,18 @@ - - - - - - - - - - - + + + + + + + + + + - - +

Cannot match any value component for {{property.objectType}}

diff --git a/src/app/workspace/resource/resource-instance-form/select-properties/switch-properties/switch-properties.component.ts b/src/app/workspace/resource/resource-instance-form/select-properties/switch-properties/switch-properties.component.ts index 2fbe9287e3..44ba10e11d 100644 --- a/src/app/workspace/resource/resource-instance-form/select-properties/switch-properties/switch-properties.component.ts +++ b/src/app/workspace/resource/resource-instance-form/select-properties/switch-properties/switch-properties.component.ts @@ -1,4 +1,4 @@ -import { AfterViewInit, Component, Input, OnInit, ViewChild } from '@angular/core'; +import { Component, Input, OnInit, ViewChild } from '@angular/core'; import { FormGroup } from '@angular/forms'; import { Constants, ReadResource, ResourcePropertyDefinition } from '@dasch-swiss/dsp-js'; import { BaseValueComponent } from 'src/app/base-value.component'; @@ -8,7 +8,7 @@ import { BaseValueComponent } from 'src/app/base-value.component'; templateUrl: './switch-properties.component.html', styleUrls: ['./switch-properties.component.scss'] }) -export class SwitchPropertiesComponent implements OnInit, AfterViewInit { +export class SwitchPropertiesComponent implements OnInit { @ViewChild('createVal') createValueComponent: BaseValueComponent; @@ -20,23 +20,21 @@ export class SwitchPropertiesComponent implements OnInit, AfterViewInit { @Input() formName: string; + @Input() isRequiredProp: boolean; + mode = 'create'; constants = Constants; constructor() { } ngOnInit(): void { - // console.log('prop', this.property); - } - - ngAfterViewInit() { - // console.log('createValueComponent', this.createValueComponent); - // this.saveNewValue(); + // the input isRequiredProp provided by KeyValuePair is stored as a number + // a conversion from a number to a boolean is required by the input valueRequiredValidator + this.isRequiredProp = !!+this.isRequiredProp; } saveNewValue() { const createVal = this.createValueComponent.getNewValue(); - console.log('createVal', createVal); } }