From 51e539d88553a635bb4663dc8d185d79699ec03d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Kilchenmann?= Date: Thu, 21 Oct 2021 10:08:45 +0200 Subject: [PATCH] feat(ontology): bring back the name input field (DEV-157) (#559) * feat(ontology): bring back the name input field (DEV-157) * refactor(ontology): clean up prop form * fix(string-literal): correct string literal touched handler * style(ontology): fix field error in case of string literal * refactor(ontology): clean up template * test(ontology): fix tests --- .../string-literal-input.component.ts | 2 +- .../ontology-form/ontology-form.component.ts | 6 +- src/app/project/ontology/ontology.service.ts | 13 ++- .../property-form.component.html | 40 +++++++--- .../property-form.component.scss | 17 ---- .../property-form.component.spec.ts | 8 +- .../property-form/property-form.component.ts | 58 +++++++++++--- .../resource-class-form.component.html | 33 ++++++-- .../resource-class-form.component.ts | 80 +++++++++++++------ .../workspace/resource/values/custom-regex.ts | 3 + src/assets/style/_elements.scss | 4 +- src/assets/style/_ontology.scss | 24 ++++++ src/assets/style/main.scss | 1 + 13 files changed, 209 insertions(+), 80 deletions(-) create mode 100644 src/assets/style/_ontology.scss diff --git a/src/app/main/action/string-literal-input/string-literal-input.component.ts b/src/app/main/action/string-literal-input/string-literal-input.component.ts index a67b8d65f7..70ad2bb12f 100644 --- a/src/app/main/action/string-literal-input/string-literal-input.component.ts +++ b/src/app/main/action/string-literal-input/string-literal-input.component.ts @@ -144,7 +144,7 @@ export class StringLiteralInputComponent implements OnInit, OnChanges { const form = this.form; const control = form.get('text'); - this.touched.emit(control && control.dirty); + this.touched.emit(control.dirty || control.touched); this.updateStringLiterals(this.language, this.form.controls.text.value); diff --git a/src/app/project/ontology/ontology-form/ontology-form.component.ts b/src/app/project/ontology/ontology-form/ontology-form.component.ts index d2096a8fb2..5df7ab9935 100644 --- a/src/app/project/ontology/ontology-form/ontology-form.component.ts +++ b/src/app/project/ontology/ontology-form/ontology-form.component.ts @@ -13,6 +13,7 @@ import { CacheService } from 'src/app/main/cache/cache.service'; import { DspApiConnectionToken } from 'src/app/main/declarations/dsp-api-tokens'; import { existingNamesValidator } from 'src/app/main/directive/existing-name/existing-name.directive'; import { ErrorHandlerService } from 'src/app/main/error/error-handler.service'; +import { CustomRegex } from 'src/app/workspace/resource/values/custom-regex'; import { OntologyService } from '../ontology.service'; export interface NewOntology { @@ -52,9 +53,6 @@ export class OntologyFormComponent implements OnInit { lastModificationDate: string; - // regex to check ontology name: shouldn't start with a number or with 'v' followed by a number, spaces or special characters are not allowed - nameRegex = /^(?![vV]+[0-9])+^([a-zA-Z])[a-zA-Z0-9_.-]*$/; - // ontology name must not contain one of the following words forbiddenNames: string[] = [ 'knora', @@ -183,7 +181,7 @@ export class OntologyFormComponent implements OnInit { Validators.minLength(this.nameMinLength), Validators.maxLength(this.nameMaxLength), existingNamesValidator(this.existingNames), - Validators.pattern(this.nameRegex) + Validators.pattern(CustomRegex.ID_NAME_REGEX) ]), label: new FormControl({ value: this.ontologyLabel, disabled: false diff --git a/src/app/project/ontology/ontology.service.ts b/src/app/project/ontology/ontology.service.ts index cbea3f7ebf..f178661529 100644 --- a/src/app/project/ontology/ontology.service.ts +++ b/src/app/project/ontology/ontology.service.ts @@ -1,5 +1,5 @@ import { Injectable } from '@angular/core'; -import { Cardinality } from '@dasch-swiss/dsp-js'; +import { Cardinality, Constants } from '@dasch-swiss/dsp-js'; /** * helper methods for the ontology editor @@ -46,6 +46,17 @@ export class OntologyService { return array[pos].toLowerCase(); } + /** + * get the name from the iri + * @param iri + * @returns name from iri + */ + getNameFromIri(iri: string): string { + const array = iri.split(Constants.HashDelimiter); + + return array[1]; + } + /** * convert cardinality values (multiple? & required?) from form to DSP-JS cardinality enum 1-n, 0-n, 1, 0-1 * @param {boolean} multiple 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 ca148a3587..a587b8467d 100644 --- a/src/app/project/ontology/property-form/property-form.component.html +++ b/src/app/project/ontology/property-form/property-form.component.html @@ -4,15 +4,17 @@

- This property already exists. If you want to modify it, go to the "properties" view. + You're adding an already existing property to this class. + The property can't modified here. If you want to modify it, + go to the "properties" view.

- - + + {{propertyForm.controls['propType'].value.icon}}  Property type @@ -31,20 +33,40 @@ + + + + fingerprint  + + Property name * + + + {{formErrors.name}} + + +
- + - - Label is required + + {{ formErrors.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 a7b29b136f..77d0b2debc 100644 --- a/src/app/project/ontology/property-form/property-form.component.scss +++ b/src/app/project/ontology/property-form/property-form.component.scss @@ -26,23 +26,6 @@ } } -.property-type { - .property-type-icon { - width: 36px; - padding: 0 8px; - display: block; - } - - mat-label, - mat-select, - input { - margin-left: 12px; - } - mat-select { - width: calc(100% - 12px); - } -} - .cardinality { .mat-slide-toggle { diff --git a/src/app/project/ontology/property-form/property-form.component.spec.ts b/src/app/project/ontology/property-form/property-form.component.spec.ts index b0cc82fc94..0da25649c4 100644 --- a/src/app/project/ontology/property-form/property-form.component.spec.ts +++ b/src/app/project/ontology/property-form/property-form.component.spec.ts @@ -243,17 +243,17 @@ describe('PropertyFormComponent', () => { }); - it('should update labels when the value changes', () => { + it('should update labels when the value changes; error message should disapear', () => { const hostCompDe = simpleTextHostFixture.debugElement; const submitButton: DebugElement = hostCompDe.query(By.css('button.submit')); expect(submitButton.nativeElement.innerText).toContain('Update'); - simpleTextHostComponent.propertyFormComponent.handleData([], 'labels'); + simpleTextHostComponent.propertyFormComponent.handleData([{ language: 'de', value: 'New Label' }], 'label'); simpleTextHostFixture.detectChanges(); - const formInvalidMessageDe: DebugElement = hostCompDe.query(By.css('mat-hint')); - expect(formInvalidMessageDe.nativeElement.innerText).toEqual(' Label is required '); + const formInvalidMessageDe: DebugElement = hostCompDe.query(By.css('string-literal-error')); + expect(formInvalidMessageDe).toBeFalsy(); }); 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 6ef5ee9156..162f7b0c79 100644 --- a/src/app/project/ontology/property-form/property-form.component.ts +++ b/src/app/project/ontology/property-form/property-form.component.ts @@ -20,7 +20,9 @@ import { } from '@dasch-swiss/dsp-js'; import { CacheService } from 'src/app/main/cache/cache.service'; import { DspApiConnectionToken } from 'src/app/main/declarations/dsp-api-tokens'; +import { existingNamesValidator } from 'src/app/main/directive/existing-name/existing-name.directive'; import { ErrorHandlerService } from 'src/app/main/error/error-handler.service'; +import { CustomRegex } from 'src/app/workspace/resource/values/custom-regex'; import { AutocompleteItem } from 'src/app/workspace/search/advanced-search/resource-and-property-selection/search-select-property/specify-property-value/operator'; import { DefaultProperties, DefaultProperty, PropertyCategory, PropertyInfoObject } from '../default-data/default-properties'; import { OntologyService } from '../ontology.service'; @@ -65,11 +67,17 @@ export class PropertyFormComponent implements OnInit { propertyForm: FormGroup; formErrors = { + 'name': '', 'label': '', 'guiAttr': '' }; validationMessages = { + 'name': { + 'required': 'Name is required.', + 'existingName': 'This name is already taken. Please choose another one.', + 'pattern': 'Name shouldn\'t start with a number or v + number and spaces or special characters (except dash, dot and underscore) are not allowed.' + }, 'label': { 'required': 'Label is required.', }, @@ -101,9 +109,15 @@ export class PropertyFormComponent implements OnInit { error = false; labels: StringLiteral[] = []; + labelsTouched: boolean; comments: StringLiteral[] = []; guiAttributes: string[] = []; + // list of existing property names + existingNames: [RegExp] = [ + new RegExp('anEmptyRegularExpressionWasntPossible') + ]; + dspConstants = Constants; constructor( @@ -111,7 +125,7 @@ export class PropertyFormComponent implements OnInit { private _cache: CacheService, private _errorHandler: ErrorHandlerService, private _fb: FormBuilder, - private _ontologyService: OntologyService + private _os: OntologyService ) { } ngOnInit() { @@ -127,6 +141,16 @@ export class PropertyFormComponent implements OnInit { // a) in case of link value: // set list of resource classes from response; needed for linkValue this.resourceClasses = response.getAllClassDefinitions(); + + // set list of all existing property names to avoid same name twice + Object.entries(this.ontology.properties).forEach( + ([key]) => { + const name = this._os.getNameFromIri(key); + this.existingNames.push( + new RegExp('(?:^|W)' + name.toLowerCase() + '(?:$|W)') + ); + } + ); }, (error: ApiResponseError) => { this._errorHandler.showMessage(error); @@ -168,7 +192,7 @@ export class PropertyFormComponent implements OnInit { // slice array // this slice value will be kept - // because there was the idea to shorten the array of restrcited elements + // because there was the idea to shorten the array of restricted elements // in case e.g. richtext can't be changed to simple text, then we shouldn't list the simple text item const slice = 0; @@ -181,6 +205,14 @@ export class PropertyFormComponent implements OnInit { } this.propertyForm = this._fb.group({ + 'name': new FormControl({ + value: (this.propertyInfo.propDef ? this._os.getNameFromIri(this.propertyInfo.propDef.id) : ''), + disabled: this.propertyInfo.propDef + }, [ + Validators.required, + existingNamesValidator(this.existingNames), + Validators.pattern(CustomRegex.ID_NAME_REGEX) + ]), 'propType': new FormControl({ value: this.propertyInfo.propType, disabled: disablePropType || this.resClassIri @@ -217,11 +249,9 @@ export class PropertyFormComponent implements OnInit { return; } - const form = this.propertyForm; - Object.keys(this.formErrors).map(field => { this.formErrors[field] = ''; - const control = form.get(field); + const control = this.propertyForm.get(field); if (control && control.dirty && !control.valid) { const messages = this.validationMessages[field]; Object.keys(control.errors).map(key => { @@ -232,14 +262,20 @@ export class PropertyFormComponent implements OnInit { }); } - handleData(data: StringLiteral[], type: string) { + handleData(data: StringLiteral[], type: 'label' | 'comment') { switch (type) { - case 'labels': + case 'label': this.labels = data; + const messages = this.validationMessages[type]; + this.formErrors[type] = ''; + + if (this.labelsTouched && !this.labels.length) { + this.formErrors[type] = messages['required']; + } break; - case 'comments': + case 'comment': this.comments = data; break; } @@ -420,7 +456,7 @@ export class PropertyFormComponent implements OnInit { // create mode: new property incl. gui type and attribute // submit property // set resource property name / id: randomized string - const uniquePropName: string = this._ontologyService.setUniqueName(this.ontology.id); + // const uniquePropName: string = this._os.setUniqueName(this.ontology.id); const onto = new UpdateOntology(); @@ -429,7 +465,7 @@ export class PropertyFormComponent implements OnInit { // prepare payload for property const newResProp = new CreateResourceProperty(); - newResProp.name = uniquePropName; + newResProp.name = this.propertyForm.controls['name'].value; newResProp.label = this.labels; newResProp.comment = (this.comments.length ? this.comments : this.labels); const guiAttr = this.propertyForm.controls['guiAttr'].value; @@ -488,7 +524,7 @@ export class PropertyFormComponent implements OnInit { const propCard: IHasProperty = { propertyIndex: prop.id, - cardinality: this._ontologyService.translateCardinality(this.propertyForm.value.multiple, this.propertyForm.value.required), + cardinality: this._os.translateCardinality(this.propertyForm.value.multiple, this.propertyForm.value.required), guiOrder: this.guiOrder // add new property to the end of current list of properties }; 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 85acdcaf30..e9ef03646b 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 @@ -5,11 +5,26 @@
+ + + + fingerprint  + + Property name * + + + {{formErrors.name}} + + + -
- + + (touched)="resourceClassLabelsTouched = $event" + (dataChanged)="handleData($event, 'label')"> {{ formErrors.label }} @@ -18,8 +33,16 @@
- + + + + {{ formErrors.comment }} +
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 b3491e2bc6..37a813948b 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 @@ -15,7 +15,9 @@ import { StringLiteralV2 } from '@dasch-swiss/dsp-js/src/models/v2/string-litera import { AppGlobal } from 'src/app/app-global'; import { CacheService } from 'src/app/main/cache/cache.service'; import { DspApiConnectionToken } from 'src/app/main/declarations/dsp-api-tokens'; +import { existingNamesValidator } from 'src/app/main/directive/existing-name/existing-name.directive'; import { ErrorHandlerService } from 'src/app/main/error/error-handler.service'; +import { CustomRegex } from 'src/app/workspace/resource/values/custom-regex'; import { OntologyService } from '../ontology.service'; // nested form components; solution from: @@ -87,19 +89,29 @@ export class ResourceClassFormComponent implements OnInit, AfterViewChecked { // label and comment are stringLiterals resourceClassLabels: StringLiteralV2[] = []; + resourceClassLabelsTouched: boolean; resourceClassComments: StringLiteralV2[] = []; + resourceClassCommentsTouched: boolean; - // resource class name should be unique - existingResourceClassNames: [RegExp]; + // list of existing class names + existingNames: [RegExp] = [ + new RegExp('anEmptyRegularExpressionWasntPossible') + ]; // form errors on the following fields: formErrors = { + 'name': '', 'label': '', 'comment': '' }; // in case of form error: show message validationMessages = { + 'name': { + 'required': 'Name is required.', + 'existingName': 'This name is already taken. Please choose another one.', + 'pattern': 'Name shouldn\'t start with a number or v + number and spaces or special characters (except dash, dot and underscore) are not allowed.' + }, 'label': { 'required': 'Label is required.' }, @@ -120,16 +132,11 @@ export class ResourceClassFormComponent implements OnInit, AfterViewChecked { private _cdr: ChangeDetectorRef, private _errorHandler: ErrorHandlerService, private _fb: FormBuilder, - private _ontologyService: OntologyService + private _os: OntologyService ) { } ngOnInit() { - // init existing names - this.existingResourceClassNames = [ - new RegExp('anEmptyRegularExpressionWasntPossible') - ]; - // set file representation or default resource class as title this.resourceClassTitle = this.name; @@ -142,15 +149,14 @@ export class ResourceClassFormComponent implements OnInit, AfterViewChecked { // 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); - for (const c of classKeys) { - this.existingResourceClassNames.push( - new RegExp('(?:^|W)' + c.split('#')[1] + '(?:$|W)') - ); - } - - // const propKeys: string[] = Object.keys(response.properties); - + Object.entries(this.ontology.classes).forEach( + ([key]) => { + const name = this._os.getNameFromIri(key); + this.existingNames.push( + new RegExp('(?:^|W)' + name.toLowerCase() + '(?:$|W)') + ); + } + ); }, (error: ApiResponseError) => { this._errorHandler.showMessage(error); @@ -185,6 +191,14 @@ export class ResourceClassFormComponent implements OnInit, AfterViewChecked { } this.resourceClassForm = this._fb.group({ + name: new FormControl({ + value: (this.edit ? this._os.getNameFromIri(this.iri) : ''), + disabled: this.edit + }, [ + Validators.required, + existingNamesValidator(this.existingNames), + Validators.pattern(CustomRegex.ID_NAME_REGEX) + ]), label: new FormControl({ value: this.resourceClassLabels, disabled: false }, [ @@ -214,7 +228,6 @@ export class ResourceClassFormComponent implements OnInit, AfterViewChecked { Object.keys(control.errors).map(key => { this.formErrors[field] += messages[key] + ' '; }); - } }); @@ -225,19 +238,38 @@ export class ResourceClassFormComponent implements OnInit, AfterViewChecked { * @param {StringLiteral[]} data * @param {string} type */ - handleData(data: StringLiteral[], type: string) { + handleData(data: StringLiteral[], type: 'label' | 'comment') { switch (type) { - case 'labels': + case 'label': this.resourceClassLabels = data; + this.handleError(this.resourceClassLabelsTouched, type); break; - case 'comments': + case 'comment': this.resourceClassComments = data; + this.handleError(this.resourceClassCommentsTouched, type); break; } } + /** + * error handle for string literals input + * @param touched + * @param type + */ + handleError(touched: boolean, type: 'label' | 'comment') { + + const checkValue = (type === 'label' ? this.resourceClassLabels : this.resourceClassComments); + const messages = this.validationMessages[type]; + + this.formErrors[type] = ''; + if (touched && !checkValue.length) { + this.formErrors[type] = messages['required']; + } + + } + // // submit @@ -296,10 +328,6 @@ export class ResourceClassFormComponent implements OnInit, AfterViewChecked { // create mode // submit resource class data to knora and create resource class incl. cardinality - // set resource class name / id: randomized string - const uniqueClassName: string = this._ontologyService.setUniqueName(this.ontology.id); - // or const uniqueClassName: string = this._resourceClassFormService.setUniqueName(this.ontology.id, this.resourceClassLabels[0].value, 'class'); - const onto = new UpdateOntology(); onto.id = this.ontology.id; @@ -307,7 +335,7 @@ export class ResourceClassFormComponent implements OnInit, AfterViewChecked { const newResClass = new CreateResourceClass(); - newResClass.name = uniqueClassName; + newResClass.name = this.resourceClassForm.controls['name'].value; newResClass.label = this.resourceClassLabels; newResClass.comment = this.resourceClassComments; newResClass.subClassOf = [this.iri]; diff --git a/src/app/workspace/resource/values/custom-regex.ts b/src/app/workspace/resource/values/custom-regex.ts index 244f005b77..5c7042d447 100644 --- a/src/app/workspace/resource/values/custom-regex.ts +++ b/src/app/workspace/resource/values/custom-regex.ts @@ -24,4 +24,7 @@ export class CustomRegex { public static readonly SHORTNAME_REGEX = /^[a-zA-Z]+\S*$/; public static readonly ONTOLOGYNAME_REGEX = /^(?![vV][0-9]|[0-9]|[\u00C0-\u017F]).[a-zA-Z0-9]+\S*$/; + + // regex to check ontology name: shouldn't start with a number or with 'v' followed by a number, spaces or special characters are not allowed + public static readonly ID_NAME_REGEX = /^(?![vV]+[0-9])+^([a-zA-Z])[a-zA-Z0-9_.-]*$/; } diff --git a/src/assets/style/_elements.scss b/src/assets/style/_elements.scss index 98665f663b..a6447d6507 100644 --- a/src/assets/style/_elements.scss +++ b/src/assets/style/_elements.scss @@ -124,13 +124,13 @@ a, // // margin element .more-space-top { - margin-top: 48px !important; + margin-top: 32px !important; } .more-space-bottom { margin-bottom: 24px !important; &.table { - margin-bottom: 48px !important; + margin-bottom: 32px !important; } } diff --git a/src/assets/style/_ontology.scss b/src/assets/style/_ontology.scss new file mode 100644 index 0000000000..692ea2fed8 --- /dev/null +++ b/src/assets/style/_ontology.scss @@ -0,0 +1,24 @@ + +// ontology +.ontology-form-field { + + .ontology-prefix-icon { + width: 36px; + padding: 0 8px; + display: block; + } + + mat-label, + mat-select, + input { + margin-left: 12px; + } + mat-select { + width: calc(100% - 12px); + } + + .ontology-error-with-prefix { + position: relative; + left: 64px; + } +} diff --git a/src/assets/style/main.scss b/src/assets/style/main.scss index eaf6fd409a..b5f0376f62 100644 --- a/src/assets/style/main.scss +++ b/src/assets/style/main.scss @@ -4,6 +4,7 @@ @import "elements"; @import "viewer"; @import "search"; +@import "ontology"; body { font-family: Roboto, "Helvetica Neue", sans-serif;