Skip to content

Commit 5885b04

Browse files
flavensflaurens
andauthored
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>
1 parent 690a55e commit 5885b04

13 files changed

Lines changed: 228 additions & 69 deletions

‎src/app/app.module.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ import { ResultsComponent } from './workspace/results/results.component';
7272

7373
import { environment } from '../environments/environment';
7474
import { ExternalLinksDirective } from './main/directive/external-links.directive';
75+
import { InvalidControlScrollDirective } from './main/directive/invalid-control-scroll.directive';
7576
import { SelectProjectComponent } from './workspace/resource/resource-instance-form/select-project/select-project.component';
7677
import { SelectOntologyComponent } from './workspace/resource/resource-instance-form/select-ontology/select-ontology.component';
7778
import { SelectResourceClassComponent } from './workspace/resource/resource-instance-form/select-resource-class/select-resource-class.component';
@@ -145,6 +146,7 @@ export function HttpLoaderFactory(httpClient: HttpClient) {
145146
HelpComponent,
146147
FooterComponent,
147148
ExternalLinksDirective,
149+
InvalidControlScrollDirective,
148150
ResourceInstanceFormComponent,
149151
SelectProjectComponent,
150152
SelectOntologyComponent,
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { Component, OnInit } from '@angular/core';
2+
import { ComponentFixture, TestBed } from '@angular/core/testing';
3+
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
4+
import { InvalidControlScrollDirective } from './invalid-control-scroll.directive';
5+
6+
@Component({
7+
template: `
8+
<form [formGroup]="form" (ngSubmit)="onSubmit()" appInvalidControlScroll>
9+
<div class="form-group">
10+
<label for="control1">Field 1</label>
11+
<input class="form-control" formControlName="control1"/>
12+
</div>
13+
<div class="form-group">
14+
<label for="control1">Field 2</label>
15+
<input class="form-control" formControlName="control2"/>
16+
</div>
17+
<div class="form-group">
18+
<label for="control1">Field 3</label>
19+
<input class="form-control" formControlName="control3"/>
20+
</div>
21+
<button type="submit" class="btn btn-primary">Submit</button>
22+
</form>
23+
`
24+
})
25+
class TestLinkHostComponent implements OnInit {
26+
27+
form: FormGroup;
28+
29+
constructor() { }
30+
31+
ngOnInit() {
32+
this.form = new FormGroup({
33+
control1: new FormControl(),
34+
control2: new FormControl(),
35+
control3: new FormControl()
36+
});
37+
}
38+
39+
onSubmit() {
40+
console.log('form submitted');
41+
}
42+
}
43+
44+
describe('InvalidControlScrollDirective', () => {
45+
46+
let testHostComponent: TestLinkHostComponent;
47+
let testHostFixture: ComponentFixture<TestLinkHostComponent>;
48+
49+
beforeEach(() => {
50+
TestBed.configureTestingModule({
51+
declarations: [
52+
InvalidControlScrollDirective,
53+
TestLinkHostComponent
54+
],
55+
imports: [
56+
ReactiveFormsModule
57+
]
58+
});
59+
60+
testHostFixture = TestBed.createComponent(TestLinkHostComponent);
61+
testHostComponent = testHostFixture.componentInstance;
62+
testHostFixture.detectChanges();
63+
64+
expect(testHostComponent).toBeTruthy();
65+
66+
});
67+
68+
it('should create an instance', () => {
69+
expect(testHostComponent).toBeTruthy();
70+
});
71+
});
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { Directive, ElementRef, HostListener } from '@angular/core';
2+
import { FormGroupDirective } from '@angular/forms';
3+
4+
@Directive({
5+
selector: '[appInvalidControlScroll]'
6+
})
7+
export class InvalidControlScrollDirective {
8+
9+
constructor(
10+
private _el: ElementRef,
11+
private _formGroupDir: FormGroupDirective
12+
) { }
13+
14+
@HostListener("ngSubmit") submitData() {
15+
if (this._formGroupDir.control.invalid) {
16+
this._scrollToFirstInvalidControl();
17+
}
18+
}
19+
20+
/**
21+
* Target the first invalid element of the resource-instance form (2nd panel property) and scroll to it
22+
*/
23+
private _scrollToFirstInvalidControl() {
24+
// target the first invalid form field
25+
const firstInvalidControl: HTMLElement = this._el.nativeElement.querySelector(
26+
"form .ng-invalid"
27+
);
28+
29+
// scroll to the first invalid element in a smooth way
30+
firstInvalidControl.scrollIntoView({
31+
behavior: "smooth",
32+
block: "nearest",
33+
inline: "nearest"
34+
});
35+
}
36+
37+
}

‎src/app/workspace/resource/resource-instance-form/resource-instance-form.component.html‎

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,11 @@
5050
</span>
5151
<span class="fill-remaining-space"></span>
5252
<span>
53-
<button mat-raised-button type="button" color="primary" [disabled]="!selectResourceForm.valid || this.errorMessage"
53+
<button
54+
mat-raised-button
55+
type="button"
56+
color="primary"
57+
[disabled]="!selectResourceForm.valid || this.errorMessage"
5458
(click)="nextStep()" class="form-next">
5559
Next
5660
</button>
@@ -60,7 +64,7 @@
6064
</form>
6165

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

6569
<!-- create property values -->
6670
<app-select-properties
@@ -84,7 +88,7 @@
8488
</button>
8589
<span class="fill-remaining-space"></span>
8690
<span>
87-
<button mat-raised-button type="submit" color="primary" [disabled]="!propertiesParentForm.valid" class="form-submit">
91+
<button mat-raised-button type="submit" color="primary" class="form-submit">
8892
{{ 'appLabels.form.action.submit' | translate}}
8993
</button>
9094
</span>

‎src/app/workspace/resource/resource-instance-form/resource-instance-form.component.ts‎

Lines changed: 35 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -133,47 +133,52 @@ export class ResourceInstanceFormComponent implements OnInit, OnDestroy {
133133

134134
submitData() {
135135

136-
const createResource = new CreateResource();
136+
if (this.propertiesParentForm.valid) {
137137

138-
createResource.label = this.resourceLabel;
138+
const createResource = new CreateResource();
139139

140-
createResource.type = this.selectedResourceClass.id;
140+
createResource.label = this.resourceLabel;
141141

142-
createResource.attachedToProject = this.selectedProject;
142+
createResource.type = this.selectedResourceClass.id;
143143

144-
this.selectPropertiesComponent.switchPropertiesComponent.forEach((child) => {
145-
const createVal = child.createValueComponent.getNewValue();
146-
const iri = child.property.id;
147-
if (createVal instanceof CreateValue) {
148-
if (this.propertiesObj[iri]) {
149-
// if a key already exists, add the createVal to the array
150-
this.propertiesObj[iri].push(createVal);
151-
} else {
152-
// if no key exists, add one and add the createVal as the first value of the array
153-
this.propertiesObj[iri] = [createVal];
144+
createResource.attachedToProject = this.selectedProject;
145+
146+
this.selectPropertiesComponent.switchPropertiesComponent.forEach((child) => {
147+
const createVal = child.createValueComponent.getNewValue();
148+
const iri = child.property.id;
149+
if (createVal instanceof CreateValue) {
150+
if (this.propertiesObj[iri]) {
151+
// if a key already exists, add the createVal to the array
152+
this.propertiesObj[iri].push(createVal);
153+
} else {
154+
// if no key exists, add one and add the createVal as the first value of the array
155+
this.propertiesObj[iri] = [createVal];
156+
}
154157
}
155-
}
156158

157-
});
159+
});
158160

159-
createResource.properties = this.propertiesObj;
161+
createResource.properties = this.propertiesObj;
160162

161-
this._dspApiConnection.v2.res.createResource(createResource).subscribe(
162-
(res: ReadResource) => {
163-
this.resource = res;
163+
this._dspApiConnection.v2.res.createResource(createResource).subscribe(
164+
(res: ReadResource) => {
165+
this.resource = res;
164166

165-
// navigate to the resource viewer page
166-
this._router.navigateByUrl('/resource', { skipLocationChange: true }).then(() =>
167-
this._router.navigate(['/resource/' + encodeURIComponent(this.resource.id)])
168-
);
167+
// navigate to the resource viewer page
168+
this._router.navigateByUrl('/resource', { skipLocationChange: true }).then(() =>
169+
this._router.navigate(['/resource/' + encodeURIComponent(this.resource.id)])
170+
);
169171

170-
this.closeDialog.emit();
171-
},
172-
(error: ApiResponseError) => {
173-
this._errorHandler.showMessage(error);
174-
}
175-
);
172+
this.closeDialog.emit();
173+
},
174+
(error: ApiResponseError) => {
175+
this._errorHandler.showMessage(error);
176+
}
177+
);
176178

179+
} else {
180+
this.propertiesParentForm.markAllAsTouched();
181+
}
177182
}
178183

179184
/**

‎src/app/workspace/resource/resource-instance-form/select-project/select-project.component.spec.ts‎

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -60,13 +60,7 @@ describe('SelectProjectComponent', () => {
6060
FormsModule,
6161
BrowserAnimationsModule,
6262
MatFormFieldModule,
63-
MatSelectModule ],
64-
providers: [
65-
{
66-
provide: DspApiConnectionToken,
67-
useValue: new KnoraApiConnection(TestConfig.ApiConfig)
68-
}
69-
]
63+
MatSelectModule ]
7064
})
7165
.compileComponents();
7266
}));

‎src/app/workspace/resource/resource-instance-form/select-project/select-project.component.ts‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ export class SelectProjectComponent implements OnInit, OnDestroy, AfterViewInit
3636
projectChangesSubscription: Subscription;
3737

3838
constructor(
39-
@Inject(DspApiConnectionToken) private _dspApiConnection: KnoraApiConnection,
4039
@Inject(FormBuilder) private _fb: FormBuilder) { }
4140

4241
ngOnInit(): void {

‎src/app/workspace/resource/resource-instance-form/select-properties/select-properties.component.html‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,17 @@
33
<div *ngFor="let prop of properties; let last = last;" [class.border-bottom]="!last">
44
<div class="property" *ngIf="!prop.isLinkProperty || prop">
55
<div class="property-label">
6-
<h3 class="label mat-subheading-1"
6+
<span>
7+
<h3 class="label mat-subheading-1"
78
[class.label-info]="prop.comment"
89
[matTooltip]="prop.comment"
910
matTooltipPosition="above">
1011
{{prop.label}}
1112
</h3>
13+
</span>
14+
<span *ngIf="propertyValuesKeyValuePair[prop.id + '-cardinality'][0] === 1" class="propIsRequired">
15+
*
16+
</span>
1217
</div>
1318
<div class="property-value large-field">
1419
<div *ngFor="let val of propertyValuesKeyValuePair[prop.id]; let i=index">
@@ -19,7 +24,8 @@
1924
[property]="prop"
2025
[parentResource]="parentResource"
2126
[parentForm]="parentForm"
22-
[formName]="prop.label + '_' + i">
27+
[formName]="prop.label + '_' + i"
28+
[isRequiredProp]="propertyValuesKeyValuePair[prop.id + '-cardinality']">
2329
</app-switch-properties>
2430
</div>
2531
<div class="buttons">

‎src/app/workspace/resource/resource-instance-form/select-properties/select-properties.component.scss‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,21 @@
4040

4141
.label {
4242
text-align: right;
43+
display: block;
44+
float: left;
45+
width: 95%;
4346
}
4447

4548
.label-info {
4649
cursor: help;
4750
}
51+
52+
.propIsRequired {
53+
color: red;
54+
display: block;
55+
float: right;
56+
width: 5%;
57+
}
4858
}
4959

5060
.property-value {

‎src/app/workspace/resource/resource-instance-form/select-properties/select-properties.component.spec.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,8 +97,8 @@ describe('SelectPropertiesComponent', () => {
9797
}
9898
}
9999

100-
// each property has two entries in the keyValuePair object
101-
expect(propsArray.length).toEqual(18 * 2);
100+
// each property has three entries in the keyValuePair object
101+
expect(propsArray.length).toEqual(18 * 3);
102102
});
103103

104104
describe('Add/Delete functionality', () => {

0 commit comments

Comments
 (0)