Skip to content
Permalink
Browse files
feat(new-resource-form): make visible the required prop fields (DSP-1115
) (#342)

* feat(select-properties): check if the prop is required according to its cardinality

* test(select-properties): fix test

* style(select-properties): fix the alignment label-asterisk

* feat(switch-properties): add input valueRequiredValidator

* fix(switch-properties): pass to the input valueRequiredValidator a variable instead of false

* feat(resource-instance-form): scroll to the first invalid form field

* fix(invalid-control-scroll-container): fix the spec file

* fix(invalid-control-scroll): fix basic test

* refactor(invalid-control-scroll): simplify the directives + update scrolling config

* refactor: clean up + add some explanations

* refactor(select-properties): update comment

* refactor(select-properties): isPropRequired(id) returns a boolean

* refactor(switch-properties): update comment

* refactor(select-properties): update comments

Co-authored-by: flaurens <flavie.laurens@unibas.ch>
  • Loading branch information
flavens and flaurens committed Jan 19, 2021
1 parent 690a55e commit 5885b041c2c7c223277a50ccc0495872b1d099fe
@@ -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,
@@ -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: `
<form [formGroup]="form" (ngSubmit)="onSubmit()" appInvalidControlScroll>
<div class="form-group">
<label for="control1">Field 1</label>
<input class="form-control" formControlName="control1"/>
</div>
<div class="form-group">
<label for="control1">Field 2</label>
<input class="form-control" formControlName="control2"/>
</div>
<div class="form-group">
<label for="control1">Field 3</label>
<input class="form-control" formControlName="control3"/>
</div>
<button type="submit" class="btn btn-primary">Submit</button>
</form>
`
})
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<TestLinkHostComponent>;

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();
});
});
@@ -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"
});
}

}
@@ -50,7 +50,11 @@
</span>
<span class="fill-remaining-space"></span>
<span>
<button mat-raised-button type="button" color="primary" [disabled]="!selectResourceForm.valid || this.errorMessage"
<button
mat-raised-button
type="button"
color="primary"
[disabled]="!selectResourceForm.valid || this.errorMessage"
(click)="nextStep()" class="form-next">
Next
</button>
@@ -60,7 +64,7 @@
</form>

<!-- Step2: create property values and submit data -->
<form *ngIf="propertiesParentForm && !showNextStepForm" [formGroup]="propertiesParentForm" class="resource-instance-form stepTwo form-content" (ngSubmit)="submitData()">
<form *ngIf="propertiesParentForm && !showNextStepForm" [formGroup]="propertiesParentForm" class="resource-instance-form stepTwo form-content" (ngSubmit)="submitData()" appInvalidControlScroll>

<!-- create property values -->
<app-select-properties
@@ -84,7 +88,7 @@
</button>
<span class="fill-remaining-space"></span>
<span>
<button mat-raised-button type="submit" color="primary" [disabled]="!propertiesParentForm.valid" class="form-submit">
<button mat-raised-button type="submit" color="primary" class="form-submit">
{{ 'appLabels.form.action.submit' | translate}}
</button>
</span>
@@ -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();
}
}

/**
@@ -60,13 +60,7 @@ describe('SelectProjectComponent', () => {
FormsModule,
BrowserAnimationsModule,
MatFormFieldModule,
MatSelectModule ],
providers: [
{
provide: DspApiConnectionToken,
useValue: new KnoraApiConnection(TestConfig.ApiConfig)
}
]
MatSelectModule ]
})
.compileComponents();
}));
@@ -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 {
@@ -3,12 +3,17 @@
<div *ngFor="let prop of properties; let last = last;" [class.border-bottom]="!last">
<div class="property" *ngIf="!prop.isLinkProperty || prop">
<div class="property-label">
<h3 class="label mat-subheading-1"
<span>
<h3 class="label mat-subheading-1"
[class.label-info]="prop.comment"
[matTooltip]="prop.comment"
matTooltipPosition="above">
{{prop.label}}
</h3>
</span>
<span *ngIf="propertyValuesKeyValuePair[prop.id + '-cardinality'][0] === 1" class="propIsRequired">
*
</span>
</div>
<div class="property-value large-field">
<div *ngFor="let val of propertyValuesKeyValuePair[prop.id]; let i=index">
@@ -19,7 +24,8 @@
[property]="prop"
[parentResource]="parentResource"
[parentForm]="parentForm"
[formName]="prop.label + '_' + i">
[formName]="prop.label + '_' + i"
[isRequiredProp]="propertyValuesKeyValuePair[prop.id + '-cardinality']">
</app-switch-properties>
</div>
<div class="buttons">
@@ -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 {
@@ -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', () => {
@@ -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,13 +47,19 @@ 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];
}
}
}

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
*/
Loading

0 comments on commit 5885b04

Please sign in to comment.