Skip to content
Permalink
Browse files
feat(advanced search): specify linked resource (DSP-1587) (#293)
* feat(advanced search): make component to search for a resource

* feat(advanced search): get class of linked resource

* feat(advanced search): distinguish between specifying and searching for a linked resource

* feat(advanced search): distinguish between specifying and searching for a linked resource

* feat(gravsearch generation): prepare adaption

* refactor(gravsearch service): make method for props handling

* refactor(gravsearch service): make method for props handling

* refactor(gravsearch service): generate method for props handling

* refactor(gravsearch service): avoid redundant statements

* test(gravsearch service): adapt expectation

* refactor(gravsearch service): make var names unique

* refactor(gravsearch service): make var names unique

* refactor(gravsearch service): improve type checking

* refactor(gravsearch service): improve comments

* feature(advanced search): introduce top level flag

* test(advanced search): add test for comp op

* test(advanced search): add new Input in mocked components

* test(advanced search): add test for SearchResourceComponent

* test(gravsearch service): add test for Gravsearch service

* feat(gravsearch service): add variable for rec. call count

* feat(gravsearch service): add optional res. class restriction on linked res.

* feat(advanced search): only show sort criterion for main resource on top level

* docs(advanced search): adapt design docs (ongoing)

* refactor(advanced search): move select res. class comp.

* refactor(advanced search): move select prop. comp.

* feat(advanced search): add fallback for object class constraint

* feat(advanced search): determine ontology from resource class

* docs(advanced search): adapt design docs

* docs(gravsearch service): add comment
  • Loading branch information
tobiasschweizer committed May 25, 2021
1 parent a797670 commit 42e3311
Show file tree
Hide file tree
Showing 66 changed files with 753 additions and 191 deletions.
@@ -208,18 +208,20 @@ The advanced search consists of the following components:

- `AdvancedSearchComponent`: Main form: Reset and submit buttons, buttons to add and remove properties.
- `SelectOntologyComponent`: Select an ontology from a list.
- `SelectResourceClassComponent`: Select a resource class from a list.
- `SelectPropertyComponent`: Select a property from a list.
- `SpecifyPropertyValueComponent`: Specify a comparison operator and a value for a chosen property.
- `SearchBooleanValueComponent`: Specify a Boolean value.
- `SearchDateValueComponent`: Specify a date value.
- `SearchDecimalValueComponent`: Specify a decimal value.
- `SearchIntegerValueComponent`: Specify an integer value.
- `SearchLinkValueComponent`: Specify the target of a link property.
- `SearchListValueComponent`: Specify a list value.
- `SearchDisplayListComponent`: Displays the children of a list node recursively.
- `TextValueComponent`: Specify a text value.
- `UriValueComponent`: Specify a URI value.
- `ResourceAndPropertySelectionComponent`: Parent component that contains `SelectResourceClassComponent` and `SelectPropertyComponent` for recursive reuse.
- `SelectResourceClassComponent`: Select a resource class from a list.
- `SelectPropertyComponent`: Select a property from a list.
- `SpecifyPropertyValueComponent`: Specify a comparison operator and a value for a chosen property.
- `SearchBooleanValueComponent`: Specify a Boolean value.
- `SearchDateValueComponent`: Specify a date value.
- `SearchDecimalValueComponent`: Specify a decimal value.
- `SearchIntegerValueComponent`: Specify an integer value.
- `SearchLinkValueComponent`: Specify the target of a link property.
- `SearchResourceComponent`: Specify the class and/or properties of a linked resource (uses `ResourceAndPropertySelectionComponent`, see below).
- `SearchListValueComponent`: Specify a list value.
- `SearchDisplayListComponent`: Displays the children of a list node recursively.
- `TextValueComponent`: Specify a text value.
- `UriValueComponent`: Specify a URI value.

#### Component Interaction

@@ -235,6 +237,18 @@ When a property is chosen, a comparison operator can be specified.
Once a comparison operator is specified other than "EXISTS", a value can be specified using `SpecifyPropertyValueComponent`.
Depending on the value type of the property, `SpecifyPropertyValueComponent` chooses the apt component to let the user enter a value.

##### Recursive Use of ResourceAndPropertySelectionComponent

`ResourceAndPropertySelectionComponent` is used in the main form and in `SearchResourceComponent`'s template (if the user chooses the operator `Match` for a linking property)
to allow for searching linked resources by specifying their class and/or properties.

Only one level of recursion is allowed, i.e. linking properties on the linked resource **cannot** use the `Match` operator.

Sort criteria can only be chosen on the level of the main resource.
The boolean `@Input` `toplevel` distinguishes the top level from the level below.



#### Form Validation

`AdvancedSearchComponent` creates the main form that is then passed down to the child components.
@@ -5,7 +5,7 @@
(ontologySelected)="setActiveOntology($event)"></dsp-select-ontology>
</div>

<dsp-resource-and-property-selection *ngIf="activeOntology !== undefined" #resAndPropSel [formGroup]="form" [activeOntology]="activeOntology">
<dsp-resource-and-property-selection *ngIf="activeOntology !== undefined" #resAndPropSel [formGroup]="form" [activeOntology]="activeOntology" [topLevel]="true">
</dsp-resource-and-property-selection>

<div class="dsp-form-action">
@@ -48,6 +48,8 @@ class TestSelectResourceClassAndPropertyComponent {

@Input() resClassRestriction?: string;

@Input() topLevel: boolean;

}

/**
@@ -17,7 +17,7 @@ import { DspApiConnectionToken } from '../../core/core.module';
import { SearchParams } from '../../viewer/views/list-view/list-view.component';
import { GravsearchGenerationService } from '../services/gravsearch-generation.service';
import { ResourceAndPropertySelectionComponent } from './resource-and-property-selection/resource-and-property-selection.component';
import { PropertyWithValue } from './select-property/specify-property-value/operator';
import { PropertyWithValue } from './resource-and-property-selection/select-property/specify-property-value/operator';

@Component({
selector: 'dsp-advanced-search',
@@ -13,7 +13,7 @@
<div *ngFor="let prop of activeProperties; let i = index">

<dsp-select-property #property [activeResourceClass]="activeResourceClass" [formGroup]="form" [index]="i"
[properties]="properties"></dsp-select-property>
[properties]="properties" [topLevel]="topLevel"></dsp-select-property>

</div>
</div>
@@ -50,6 +50,8 @@ class TestSelectPropertyComponent {

@Input() activeResourceClass: ResourceClassDefinition;

@Input() topLevel: boolean;

}

/**
@@ -16,8 +16,8 @@ import {
ResourceClassDefinition,
ResourcePropertyDefinition
} from '@dasch-swiss/dsp-js';
import { SelectResourceClassComponent } from '../select-resource-class/select-resource-class.component';
import { SelectPropertyComponent } from '../select-property/select-property.component';
import { SelectResourceClassComponent } from './select-resource-class/select-resource-class.component';
import { SelectPropertyComponent } from './select-property/select-property.component';
import { DspApiConnectionToken } from '../../../core/core.module';


@@ -34,6 +34,8 @@ export class ResourceAndPropertySelectionComponent implements OnInit, OnChanges

@Input() resourceClassRestriction?: string;

@Input() topLevel;

form: FormGroup;

activeResourceClass: ResourceClassDefinition;
@@ -6,7 +6,7 @@
</mat-form-field>

<dsp-specify-property-value #specifyPropertyValue [formGroup]="form" *ngIf="propertySelected !== undefined"
[property]="propertySelected"></dsp-specify-property-value>
[property]="propertySelected" [topLevel]="topLevel"></dsp-specify-property-value>

<mat-checkbox *ngIf="propertySelected !== undefined && sortCriterion()" [formControlName]="'isSortCriterion'" matTooltip="Sort criterion"></mat-checkbox>
</span>
@@ -26,7 +26,7 @@ import { ComparisonOperatorAndValue, Equals, ValueLiteral } from './specify-prop
*/
@Component({
template: `
<dsp-select-property #selectProp [formGroup]="form" [index]="0" [activeResourceClass]="activeResourceClass" [properties]="propertyDefs"></dsp-select-property>`
<dsp-select-property #selectProp [formGroup]="form" [index]="0" [activeResourceClass]="activeResourceClass" [properties]="propertyDefs" [topLevel]="topLevel"></dsp-select-property>`
})
class TestHostComponent implements OnInit {

@@ -38,6 +38,8 @@ class TestHostComponent implements OnInit {

activeResourceClass: ResourceClassDefinition;

topLevel: boolean;

constructor(@Inject(FormBuilder) private _fb: FormBuilder) {
}

@@ -47,6 +49,8 @@ class TestHostComponent implements OnInit {
const resProps = MockOntology.mockReadOntology('http://0.0.0.0:3333/ontology/0001/anything/v2').getPropertyDefinitionsByType(ResourcePropertyDefinition);

this.propertyDefs = resProps;

this.topLevel = true;
}

}
@@ -64,12 +68,13 @@ class TestSpecifyPropertyValueComponent implements OnInit {

@Input() property: ResourcePropertyDefinition;

@Input() topLevel: boolean;

getComparisonOperatorAndValueLiteralForProperty(): ComparisonOperatorAndValue {
return new ComparisonOperatorAndValue(new Equals(), new ValueLiteral('1', 'http://www.w3.org/2001/XMLSchema#integer'));
}

ngOnInit() {

}

}
@@ -174,7 +179,7 @@ describe('SelectPropertyComponent', () => {

});

it('should show the sort checkbox when a property with cardinality 1 is selected', async () => {
it('should show the sort checkbox when a property with cardinality 1 is selected for the top level resource', async () => {

const select = await loader.getHarness(MatSelectHarness);
await select.open();
@@ -201,6 +206,35 @@ describe('SelectPropertyComponent', () => {

});

it('should not show the sort checkbox when a property with cardinality 1 is selected for a linked resource', async () => {

testHostComponent.topLevel = false;

const select = await loader.getHarness(MatSelectHarness);
await select.open();

const options = await select.getOptions();
expect(await options[4].getText()).toEqual('Date');

await options[4].click();

const resClass = new ResourceClassDefinition();
resClass.propertiesList = [{
propertyIndex: 'http://0.0.0.0:3333/ontology/0001/anything/v2#hasDate',
cardinality: Cardinality._1,
isInherited: true
}];

testHostComponent.activeResourceClass = resClass;

testHostFixture.detectChanges();

const checkbox = await loader.getAllHarnesses(MatCheckboxHarness);

expect(checkbox.length).toEqual(0);

});

it('should get the specified value for the selected property', async () => {

const select = await loader.getHarness(MatSelectHarness);
@@ -2,7 +2,7 @@ import { Component, Inject, Input, OnDestroy, OnInit, ViewChild } from '@angular
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Cardinality, IHasProperty, ResourceClassDefinition, ResourcePropertyDefinition } from '@dasch-swiss/dsp-js';
import { Subscription } from 'rxjs';
import { SortingService } from '../../../action/services/sorting.service';
import { SortingService } from '../../../../action/services/sorting.service';
import { ComparisonOperatorAndValue, PropertyWithValue } from './specify-property-value/operator';
import { SpecifyPropertyValueComponent } from './specify-property-value/specify-property-value.component';

@@ -22,6 +22,8 @@ export class SelectPropertyComponent implements OnInit, OnDestroy {
// index of the given property (unique)
@Input() index: number;

@Input() topLevel: boolean;

// properties that can be selected from
private _properties: ResourcePropertyDefinition[];

@@ -107,12 +109,13 @@ export class SelectPropertyComponent implements OnInit, OnDestroy {
*/
sortCriterion(): boolean {

// TODO: this method is called from teh template. It is called on each change detection cycle.
// TODO: this method is called from the template. It is called on each change detection cycle.
// TODO: this is acceptable because this method has no side-effects
// TODO: find a better way: evaluate once and store the result in a class member

// check if a resource class is selected and if the property's cardinality is 1 for the selected resource class
if (this._activeResourceClass !== undefined && this.propertySelected !== undefined && !this.propertySelected.isLinkProperty) {
// sort criterion is only available for main resource on top level
if (this.topLevel && this._activeResourceClass !== undefined && this.propertySelected !== undefined && !this.propertySelected.isLinkProperty) {

const cardinalities: IHasProperty[] = this._activeResourceClass.propertiesList.filter(
(card: IHasProperty) => {
@@ -213,6 +213,26 @@ export class IRI implements Value {

}

/**
* Represents a linked resource.
*/
export class LinkedResource implements Value {

/**
* Constructs a [LinkedResource].
*
* @param properties the properties of the linked resource.
* @param resourceClass the class of the linked resource, if any.
*/
constructor(public properties: PropertyWithValue[], public resourceClass?: string) {
}

public toSparql(): string {
throw Error('invalid call of toSparql');
}

}

/**
* An abstract interface that represents a value.
* This interface has to be implemented for all value types (value component classes).
@@ -7,7 +7,7 @@ import { MatDatepickerModule } from '@angular/material/datepicker';
import { MatInputModule } from '@angular/material/input';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { CalendarDate, CalendarPeriod, GregorianCalendarDate } from 'jdnconvertiblecalendar';
import { DspViewerModule } from '../../../../../viewer/viewer.module';
import { DspViewerModule } from '../../../../../../viewer/viewer.module';
import { ValueLiteral } from '../operator';
import { SearchDateValueComponent } from './search-date-value.component';

@@ -2,7 +2,7 @@ import { Component, Inject, Input, OnDestroy, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Constants } from '@dasch-swiss/dsp-js';
import { JDNConvertibleCalendar } from 'jdnconvertiblecalendar';
import { CalendarHeaderComponent } from '../../../../../viewer/values/date-value/calendar-header/calendar-header.component';
import { CalendarHeaderComponent } from '../../../../../../viewer/values/date-value/calendar-header/calendar-header.component';
import { PropertyValue, Value, ValueLiteral } from '../operator';

// https://stackoverflow.com/questions/45661010/dynamic-nested-reactive-form-expressionchangedafterithasbeencheckederror
@@ -14,7 +14,7 @@ import {
SearchEndpointV2
} from '@dasch-swiss/dsp-js';
import { of } from 'rxjs';
import { DspApiConnectionToken } from '../../../../../core/core.module';
import { DspApiConnectionToken } from '../../../../../../core/core.module';
import { IRI } from '../operator';
import { SearchLinkValueComponent } from './search-link-value.component';

@@ -1,7 +1,7 @@
import { Component, Inject, Input, OnDestroy, OnInit } from '@angular/core';
import { FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms';
import { Constants, KnoraApiConnection, ReadResource, ReadResourceSequence } from '@dasch-swiss/dsp-js';
import { DspApiConnectionToken } from '../../../../../core/core.module';
import { DspApiConnectionToken } from '../../../../../../core/core.module';
import { IRI, PropertyValue, Value } from '../operator';

// https://stackoverflow.com/questions/45661010/dynamic-nested-reactive-form-expressionchangedafterithasbeencheckederror
@@ -11,7 +11,7 @@ import { By } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { ListNodeV2, ListsEndpointV2, MockList, MockOntology, ResourcePropertyDefinition } from '@dasch-swiss/dsp-js';
import { of } from 'rxjs';
import { DspApiConnectionToken } from '../../../../../core/core.module';
import { DspApiConnectionToken } from '../../../../../../core/core.module';
import { IRI } from '../operator';
import { SearchListValueComponent } from './search-list-value.component';

@@ -8,8 +8,8 @@ import {
ListNodeV2,
ResourcePropertyDefinition
} from '@dasch-swiss/dsp-js';
import { NotificationService } from '../../../../../action/services/notification.service';
import { DspApiConnectionToken } from '../../../../../core/core.module';
import { NotificationService } from '../../../../../../action/services/notification.service';
import { DspApiConnectionToken } from '../../../../../../core/core.module';
import { IRI, PropertyValue, Value } from '../operator';

// https://stackoverflow.com/questions/45661010/dynamic-nested-reactive-form-expressionchangedafterithasbeencheckederror
@@ -0,0 +1,2 @@
<dsp-resource-and-property-selection #resAndPropSel [formGroup]="form" [activeOntology]="ontology"
[resourceClassRestriction]="restrictResourceClass" [topLevel]="false"></dsp-resource-and-property-selection>

0 comments on commit 42e3311

Please sign in to comment.