Skip to content

Commit

Permalink
DSP-1062 - Form "back" button functionality polish (#329)
Browse files Browse the repository at this point in the history
* enhancement (resource-instance-form): when clicking on the back button, the users previous selections will be preserved

* refactor (resource-instance-form): ensure correct parent FormGroup validity when clicking the back button and selecting different items

* refactor (resource-instance-form): check if resourceClassIri is null before the rest of the logic

* refactor (resource-instance-form): reset the resource label as well on ontology selection change
  • Loading branch information
mdelez committed Nov 16, 2020
1 parent ac0bc90 commit b9a464e
Show file tree
Hide file tree
Showing 6 changed files with 135 additions and 45 deletions.
Expand Up @@ -6,6 +6,7 @@
<app-select-project
[formGroup]="selectResourceForm"
[usersProjects]="usersProjects"
[selectedProject]="selectedProject"
(projectSelected)="selectOntologies($event)">
</app-select-project>
</div>
Expand All @@ -16,6 +17,7 @@
<app-select-ontology
[formGroup]="selectResourceForm"
[ontologiesMetadata]="ontologiesMetadata"
[selectedOntology]="selectedOntology"
(ontologySelected)="selectResourceClasses($event)">
</app-select-ontology>
</div>
Expand All @@ -24,8 +26,11 @@
<!-- select one resource -->
<span *ngIf="resourceClasses?.length > 0">
<app-select-resource-class
#selectResourceClass
[formGroup]="selectResourceForm"
[resourceClassDefinitions]="resourceClasses"
[selectedResourceClass]="selectedResourceClass"
[chosenResourceLabel]="resourceLabel"
(resourceClassSelected)="selectProperties($event)"
(resourceLabel)="getResourceLabel($event)">
</app-select-resource-class>
Expand Down
Expand Up @@ -10,6 +10,7 @@ import {
CreateValue,
KnoraApiConnection,
OntologiesMetadata,
OntologyMetadata,
PropertyDefinition,
ReadResource,
ResourceClassAndPropertyDefinitions,
Expand All @@ -22,6 +23,7 @@ import { DspApiConnectionToken, Session, SessionService, SortingService } from '
import { Subscription } from 'rxjs';
import { CacheService } from 'src/app/main/cache/cache.service';
import { SelectPropertiesComponent } from './select-properties/select-properties.component';
import { SelectResourceClassComponent } from './select-resource-class/select-resource-class.component';

// https://dev.to/krumpet/generic-type-guard-in-typescript-258l
type Constructor<T> = { new(...args: any[]): T };
Expand Down Expand Up @@ -50,6 +52,7 @@ export class ResourceInstanceFormComponent implements OnInit, OnDestroy {
@Output() updateParent: EventEmitter<{ title: string, subtitle: string }> = new EventEmitter<{ title: string, subtitle: string }>();

@ViewChild('selectProps') selectPropertiesComponent: SelectPropertiesComponent;
@ViewChild('selectResourceClass') selectResourceClassComponent: SelectResourceClassComponent;

// forms
selectResourceForm: FormGroup;
Expand Down Expand Up @@ -100,7 +103,7 @@ export class ResourceInstanceFormComponent implements OnInit, OnDestroy {
// initialize projects to be used for the project selection in the creation form
this.initializeProjects();

// boolean to show onl the first step of the form (= selectResourceForm)
// boolean to show only the first step of the form (= selectResourceForm)
this.showNextStepForm = true;

}
Expand Down Expand Up @@ -198,19 +201,38 @@ export class ResourceInstanceFormComponent implements OnInit, OnDestroy {
*/
selectOntologies(projectIri: string) {
if (projectIri) {
this.selectedProject = projectIri;
// if this method is called with the same value as the current selectedProject, there is no need to do anything
if (projectIri !== this.selectedProject) {
// any time the project is changed:

this._dspApiConnection.v2.onto.getOntologiesByProjectIri(projectIri).subscribe(
(response: OntologiesMetadata) => {
// filter out system ontologies
response.ontologies = response.ontologies.filter(onto => onto.attachedToProject !== Constants.SystemProjectIRI);
// reset the selected ontology because it will be invalid
this.selectedOntology = undefined;

this.ontologiesMetadata = response;
},
(error: ApiResponseError) => {
console.error(error);
}
);
// reset the ontologies metadata before it is generated again to trigger the select-ontology OnInit method
this.ontologiesMetadata = undefined;

// reset resourceClasses to hide the select-resource-class form in case it is already visible
this.resourceClasses = undefined;

// remove the form control to ensure the parent Formgroups validity is correct
// this will be added to the parent Formgroup again when the select-ontology OnInit method is called
this.selectResourceForm.removeControl('ontologies');

// assign the selected iri to selectedProject
this.selectedProject = projectIri;

this._dspApiConnection.v2.onto.getOntologiesByProjectIri(projectIri).subscribe(
(response: OntologiesMetadata) => {
// filter out system ontologies
response.ontologies = response.ontologies.filter(onto => onto.attachedToProject !== Constants.SystemProjectIRI);

this.ontologiesMetadata = response;
},
(error: ApiResponseError) => {
console.error(error);
}
);
}
} else {
this.errorMessage = 'You are not part of any project.';
}
Expand All @@ -221,18 +243,37 @@ export class ResourceInstanceFormComponent implements OnInit, OnDestroy {
* @param ontologyIri
*/
selectResourceClasses(ontologyIri: string) {
// reset errorMessage, it will be reassigned in the else clause if needed
this.errorMessage = undefined;

if (ontologyIri) {
this.selectedOntology = ontologyIri;
// if this method is called with the same value as the current selectedOntology, there is no need to do anything
if (ontologyIri !== this.selectedOntology) {

this._dspApiConnection.v2.ontologyCache.getOntology(ontologyIri).subscribe(
onto => {
this.resourceClasses = this._makeResourceClassesArray(onto.get(ontologyIri).classes);
},
(error: ApiResponseError) => {
console.error(error);
// reset selectedResourceClass since it will be invalid
this.selectedResourceClass = undefined;
this.resourceLabel = undefined;

// if there is already a select-resource-class component (i.e. the user clicked the back button), reset the resource label
if (this.selectResourceClassComponent) {
this.selectResourceClassComponent.form.controls.label.setValue(null);
}
);

// remove the form control to ensure the parent Formgroups validity is correct
// this will be added to the parent Formgroup again when the select-resource-class OnInit method is called
this.selectResourceForm.removeControl('resources');

this.selectedOntology = ontologyIri;

this._dspApiConnection.v2.ontologyCache.getOntology(ontologyIri).subscribe(
onto => {
this.resourceClasses = this._makeResourceClassesArray(onto.get(ontologyIri).classes);
},
(error: ApiResponseError) => {
console.error(error);
}
);
}
} else {
this.errorMessage = 'No ontology defined for the selected project.';
}
Expand All @@ -251,28 +292,30 @@ export class ResourceInstanceFormComponent implements OnInit, OnDestroy {
* @param resourceClassIri
*/
selectProperties(resourceClassIri: string) {
if (resourceClassIri) {
// if the client undoes the selection of a resource class, use the active ontology as a fallback
if (resourceClassIri === null) {
this.selectResourceClasses(this.selectedOntology);
} else {
this._dspApiConnection.v2.ontologyCache.getResourceClassDefinition(resourceClassIri).subscribe(
onto => {
this.ontologyInfo = onto;
// reset errorMessage, it will be reassigned in the else clause if needed
this.errorMessage = undefined;

// if the client undoes the selection of a resource class, use the active ontology as a fallback
if (resourceClassIri === null) {
this.selectResourceClasses(this.selectedOntology);
} else if (resourceClassIri) {
this._dspApiConnection.v2.ontologyCache.getResourceClassDefinition(resourceClassIri).subscribe(
onto => {
this.ontologyInfo = onto;

this.selectedResourceClass = onto.classes[resourceClassIri];
// console.log('selectedResourceClass', this.selectedResourceClass);
this.selectedResourceClass = onto.classes[resourceClassIri];
// console.log('selectedResourceClass', this.selectedResourceClass);

this.properties = this._makeResourceProperties(onto.properties);
// console.log('properties', this.properties);
this.properties = this._makeResourceProperties(onto.properties);
// console.log('properties', this.properties);

this.convertPropObjectAsArray();
}
);
}
this.convertPropObjectAsArray();
}
);
} else {
this.errorMessage = 'No resource class defined for the selected ontology.';
}

}

/**
Expand Down
Expand Up @@ -16,6 +16,9 @@ export class SelectOntologyComponent implements OnInit, OnDestroy {

@Input() ontologiesMetadata: OntologiesMetadata;

// optional input to provide the component with a pre-selected ontology
@Input() selectedOntology?: string;

@Output() ontologySelected = new EventEmitter<string>();

form: FormGroup;
Expand Down Expand Up @@ -45,6 +48,11 @@ export class SelectOntologyComponent implements OnInit, OnDestroy {
// add form to the parent form group
this.formGroup.addControl('ontologies', this.form);
});

// check if there is a pre-selected ontology, if so, set the value of the form control to this value
if (this.selectedOntology) {
this.form.controls.ontologies.setValue(this.selectedOntology);
}
}

ngOnDestroy() {
Expand Down
Expand Up @@ -77,9 +77,15 @@ describe('SelectProjectComponent', () => {
expect(testHostComponent.selectProject).toBeTruthy();
});

it('should add a new control to the parent form', () => {
expect(testHostComponent.form.contains('projects')).toBe(true);
});
it('should add a new control to the parent form', async(() => {
// the control is added to the form as an async operation
// https://angular.io/guide/testing#async-test-with-async
testHostFixture.whenStable().then(
() => {
expect(testHostComponent.form.contains('projects')).toBe(true);
}
);
}));

it('should initialise the projects', () => {

Expand Down
Expand Up @@ -3,6 +3,8 @@ import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { StoredProject } from '@dasch-swiss/dsp-js';
import { Subscription } from 'rxjs';

const resolvedPromise = Promise.resolve(null);

@Component({
selector: 'app-select-project',
templateUrl: './select-project.component.html',
Expand All @@ -14,6 +16,9 @@ export class SelectProjectComponent implements OnInit, OnDestroy {

@Input() usersProjects: StoredProject[];

// optional input to provide the component with a pre-selected project
@Input() selectedProject?: string;

@Output() projectSelected = new EventEmitter<string>();

form: FormGroup;
Expand All @@ -34,14 +39,21 @@ export class SelectProjectComponent implements OnInit, OnDestroy {
this.projectSelected.emit(data.projects);
});

// add form to the parent form group
this.formGroup.addControl('projects', this.form);
resolvedPromise.then(() => {
// add form to the parent form group
this.formGroup.addControl('projects', this.form);
});

// if there is only one project to choose from, select it automatically
if (this.usersProjects.length === 1) {
this.form.controls.projects.setValue(this.usersProjects[0].id);
}

// check if there is a pre-selected project, if so, set the value of the form control to this value
if (this.selectedProject) {
this.form.controls.projects.setValue(this.selectedProject);
}

}

ngOnDestroy() {
Expand Down
Expand Up @@ -16,6 +16,12 @@ export class SelectResourceClassComponent implements OnInit, OnDestroy {

@Input() resourceClassDefinitions: ResourceClassDefinition[];

// optional input to provide the component with a pre-selected ResourceClassDefinition
@Input() selectedResourceClass?: ResourceClassDefinition;

// optional input to provide the component with a pre-chosen label
@Input() chosenResourceLabel?: string;

@Output() resourceClassSelected = new EventEmitter<string>();

@Output() resourceLabel = new EventEmitter<string>();
Expand All @@ -37,13 +43,13 @@ export class SelectResourceClassComponent implements OnInit, OnDestroy {
});

// emit Iri of the resource when selected
this.resourceChangesSubscription = this.form.valueChanges.subscribe((data) => {
this.resourceClassSelected.emit(data.resources);
this.resourceChangesSubscription = this.form.controls.resources.valueChanges.subscribe((data) => {
this.resourceClassSelected.emit(data);
});

// emit label of the resource when selected
this.resourceChangesSubscription = this.form.valueChanges.subscribe((data) => {
this.resourceLabel.emit(data.label);
this.resourceChangesSubscription = this.form.controls.label.valueChanges.subscribe((data) => {
this.resourceLabel.emit(data);
});

// if there is only one Resource Class Definition to choose from, select it automatically
Expand All @@ -55,6 +61,16 @@ export class SelectResourceClassComponent implements OnInit, OnDestroy {
// add form to the parent form group
this.formGroup.addControl('resources', this.form);
});

// check if there is a pre-selected ResourceClassDefinition, if so, set the value of the form control to this value
if (this.selectedResourceClass) {
this.form.controls.resources.setValue(this.selectedResourceClass.id);
}

// check if there is a pre-chosen label, if so, set the value of the form control to this value
if (this.chosenResourceLabel) {
this.form.controls.label.setValue(this.chosenResourceLabel);
}
}

ngOnDestroy() {
Expand Down

0 comments on commit b9a464e

Please sign in to comment.