From aa565b3f5265cc0416e05282db400bfe9194836e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Kilchenmann?= Date: Mon, 22 Feb 2021 09:23:36 +0100 Subject: [PATCH] feat(ontology): refactor list of properties in resource class (DSP-1360) (#389) * feat(ontology): own component for list of properties in res class * test(ontology): update test for property info component * feat(ontology): better list of props in class * chore(cache): better error message in cache * chore(ontology): better cache handling to use in child component * feat(ontology): new list of properties in res class * style(ontology): refactor res class viewer * chore(ontology): update property info component * refactor(ontology): clean up code * refactor(ontology): clean up code * refactor(ontology): clean up code * test(ontology): bug fix in tests * chore(deps): update package-lock.json * fix(ontology): avoid `null:null` in prop type tooltip * chore(ontology): add missing space * fix(ontology): display prop even without gui order * style(ontology): fix design issue in res class form * refactor(ontology): clean up code * refactor(ontology): clean up code * feat(ontology): support all project ontologies in list of properties * chore(ontology): delete console.logs * refactor(ontology): fix typo in comments * fix(ontology): fix cache issues in main ontology view * refactor(ontology): delete console.logs * test(ontology): add more property info tests --- src/app/app.module.ts | 4 +- src/app/main/cache/cache.service.ts | 2 +- .../default-data/default-properties.ts | 4 +- .../ontology-form.component.html | 2 +- .../project/ontology/ontology.component.html | 34 +-- .../project/ontology/ontology.component.scss | 12 +- .../ontology/ontology.component.spec.ts | 4 +- .../project/ontology/ontology.component.ts | 145 +++++++---- .../property-form.component.html | 4 +- .../property-form/property-form.component.ts | 10 +- .../property-info.component.html | 23 ++ .../property-info.component.scss | 37 +++ .../property-info.component.spec.ts | 240 ++++++++++++++++++ .../property-info/property-info.component.ts | 133 ++++++++++ .../resource-class-form.component.html | 3 +- .../resource-class-form.component.scss | 5 +- .../resource-class-form.component.ts | 5 +- .../resource-class-form.service.ts | 90 +++---- 18 files changed, 610 insertions(+), 147 deletions(-) create mode 100644 src/app/project/ontology/property-info/property-info.component.html create mode 100644 src/app/project/ontology/property-info/property-info.component.scss create mode 100644 src/app/project/ontology/property-info/property-info.component.spec.ts create mode 100644 src/app/project/ontology/property-info/property-info.component.ts diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 44853ef00a..5437945c29 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -90,6 +90,7 @@ import { PersonTemplateComponent } from './project/board/person-template/person- import { AddressTemplateComponent } from './project/board/address-template/address-template.component'; import { OrganisationTemplateComponent } from './project/board/organisation-template/organisation-template.component'; import { EditListItemComponent } from './project/list/list-item-form/edit-list-item/edit-list-item.component'; +import { PropertyInfoComponent } from './project/ontology/property-info/property-info.component'; // translate: AoT requires an exported function for factories export function HttpLoaderFactory(httpClient: HttpClient) { @@ -162,7 +163,8 @@ export function HttpLoaderFactory(httpClient: HttpClient) { PersonTemplateComponent, AddressTemplateComponent, OrganisationTemplateComponent, - EditListItemComponent + EditListItemComponent, + PropertyInfoComponent ], imports: [ AppRoutingModule, diff --git a/src/app/main/cache/cache.service.ts b/src/app/main/cache/cache.service.ts index 14fe2ea33f..b7f95e4df0 100644 --- a/src/app/main/cache/cache.service.ts +++ b/src/app/main/cache/cache.service.ts @@ -71,7 +71,7 @@ export class CacheService { }); } else { - return throwError('Requested key is not available in Cache'); + return throwError('Requested key "' + key + '" is not available in Cache'); } } diff --git a/src/app/project/ontology/default-data/default-properties.ts b/src/app/project/ontology/default-data/default-properties.ts index 2662181f31..ad81a63276 100644 --- a/src/app/project/ontology/default-data/default-properties.ts +++ b/src/app/project/ontology/default-data/default-properties.ts @@ -1,6 +1,6 @@ import { Constants } from '@dasch-swiss/dsp-js'; -export interface Property { +export interface Category { group: string; elements: PropertyType[]; } @@ -15,7 +15,7 @@ export interface PropertyType { } export class DefaultProperties { - public static data: Property[] = [ + public static data: Category[] = [ { group: 'Text', elements: [ diff --git a/src/app/project/ontology/ontology-form/ontology-form.component.html b/src/app/project/ontology/ontology-form/ontology-form.component.html index 1b697b494c..470c9148dc 100644 --- a/src/app/project/ontology/ontology-form/ontology-form.component.html +++ b/src/app/project/ontology/ontology-form/ontology-form.component.html @@ -1,7 +1,7 @@
- +
-

This is a first version of the data model editor. Some features may not work as - intended.

+

+ This is a first version of the data model editor. Some features may not work as intended. +

@@ -54,7 +55,7 @@

-
+
@@ -128,7 +129,7 @@

{{ontology?.label}}

- +
@@ -157,19 +158,18 @@

-
-
    - - -
  • - {{ontology?.properties[prop.propertyIndex].label}} -
  • -
    - -
-
+ + + + + + +

diff --git a/src/app/project/ontology/ontology.component.scss b/src/app/project/ontology/ontology.component.scss index 061c638563..fe740f0cd6 100644 --- a/src/app/project/ontology/ontology.component.scss +++ b/src/app/project/ontology/ontology.component.scss @@ -1,7 +1,7 @@ @import "~@angular/material/theming"; @import "../../../assets/style/config"; -$width: 304px; +$width: 340px; .app-toolbar-action { &.select-form { @@ -50,10 +50,9 @@ $width: 304px; .ontology-editor-grid { display: grid; + grid-template-rows: auto; // value would be "masonry" as soon it's implemented in all browsers grid-template-columns: repeat(auto-fill, minmax($width, 1fr)); - grid-column-gap: 12px; - grid-row-gap: 12px; - // padding-top: 60px; + grid-gap: 6px; } } } @@ -65,7 +64,7 @@ $width: 304px; @include mat-elevation-transition; @include mat-elevation(2); padding: 12px; - margin: 12px; + margin: 6px; background-color: #fff; .resource-class-header { @@ -77,7 +76,8 @@ $width: 304px; } .resource-class-properties { - li { + li.property-info { + list-style-type: none; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; diff --git a/src/app/project/ontology/ontology.component.spec.ts b/src/app/project/ontology/ontology.component.spec.ts index 2e7b00806f..32ccff87ef 100644 --- a/src/app/project/ontology/ontology.component.spec.ts +++ b/src/app/project/ontology/ontology.component.spec.ts @@ -28,6 +28,7 @@ import { ErrorComponent } from 'src/app/main/error/error.component'; import { TestConfig } from 'test.config'; import { OntologyVisualizerComponent } from './ontology-visualizer/ontology-visualizer.component'; import { OntologyComponent } from './ontology.component'; +import { PropertyInfoComponent } from './property-info/property-info.component'; describe('OntologyComponent', () => { let component: OntologyComponent; @@ -39,7 +40,8 @@ describe('OntologyComponent', () => { OntologyComponent, OntologyVisualizerComponent, DialogComponent, - ErrorComponent + ErrorComponent, + PropertyInfoComponent ], imports: [ BrowserAnimationsModule, diff --git a/src/app/project/ontology/ontology.component.ts b/src/app/project/ontology/ontology.component.ts index fa37062bbb..637f5ecce1 100644 --- a/src/app/project/ontology/ontology.component.ts +++ b/src/app/project/ontology/ontology.component.ts @@ -7,6 +7,7 @@ import { ApiResponseData, ApiResponseError, ClassDefinition, + Constants, DeleteOntologyResponse, DeleteResourceClass, KnoraApiConnection, @@ -55,17 +56,18 @@ export class OntologyComponent implements OnInit { // project data project: ReadProject; - // ontologies - ontologies: OntologyMetadata[]; + // all project ontologies + ontologies: ReadOntology[] = []; + // existing project ontology names existingOntologyNames: string[] = []; - // ontology JSON-LD object + // current/selected ontology ontology: ReadOntology; ontoClasses: ClassDefinition[]; - // selected ontology + // selected ontology id ontologyIri: string = undefined; // form to select ontology from list @@ -83,7 +85,7 @@ export class OntologyComponent implements OnInit { }; /** - * list of all default resource classs (sub class of) + * list of all default resource classes (sub class of) */ resourceClass: DefaultClass[] = DefaultResourceClasses.data; @@ -114,19 +116,16 @@ export class OntologyComponent implements OnInit { // get ontology iri from route if (this._route.snapshot && this._route.snapshot.params.id) { this.ontologyIri = decodeURIComponent(this._route.snapshot.params.id); - this.getOntology(this.ontologyIri); - } - - // set the page title - if (this.ontologyIri) { + // set the page title in case of only one project ontology this._titleService.setTitle('Project ' + this.projectcode + ' | Data model'); } else { + // set the page title in case of more than one existing project ontologies this._titleService.setTitle('Project ' + this.projectcode + ' | Data models'); } } ngOnInit() { - this.loading = true; + // this.loading = true; // get information about the logged-in user this.session = this._session.getSession(); @@ -150,9 +149,6 @@ export class OntologyComponent implements OnInit { // get the ontologies for this project this.initList(); - // cache other things like ontology and lists - this.setCache(); - this.ontologyForm = this._fb.group({ ontology: new FormControl({ value: this.ontologyIri, disabled: false @@ -161,8 +157,6 @@ export class OntologyComponent implements OnInit { this.ontologyForm.valueChanges.subscribe(val => this.onValueChanged(val.ontology)); - this.loading = false; - }, (error: ApiResponseError) => { this._errorHandler.showMessage(error); @@ -171,6 +165,26 @@ export class OntologyComponent implements OnInit { ); } + + /** + * Asyncs for each: Get all ontologies of project as ReadOntology + * @param ontologies + * @param callback + */ + async asyncForEach(ontologies: OntologyMetadata[], callback: any) { + for (let i = 0; i < ontologies.length; i++) { + // set list of already existing ontology names + // it will be used in ontology form + // because ontology name has to be unique + let name = this._resourceClassFormService.getOntologyName(ontologies[i].id); + this.existingOntologyNames.push(name); + + // get each ontology + this.getOntology(ontologies[i].id, true); + await callback(ontologies[i]); + } + } + /** * build the list of ontologies */ @@ -178,36 +192,44 @@ export class OntologyComponent implements OnInit { this.loading = true; - // reset existing ontology names + // reset existing ontology names and ontologies this.existingOntologyNames = []; + this.ontologies = []; + + const waitFor = (ms: number) => new Promise(r => setTimeout(r, ms)); this._dspApiConnection.v2.onto.getOntologiesByProjectIri(this.project.id).subscribe( (response: OntologiesMetadata) => { - this.ontologies = response.ontologies; - // get list of already existing ontology names - // name has to be unique - for (const ontology of response.ontologies) { - let name = this._resourceClassFormService.getOntologyName(ontology.id); - this.existingOntologyNames.push(name); + const loadAndCache = async () => { + await this.asyncForEach(response.ontologies, async (onto: OntologyMetadata) => { + await waitFor(200); + if (this.ontologies.length === response.ontologies.length) { + this.setCache(); + } + }); } - // in case project has only one ontology: open this ontology - // because there will be no form to select ontlogy - if (response.ontologies.length === 1) { - // open this ontology - this.openOntologyRoute(this.ontologies[0].id); - this.getOntology(this.ontologies[0].id); + if (!response.ontologies.length) { + this.setCache(); + } else { + // in case project has only one ontology: open this ontology + // because there will be no form to select ontlogy + if (response.ontologies.length === 1) { + // open this ontology + this.openOntologyRoute(response.ontologies[0].id); + this.ontologyIri = response.ontologies[0].id; + } + loadAndCache(); } - this.loading = false; }, (error: ApiResponseError) => { // temporary solution. There's a bug in js-lib in case of 0 ontologies // s. youtrack issue DSP-863 this.ontologies = []; - this._errorHandler.showMessage(error); + this.loading = false; } ) } @@ -226,37 +248,46 @@ export class OntologyComponent implements OnInit { // open ontology route by iri openOntologyRoute(id: string) { + this.loadOntology = true; const goto = 'project/' + this.projectcode + '/ontologies/' + encodeURIComponent(id); this._router.navigateByUrl(goto, { skipLocationChange: false }); } - // get ontology - getOntology(id: string) { - - this.ontoClasses = []; - - this.loadOntology = true; - + // get ontology info + getOntology(id: string, updateOntologiesList: boolean = false) { this._dspApiConnection.v2.onto.getOntology(id, true).subscribe( (response: ReadOntology) => { - this.ontology = response; + if (updateOntologiesList) { + this.ontologies.push(response); + } - if (!this.ontoClasses.length) { - const classKeys: string[] = Object.keys(response.classes); + // get current ontology as a separate part + if (response.id === this.ontologyIri) { + this.ontology = response; + // the ontology is the selected one + // grab the onto class information to display + this.ontoClasses = []; + const classKeys: string[] = Object.keys(response.classes); + // create list of resource classes without standoff classes for (const c of classKeys) { - this.ontoClasses.push(this.ontology.classes[c]); + const splittedSubClass = this.ontology.classes[c].subClassOf[0].split('#'); + + if (splittedSubClass[0] !== Constants.StandoffOntology && splittedSubClass[1] !== 'StandoffTag' && splittedSubClass[1] !== 'StandoffLinkTag') { + this.ontoClasses.push(this.ontology.classes[c]); + } } + + this.loadOntology = false; } - this.loadOntology = false; + }, (error: ApiResponseError) => { this._errorHandler.showMessage(error); this.loadOntology = false; } ); - } resetOntology(id: string) { @@ -294,14 +325,13 @@ export class OntologyComponent implements OnInit { dialogRef.afterClosed().subscribe((ontologyId: string) => { - // reset view in any case - this.initList(); - // in case of new ontology, go to correct route and update the view if (ontologyId) { this.ontologyIri = ontologyId; // reset and open selected ontology this.ontologyForm.controls['ontology'].setValue(this.ontologyIri); + } else { + this.initList(); } }); } @@ -333,6 +363,7 @@ export class OntologyComponent implements OnInit { dialogRef.afterClosed().subscribe(result => { // update the view + this.initList(); this.getOntology(this.ontologyIri); }); } @@ -364,6 +395,7 @@ export class OntologyComponent implements OnInit { dialogRef.afterClosed().subscribe(result => { // update the view + this.initList(); this.getOntology(this.ontologyIri); }); } @@ -402,8 +434,6 @@ export class OntologyComponent implements OnInit { ontology.lastModificationDate = this.ontology.lastModificationDate; this._dspApiConnection.v2.onto.deleteOntology(ontology).subscribe( (response: DeleteOntologyResponse) => { - this.loading = false; - this.loadOntology = false; // reset current ontology this.ontology = undefined; // get the ontologies for this project @@ -432,7 +462,8 @@ export class OntologyComponent implements OnInit { this._dspApiConnection.v2.onto.deleteResourceClass(resClass).subscribe( (response: OntologyMetadata) => { this.loading = false; - this.getOntology(this.ontologyIri); + this.resetOntology(this.ontologyIri); + // this.getOntology(this.ontologyIri); }, (error: ApiResponseError) => { this._errorHandler.showMessage(error); @@ -456,22 +487,26 @@ export class OntologyComponent implements OnInit { } setCache() { + // set cache for current ontology + this._cache.set('currentOntology', this.ontology); + this._cache.set('currentProjectOntologies', this.ontologies); - // get all lists; will be used to set gui attribute in list property + // get all lists from the project + // it will be used to set gui attribute in a list property this._dspApiConnection.admin.listsEndpoint.getListsInProject(this.project.id).subscribe( (response: ApiResponseData) => { this._cache.set('currentOntologyLists', response.body.lists); - // console.log('set currentOntologyLists', response.body.lists); + this.loading = false; + this.loadOntology = false; }, (error: ApiResponseError) => { - // console.error('currentOntologyLists', error) this._errorHandler.showMessage(error); + this.loading = false; + this.loadOntology = false; } ); - // set cache for current ontology - this._cache.set('currentOntology', this.ontology); } } diff --git a/src/app/project/ontology/property-form/property-form.component.html b/src/app/project/ontology/property-form/property-form.component.html index bc98e1a7f9..ff84b44a65 100644 --- a/src/app/project/ontology/property-form/property-form.component.html +++ b/src/app/project/ontology/property-form/property-form.component.html @@ -67,7 +67,7 @@ Select resource class - + {{item.label}} @@ -81,7 +81,7 @@ diff --git a/src/app/project/ontology/property-form/property-form.component.ts b/src/app/project/ontology/property-form/property-form.component.ts index 558fe41e6a..98a05b219a 100644 --- a/src/app/project/ontology/property-form/property-form.component.ts +++ b/src/app/project/ontology/property-form/property-form.component.ts @@ -18,7 +18,7 @@ import { Observable } from 'rxjs'; import { map, startWith } from 'rxjs/operators'; import { CacheService } from 'src/app/main/cache/cache.service'; import { ErrorHandlerService } from 'src/app/main/error/error-handler.service'; -import { DefaultProperties, Property, PropertyType } from '../default-data/default-properties'; +import { DefaultProperties, Category, PropertyType } from '../default-data/default-properties'; // TODO: should be removed and replaced by AutocompleteItem from dsp-ui @@ -59,15 +59,15 @@ export class PropertyFormComponent implements OnInit { required = new FormControl(); // selection of default property types - propertyTypes: Property[] = DefaultProperties.data; + propertyTypes: Category[] = DefaultProperties.data; showGuiAttr: boolean = false; // list of project specific lists (TODO: probably we have to add default knora lists?!) lists: ListNodeInfo[]; - // resource classs in this ontology - reresourceClasss: ClassDefinition[] = []; + // resource classes in this ontology + resourceClass: ClassDefinition[] = []; // list of existing properties properties: AutocompleteItem[] = []; @@ -131,7 +131,7 @@ export class PropertyFormComponent implements OnInit { // set list of resource classes from response; needed for linkValue const classKeys: string[] = Object.keys(response.classes); for (const c of classKeys) { - this.reresourceClasss.push(this.ontology.classes[c]); + this.resourceClass.push(this.ontology.classes[c]); } // b) in case of already existing label: diff --git a/src/app/project/ontology/property-info/property-info.component.html b/src/app/project/ontology/property-info/property-info.component.html new file mode 100644 index 0000000000..c5d1168f6a --- /dev/null +++ b/src/app/project/ontology/property-info/property-info.component.html @@ -0,0 +1,23 @@ + + {{propCard.guiOrder}}) + +
+ + + + + + {{propType?.icon}} + + + + {{propDef?.label}} + + + +
+
+ {{propInfo.multiple ? 'check_box' : 'check_box_outline_blank' }} multiple + {{propInfo.required ? 'check_box' : 'check_box_outline_blank' }} required +
+
diff --git a/src/app/project/ontology/property-info/property-info.component.scss b/src/app/project/ontology/property-info/property-info.component.scss new file mode 100644 index 0000000000..29793d34ca --- /dev/null +++ b/src/app/project/ontology/property-info/property-info.component.scss @@ -0,0 +1,37 @@ +@import "~@angular/material/theming"; +@import "../../../../assets/style/config"; + +.mat-list-item { + height: 56px !important; +} + +.additional-info { + color: $primary_700; +} + +.mat-list-icon.order { + font-size: 16px; + height: 16px; + width: 16px; +} + +.mat-line.title { + line-height: 1.8; + .mat-icon { + top: 4px; + position: relative; + } +} + + +.mat-line.info { + font-size: small; + + .mat-icon { + width: 12px; + height: 12px; + font-size: small; + top: 2px; + position: relative; + } +} diff --git a/src/app/project/ontology/property-info/property-info.component.spec.ts b/src/app/project/ontology/property-info/property-info.component.spec.ts new file mode 100644 index 0000000000..950cc37619 --- /dev/null +++ b/src/app/project/ontology/property-info/property-info.component.spec.ts @@ -0,0 +1,240 @@ +import { Component, DebugElement, ViewChild } from '@angular/core'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { MatDialogModule } from '@angular/material/dialog'; +import { MatIconModule } from '@angular/material/icon'; +import { MatListModule } from '@angular/material/list'; +import { MatSnackBarModule } from '@angular/material/snack-bar'; +import { MatTooltipModule } from '@angular/material/tooltip'; +import { By } from '@angular/platform-browser'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { Constants, IHasProperty, MockOntology, ReadOntology, ResourcePropertyDefinitionWithAllLanguages } from '@dasch-swiss/dsp-js'; +import { of } from 'rxjs'; +import { CacheService } from 'src/app/main/cache/cache.service'; +import { PropertyInfoComponent } from './property-info.component'; + +/** + * Test host component to simulate parent component + * Property is of type simple text + */ +@Component({ + template: '' +}) +class SimpleTextHostComponent { + + @ViewChild('propertyInfo') propertyInfoComponent: PropertyInfoComponent; + + propertyCardinality: IHasProperty = { + propertyIndex: "http://0.0.0.0:3333/ontology/1111/Notizblogg/v2#notgkygty", + cardinality: 0, + guiOrder: 1, + isInherited: false + }; + propertyDefinition: ResourcePropertyDefinitionWithAllLanguages = { + "id": "http://0.0.0.0:3333/ontology/1111/Notizblogg/v2#notgkygty", + "subPropertyOf": ["http://api.knora.org/ontology/knora-api/v2#hasValue"], + "comment": "Beschreibt einen Namen", + "label": "Name", + "guiElement": "http://api.knora.org/ontology/salsah-gui/v2#SimpleText", + "objectType": "http://api.knora.org/ontology/knora-api/v2#TextValue", + "isLinkProperty": false, + "isLinkValueProperty": false, + "isEditable": true, + "guiAttributes": [], + "comments": [{ + "language": "de", + "value": "Beschreibt einen Namen" + }], + "labels": [{ + "language": "de", + "value": "Name" + }] + }; + +} + +/** + * Test host component to simulate parent component + * Property is of type resource link + */ +@Component({ + template: '' +}) +class LinkHostComponent { + + @ViewChild('propertyInfo') propertyInfoComponent: PropertyInfoComponent; + + propertyCardinality: IHasProperty = { + "propertyIndex": "http://0.0.0.0:3333/ontology/0001/anything/v2#hasOtherThing", + "cardinality": 2, + "guiOrder": 1, + "isInherited": false + }; + propertyDefinition: ResourcePropertyDefinitionWithAllLanguages = { + "id": "http://0.0.0.0:3333/ontology/0001/anything/v2#hasOtherThing", + "subPropertyOf": ["http://api.knora.org/ontology/knora-api/v2#hasLinkTo"], + "label": "Ein anderes Ding", + "guiElement": "http://api.knora.org/ontology/salsah-gui/v2#Searchbox", + "subjectType": "http://0.0.0.0:3333/ontology/0001/anything/v2#Thing", + "objectType": "http://0.0.0.0:3333/ontology/0001/anything/v2#Thing", + "isLinkProperty": true, + "isLinkValueProperty": false, + "isEditable": true, + "guiAttributes": [], + "comments": [], + "labels": [{ + "language": "de", + "value": "Ein anderes Ding" + }, { + "language": "en", + "value": "Another thing" + }, { + "language": "fr", + "value": "Une autre chose" + }, { + "language": "it", + "value": "Un'altra cosa" + }] + }; + +} + +describe('PropertyInfoComponent', () => { + let simpleTextHostComponent: SimpleTextHostComponent; + let simpleTextHostFixture: ComponentFixture; + + let linkHostComponent: LinkHostComponent; + let linkHostFixture: ComponentFixture; + + beforeEach(async(() => { + const dspConnSpy = { + v2: { + ontologyCache: jasmine.createSpyObj('ontologyCache', ['getOntology']), + } + }; + + const cacheServiceSpy = jasmine.createSpyObj('CacheService', ['get']); + + TestBed.configureTestingModule({ + declarations: [ + LinkHostComponent, + SimpleTextHostComponent, + PropertyInfoComponent + ], + imports: [ + BrowserAnimationsModule, + MatDialogModule, + MatIconModule, + MatListModule, + MatSnackBarModule, + MatTooltipModule + ], + providers: [ + { + provide: CacheService, + useValue: cacheServiceSpy + } + ] + }) + .compileComponents(); + })); + + beforeEach(() => { + // mock cache service for currentOntology + const cacheSpy = TestBed.inject(CacheService); + + (cacheSpy as jasmine.SpyObj).get.and.callFake( + () => { + let response: ReadOntology = MockOntology.mockReadOntology('http://0.0.0.0:3333/ontology/0001/anything/v2'); + return of(response); + } + ); + + simpleTextHostFixture = TestBed.createComponent(SimpleTextHostComponent); + simpleTextHostComponent = simpleTextHostFixture.componentInstance; + simpleTextHostFixture.detectChanges(); + + expect(simpleTextHostComponent).toBeTruthy(); + }); + + beforeEach(() => { + linkHostFixture = TestBed.createComponent(LinkHostComponent); + linkHostComponent = linkHostFixture.componentInstance; + linkHostFixture.detectChanges(); + + expect(linkHostComponent).toBeTruthy(); + }); + + it('should create an instance', () => { + expect(simpleTextHostComponent.propertyInfoComponent).toBeTruthy(); + }); + + it('expect cardinality 0 = only one but required value (1)', () => { + expect(simpleTextHostComponent.propertyInfoComponent).toBeTruthy(); + expect(simpleTextHostComponent.propertyInfoComponent.propCard).toBeDefined(); + expect(simpleTextHostComponent.propertyInfoComponent.propCard.cardinality).toBe(0); + + const hostCompDe = simpleTextHostFixture.debugElement; + + const multipleIcon: DebugElement = hostCompDe.query(By.css('.multiple')); + const requiredIcon: DebugElement = hostCompDe.query(By.css('.required')); + + // cardinality 0 means "no multiple values" + expect(multipleIcon.nativeElement.innerText).toEqual('check_box_outline_blank'); + // and cardinality 0 means also "required value" + expect(requiredIcon.nativeElement.innerText).toEqual('check_box'); + + }); + + it('expect property type "text" and gui element "simple input"', () => { + expect(simpleTextHostComponent.propertyInfoComponent).toBeTruthy(); + expect(simpleTextHostComponent.propertyInfoComponent.propDef).toBeDefined(); + expect(simpleTextHostComponent.propertyInfoComponent.propDef.guiElement).toBe(Constants.SalsahGui + Constants.HashDelimiter + 'SimpleText'); + + const hostCompDe = simpleTextHostFixture.debugElement; + + const typeIcon: DebugElement = hostCompDe.query(By.css('.type')); + + // property type and gui element should be Text: simple Text + expect(typeIcon.nativeElement.innerText).toEqual('short_text'); + + }); + + it('expect property type "link" and gui element "search box"', () => { + expect(linkHostComponent.propertyInfoComponent).toBeTruthy(); + expect(linkHostComponent.propertyInfoComponent.propDef).toBeDefined(); + expect(linkHostComponent.propertyInfoComponent.propDef.guiElement).toBe(Constants.SalsahGui + Constants.HashDelimiter + 'Searchbox'); + + const hostCompDe = linkHostFixture.debugElement; + + const typeIcon: DebugElement = hostCompDe.query(By.css('.type')); + + // expect "link" icon + expect(typeIcon.nativeElement.innerText).toEqual('link'); + }); + + it('expect link to other resource called "Thing"', () => { + expect(linkHostComponent.propertyInfoComponent).toBeTruthy(); + expect(linkHostComponent.propertyInfoComponent.propDef).toBeDefined(); + + const hostCompDe = linkHostFixture.debugElement; + + const attribute: DebugElement = hostCompDe.query(By.css('.attribute')); + // expect linked resource called "Thing" + expect(attribute.nativeElement.innerText).toContain('Thing'); + }); + + it('expect cardinality 2 = not required but multiple values (0-n)', () => { + expect(linkHostComponent.propertyInfoComponent).toBeTruthy(); + expect(linkHostComponent.propertyInfoComponent.propDef).toBeDefined(); + + const hostCompDe = linkHostFixture.debugElement; + + const multipleIcon: DebugElement = hostCompDe.query(By.css('.multiple')); + const requiredIcon: DebugElement = hostCompDe.query(By.css('.required')); + + // cardinality 2 means "multiple values" + expect(multipleIcon.nativeElement.innerText).toEqual('check_box'); + // and cardinality 2 means also "not required value" + expect(requiredIcon.nativeElement.innerText).toEqual('check_box_outline_blank'); + }); +}); diff --git a/src/app/project/ontology/property-info/property-info.component.ts b/src/app/project/ontology/property-info/property-info.component.ts new file mode 100644 index 0000000000..0fc815961e --- /dev/null +++ b/src/app/project/ontology/property-info/property-info.component.ts @@ -0,0 +1,133 @@ +import { AfterContentInit, Component, Input, OnInit } from '@angular/core'; +import { MatIconRegistry } from '@angular/material/icon'; +import { DomSanitizer } from '@angular/platform-browser'; +import { + ApiResponseData, + Constants, + IHasProperty, + ListsResponse, + ReadOntology, + ResourcePropertyDefinitionWithAllLanguages +} from '@dasch-swiss/dsp-js'; +import { CacheService } from 'src/app/main/cache/cache.service'; +import { Category, DefaultProperties, PropertyType } from '../default-data/default-properties'; +import { Property } from '../resource-class-form/resource-class-form.service'; + +@Component({ + selector: 'app-property-info', + templateUrl: './property-info.component.html', + styleUrls: ['./property-info.component.scss'] +}) +export class PropertyInfoComponent implements OnInit, AfterContentInit { + + @Input() propDef: ResourcePropertyDefinitionWithAllLanguages; + + @Input() propCard: IHasProperty; + + @Input() projectcode: string; + + propInfo: Property = new Property(); + + propType: PropertyType; + + // list of default property types + propertyTypes: Category[] = DefaultProperties.data; + + propAttribute: string; + + constructor( + private _cache: CacheService, + private _domSanitizer: DomSanitizer, + private _matIconRegistry: MatIconRegistry + ) { + + // special icons for property type + this._matIconRegistry.addSvgIcon( + 'integer_icon', + this._domSanitizer.bypassSecurityTrustResourceUrl('/assets/images/integer-icon.svg') + ); + this._matIconRegistry.addSvgIcon( + 'decimal_icon', + this._domSanitizer.bypassSecurityTrustResourceUrl('/assets/images/decimal-icon.svg') + ); + } + + ngOnInit(): void { + // convert cardinality from js-lib convention to app convention + switch (this.propCard.cardinality) { + case 0: + this.propInfo.multiple = false; + this.propInfo.required = true; + break; + case 1: + this.propInfo.multiple = false; + this.propInfo.required = false; + break; + case 2: + this.propInfo.multiple = true; + this.propInfo.required = false; + break; + case 3: + this.propInfo.multiple = true; + this.propInfo.required = true; + break; + } + + // find gui ele from list of default property-types to set type value + if (this.propDef.guiElement) { + for (let group of this.propertyTypes) { + this.propType = group.elements.find(i => i.gui_ele === this.propDef.guiElement && (i.objectType === this.propDef.objectType || i.subPropOf === this.propDef.subPropertyOf[0])); + + if (this.propType) { + break; + } + } + } + + } + + ngAfterContentInit() { + + if (this.propDef.isLinkProperty) { + // this property is a link property to another resource class + // get current ontology to get linked res class information + this._cache.get('currentOntology').subscribe( + (response: ReadOntology) => { + // get the base ontology of object type + const baseOnto = this.propDef.objectType.split('#')[0]; + if (baseOnto !== response.id) { + // get class info from another ontology + this._cache.get('currentProjectOntologies').subscribe( + (response: ReadOntology[]) => { + const onto = response.find(i => i.id === baseOnto); + if (!onto && this.propDef.objectType === Constants.Region) { + this.propAttribute = 'Region'; + } else { + this.propAttribute = onto.classes[this.propDef.objectType].label; + } + } + ) + } else { + this.propAttribute = response.classes[this.propDef.objectType].label; + } + + } + ); + } + + if (this.propDef.objectType === Constants.ListValue) { + // this property is a list property + // get current ontology lists to get linked list information + this._cache.get('currentOntologyLists').subscribe( + (response: ApiResponseData) => { + const re: RegExp = /\<([^)]+)\>/; + const listIri = this.propDef.guiAttributes[0].match(re)[1]; + const listUrl = `/project/${this.projectcode}/lists/${encodeURIComponent(listIri)}`; + this.propAttribute = `${response[0].labels[0].value}`; + } + ); + } + + } + +} diff --git a/src/app/project/ontology/resource-class-form/resource-class-form.component.html b/src/app/project/ontology/resource-class-form/resource-class-form.component.html index aeb503278e..7b978284b8 100644 --- a/src/app/project/ontology/resource-class-form/resource-class-form.component.html +++ b/src/app/project/ontology/resource-class-form/resource-class-form.component.html @@ -50,13 +50,12 @@
-
Default language for the labels - + {{ option.value }} diff --git a/src/app/project/ontology/resource-class-form/resource-class-form.component.scss b/src/app/project/ontology/resource-class-form/resource-class-form.component.scss index be5be95e6e..a0b45b8eb6 100644 --- a/src/app/project/ontology/resource-class-form/resource-class-form.component.scss +++ b/src/app/project/ontology/resource-class-form/resource-class-form.component.scss @@ -37,9 +37,10 @@ } .properties-language { - position: absolute; - margin-left: 600px; margin-top: -$header-height; + margin-right: 0; + margin-left: auto; + display: table; } // properties diff --git a/src/app/project/ontology/resource-class-form/resource-class-form.component.ts b/src/app/project/ontology/resource-class-form/resource-class-form.component.ts index 445939423d..872cf5cc41 100644 --- a/src/app/project/ontology/resource-class-form/resource-class-form.component.ts +++ b/src/app/project/ontology/resource-class-form/resource-class-form.component.ts @@ -173,7 +173,7 @@ export class ResourceClassFormComponent implements OnInit, OnDestroy, AfterViewC this.lastModificationDate = this.ontology.lastModificationDate; - // get all ontology resource classs: + // get all ontology resource classes: // can be used to select resource class as gui attribute in link property, // but also to avoid same name which should be unique const classKeys: string[] = Object.keys(response.classes); @@ -250,6 +250,7 @@ export class ResourceClassFormComponent implements OnInit, OnDestroy, AfterViewC // set default property language from resource class / first element this.resourceClassForm.controls.language.setValue(ontoClasses[key].labels[0].language); + this.resourceClassForm.controls.language.disable(); } }); } @@ -538,7 +539,7 @@ export class ResourceClassFormComponent implements OnInit, OnDestroy, AfterViewC newResProp.label = [ { 'value': prop.label, - 'language': this.resourceClassForm.value['language'] + 'language': this.resourceClassForm.controls.language.value } ]; if (prop.guiAttr) { diff --git a/src/app/project/ontology/resource-class-form/resource-class-form.service.ts b/src/app/project/ontology/resource-class-form/resource-class-form.service.ts index 57f0f2b23f..1948d40906 100644 --- a/src/app/project/ontology/resource-class-form/resource-class-form.service.ts +++ b/src/app/project/ontology/resource-class-form/resource-class-form.service.ts @@ -1,8 +1,8 @@ import { Injectable } from '@angular/core'; import { FormArray, FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms'; -import { Cardinality, Constants, IHasProperty, PropertyDefinition, ResourceClassDefinition, ResourcePropertyDefinition, StringLiteral } from '@dasch-swiss/dsp-js'; +import { Cardinality, IHasProperty, PropertyDefinition, ResourceClassDefinition, ResourcePropertyDefinition } from '@dasch-swiss/dsp-js'; import { BehaviorSubject, Observable } from 'rxjs'; -import { DefaultProperties, PropertyType } from '../default-data/default-properties'; +import { PropertyType } from '../default-data/default-properties'; // property data structure export class Property { @@ -33,7 +33,6 @@ export class Property { } } - // property form controls export class PropertyForm { iri = new FormControl(); @@ -78,7 +77,6 @@ export class ResourceClass { } } - // resource class form controls export class ResourceClassForm { language = new FormControl(); @@ -134,55 +132,47 @@ export class ResourceClassFormService { // get cardinality and gui order and grab property definition resClass.propertiesList.forEach((prop: IHasProperty) => { - if (prop.guiOrder >= 0) { - - // get property definition - Object.keys(ontoProperties).forEach(key => { - if (ontoProperties[key].id === prop.propertyIndex && !ontoProperties[key].isLinkValueProperty) { - const propDef: ResourcePropertyDefinition = ontoProperties[key]; - - const property: Property = new Property(); - // property.propDef = ontoProperties[key]; - - property.label = propDef.label; - - if(ontoProperties[key].isLinkProperty) { - property.guiAttr = propDef.objectType; - } else { - property.guiAttr = propDef.guiAttributes[0]; - } - property.iri = prop.propertyIndex; - - // convert cardinality - switch (prop.cardinality) { - case 0: - property.multiple = false; - property.required = true; - break; - case 1: - property.multiple = false; - property.required = false; - break; - case 2: - property.multiple = true; - property.required = false; - break; - case 3: - property.multiple = true; - property.required = true; - break; - } - - // find property type in list of default properties - // just a test - // property.type = DefaultProperties.data[0].elements[0]; - - this.addProperty(property); + // get property definition + Object.keys(ontoProperties).forEach(key => { + if (ontoProperties[key].id === prop.propertyIndex && !ontoProperties[key].isLinkValueProperty) { + const propDef: ResourcePropertyDefinition = ontoProperties[key]; + + const property: Property = new Property(); + + property.label = propDef.label; + + if (ontoProperties[key].isLinkProperty) { + property.guiAttr = propDef.objectType; + } else { + property.guiAttr = propDef.guiAttributes[0]; + } + property.iri = prop.propertyIndex; + + // convert cardinality + switch (prop.cardinality) { + case 0: + property.multiple = false; + property.required = true; + break; + case 1: + property.multiple = false; + property.required = false; + break; + case 2: + property.multiple = true; + property.required = false; + break; + case 3: + property.multiple = true; + property.required = true; + break; } - }); - } + this.addProperty(property); + + } + }); });