From 5a766c1db06d862b2edfba49347a659f4233d1c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Kilchenmann?= Date: Fri, 12 Feb 2021 14:25:25 +0100 Subject: [PATCH] feat(ontology): update cardinality in resource class (DSP-1266) (#377) * style(ontology): update ontology editor style * feat(ontology): init "update cardinality" funcitonality * chore(ontology): prepare resclass from for card update * feat(ontology): update card * chore(dsp-js): using yalc for dsp-js for the moment * chore(dsp-api): using latest dsp-js version from main branch * chore(dependency): update js-lib to latest version * refactor(ontology): clean up code * feat(ontology): set and replace cardinality * style(ontology): change position of add prop button * feat(ontology): create prop and class name from randomized string * refactor(ontology): fix typo * feat(ontology): disable own res class in res pointer * feat(ontology): replace prop cardinality in res class * feat(ontology): disable edit menu in case of missing lmd * fix(ontology): fix cache issue * refactor(ontology): delete console.log * chore(ontology): replace ontology with data model * refactor(ontology): delete commented code * chore(deps): bump dsp-js from 1.3.0 to 2.0.1 and dsp-ui 1.2.1 to 1.2.2 --- package-lock.json | 45 ++-- package.json | 4 +- src/app/main/dialog/dialog.component.html | 31 ++- .../default-data/default-properties.ts | 2 +- .../project/ontology/ontology.component.html | 121 +++++----- .../project/ontology/ontology.component.scss | 150 ++++++------ .../project/ontology/ontology.component.ts | 69 +++++- .../property-form.component.html | 10 +- .../property-form.component.scss | 4 +- .../property-form/property-form.component.ts | 57 +++-- .../resource-class-form.component.html | 31 +-- .../resource-class-form.component.scss | 5 +- .../resource-class-form.component.ts | 219 +++++++++++------- .../resource-class-form.service.ts | 89 ++++++- src/assets/style/_config.scss | 1 + 15 files changed, 547 insertions(+), 291 deletions(-) diff --git a/package-lock.json b/package-lock.json index de656867c5..8bc168b504 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,8 +18,8 @@ "@angular/platform-browser-dynamic": "~9.1.12", "@angular/router": "~9.1.12", "@ckeditor/ckeditor5-angular": "^1.2.3", - "@dasch-swiss/dsp-js": "^1.3.0", - "@dasch-swiss/dsp-ui": "^1.2.1", + "@dasch-swiss/dsp-js": "^2.0.1", + "@dasch-swiss/dsp-ui": "^1.2.2", "@ngx-translate/core": "^13.0.0", "@ngx-translate/http-loader": "^6.0.0", "3d-force-graph": "^1.60.12", @@ -1894,21 +1894,38 @@ } }, "node_modules/@dasch-swiss/dsp-js": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@dasch-swiss/dsp-js/-/dsp-js-1.3.0.tgz", - "integrity": "sha512-wF3ib5sc3DWhJkAu8NOdgNHc40P38NtCtrxt3HR0cletc+HXrdhvVbM2hzfkuIyDPnnnY6a7QEguBrhkVlwZ9A==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@dasch-swiss/dsp-js/-/dsp-js-2.0.1.tgz", + "integrity": "sha512-6S/N2GBPxwixGGI0lflgqXcSCt4H0Io3QWeLFKbSKTYaMOSl2Xmo479KLDl9xw9bv6HIe3ZrNHv2b8CT2PXsxA==", "dependencies": { "@types/jsonld": "^1.5.0", "json2typescript": "1.4.1", "jsonld": "^1.8.0" + }, + "peerDependencies": { + "rxjs": "6.x" } }, "node_modules/@dasch-swiss/dsp-ui": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@dasch-swiss/dsp-ui/-/dsp-ui-1.2.1.tgz", - "integrity": "sha512-t5/UBLGwYVes2SN2nndCB9wZb/8OWAEZG4idY1qbCumgL057BG0QaaZ6i1AjgLMwbsMoptYVhI9ODbHWxltlrA==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@dasch-swiss/dsp-ui/-/dsp-ui-1.2.2.tgz", + "integrity": "sha512-qJUHZiaFOhGazTSA4gS6Y40ulW0/s/4z8XLKuIg3/IFwdTAvPCFGFIZ471hd2yzD+jsia4V5t2SRB5os7/4npw==", "dependencies": { "tslib": "^1.10.0" + }, + "peerDependencies": { + "@angular/cdk": "^9.0.0", + "@angular/common": "^9.0.0", + "@angular/core": "^9.0.0", + "@angular/material": "^9.0.0", + "@ckeditor/ckeditor5-angular": "^1.2.3", + "@dasch-swiss/dsp-js": "^2.0.1", + "ckeditor5-custom-build": "github:dasch-swiss/ckeditor_custom_build", + "jdnconvertiblecalendar": "^0.0.5", + "jdnconvertiblecalendardateadapter": "^0.0.13", + "ngx-color-picker": "^10.0.1", + "openseadragon": "^2.4.2", + "svg-overlay": "github:openseadragon/svg-overlay" } }, "node_modules/@istanbuljs/schema": { @@ -19259,9 +19276,9 @@ } }, "@dasch-swiss/dsp-js": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@dasch-swiss/dsp-js/-/dsp-js-1.3.0.tgz", - "integrity": "sha512-wF3ib5sc3DWhJkAu8NOdgNHc40P38NtCtrxt3HR0cletc+HXrdhvVbM2hzfkuIyDPnnnY6a7QEguBrhkVlwZ9A==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@dasch-swiss/dsp-js/-/dsp-js-2.0.1.tgz", + "integrity": "sha512-6S/N2GBPxwixGGI0lflgqXcSCt4H0Io3QWeLFKbSKTYaMOSl2Xmo479KLDl9xw9bv6HIe3ZrNHv2b8CT2PXsxA==", "requires": { "@types/jsonld": "^1.5.0", "json2typescript": "1.4.1", @@ -19269,9 +19286,9 @@ } }, "@dasch-swiss/dsp-ui": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@dasch-swiss/dsp-ui/-/dsp-ui-1.2.1.tgz", - "integrity": "sha512-t5/UBLGwYVes2SN2nndCB9wZb/8OWAEZG4idY1qbCumgL057BG0QaaZ6i1AjgLMwbsMoptYVhI9ODbHWxltlrA==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@dasch-swiss/dsp-ui/-/dsp-ui-1.2.2.tgz", + "integrity": "sha512-qJUHZiaFOhGazTSA4gS6Y40ulW0/s/4z8XLKuIg3/IFwdTAvPCFGFIZ471hd2yzD+jsia4V5t2SRB5os7/4npw==", "requires": { "tslib": "^1.10.0" } diff --git a/package.json b/package.json index 2d1fcfe0e7..5b98f13e3b 100644 --- a/package.json +++ b/package.json @@ -31,8 +31,8 @@ "@angular/platform-browser-dynamic": "~9.1.12", "@angular/router": "~9.1.12", "@ckeditor/ckeditor5-angular": "^1.2.3", - "@dasch-swiss/dsp-js": "^1.3.0", - "@dasch-swiss/dsp-ui": "^1.2.1", + "@dasch-swiss/dsp-js": "^2.0.1", + "@dasch-swiss/dsp-ui": "^1.2.2", "@ngx-translate/core": "^13.0.0", "@ngx-translate/http-loader": "^6.0.0", "3d-force-graph": "^1.60.12", diff --git a/src/app/main/dialog/dialog.component.html b/src/app/main/dialog/dialog.component.html index 9857c98371..4d3d1d28c0 100644 --- a/src/app/main/dialog/dialog.component.html +++ b/src/app/main/dialog/dialog.component.html @@ -12,13 +12,15 @@
- + +
- + +
@@ -95,13 +97,15 @@
- + +
- + + Do you want to deactivate this project?
+
+ + + + +
+
@@ -242,7 +256,8 @@
- + +
diff --git a/src/app/project/ontology/default-data/default-properties.ts b/src/app/project/ontology/default-data/default-properties.ts index 947d28f16c..2662181f31 100644 --- a/src/app/project/ontology/default-data/default-properties.ts +++ b/src/app/project/ontology/default-data/default-properties.ts @@ -147,7 +147,7 @@ export class DefaultProperties { icon: 'link', label: 'Other resource e.g. Person', subPropOf: Constants.HasLinkTo, - objectType: Constants.Resource, + objectType: Constants.LinkValue, gui_ele: Constants.SalsahGui + Constants.HashDelimiter + 'Searchbox', // 'Autocomplete', group: 'Link' }, diff --git a/src/app/project/ontology/ontology.component.html b/src/app/project/ontology/ontology.component.html index 096b199529..1a5ae96168 100644 --- a/src/app/project/ontology/ontology.component.html +++ b/src/app/project/ontology/ontology.component.html @@ -1,6 +1,7 @@
-

This is a first version of the ontology 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.

@@ -53,7 +54,7 @@

-
+
@@ -83,7 +84,7 @@

{{ontology?.label}}

-

Ontology configuration

+

Data model configuration

- - - + + + +
- - -
- - -

- {{resClass.label | dspTruncate: 24}}

- - - - - - -
-
-
-
    - - +
    + + +

    + {{resClass.label | dspTruncate: 24}}

    + + + + + + + +
    +
    +
    +
      + + -
    • - {{ontology?.properties[prop.propertyIndex].label}} -
    • -
      - -
    -
    +
  • + {{ontology?.properties[prop.propertyIndex].label}} +
  • + -
    +
+
+
+ +
diff --git a/src/app/project/ontology/ontology.component.scss b/src/app/project/ontology/ontology.component.scss index f18f8fda3f..061c638563 100644 --- a/src/app/project/ontology/ontology.component.scss +++ b/src/app/project/ontology/ontology.component.scss @@ -4,94 +4,104 @@ $width: 304px; .app-toolbar-action { - &.select-form { - margin-top: -18px; - } + &.select-form { + margin-top: -18px; + } - .form-content { - display: contents; + .form-content { + display: contents; - .select-ontology { - margin: 0 24px 0 0; - } + .select-ontology { + margin: 0 24px 0 0; } + } - .create-data-model-btn { - margin: 0 0 16px; - min-width: 140px; - } + .create-data-model-btn { + margin: 0 0 16px; + min-width: 140px; + } } .ontology-editor { - width: 98vw; - max-width: 1280px; - min-height: calc(100vh - #{$header-height} - #{$tab-bar-margin} - 100px); - border: 1px dotted $primary_200; - background: $primary_200; - position: relative; - margin: 0 auto 60px auto; + width: 98vw; + max-width: 1280px; + min-height: calc(100vh - #{$header-height} - #{$tab-bar-margin} - 100px); + border: 1px dotted $primary_200; + background: $primary_200; + position: relative; + margin: 0 auto 60px auto; } .ontology-viewer { - width: 98vw; - max-width: 1280px; - min-height: calc(100vh - #{$header-height} - #{$tab-bar-margin} - 100px); - border: 1px dotted $primary_200; - position: relative; - margin: 0 auto 60px auto; + width: 98vw; + max-width: 1280px; + min-height: calc(100vh - #{$header-height} - #{$tab-bar-margin} - 100px); + border: 1px dotted $primary_200; + position: relative; + margin: 0 auto 60px auto; } -.ontology-editor-canvas { - display: grid; - grid-template-columns: repeat(auto-fill, minmax($width, 1fr)); - grid-column-gap: 12px; - grid-row-gap: 12px; - padding-top: 60px; +.ontology-editor-container { + .ontology-editor-subheader { + height: $sub-header-height; + } + + .ontology-editor-canvas { + min-height: calc(100vh - #{$sub-header-height} - #{$header-height} - #{$tab-bar-margin} - 100px); + + .ontology-editor-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax($width, 1fr)); + grid-column-gap: 12px; + grid-row-gap: 12px; + // padding-top: 60px; + } + } } .resource-class { - min-height: 120px; - height: auto; - position: relative; - @include mat-elevation-transition; - @include mat-elevation(2); - padding: 12px; - margin: 12px; - background-color: #fff; - - .resource-class-header { - margin: -12px -12px 12px -12px; - width: calc(100% + 24px); - min-height: 48px !important; - height: 48px; - cursor: move; - } + min-height: 120px; + height: auto; + position: relative; + @include mat-elevation-transition; + @include mat-elevation(2); + padding: 12px; + margin: 12px; + background-color: #fff; + + .resource-class-header { + margin: -12px -12px 12px -12px; + width: calc(100% + 24px); + min-height: 48px !important; + height: 48px; + cursor: move; + } - .resource-class-properties { - li { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - margin-left: -24px; - list-style-position: inside; - } + .resource-class-properties { + li { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + margin-left: -24px; + list-style-position: inside; } + } - &:active { - @include mat-elevation(8); - } + &:active { + @include mat-elevation(8); + } } .ontology-editor-header { - border-bottom: 1px solid rgba(0, 0, 0, 0.12); - position: sticky !important; - top: 0; + border-bottom: 1px solid rgba(0, 0, 0, 0.12); + position: sticky !important; + top: 0; - .switch-view-label { - margin-right: 16px; - } + .switch-view-label { + margin-right: 16px; + } - button.active { - background-color: $black-12-opacity; - } + button.active { + background-color: $black-12-opacity; + } } /* .ontology-container { @@ -131,9 +141,9 @@ $width: 304px; */ .add-resource-class { - position: absolute; - right: 12px; - top: 80px; + position: absolute; + right: 12px; + top: 80px; } .mat-caption.space-reducer { diff --git a/src/app/project/ontology/ontology.component.ts b/src/app/project/ontology/ontology.component.ts index d9fad80e7f..157feb0a6b 100644 --- a/src/app/project/ontology/ontology.component.ts +++ b/src/app/project/ontology/ontology.component.ts @@ -10,11 +10,13 @@ import { DeleteOntologyResponse, DeleteResourceClass, KnoraApiConnection, + ListsResponse, OntologiesMetadata, OntologyMetadata, ProjectResponse, ReadOntology, ReadProject, + ResourceClassDefinition, UpdateOntology } from '@dasch-swiss/dsp-js'; import { DspApiConnectionToken, Session, SessionService } from '@dasch-swiss/dsp-ui'; @@ -148,6 +150,9 @@ 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 @@ -308,19 +313,54 @@ export class OntologyComponent implements OnInit { */ openResourceClassForm(mode: 'createResourceClass' | 'editResourceClass', subClassOf: DefaultClass): void { - // set ontology cache - this._cache.set('currentOntology', this.ontology); + // set cache for ontology and lists + this.setCache(); const dialogConfig: MatDialogConfig = { + disableClose: true, width: '840px', maxHeight: '90vh', position: { top: '112px' }, - data: { name: subClassOf.iri, title: subClassOf.label, subtitle: 'Customize resource class', mode: mode, project: this.project.id } + data: { id: subClassOf.iri, title: subClassOf.label, subtitle: 'Customize resource class', mode: mode, project: this.project.id } }; - const dialogRef = this._dialog.open(DialogComponent, dialogConfig); + const dialogRef = this._dialog.open( + DialogComponent, + dialogConfig + ); + + dialogRef.afterClosed().subscribe(result => { + // update the view + this.getOntology(this.ontologyIri); + }); + } + + + /** + * Updates cardinality + * @param subClassOf resource class + */ + updateCard(subClassOf: ResourceClassDefinition) { + + // set cache for ontology and lists + this.setCache(); + + const dialogConfig: MatDialogConfig = { + disableClose: true, + width: '840px', + maxHeight: '90vh', + position: { + top: '112px' + }, + data: { mode: 'updateCardinality', id: subClassOf.id, title: subClassOf.label, subtitle: 'Update the metadata fields of resource class', project: this.project.id } + }; + + const dialogRef = this._dialog.open( + DialogComponent, + dialogConfig + ); dialogRef.afterClosed().subscribe(result => { // update the view @@ -382,7 +422,7 @@ export class OntologyComponent implements OnInit { break; case 'ResourceClass': - // delete reresource class and refresh the view + // delete resource class and refresh the view this.loadOntology = true; const resClass: DeleteResourceClass = new DeleteResourceClass(); resClass.id = id; @@ -415,4 +455,23 @@ export class OntologyComponent implements OnInit { this.view = view; } + setCache() { + + // get all lists; will be used to set gui attribute in 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); + + }, + (error: ApiResponseError) => { + // console.error('currentOntologyLists', error) + this._errorHandler.showMessage(error); + } + ); + + // 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 2256c19c4a..bc98e1a7f9 100644 --- a/src/app/project/ontology/property-form/property-form.component.html +++ b/src/app/project/ontology/property-form/property-form.component.html @@ -16,9 +16,9 @@ - + + matTooltip="Label: {{ prop?.label }} (Name is part of the id: {{prop?.name}})" matTooltipPosition="after"> {{ prop?.label }} @@ -63,11 +63,11 @@
-
+
- Select reresource class + Select resource class - + {{item.label}} diff --git a/src/app/project/ontology/property-form/property-form.component.scss b/src/app/project/ontology/property-form/property-form.component.scss index e3dc33b29f..ee7c59b8e9 100644 --- a/src/app/project/ontology/property-form/property-form.component.scss +++ b/src/app/project/ontology/property-form/property-form.component.scss @@ -28,7 +28,9 @@ } .reset-button { - margin: 0 0 0 auto; + position: absolute; + right: 0; + bottom: 12px; } .hidden { 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 31c634982c..558fe41e6a 100644 --- a/src/app/project/ontology/property-form/property-form.component.ts +++ b/src/app/project/ontology/property-form/property-form.component.ts @@ -46,6 +46,10 @@ export class PropertyFormComponent implements OnInit { @Input() index: number; + @Input() ontology?: ReadOntology; + + @Input() resClassIri?: string; + @Output() deleteProperty: EventEmitter = new EventEmitter(); iri = new FormControl(); @@ -62,10 +66,7 @@ export class PropertyFormComponent implements OnInit { // list of project specific lists (TODO: probably we have to add default knora lists?!) lists: ListNodeInfo[]; - // current ontology - ontology: ReadOntology; - - // reresource classs in this ontology + // resource classs in this ontology reresourceClasss: ClassDefinition[] = []; // list of existing properties @@ -78,6 +79,8 @@ export class PropertyFormComponent implements OnInit { existingProperty: boolean; + loading = false; + Constants = Constants; constructor( @@ -104,6 +107,19 @@ export class PropertyFormComponent implements OnInit { if (this.propertyForm) { // init list of property types with first element this.propertyForm.patchValue({ type: this.propertyTypes[0].elements[0] }); + + if (this.propertyForm.value.label) { + + const existingProp: AutocompleteItem = { + iri: this.propertyForm.value.iri, + label: this.propertyForm.value.label, + name: '' + } + + // edit mode: this prop value exists already + this.loading = true; + this.updateFieldsDependingOnLabel(existingProp); + } } this._cache.get('currentOntology').subscribe( @@ -112,7 +128,7 @@ export class PropertyFormComponent implements OnInit { // set various lists to select from // a) in case of link value: - // set list of reresource classes from response; needed for linkValue + // 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]); @@ -122,7 +138,8 @@ export class PropertyFormComponent implements OnInit { // set list of properties from response; needed for autocomplete in label to reuse existing property const propKeys: string[] = Object.keys(response.properties); for (const p of propKeys) { - if (this.ontology.properties[p].objectType !== 'http://api.knora.org/ontology/knora-api/v2#LinkValue') { + const prop = this.ontology.properties[p]; + if (prop.objectType !== Constants.LinkValue && prop.objectType !== this.resClassIri) { const existingProperty: AutocompleteItem = { iri: this.ontology.properties[p].id, name: this.ontology.properties[p].id.split('#')[1], @@ -144,9 +161,6 @@ export class PropertyFormComponent implements OnInit { this._cache.get('currentOntologyLists').subscribe( (response: ListNodeInfo[]) => { this.lists = response; - }, - (error: ApiResponseError) => { - this._errorHandler.showMessage(error); } ); @@ -165,7 +179,7 @@ export class PropertyFormComponent implements OnInit { */ filter(list: AutocompleteItem[], label: string) { return list.filter(prop => - prop.label.toLowerCase().includes(label.toLowerCase()) + prop.label?.toLowerCase().includes(label.toLowerCase()) ); } @@ -175,10 +189,10 @@ export class PropertyFormComponent implements OnInit { this.propertyForm.controls['guiAttr'].setValue(undefined); // depending on the selected property type, // we have to define gui element attributes - // e.g. iri of list or connected reresource class + // e.g. iri of list or connected resource class switch (event.value.objectType) { case Constants.ListValue: - case Constants.Resource: + case Constants.LinkValue: this.showGuiAttr = true; this.propertyForm.controls['guiAttr'].setValidators([ Validators.required @@ -197,19 +211,22 @@ export class PropertyFormComponent implements OnInit { /** * @param {MatOption} option */ - updateFieldsDependingOnLabel(option: MatOption) { - this.propertyForm.controls['iri'].setValue(option.value.iri); + updateFieldsDependingOnLabel(option: AutocompleteItem) { + this.propertyForm.controls['iri'].setValue(option.iri); - this.propertyForm.controls['label'].setValue(option.value.label); + // set label and disable the input + this.propertyForm.controls['label'].setValue(option.label); this.propertyForm.controls['label'].disable(); - if (this.ontology.properties[option.value.iri] instanceof ResourcePropertyDefinition) { - const tempProp: any | ResourcePropertyDefinition = this.ontology.properties[option.value.iri]; + // find corresponding property type + + if (this.ontology.properties[option.iri] instanceof ResourcePropertyDefinition) { + const tempProp: any | ResourcePropertyDefinition = this.ontology.properties[option.iri]; let obj: PropertyType; // find gui ele from list of default property-types to set type value for (let group of this.propertyTypes) { - obj = group.elements.find(i => i.gui_ele === tempProp.guiElement && i.objectType === tempProp.objectType); + obj = group.elements.find(i => i.gui_ele === tempProp.guiElement && (i.objectType === tempProp.objectType || i.subPropOf === tempProp.subPropertyOf[0])); if (obj) { this.propertyForm.controls['type'].setValue(obj); @@ -233,6 +250,7 @@ export class PropertyFormComponent implements OnInit { this.propertyForm.controls['guiAttr'].setValue(listIri); this.propertyForm.controls['guiAttr'].disable(); break; + // prop type is resource pointer case Constants.SalsahGui + Constants.HashDelimiter + 'Searchbox': @@ -243,13 +261,10 @@ export class PropertyFormComponent implements OnInit { default: this.showGuiAttr = false; - } - } this.propertyForm.controls['type'].disable(); this.existingProperty = true; - } resetProperty(ev: Event) { 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 81c09d7a7e..4d7a0f4229 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 @@ -3,7 +3,7 @@ - +
@@ -12,8 +12,7 @@
+ (enter)="nextStep($event)" (dataChanged)="handleData($event, 'labels')"> {{ formErrors.label }} @@ -45,7 +44,7 @@
- +
@@ -53,6 +52,8 @@
+ +
- + + - +
- +
- + -
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 7066d442ab..248d734804 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 @@ -76,14 +76,13 @@ button { } } - &.delete-line, - &.add-new-line { + &.delete-line { position: absolute; right: 12px; } &.add-new-line { - bottom: 12px; + top: 12px; } &.delete-line { 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 7ca82b0363..e87ae3241c 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 @@ -2,19 +2,20 @@ import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop'; import { AfterViewChecked, ChangeDetectorRef, Component, EventEmitter, Inject, Input, OnDestroy, OnInit, Output } from '@angular/core'; import { FormArray, FormGroup } from '@angular/forms'; import { - ApiResponseData, ApiResponseError, + ClassDefinition, Constants, CreateResourceClass, CreateResourceProperty, + IHasProperty, KnoraApiConnection, - ListsResponse, + PropertyDefinition, ReadOntology, ResourceClassDefinitionWithAllLanguages, ResourcePropertyDefinitionWithAllLanguages, StringLiteral, UpdateOntology, - UpdateOntologyResourceClassCardinality + UpdateResourceClassCardinality } from '@dasch-swiss/dsp-js'; import { StringLiteralV2 } from '@dasch-swiss/dsp-js/src/models/v2/string-literal-v2'; import { DspApiConnectionToken } from '@dasch-swiss/dsp-ui'; @@ -40,10 +41,11 @@ export class ResourceClassFormComponent implements OnInit, OnDestroy, AfterViewC @Input() projectIri: string; /** - * selected resource class is a subclass from knora base (baseClassIri) + * create mode: iri selected resource class is a subclass from knora base (baseClassIri) + * edit mode: iri of resource class * e.g. knora-api:StillImageRepresentation */ - @Input() subClassOf: string; + @Input() iri: string; /** * name of resource class e.g. Still image @@ -53,6 +55,19 @@ export class ResourceClassFormComponent implements OnInit, OnDestroy, AfterViewC // store name as resourceClassTitle on init; in this case it can't be overwritten in the next / prev navigation resourceClassTitle: string; + // two step form: which should be active? + /** + * two step form: which should be active? + * true => step 1 shows label and comment of resource class + * false => step 2 shows list of properties of resource class + */ + @Input() showResourceClassForm: boolean = true; + + /** + * edit mode (true); otherwise create mode + */ + @Input() edit: boolean; + /** * emit event, when closing dialog */ @@ -66,6 +81,9 @@ export class ResourceClassFormComponent implements OnInit, OnDestroy, AfterViewC // current ontology; will get it from cache by key 'currentOntology' ontology: ReadOntology; + // set a list of properties to set res class cardinality for + propsForCard: Property[] = []; + // success of sending data success = false; @@ -81,9 +99,6 @@ export class ResourceClassFormComponent implements OnInit, OnDestroy, AfterViewC // in case of an error error: boolean; - // two step form: which should be active? - showResourceClassForm: boolean = true; - // form group, form array (for properties) errors and validation messages resourceClassForm: FormGroup; @@ -97,7 +112,7 @@ export class ResourceClassFormComponent implements OnInit, OnDestroy, AfterViewC // container for properties properties: FormArray; - // reresource class name should be unique + // resource class name should be unique existingResourceClassNames: [RegExp]; existingPropertyNames: [RegExp]; @@ -137,7 +152,7 @@ export class ResourceClassFormComponent implements OnInit, OnDestroy, AfterViewC new RegExp('anEmptyRegularExpressionWasntPossible') ]; - // set file representation or default reresource class as title + // set file representation or default resource class as title this.resourceClassTitle = this.name; this._cache.get('currentOntology').subscribe( @@ -153,10 +168,10 @@ export class ResourceClassFormComponent implements OnInit, OnDestroy, AfterViewC for (const c of classKeys) { this.existingResourceClassNames.push( new RegExp('(?:^|W)' + c.split('#')[1] + '(?:$|W)') - ) + ); } - const propKeys: string[] = Object.keys(response.properties); + // const propKeys: string[] = Object.keys(response.properties); }, (error: ApiResponseError) => { @@ -164,16 +179,6 @@ export class ResourceClassFormComponent implements OnInit, OnDestroy, AfterViewC } ); - // get all lists; will be used to set guit attribut in list property - this._dspApiConnection.admin.listsEndpoint.getListsInProject(this.projectIri).subscribe( - (response: ApiResponseData) => { - this._cache.set('currentOntologyLists', response.body.lists); - }, - (error: ApiResponseError) => { - this._errorHandler.showMessage(error); - } - ); - this.buildForm(); this._cdr.detectChanges(); @@ -196,12 +201,36 @@ export class ResourceClassFormComponent implements OnInit, OnDestroy, AfterViewC // reset properties this._resourceClassFormService.resetProperties(); - this.resourceClassFormSub = this._resourceClassFormService.resourceClassForm$ - .subscribe(resourceClass => { - this.resourceClassForm = resourceClass; - this.properties = this.resourceClassForm.get('properties') as FormArray; + if (this.edit) { + // edit mode + // get resource class and property definition first + + // get list of ontology properties + const ontoProperties: PropertyDefinition[] = this.ontology.getAllPropertyDefinitions(); + + // find prop cardinality in resource class + const ontoClasses: ClassDefinition[] = this.ontology.getAllClassDefinitions(); + Object.keys(ontoClasses).forEach(key => { + if (ontoClasses[key].id === this.iri) { + + this._resourceClassFormService.setProperties(ontoClasses[key], ontoProperties); + + this.resourceClassFormSub = this._resourceClassFormService.resourceClassForm$ + .subscribe(resourceClass => { + this.resourceClassForm = resourceClass; + this.properties = this.resourceClassForm.get('properties') as FormArray; + }); + } }); + } else { + // create mode + this.resourceClassFormSub = this._resourceClassFormService.resourceClassForm$ + .subscribe(resourceClass => { + this.resourceClassForm = resourceClass; + this.properties = this.resourceClassForm.get('properties') as FormArray; + }); + } this.resourceClassForm.valueChanges.subscribe(data => this.onValueChanged(data)); } @@ -225,6 +254,9 @@ export class ResourceClassFormComponent implements OnInit, OnDestroy, AfterViewC } + // + // property form: handle list of properties + /** * add property line */ @@ -278,6 +310,7 @@ export class ResourceClassFormComponent implements OnInit, OnDestroy, AfterViewC /** * Go to next step: from resource-class form forward to properties form + * In create mode only */ nextStep(ev: Event) { @@ -285,7 +318,7 @@ export class ResourceClassFormComponent implements OnInit, OnDestroy, AfterViewC this.showResourceClassForm = false; // use response to go further with properties - this.updateParent.emit({ title: this.resourceClassLabels[0].value, subtitle: 'Define the metadata for resource class' }); + this.updateParent.emit({ title: this.resourceClassLabels[0].value, subtitle: 'Define the metadata fields for the resource class' }); // load one first property line if (!this.resourceClassForm.value.properties.length) { @@ -294,10 +327,11 @@ export class ResourceClassFormComponent implements OnInit, OnDestroy, AfterViewC } /** * Go to previous step: from properties form back to resource-class form + * In create mode only */ prevStep(ev: Event) { ev.preventDefault(); - this.updateParent.emit({ title: this.resourceClassTitle, subtitle: 'Customize resource class' }); + this.updateParent.emit({ title: this.resourceClassTitle, subtitle: 'Customize the resource class' }); this.showResourceClassForm = true; } @@ -309,45 +343,47 @@ export class ResourceClassFormComponent implements OnInit, OnDestroy, AfterViewC */ submitData() { this.loading = true; + if (this.edit) { + // edit mode + // submit properties and set cardinality + this.submitProps(this.resourceClassForm.value.properties, this.iri); - // set resource class name / id - const uniqueClassName: string = this._resourceClassFormService.setUniqueName(this.ontology.id, this.resourceClassLabels[0].value); + } else { + // create mode + // submit resource class data to knora and create resource class incl. cardinality - const onto = new UpdateOntology(); - - onto.id = this.ontology.id; - onto.lastModificationDate = this.lastModificationDate; + // set resource class name / id: randomized string + const uniqueClassName: string = this._resourceClassFormService.setUniqueName(this.ontology.id); + // OR const uniqueClassName: string = this._resourceClassFormService.setUniqueName(this.ontology.id, this.resourceClassLabels[0].value, 'class'); - const newResClass = new CreateResourceClass(); + const onto = new UpdateOntology(); - newResClass.name = uniqueClassName - newResClass.label = this.resourceClassLabels; - newResClass.comment = this.resourceClassComments; - newResClass.subClassOf = [this.subClassOf]; + onto.id = this.ontology.id; + onto.lastModificationDate = this.lastModificationDate; - onto.entity = newResClass; + const newResClass = new CreateResourceClass(); - // submit resource class data to knora and create resource class incl. cardinality - // console.log('submit resource class data:', resourceClassData); - // let i: number = 0; - this._dspApiConnection.v2.onto.createResourceClass(onto).subscribe( - (classResponse: ResourceClassDefinitionWithAllLanguages) => { - // console.log('classResponse', classResponse); - // need lmd from classResponse - this.lastModificationDate = classResponse.lastModificationDate; + newResClass.name = uniqueClassName + newResClass.label = this.resourceClassLabels; + newResClass.comment = this.resourceClassComments; + newResClass.subClassOf = [this.iri]; - // post prop data; one by one - this.submitProps(this.resourceClassForm.value.properties, classResponse.id); + onto.entity = newResClass; + this._dspApiConnection.v2.onto.createResourceClass(onto).subscribe( + (classResponse: ResourceClassDefinitionWithAllLanguages) => { + // need lmd from classResponse + this.lastModificationDate = classResponse.lastModificationDate; - }, - (error: ApiResponseError) => { - this._errorHandler.showMessage(error); - } - ); + // submit properties and set cardinality + this.submitProps(this.resourceClassForm.value.properties, classResponse.id); + }, + (error: ApiResponseError) => { + this._errorHandler.showMessage(error); + } + ); + } - // show message to close dialog box - // this.closeMessage(); } /** @@ -368,11 +404,12 @@ export class ResourceClassFormComponent implements OnInit, OnDestroy, AfterViewC // submit prop // console.log('first pipe operator...waiting...prepare and submit prop', prop); if (prop.iri) { - // the defined prop exists already in this ontology. We can proceed with cardinality. - this.setCardinality(prop.iri, classIri, prop.multiple, prop.required, i); + // already existing property; add it to the new list of properties + + this.propsForCard.push(prop); } else { // the defined prop does not exist yet. We have to create it. - this.createProp(prop, classIri, i); + this.createProp(prop, classIri); } return new Promise(resolve => setTimeout(() => resolve(prop), 1200)); } @@ -386,30 +423,29 @@ export class ResourceClassFormComponent implements OnInit, OnDestroy, AfterViewC )) .subscribe( (prop: Property) => { - // this.getOntology(this.ontologyId); if (i > props.length) { // console.log('at the end: created', prop) - // TODO: reset ontology cache - // close the dialog box - this.loading = false; - this.closeDialog.emit(); + // all properties are created and exist + // set the cardinality + this.setCardinality(this.propsForCard, classIri); } } ); } - createProp(prop: Property, classIri: string, index: number) { + createProp(prop: Property, classIri?: string) { return new Promise((resolve, reject) => { - // set resource property name / id - const uniquePropName: string = this._resourceClassFormService.setUniqueName(this.ontology.id, prop.label); + // set resource property name / id: randomized string + const uniquePropName: string = this._resourceClassFormService.setUniqueName(this.ontology.id); const onto = new UpdateOntology(); onto.id = this.ontology.id; + onto.lastModificationDate = this.lastModificationDate; // prepare payload for property const newResProp = new CreateResourceProperty(); @@ -448,20 +484,19 @@ export class ResourceClassFormComponent implements OnInit, OnDestroy, AfterViewC if (prop.type.subPropOf === Constants.HasLinkTo) { newResProp.objectType = prop.guiAttr; - newResProp.subjectType = classIri; + // newResProp.subjectType = classIri; } else { newResProp.objectType = prop.type.objectType; } - onto.lastModificationDate = this.lastModificationDate; - onto.entity = newResProp; this._dspApiConnection.v2.onto.createResourceProperty(onto).subscribe( (response: ResourcePropertyDefinitionWithAllLanguages) => { this.lastModificationDate = response.lastModificationDate; - // update cardinality - this.setCardinality(response.id, classIri, prop.multiple, prop.required, index); + // prepare prop for cardinality + prop.iri = response.id; + this.propsForCard.push(prop); }, (error: ApiResponseError) => { this._errorHandler.showMessage(error); @@ -470,32 +505,42 @@ export class ResourceClassFormComponent implements OnInit, OnDestroy, AfterViewC }) } - setCardinality(propIri: string, classIri: string, multiple: boolean, required: boolean, index: number) { + setCardinality(props: Property[], classIri: string) { + const onto = new UpdateOntology(); - const addCard = new UpdateOntologyResourceClassCardinality(); + onto.lastModificationDate = this.lastModificationDate; - addCard.lastModificationDate = this.lastModificationDate; + onto.id = this.ontology.id; + + const addCard = new UpdateResourceClassCardinality(); - addCard.id = this.ontology.id; + addCard.id = classIri; - const cardinality = this._resourceClassFormService.translateCardinality(multiple, required); + addCard.cardinalities = []; - addCard.cardinalities = [ - { - propertyIndex: propIri, - cardinality: cardinality, - resourceClass: classIri, - guiOrder: index + props.forEach((prop, index) => { + const propCard: IHasProperty = { + propertyIndex: prop.iri, + cardinality: this._resourceClassFormService.translateCardinality(prop.multiple, prop.required), + guiOrder: index + 1 } - ]; + addCard.cardinalities.push(propCard); + }); + + onto.entity = addCard; - this._dspApiConnection.v2.onto.addCardinalityToResourceClass(addCard).subscribe( + this._dspApiConnection.v2.onto.replaceCardinalityOfResourceClass(onto).subscribe( (res: ResourceClassDefinitionWithAllLanguages) => { this.lastModificationDate = res.lastModificationDate; + // close the dialog box + this.loading = false; + this.closeDialog.emit(); + }, + (error: ApiResponseError) => { + this._errorHandler.showMessage(error); } ); - } } 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 e0dd89cd6a..645c0e10c3 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 } from '@dasch-swiss/dsp-js'; +import { Cardinality, Constants, IHasProperty, PropertyDefinition, ResourceClassDefinition, ResourcePropertyDefinition } from '@dasch-swiss/dsp-js'; import { BehaviorSubject, Observable } from 'rxjs'; -import { PropertyType } from '../default-data/default-properties'; +import { DefaultProperties, PropertyType } from '../default-data/default-properties'; // property data structure export class Property { @@ -83,7 +83,12 @@ export class ResourceClassForm { constructor(resourceClass: ResourceClass) { if (resourceClass.properties) { - this.properties.setValue([resourceClass.properties]); + let i = 0; + this.properties.setControl + resourceClass.properties.forEach(prop => { + this.properties[i] = new FormControl(prop); + i++; + }); } } } @@ -113,17 +118,83 @@ export class ResourceClassFormService { this.resourceClassForm.next(currentResourceClass); } + /** + * Sets properties in case of update resource class' cardinalities + * @param resClass + */ + setProperties(resClass: ResourceClassDefinition, ontoProperties: PropertyDefinition[]) { + + const updateResClass = new ResourceClass(); + + updateResClass.properties = []; + + // 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); + + } + }); + + } + + }); + + } + /** * add new property line */ - addProperty() { + addProperty(prop?: Property) { const currentResourceClass = this.resourceClassForm.getValue(); const currentProperties = currentResourceClass.get('properties') as FormArray; currentProperties.push( this._fb.group( - new PropertyForm(new Property('', '', {}, false, false)) + new PropertyForm((prop ? prop : new Property('', '', {}, false, false))) ) ); @@ -149,12 +220,12 @@ export class ResourceClassFormService { * @param [label] * @returns unique name */ - setUniqueName(ontologyIri: string, label?: string): string { + setUniqueName(ontologyIri: string, label?: string, type?: 'class' | 'prop'): string { - if (label) { + if (label && type) { // build name from label // normalize and replace spaces and special chars - return label.normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[\u00a0-\u024f]/g, '').replace(/[\])}[{(]/g, '').replace(/\s+/g, '-').replace(/\//g, '-').toLowerCase(); + return type + '-' + label.normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[\u00a0-\u024f]/g, '').replace(/[\])}[{(]/g, '').replace(/\s+/g, '-').replace(/\//g, '-').toLowerCase(); } else { // build randomized name // The name starts with the three first character of ontology iri to avoid a start with a number followed by randomized string @@ -198,6 +269,6 @@ export class ResourceClassFormService { // Cardinality 0-1 (optional) return Cardinality._0_1; } - } + } diff --git a/src/assets/style/_config.scss b/src/assets/style/_config.scss index 29afb876ce..46727e4a67 100644 --- a/src/assets/style/_config.scss +++ b/src/assets/style/_config.scss @@ -20,4 +20,5 @@ $warn: #ef5350; $active: #66bb6a; $header-height: 72px; +$sub-header-height: 64px; $tab-bar-margin: 36px;