diff --git a/docs/assets/images/data-model-class-select-representation.png b/docs/assets/images/data-model-class-select-representation.png new file mode 100644 index 0000000000..89d1b3f42d Binary files /dev/null and b/docs/assets/images/data-model-class-select-representation.png differ diff --git a/docs/assets/images/data-model-create.png b/docs/assets/images/data-model-create.png new file mode 100644 index 0000000000..e4799e513f Binary files /dev/null and b/docs/assets/images/data-model-create.png differ diff --git a/docs/assets/images/data-model-property-create.png b/docs/assets/images/data-model-property-create.png new file mode 100644 index 0000000000..a39f73e76c Binary files /dev/null and b/docs/assets/images/data-model-property-create.png differ diff --git a/docs/assets/images/data-model-property-select-existing.png b/docs/assets/images/data-model-property-select-existing.png new file mode 100644 index 0000000000..e8eaf94d80 Binary files /dev/null and b/docs/assets/images/data-model-property-select-existing.png differ diff --git a/docs/assets/images/data-model-property-select-type.png b/docs/assets/images/data-model-property-select-type.png new file mode 100644 index 0000000000..8a4f1a63bc Binary files /dev/null and b/docs/assets/images/data-model-property-select-type.png differ diff --git a/docs/assets/images/project-create.png b/docs/assets/images/project-create.png new file mode 100644 index 0000000000..241203b00b Binary files /dev/null and b/docs/assets/images/project-create.png differ diff --git a/docs/assets/style/theme.css b/docs/assets/style/theme.css index 38339e095c..3ed149c4ac 100644 --- a/docs/assets/style/theme.css +++ b/docs/assets/style/theme.css @@ -2,3 +2,8 @@ .md-tabs { background-color: rgb(89, 73, 167) !important; } + +em { + font-size: smaller; + text-align: center; +} diff --git a/docs/how-to-use/project.md b/docs/how-to-use/project.md index c553774f4b..96f517aac2 100644 --- a/docs/how-to-use/project.md +++ b/docs/how-to-use/project.md @@ -2,9 +2,9 @@ ## Project -Once you are [logged in](/user-guide/#login), the dashboard displays the list of your project(s). If you are a project administrator or a system administrator, you can edit the project information or archive your project from the project menu. Archived projects are stored in a list on your dashboard and they can be "reactivated" at any time. +Once you are [logged in](/user-guide/#login), the dashboard displays the list of your project(s). If you are a project administrator or a system administrator, you can edit the project information or archive your project from the project menu. Archived projects are stored in a list on your dashboard and they can be "reactivated" at any time. -![Project list and project menu](../assets/images/project-list.png)*https://app2.dasch.swiss/dashboard - By clicking on the project name, you get access to the full project information.* +![Project list and project menu](../assets/images/project-list.png)* - By clicking on the project name, you get access to the full project information.* System administrator can create your new research project. Essential information are required such as the project name, a short project description and institution information. @@ -12,20 +12,19 @@ System administrator can create your new research project. Essential information As project administrator or system administrator, you can define your project, add your team members, create permission groups and as most important, define your data model (ontology) and the lists of your project. -![Project management available functionalities](../assets/images/project-info.png)*https://app2.dasch.swiss/project/0803/info - Project management functionalities; e.g. Incunabula project. Project information page is displayed without restricted content, the other functionalities are reserved for project admin and system admin.* +![Project management available functionalities](../assets/images/project-info.png)* - Project management functionalities; e.g. Incunabula project. Project information page is displayed without restricted content, the other functionalities are reserved for project admin and system admin.* --- ## Collaboration -⚠ *WORK IN PROGRESS* As system admin, you can add users as project member and define their permissions roles: *Who is able to edit or to see data in the project?* Data includes the research sources and their metadata. [Permissions](/user-guide/project/#permission-groups) can be set for the entire project or for single metadata fields. A user menu with different actions is accessible for each member of the project (link to the right side of the user line). The admin can appoint another user as project admin (or remove this permission), edit user's information, change user's password if forgotten, and remove a user. -![Collaboration page](../assets/images/project-collaboration.png)*https://app2.dasch.swiss/project/0803/collaboration - Collaboration page where project admin and system admin can add new user to the team.* +![Collaboration page](../assets/images/project-collaboration.png)* - Collaboration page where project admin and system admin can add new user to the team.* -***Permissions for project admins to add new users as project member will be implemented soon.*** +> **_NOTE:_** **Permissions for project admins to add new users as project member will be implemented soon.** --- @@ -37,7 +36,6 @@ Project admin can create one or several permission groups in their project to se --- ## Data model -⚠ *NOT YET IMPLEMENTED* (only mockups are presented) The most important step in the project is the definition of the data model. DSP-APP will provide a tool for easy creation of data models. First, you have to know which data and sources you want to work with. The data model can be flexible and customizable. With DSP-APP, you can comply with the FAIR data standard, but compliance is not required to analyze your data. @@ -54,19 +52,38 @@ The questions to answer in creating your data model: - data about the person you interviewed - location where the photograph was taken -Diagram 1 shows the relationships of the data by source type from these experiences. +Diagram 1 shows the relationships of the data by resource classes from these experiences. + +![Relationship of the data by resource classes](../assets/images/diagram-data-model.png)*Relationship of the data by resource classes* + +### Create your data model + +Go to your project, select the tab `Data model` and click on button `New data model` or `Create your first data model` (in case of a brand new project). This will open a form in a dialog box where you have to set a unique name and a label. Optional you can also add a short description of form of a comment. -![Relationship of the data by source type](../assets/images/diagram-data-model.png)*Relationship of the data by source type* +> **_NOTE:_** There are some rules for the unique name: +> +> - must be at least 3 characters long +> - shouldn't begin with a number +> - shouldn't begin with the letter v and a number +> - spaces or special characters are not allowed +> - the term "ontology" is not allowed +> - the unique name can't be changed afterwards + +The label is a combination of project's shortname and the unique name. You can replace with any other string. + +![Data model editor | Step 1: Start by creating a new data model.](../assets/images/data-model-create.png)*Data model editor | Step 1: Start by creating a new data model.* --- -### Select your SOURCE TYPES +### Create the resource CLASSES -In the data model editor, you select your source types from a predefined list on the right-hand side. Later, you will be able to customize the source type or define an additional default source type, if the one you need doesn't exist by default. +In the data model editor, you create your resource classes by selecting a default representation type from a predefined list on the right-hand side. -![Data model editor 1: Select all your main source types by drag and drop; e.g. for an interview, select the source type "Audio / Sound / Interview".](../assets/images/data-model-add-source.png)*Data model editor 1: Select all your main source types by drag and drop; e.g. for an interview, select the source type "Audio / Sound / Interview".* +Click on `+ Create new class` and select a representation type for your resource class. -In our example with the interview and the photographs, you drag and drop the following main source types from the list on the right-hand side: +![Data model editor | Step 2: Create all your main resource classes by selecting a predefined representation type; e.g. for a taped interview, select the representation type "Audio / Sound / Interview".](../assets/images/data-model-class-select-representation.png)*Data model editor | Step 2: Create all your main resource classes by selecting a predefined representation type; e.g. for a taped interview, select the representation type "Audio / Sound / Interview".* + +In our example with the interview and the photographs, you drag and drop the following main resource classes from the list on the right-hand side: - Audio / Sound / Interview - Transcript @@ -76,30 +93,42 @@ In our example with the interview and the photographs, you drag and drop the fol --- -### Select the METADATA fields for each source type (optional) +### Select or create the PROPERTIES for each resource class -The predefined source types offer a suggested list of metadata fields. This list could help to create a data model simply and quickly. It's also possible to deselect the suggested metadata fields (e.g., no metadata), to adapt others and to customize them. + -![Data model editor 2: Add additional metadata fields to your source type; e.g. add the missing field "Person".](../assets/images/data-model-add-property.png)*Data model editor 2: Add additional metadata fields to your source type; e.g. add the missing field "Person".* +There are two possibilites to add properties to a resource class: ---- +#### Create new property by selecting from list of default property types -### Customize the SOURCE TYPES and the METADATA fields (optional) +![Data model editor | Step 3: Add properties to your resource class by selecting from list of default property types.](../assets/images/data-model-property-select-type.png)*Data model editor | Step 3: Add properties to your resource class by selecting from list of default property types.* -It's possible to customize the predefined source type and the metadata field values by clicking on the edit button of the source type. You can rename the source type, rearrange the order of the metadata fields, and set permissions. +#### Select from list of existing properties -![Data model editor 3: Customize the source type AUDIO; e.g. rename it into Interview](../assets/images/data-model-edit-source.png)*Data model editor 3: Customize the source type AUDIO; e.g. rename it into Interview* +If you have already created some properties in another resource classes or in the property section you can select one of them to add it to the resource class. This way you have to define — for example — a "Title" property only once. ---- +![Data model editor | Step 3: Add properties to your resource class by selecting from list of already existing properties](/assets/images/data-model-property-select-existing.png)*Data model editor | Step 3: Add properties to your resource class by selecting from list of already existing properties* + +In both cases it will open the property form where you have to define a label and optional a comment. If you have selected an existing property, you can also change the label and comment. Be careful in this case, because it can have an effect on other resource classes that use the same property! + +The property type can't be changed here. Depending on the property type you have to set additional attributes, e.g. in a resource link property you have to select a resource class to which the property will point to. Similar in case of a list property where you have to select the corresponding list. In both cases a the linked resource class or the list should already exist. + +![Data model editor | Step 4: Define property's label and comment and set the cardinality corresponding to the resource class.](../assets/images/data-model-property-create.png)*Data model editor | Step 4: Define property's label and comment and set the cardinality corresponding to the resource class.* + +For each property in a resource class you can define some rules. For example you can define if one of the property can have `multiple` values or if a property is a mandatory (`requiered`) field. + +After adding more than one property to a resource class you can change the displayed order by drag'n'drop the properties in the list. + + diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 1fcbd3fa85..8795316160 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -71,7 +71,6 @@ import { PropertyFormComponent } from './project/ontology/property-form/property import { PropertyInfoComponent } from './project/ontology/property-info/property-info.component'; import { ResourceClassFormComponent } from './project/ontology/resource-class-form/resource-class-form.component'; import { ResourceClassInfoComponent } from './project/ontology/resource-class-info/resource-class-info.component'; -import { ResourceClassPropertyFormComponent } from './project/ontology/resource-class-property-form/resource-class-property-form.component'; import { ResourceComponent } from './workspace/resource/resource.component'; import { ResourceInstanceFormComponent } from './workspace/resource/resource-instance-form/resource-instance-form.component'; import { ResultsComponent } from './workspace/results/results.component'; @@ -150,7 +149,6 @@ export function httpLoaderFactory(httpClient: HttpClient) { PropertyInfoComponent, ResourceClassFormComponent, ResourceClassInfoComponent, - ResourceClassPropertyFormComponent, ResourceComponent, ResourceInstanceFormComponent, ResultsComponent, diff --git a/src/app/main/dialog/dialog.component.html b/src/app/main/dialog/dialog.component.html index 53b0529374..e5049173df 100644 --- a/src/app/main/dialog/dialog.component.html +++ b/src/app/main/dialog/dialog.component.html @@ -258,8 +258,8 @@
- +
@@ -281,22 +281,21 @@
- - + +
- +
- +
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 f90d84550d..f4d77b6402 100644 --- a/src/app/project/ontology/ontology-form/ontology-form.component.ts +++ b/src/app/project/ontology/ontology-form/ontology-form.component.ts @@ -1,12 +1,10 @@ import { Component, EventEmitter, Inject, Input, OnInit, Output } from '@angular/core'; import { FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms'; import { - ApiResponseData, ApiResponseError, CreateOntology, KnoraApiConnection, OntologyMetadata, - ProjectResponse, ReadOntology, ReadProject, UpdateOntologyMetadata @@ -14,7 +12,7 @@ import { import { DspApiConnectionToken, existingNamesValidator } from '@dasch-swiss/dsp-ui'; import { CacheService } from 'src/app/main/cache/cache.service'; import { ErrorHandlerService } from 'src/app/main/error/error-handler.service'; -import { ResourceClassFormService } from '../resource-class-form/resource-class-form.service'; +import { OntologyService } from '../ontology.service'; export interface NewOntology { projectIri: string; @@ -95,7 +93,7 @@ export class OntologyFormComponent implements OnInit { private _cache: CacheService, private _errorHandler: ErrorHandlerService, private _fb: FormBuilder, - private _resourceClassFormService: ResourceClassFormService + private _ontologyService: OntologyService ) { } ngOnInit() { @@ -121,7 +119,7 @@ export class OntologyFormComponent implements OnInit { (response: ReadOntology) => { // add values to the ontology form this.ontologyForm.controls['name'].disable(); - const name = this._resourceClassFormService.getOntologyName(this.iri); + const name = this._ontologyService.getOntologyName(this.iri); this.ontologyForm.controls['name'].setValue(name); this.ontologyForm.controls['label'].setValue(response.label); this.ontologyForm.controls['label'].setValidators( diff --git a/src/app/project/ontology/ontology-visualizer/ontology-visualizer.component.ts b/src/app/project/ontology/ontology-visualizer/ontology-visualizer.component.ts index 988c8328b5..394653659f 100644 --- a/src/app/project/ontology/ontology-visualizer/ontology-visualizer.component.ts +++ b/src/app/project/ontology/ontology-visualizer/ontology-visualizer.component.ts @@ -2,7 +2,7 @@ import { Component, Inject, Input, OnInit, Output } from '@angular/core'; import { ClassDefinition, KnoraApiConnection, ReadOntology } from '@dasch-swiss/dsp-js'; import { DspApiConnectionToken } from '@dasch-swiss/dsp-ui'; import { Link, Node } from '../../../../../node_modules/d3-force-3d'; -import { ResourceClassFormService } from '../resource-class-form/resource-class-form.service'; +import { OntologyService } from '../ontology.service'; export interface NewOntology { projectIri: string; @@ -26,7 +26,7 @@ export class OntologyVisualizerComponent implements OnInit { constructor( @Inject(DspApiConnectionToken) private _dspApiConnection: KnoraApiConnection, - private _resourceClassFormService: ResourceClassFormService) { } + private _ontologyService: OntologyService) { } isInNodes(item: string) { for (const node of this.nodes) { @@ -43,7 +43,7 @@ export class OntologyVisualizerComponent implements OnInit { let newLabel: string; if (resourceInfo[1] !== undefined) { type = resourceInfo[1]; - ontoName = this._resourceClassFormService.getOntologyName(resourceInfo[0]); + ontoName = this._ontologyService.getOntologyName(resourceInfo[0]); newLabel = ontoName + ':' + type; } else { type = iri.split('/').pop(); diff --git a/src/app/project/ontology/ontology.component.html b/src/app/project/ontology/ontology.component.html index c33e624301..b5fa76a14f 100644 --- a/src/app/project/ontology/ontology.component.html +++ b/src/app/project/ontology/ontology.component.html @@ -54,6 +54,7 @@

+ @@ -62,10 +63,10 @@

matTooltipPosition="above"> {{ontology?.label}}

-

- - Updated on: {{ontology.lastModificationDate | date:'medium'}} + + Updated on: {{lastModificationDate | date:'medium'}} @@ -96,6 +97,7 @@

--> +

Data model configuration

- @@ -177,14 +178,15 @@

- + (deleteResourceClass)="delete('ResourceClass', $event)" + (updateCardinality)="initOntology($event)">
@@ -193,11 +195,12 @@

- + (deleteResourceProperty)="delete('Property', $event)"> diff --git a/src/app/project/ontology/ontology.component.ts b/src/app/project/ontology/ontology.component.ts index 446d07d34c..4fb17b6556 100644 --- a/src/app/project/ontology/ontology.component.ts +++ b/src/app/project/ontology/ontology.component.ts @@ -18,7 +18,6 @@ import { PropertyDefinition, ReadOntology, ReadProject, - ResourceClassDefinition, UpdateOntology } from '@dasch-swiss/dsp-js'; import { DspApiConnectionToken, Session, SessionService, SortingService } from '@dasch-swiss/dsp-ui'; @@ -27,13 +26,18 @@ import { DialogComponent } from 'src/app/main/dialog/dialog.component'; import { ErrorHandlerService } from 'src/app/main/error/error-handler.service'; import { DefaultProperties, PropertyCategory, PropertyInfoObject } from './default-data/default-properties'; import { DefaultClass, DefaultResourceClasses } from './default-data/default-resource-classes'; -import { ResourceClassFormService } from './resource-class-form/resource-class-form.service'; +import { OntologyService } from './ontology.service'; export interface OntologyInfo { id: string; label: string; } +export interface CardinalityInfo { + resClass: ClassDefinition; + property: PropertyInfoObject; +} + @Component({ selector: 'app-ontology', templateUrl: './ontology.component.html', @@ -51,6 +55,7 @@ export class OntologyComponent implements OnInit { // permissions of logged-in user session: Session; + // system admin or project admin is by default false sysAdmin = false; projectAdmin = false; @@ -66,26 +71,32 @@ export class OntologyComponent implements OnInit { // existing project ontology names existingOntologyNames: string[] = []; - // current/selected ontology + // id of current ontology + ontologyIri: string = undefined; + + // current ontology ontology: ReadOntology; + // the lastModificationDate is the most important key + // when updating something inside the ontology lastModificationDate: string; + // all resource classes in the current ontology ontoClasses: ClassDefinition[]; - expandClasses = false; + // expand the resource class cards + expandClasses = true; + // all properties in the current ontology ontoProperties: PropertyDefinition[]; - // selected ontology id - ontologyIri: string = undefined; - // form to select ontology from list ontologyForm: FormGroup; // display resource classes as grid or as graph view: 'classes' | 'properties' | 'graph' = 'classes'; - // i18n setup + // i18n setup; in the user interface we use the term + // data model for ontology itemPluralMapping = { ontology: { '=1': '1 data model', @@ -99,17 +110,13 @@ export class OntologyComponent implements OnInit { defaultClasses: DefaultClass[] = DefaultResourceClasses.data; defaultProperties: PropertyCategory[] = DefaultProperties.data; - // @ViewChild(AddToDirective, { static: false }) addToHost: AddToDirective; - - // @ViewChild('addResourceClassComponent', { static: false }) addResourceClass: AddResourceClassComponent; - constructor( @Inject(DspApiConnectionToken) private _dspApiConnection: KnoraApiConnection, private _cache: CacheService, private _dialog: MatDialog, private _errorHandler: ErrorHandlerService, private _fb: FormBuilder, - private _resourceClassFormService: ResourceClassFormService, + private _ontologyService: OntologyService, private _route: ActivatedRoute, private _router: Router, private _session: SessionService, @@ -127,7 +134,7 @@ export class OntologyComponent implements OnInit { if (this._route.snapshot.params.id) { this.ontologyIri = decodeURIComponent(this._route.snapshot.params.id); } - // get view from route: classes, properties or graph + // get the selected view from route: display classes, properties or graph this.view = (this._route.snapshot.params.view ? this._route.snapshot.params.view : 'classes'); } @@ -202,13 +209,14 @@ export class OntologyComponent implements OnInit { // open this ontology this.openOntologyRoute(response.ontologies[0].id, this.view); this.ontologyIri = response.ontologies[0].id; + this.lastModificationDate = response.ontologies[0].lastModificationDate; } response.ontologies.forEach(ontoMeta => { // set list of already existing ontology names // it will be used in ontology form // because ontology name has to be unique - const name = this._resourceClassFormService.getOntologyName(ontoMeta.id); + const name = this._ontologyService.getOntologyName(ontoMeta.id); this.existingOntologyNames.push(name); // get each ontology @@ -222,47 +230,7 @@ export class OntologyComponent implements OnInit { // get all information to display this ontology // with all classes, properties and connected lists this.loadOntology = true; - this.ontology = readOnto; - this.lastModificationDate = this.ontology.lastModificationDate; - this._cache.set('currentOntology', this.ontology); - - // grab the onto class information to display - const allOntoClasses = readOnto.getAllClassDefinitions(); - - // reset the ontology classes - this.ontoClasses = []; - - // display only the classes which are not a subClass of Standoff - allOntoClasses.forEach(resClass => { - if (resClass.subClassOf.length) { - const splittedSubClass = resClass.subClassOf[0].split('#'); - if (!splittedSubClass[0].includes(Constants.StandoffOntology) && !splittedSubClass[1].includes('Standoff')) { - this.ontoClasses.push(resClass); - } - } - }); - // sort classes by label - // --> TODO: add sort functionallity to the gui - this.ontoClasses = this._sortingService.keySortByAlphabetical(this.ontoClasses, 'label'); - - // grab the onto properties information to display - const allOntoProperties = readOnto.getAllPropertyDefinitions(); - - // reset the ontology properties - this.ontoProperties = []; - - // display only the properties which are not a subjectType of Standoff - allOntoProperties.forEach(resProp => { - const standoff = (resProp.subjectType ? resProp.subjectType.includes('Standoff') : false); - if (resProp.objectType !== Constants.LinkValue && !standoff) { - this.ontoProperties.push(resProp); - } - }); - // sort properties by label - // --> TODO: add sort functionallity to the gui - this.ontoProperties = this._sortingService.keySortByAlphabetical(this.ontoProperties, 'label'); - - this.loadOntology = false; + this.resetOntologyView(readOnto); } if (response.ontologies.length === this.ontologies.length) { this.ontologies = this._sortingService.keySortByAlphabetical(this.ontologies, 'label'); @@ -274,11 +242,8 @@ export class OntologyComponent implements OnInit { this._errorHandler.showMessage(error); } ); - }); - } - }, (error: ApiResponseError) => { this.ontologies = []; @@ -288,16 +253,62 @@ export class OntologyComponent implements OnInit { ); } - // update view after selecting an ontology from dropdown - onValueChanged(id: string) { + initOntology(iri: string) { + this._dspApiConnection.v2.onto.getOntology(iri, true).subscribe( + (response: ReadOntology) => { + this.resetOntologyView(response); + }, + (error: ApiResponseError) => { + this._errorHandler.showMessage(error); + } + ); + } + initOntoClasses(allOntoClasses: ClassDefinition[]) { + + // reset the ontology classes + this.ontoClasses = []; + + // display only the classes which are not a subClass of Standoff + allOntoClasses.forEach(resClass => { + if (resClass.subClassOf.length) { + const splittedSubClass = resClass.subClassOf[0].split('#'); + if (!splittedSubClass[0].includes(Constants.StandoffOntology) && !splittedSubClass[1].includes('Standoff')) { + this.ontoClasses.push(resClass); + } + } + }); + // sort classes by label + // --> TODO: add sort functionallity to the gui + this.ontoClasses = this._sortingService.keySortByAlphabetical(this.ontoClasses, 'label'); + } + + initOntoProperties(allOntoProperties: PropertyDefinition[]) { + // reset the ontology properties + this.ontoProperties = []; + + // display only the properties which are not a subjectType of Standoff + allOntoProperties.forEach(resProp => { + const standoff = (resProp.subjectType ? resProp.subjectType.includes('Standoff') : false); + if (resProp.objectType !== Constants.LinkValue && !standoff) { + this.ontoProperties.push(resProp); + } + }); + // sort properties by label + // --> TODO: add sort functionallity to the gui + this.ontoProperties = this._sortingService.keySortByAlphabetical(this.ontoProperties, 'label'); + } + + /** + * update view after selecting an ontology from dropdown + * @param id + */ + onValueChanged(id: string) { if (!this.ontologyForm) { return; } - // reset and open selected ontology this.resetOntology(id); - } /** @@ -311,21 +322,41 @@ export class OntologyComponent implements OnInit { this._router.navigateByUrl(goto, { skipLocationChange: false }); } + /** + * resets the current view and the selected ontology + * @param id + */ resetOntology(id: string) { - this.ontology = undefined; this.ontoClasses = []; this.openOntologyRoute(id, this.view); this.initOntologiesList(); + } + + resetOntologyView(ontology: ReadOntology) { + this.ontology = ontology; + this.lastModificationDate = this.ontology.lastModificationDate; + this._cache.set('currentOntology', this.ontology); + // grab the onto class information to display + this.initOntoClasses(ontology.getAllClassDefinitions()); + + // grab the onto properties information to display + this.initOntoProperties(ontology.getAllPropertyDefinitions()); + + this.loadOntology = false; } + /** + * filters owl class + * @param owlClass + */ filterOwlClass(owlClass: any) { return (owlClass['@type'] === 'owl:class'); } /** - * opens ontology form + * opens ontology form to create or edit ontology info * @param mode * @param [iri] only in edit mode */ @@ -361,20 +392,19 @@ export class OntologyComponent implements OnInit { } /** - * opens resource class form + * opens resource class form to create or edit resource class info * @param mode * @param resClassInfo (could be subClassOf (create mode) or resource class itself (edit mode)) */ openResourceClassForm(mode: 'createResourceClass' | 'editResourceClass', resClassInfo: DefaultClass): void { const dialogConfig: MatDialogConfig = { - disableClose: true, - width: '840px', - maxHeight: '90vh', + width: '640px', + maxHeight: '80vh', position: { top: '112px' }, - data: { id: resClassInfo.iri, title: resClassInfo.label, subtitle: 'Customize resource class', mode: mode, project: this.projectCode } + data: { id: resClassInfo.iri, title: resClassInfo.label, subtitle: 'Customize resource class', mode: mode } }; const dialogRef = this._dialog.open( @@ -389,50 +419,21 @@ export class OntologyComponent implements OnInit { } /** - * opens property form + * opens property form to create or edit property info * @param mode * @param propertyInfo (could be subClassOf (create mode) or resource class itself (edit mode)) */ - openPropertyForm(mode: 'createProperty' | 'editProperty', propertyInfo: PropertyInfoObject): void { + openPropertyForm(mode: 'createProperty' | 'editProperty', propertyInfo: PropertyInfoObject,): void { - const title = (propertyInfo.propDef ? propertyInfo.propDef.label : propertyInfo.propType.label); + const title = (propertyInfo.propDef ? propertyInfo.propDef.label : propertyInfo.propType.group + ': ' + propertyInfo.propType.label); const dialogConfig: MatDialogConfig = { - disableClose: false, width: '640px', - maxHeight: '90vh', - position: { - top: '112px' - }, - data: { propInfo: propertyInfo, title: title, subtitle: 'Customize property', mode: mode, project: this.project.id } - }; - - const dialogRef = this._dialog.open( - DialogComponent, - dialogConfig - ); - - dialogRef.afterClosed().subscribe(result => { - // update the view - this.initOntologiesList(); - }); - } - - - /** - * updates cardinality - * @param subClassOf resource class - */ - updateCard(subClassOf: ResourceClassDefinition) { - - const dialogConfig: MatDialogConfig = { - disableClose: true, - width: '840px', - maxHeight: '90vh', + maxHeight: '80vh', position: { top: '112px' }, - data: { mode: 'updateCardinality', id: subClassOf.id, title: subClassOf.label, subtitle: 'Update the metadata fields of resource class', project: this.projectCode } + data: { propInfo: propertyInfo, title: title, subtitle: 'Customize property', mode: mode } }; const dialogRef = this._dialog.open( @@ -441,13 +442,13 @@ export class OntologyComponent implements OnInit { ); dialogRef.afterClosed().subscribe(result => { - // update the view - this.initOntologiesList(); + // update the view of resource class or list of properties + this.initOntology(this.ontologyIri); }); } /** - * delete either ontology or resource class + * delete either ontology, resource class or property * * @param mode Can be 'Ontology' or 'ResourceClass' * @param id @@ -553,7 +554,6 @@ export class OntologyComponent implements OnInit { this.loadOntology = false; } ); - } } diff --git a/src/app/project/ontology/ontology.service.spec.ts b/src/app/project/ontology/ontology.service.spec.ts new file mode 100644 index 0000000000..02a8b9a9ac --- /dev/null +++ b/src/app/project/ontology/ontology.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { OntologyService } from './ontology.service'; + +describe('OntologyService', () => { + let service: OntologyService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(OntologyService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/project/ontology/ontology.service.ts b/src/app/project/ontology/ontology.service.ts new file mode 100644 index 0000000000..cbea3f7ebf --- /dev/null +++ b/src/app/project/ontology/ontology.service.ts @@ -0,0 +1,71 @@ +import { Injectable } from '@angular/core'; +import { Cardinality } from '@dasch-swiss/dsp-js'; + +/** + * helper methods for the ontology editor + */ +@Injectable({ + providedIn: 'root' +}) +export class OntologyService { + + constructor() { } + + /** + * create a unique name (id) for resource classes or properties; + * + * @param ontologyIri + * @param [label] + * @returns unique name + */ + setUniqueName(ontologyIri: string, label?: string, type?: 'class' | 'prop'): string { + + if (label && type) { + // build name from label + // normalize and replace spaces and special chars + 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 + return this.getOntologyName(ontologyIri).substring(0, 3) + Math.random().toString(36).substring(2, 5) + Math.random().toString(36).substring(2, 5); + } + } + + /** + * get the ontolgoy name from ontology iri + * + * @param {string} ontologyIri + * @returns string + */ + getOntologyName(ontologyIri: string): string { + + const array = ontologyIri.split('/'); + + const pos = array.length - 2; + + return array[pos].toLowerCase(); + } + + /** + * convert cardinality values (multiple? & required?) from form to DSP-JS cardinality enum 1-n, 0-n, 1, 0-1 + * @param {boolean} multiple + * @param {boolean} required + * @returns Cardinality + */ + translateCardinality(multiple: boolean, required: boolean): Cardinality { + + if (multiple && required) { + // cardinality 1-n (at least one) + return Cardinality._1_n; + } else if (multiple && !required) { + // cardinality 0-n (may have many) + return Cardinality._0_n; + } else if (!multiple && required) { + // cardinality 1 (required) + return Cardinality._1; + } else { + // cardinality 0-1 (optional) + return Cardinality._0_1; + } + } +} 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 cf0c0eff11..5a3140653b 100644 --- a/src/app/project/ontology/property-form/property-form.component.html +++ b/src/app/project/ontology/property-form/property-form.component.html @@ -1,15 +1,28 @@ -
+ -
+

+ This property already exists. + Be careful when editing it, it could have an effect in other resource classes if it is used there. +

+ +
+ + + + + {{propertyInfo.propType?.icon}}  + + Property type + +
- @@ -18,24 +31,12 @@
-
- +
- - - - {{propertyInfo.propType?.icon}}  - - Property type - - -
@@ -56,7 +57,7 @@ Select resource class - + {{item.label}} @@ -72,7 +73,7 @@ @@ -83,6 +84,19 @@
+ +
+ + Multiple values? + + + + Required field? + +
+
@@ -91,12 +105,13 @@ {{ 'appLabels.form.action.cancel' | translate }} -
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 d093c110db..a7b29b136f 100644 --- a/src/app/project/ontology/property-form/property-form.component.scss +++ b/src/app/project/ontology/property-form/property-form.component.scss @@ -12,7 +12,7 @@ .large-field, .medium-field, .small-field { - margin: 0 2px; + margin: 8px 2px; &.string-literal-container { display: inline-block; @@ -42,3 +42,10 @@ width: calc(100% - 12px); } } + +.cardinality { + + .mat-slide-toggle { + padding: 0 16px; + } +} 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 952893489c..11f1f7ac8b 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 @@ -1,6 +1,6 @@ import { HttpClientTestingModule } from '@angular/common/http/testing'; import { Component, DebugElement, ViewChild } from '@angular/core'; -import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing'; +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { ReactiveFormsModule } from '@angular/forms'; import { MatAutocompleteModule } from '@angular/material/autocomplete'; import { MatButtonModule } from '@angular/material/button'; @@ -12,14 +12,12 @@ import { MatSelectModule } from '@angular/material/select'; import { By } from '@angular/platform-browser'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { RouterTestingModule } from '@angular/router/testing'; -import { KnoraApiConnection, MockOntology, ReadOntology } from '@dasch-swiss/dsp-js'; -import { AppInitService, DspActionModule, DspApiConfigToken, DspApiConnectionToken } from '@dasch-swiss/dsp-ui'; +import { MockOntology, ReadOntology } from '@dasch-swiss/dsp-js'; +import { DspActionModule, DspApiConnectionToken } from '@dasch-swiss/dsp-ui'; import { TranslateModule } from '@ngx-translate/core'; import { of } from 'rxjs'; import { CacheService } from 'src/app/main/cache/cache.service'; -import { TestConfig } from 'test.config'; import { PropertyInfoObject } from '../default-data/default-properties'; -import { ResourceClassFormService } from '../resource-class-form/resource-class-form.service'; import { PropertyFormComponent } from './property-form.component'; /** @@ -173,8 +171,7 @@ describe('PropertyFormComponent', () => { { provide: CacheService, useValue: cacheServiceSpyLists - }, - ResourceClassFormService + } ] }) .compileComponents(); 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 b26ed9747d..d6974d6538 100644 --- a/src/app/project/ontology/property-form/property-form.component.ts +++ b/src/app/project/ontology/property-form/property-form.component.ts @@ -5,21 +5,23 @@ import { ClassDefinition, Constants, CreateResourceProperty, + IHasProperty, KnoraApiConnection, ListNodeInfo, ReadOntology, + ResourceClassDefinitionWithAllLanguages, ResourcePropertyDefinitionWithAllLanguages, StringLiteral, UpdateOntology, + UpdateResourceClassCardinality, UpdateResourcePropertyComment, UpdateResourcePropertyLabel } from '@dasch-swiss/dsp-js'; import { AutocompleteItem, DspApiConnectionToken } from '@dasch-swiss/dsp-ui'; -import { Observable } from 'rxjs'; import { CacheService } from 'src/app/main/cache/cache.service'; import { ErrorHandlerService } from 'src/app/main/error/error-handler.service'; import { DefaultProperties, DefaultProperty, PropertyCategory, PropertyInfoObject } from '../default-data/default-properties'; -import { ResourceClassFormService } from '../resource-class-form/resource-class-form.service'; +import { OntologyService } from '../ontology.service'; @Component({ selector: 'app-property-form', @@ -34,6 +36,19 @@ export class PropertyFormComponent implements OnInit { */ @Input() propertyInfo: PropertyInfoObject; + /** + * iri of resClassIri; will be used to set cardinality + */ + @Input() resClassIri?: string; + + /** + * position of property in case of cardinality update + */ + @Input() guiOrder?: number = 0; + + /** + * output closeDialog of property form component to update parent component + */ @Output() closeDialog: EventEmitter = new EventEmitter(); /** @@ -68,7 +83,7 @@ export class PropertyFormComponent implements OnInit { lists: ListNodeInfo[]; // resource classes in this ontology - resourceClass: ClassDefinition[] = []; + resourceClasses: ClassDefinition[] = []; loading = false; @@ -85,7 +100,7 @@ export class PropertyFormComponent implements OnInit { private _cache: CacheService, private _errorHandler: ErrorHandlerService, private _fb: FormBuilder, - private _resourceClassFormService: ResourceClassFormService + private _ontologyService: OntologyService ) { } ngOnInit() { @@ -100,8 +115,7 @@ export class PropertyFormComponent implements OnInit { // set various lists to select from // a) in case of link value: // set list of resource classes from response; needed for linkValue - this.resourceClass = response.getAllClassDefinitions(); - + this.resourceClasses = response.getAllClassDefinitions(); }, (error: ApiResponseError) => { this._errorHandler.showMessage(error); @@ -133,7 +147,9 @@ export class PropertyFormComponent implements OnInit { this.propertyForm = this._fb.group({ 'guiAttr': new FormControl({ value: this.guiAttributes - }) + }), + 'multiple': new FormControl(), + 'required': new FormControl() }); this.updateAttributeField(this.propertyInfo.propType); @@ -230,7 +246,6 @@ export class PropertyFormComponent implements OnInit { this.showGuiAttr = false; } - } else { // depending on the selected property type, // we have to define gui element attributes @@ -256,9 +271,10 @@ export class PropertyFormComponent implements OnInit { } submitData() { + this.loading = true; // do something with your data if (this.propertyInfo.propDef) { - // edit mode: res property info (label and comment) + // edit mode: update res property info (label and comment) // label const onto4Label = new UpdateOntology(); onto4Label.id = this.ontology.id; @@ -280,24 +296,35 @@ export class PropertyFormComponent implements OnInit { this._dspApiConnection.v2.onto.updateResourceProperty(onto4Label).subscribe( (classLabelResponse: ResourcePropertyDefinitionWithAllLanguages) => { - this.ontology.lastModificationDate = classLabelResponse.lastModificationDate; - onto4Comment.lastModificationDate = this.ontology.lastModificationDate; + this.lastModificationDate = classLabelResponse.lastModificationDate; + onto4Comment.lastModificationDate = this.lastModificationDate; this._dspApiConnection.v2.onto.updateResourceProperty(onto4Comment).subscribe( (classCommentResponse: ResourcePropertyDefinitionWithAllLanguages) => { - this.ontology.lastModificationDate = classCommentResponse.lastModificationDate; + this.lastModificationDate = classCommentResponse.lastModificationDate; + + if (this.resClassIri && classCommentResponse.lastModificationDate) { + // edit cardinality mode: update cardinality of existing property in res class + this.setCardinality(this.propertyInfo.propDef); + } else { + // close the dialog box + this.loading = false; + this.closeDialog.emit(); + } - // close the dialog box - this.loading = false; - this.closeDialog.emit(); }, (error: ApiResponseError) => { + this.error = true; + this.loading = false; this._errorHandler.showMessage(error); } ); + }, (error: ApiResponseError) => { + this.error = true; + this.loading = false; this._errorHandler.showMessage(error); } ); @@ -307,7 +334,7 @@ export class PropertyFormComponent implements OnInit { // submit property // this.submitProps(this.resourceClassForm.value.properties, this.propertyInfo.propDef.id); // set resource property name / id: randomized string - const uniquePropName: string = this._resourceClassFormService.setUniqueName(this.ontology.id); + const uniquePropName: string = this._ontologyService.setUniqueName(this.ontology.id); const onto = new UpdateOntology(); @@ -317,7 +344,6 @@ export class PropertyFormComponent implements OnInit { // prepare payload for property const newResProp = new CreateResourceProperty(); newResProp.name = uniquePropName; - // --> TODO update prop.label and use StringLiteralInput in property-form newResProp.label = this.labels; newResProp.comment = (this.comments.length ? this.comments : this.labels); const guiAttr = this.propertyForm.controls['guiAttr'].value; @@ -363,11 +389,20 @@ export class PropertyFormComponent implements OnInit { this._dspApiConnection.v2.onto.createResourceProperty(onto).subscribe( (response: ResourcePropertyDefinitionWithAllLanguages) => { this.lastModificationDate = response.lastModificationDate; - // close the dialog box - this.loading = false; - this.closeDialog.emit(); + + if (this.resClassIri && response.lastModificationDate) { + // set cardinality + this.setCardinality(response); + } else { + // close the dialog box + this.loading = false; + this.closeDialog.emit(); + } + }, (error: ApiResponseError) => { + this.error = true; + this.loading = false; this._errorHandler.showMessage(error); } ); @@ -375,4 +410,45 @@ export class PropertyFormComponent implements OnInit { } } + setCardinality(prop: ResourcePropertyDefinitionWithAllLanguages) { + + const onto = new UpdateOntology(); + + onto.lastModificationDate = this.lastModificationDate; + + onto.id = this.ontology.id; + + const addCard = new UpdateResourceClassCardinality(); + + addCard.id = this.resClassIri; + + addCard.cardinalities = []; + + const propCard: IHasProperty = { + propertyIndex: prop.id, + cardinality: this._ontologyService.translateCardinality(this.propertyForm.value.multiple, this.propertyForm.value.required), + guiOrder: this.guiOrder // add new property to the end of current list of properties + }; + + addCard.cardinalities.push(propCard); + + onto.entity = addCard; + + this._dspApiConnection.v2.onto.addCardinalityToResourceClass(onto).subscribe( + (res: ResourceClassDefinitionWithAllLanguages) => { + + this.lastModificationDate = res.lastModificationDate; + // close the dialog box + this.loading = false; + this.closeDialog.emit(); + }, + (error: ApiResponseError) => { + this.error = true; + this.loading = false; + this._errorHandler.showMessage(error); + } + ); + + } + } diff --git a/src/app/project/ontology/property-info/property-info.component.html b/src/app/project/ontology/property-info/property-info.component.html index e5f906595a..7ac45eadec 100644 --- a/src/app/project/ontology/property-info/property-info.component.html +++ b/src/app/project/ontology/property-info/property-info.component.html @@ -17,6 +17,7 @@ matTooltipPosition="above" [innerHTML]="'→ ' + propAttribute">

+
@@ -25,7 +26,7 @@ {{propInfo.required ? 'check_box' : 'check_box_outline_blank' }} required - + Property is used in: @@ -42,9 +43,25 @@
-
-
+ +
+ +
+ + + + + + +
+ +
- -
- - - Default language for the labels - - - {{ option.value }} - - - - - -
-
- - - - - - - - -
- -
- - -
- - - - - - - - - -
-
TODO move to knora-api-js-lib - // nameRegex: RegExp = /^(?![0-9]).(?![\u00C0-\u017F]).[a-zA-Z0-9]+\S*$/; - // form errors on the following fields: formErrors = { 'label': '', @@ -150,7 +119,8 @@ export class ResourceClassFormComponent implements OnInit, OnDestroy, AfterViewC private _cache: CacheService, private _cdr: ChangeDetectorRef, private _errorHandler: ErrorHandlerService, - private _resourceClassFormService: ResourceClassFormService + private _fb: FormBuilder, + private _ontologyService: OntologyService ) { } ngOnInit() { @@ -159,9 +129,6 @@ export class ResourceClassFormComponent implements OnInit, OnDestroy, AfterViewC this.existingResourceClassNames = [ new RegExp('anEmptyRegularExpressionWasntPossible') ]; - this.existingPropertyNames = [ - new RegExp('anEmptyRegularExpressionWasntPossible') - ]; // set file representation or default resource class as title this.resourceClassTitle = this.name; @@ -196,10 +163,6 @@ export class ResourceClassFormComponent implements OnInit, OnDestroy, AfterViewC } - ngOnDestroy() { - this.resourceClassFormSub.unsubscribe(); - } - ngAfterViewChecked() { this._cdr.detectChanges(); } @@ -209,60 +172,31 @@ export class ResourceClassFormComponent implements OnInit, OnDestroy, AfterViewC buildForm() { - // reset properties - this._resourceClassFormService.resetProperties(); - if (this.edit) { - - if (this.showResourceClassForm) { - // edit mode: res class info (label and comment) - // get resource class info - const resourceClasses: ResourceClassDefinitionWithAllLanguages[] = this.ontology.getClassDefinitionsByType(ResourceClassDefinitionWithAllLanguages); - Object.keys(resourceClasses).forEach(key => { - if (resourceClasses[key].id === this.iri) { - this.resourceClassLabels = resourceClasses[key].labels; - this.resourceClassComments = resourceClasses[key].comments; - } - }); - this.resourceClassFormSub = this._resourceClassFormService.resourceClassForm$ - .subscribe(resourceClass => { - this.resourceClassForm = resourceClass; - }); - - } else { - // edit mode: res class cardinality - // 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; - }); - - // set default property language from resource class / first element - this.resourceClassForm.controls.language.setValue(ontoClasses[key].labels[0].language); - this.resourceClassForm.controls.language.disable(); - } - }); - } - - } else { - // create mode - this.resourceClassFormSub = this._resourceClassFormService.resourceClassForm$ - .subscribe(resourceClass => { - this.resourceClassForm = resourceClass; - this.properties = this.resourceClassForm.get('properties') as FormArray; - }); + // edit mode: res class info (label and comment) + // get resource class info + const resourceClasses: ResourceClassDefinitionWithAllLanguages[] = this.ontology.getClassDefinitionsByType(ResourceClassDefinitionWithAllLanguages); + Object.keys(resourceClasses).forEach(key => { + if (resourceClasses[key].id === this.iri) { + this.resourceClassLabels = resourceClasses[key].labels; + this.resourceClassComments = resourceClasses[key].comments; + } + }); } + this.resourceClassForm = this._fb.group({ + label: new FormControl({ + value: this.resourceClassLabels, disabled: false + }, [ + Validators.required + ]), + comment: new FormControl({ + value: this.resourceClassComments, disabled: false + }, [ + Validators.required + ]) + }); + this.resourceClassForm.valueChanges.subscribe(data => this.onValueChanged(data)); } @@ -286,29 +220,6 @@ export class ResourceClassFormComponent implements OnInit, OnDestroy, AfterViewC } - // - // property form: handle list of properties - - /** - * add property line - */ - addProperty() { - this._resourceClassFormService.addProperty(); - } - /** - * delete property line - */ - removeProperty(index: number) { - this._resourceClassFormService.removeProperty(index); - } - /** - * reset properties - */ - resetProperties() { - this._resourceClassFormService.resetProperties(); - this.addProperty(); - } - /** * set stringLiterals for label or comment from dsp-string-literal-input * @param {StringLiteral[]} data @@ -327,39 +238,6 @@ export class ResourceClassFormComponent implements OnInit, OnDestroy, AfterViewC } } - // - // form navigation: - - /** - * go to next step: from resource-class form forward to properties form - * In create mode only - */ - nextStep(ev: Event) { - - // go to next step: properties form - this.showResourceClassForm = false; - - // use response to go further with properties - this.updateParent.emit({ title: this.resourceClassLabels[0].value, subtitle: 'Define the metadata fields for the resource class' }); - - // set default property language from res class label - this.resourceClassForm.controls.language.setValue(this.resourceClassLabels[0].language); - - // load one first property line - if (!this.resourceClassForm.value.properties.length) { - this.addProperty(); - } - } - /** - * 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 the resource class' }); - this.showResourceClassForm = true; - } - // // submit @@ -370,63 +248,56 @@ export class ResourceClassFormComponent implements OnInit, OnDestroy, AfterViewC this.loading = true; if (this.edit) { - if (this.showResourceClassForm) { - // edit mode: res class info (label and comment) - // label - const onto4Label = new UpdateOntology(); - onto4Label.id = this.ontology.id; - onto4Label.lastModificationDate = this.lastModificationDate; - - const updateLabel = new UpdateResourceClassLabel(); - updateLabel.id = this.iri; - updateLabel.labels = this.resourceClassLabels; - onto4Label.entity = updateLabel; - - // comment - const onto4Comment = new UpdateOntology(); - onto4Comment.id = this.ontology.id; - - const updateComment = new UpdateResourceClassComment(); - updateComment.id = this.iri; - updateComment.comments = this.resourceClassComments; - onto4Comment.entity = updateComment; - - this._dspApiConnection.v2.onto.updateResourceClass(onto4Label).subscribe( - (classLabelResponse: ResourceClassDefinitionWithAllLanguages) => { - this.lastModificationDate = classLabelResponse.lastModificationDate; - onto4Comment.lastModificationDate = this.lastModificationDate; - - this._dspApiConnection.v2.onto.updateResourceClass(onto4Comment).subscribe( - (classCommentResponse: ResourceClassDefinitionWithAllLanguages) => { - this.lastModificationDate = classCommentResponse.lastModificationDate; - - // close the dialog box - this.loading = false; - this.closeDialog.emit(); - }, - (error: ApiResponseError) => { - this._errorHandler.showMessage(error); - } - ); - - }, - (error: ApiResponseError) => { - this._errorHandler.showMessage(error); - } - ); - - } else { - // edit mode: res class cardinality - // submit properties and set cardinality - this.submitProps(this.resourceClassForm.value.properties, this.iri); - } + // edit mode: res class info (label and comment) + // label + const onto4Label = new UpdateOntology(); + onto4Label.id = this.ontology.id; + onto4Label.lastModificationDate = this.lastModificationDate; + + const updateLabel = new UpdateResourceClassLabel(); + updateLabel.id = this.iri; + updateLabel.labels = this.resourceClassLabels; + onto4Label.entity = updateLabel; + + // comment + const onto4Comment = new UpdateOntology(); + onto4Comment.id = this.ontology.id; + + const updateComment = new UpdateResourceClassComment(); + updateComment.id = this.iri; + updateComment.comments = this.resourceClassComments; + onto4Comment.entity = updateComment; + + this._dspApiConnection.v2.onto.updateResourceClass(onto4Label).subscribe( + (classLabelResponse: ResourceClassDefinitionWithAllLanguages) => { + this.lastModificationDate = classLabelResponse.lastModificationDate; + onto4Comment.lastModificationDate = this.lastModificationDate; + + this._dspApiConnection.v2.onto.updateResourceClass(onto4Comment).subscribe( + (classCommentResponse: ResourceClassDefinitionWithAllLanguages) => { + this.lastModificationDate = classCommentResponse.lastModificationDate; + + // close the dialog box + this.loading = false; + this.closeDialog.emit(); + }, + (error: ApiResponseError) => { + this._errorHandler.showMessage(error); + } + ); + + }, + (error: ApiResponseError) => { + this._errorHandler.showMessage(error); + } + ); } else { // 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._resourceClassFormService.setUniqueName(this.ontology.id); + 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(); @@ -446,9 +317,9 @@ export class ResourceClassFormComponent implements OnInit, OnDestroy, AfterViewC (classResponse: ResourceClassDefinitionWithAllLanguages) => { // need lmd from classResponse this.lastModificationDate = classResponse.lastModificationDate; - - // submit properties and set cardinality - this.submitProps(this.resourceClassForm.value.properties, classResponse.id); + // close the dialog box + this.loading = false; + this.closeDialog.emit(); }, (error: ApiResponseError) => { @@ -464,165 +335,7 @@ export class ResourceClassFormComponent implements OnInit, OnDestroy, AfterViewC */ closeMessage() { this.resourceClassForm.reset(); - this.resourceClassFormSub.unsubscribe(); this.closeDialog.emit(); } - submitProps(props: Property[], classIri: string) { - - let i = 1; - from(props) - .pipe(concatMap( - (prop: Property) => { - // submit prop - // console.log('first pipe operator...waiting...prepare and submit prop', prop); - if (prop.iri) { - // 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); - } - return new Promise(resolve => setTimeout(() => resolve(prop), 1200)); - } - )) - .pipe(concatMap( - (prop: Property) => { - i++; - // console.log('second pipe operator; do sth. with prop response', prop); - return of(prop); - } - )) - .subscribe( - (prop: Property) => { - - if (i > props.length) { - // console.log('at the end: created', prop) - - // all properties are created and exist - // set the cardinality - this.setCardinality(this.propsForCard, classIri); - } - } - ); - - } - - createProp(prop: Property, classIri?: string) { - return new Promise((resolve, reject) => { - - // 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(); - newResProp.name = uniquePropName; - // --> TODO update prop.label and use StringLiteralInput in property-form - newResProp.label = [ - { - 'value': prop.label, - 'language': this.resourceClassForm.controls.language.value - } - ]; - if (prop.guiAttr) { - switch (prop.type.guiEle) { - - case Constants.SalsahGui + Constants.HashDelimiter + 'Colorpicker': - newResProp.guiAttributes = ['ncolors=' + prop.guiAttr]; - break; - case Constants.SalsahGui + Constants.HashDelimiter + 'List': - case Constants.SalsahGui + Constants.HashDelimiter + 'Pulldown': - case Constants.SalsahGui + Constants.HashDelimiter + 'Radio': - newResProp.guiAttributes = ['hlist=<' + prop.guiAttr + '>']; - break; - case Constants.SalsahGui + Constants.HashDelimiter + 'SimpleText': - // --> TODO could have two guiAttr fields: size and maxlength - // we suggest to use default value for size; we do not support this guiAttr in DSP-App - newResProp.guiAttributes = ['maxlength=' + prop.guiAttr]; - break; - case Constants.SalsahGui + Constants.HashDelimiter + 'Spinbox': - // --> TODO could have two guiAttr fields: min and max - newResProp.guiAttributes = ['min=' + prop.guiAttr, 'max=' + prop.guiAttr]; - break; - case Constants.SalsahGui + Constants.HashDelimiter + 'Textarea': - // --> TODO could have four guiAttr fields: width, cols, rows, wrap - // we suggest to use default values; we do not support this guiAttr in DSP-App - newResProp.guiAttributes = ['width=100%']; - break; - } - } - newResProp.guiElement = prop.type.guiEle; - newResProp.subPropertyOf = [prop.type.subPropOf]; - - if (prop.type.subPropOf === Constants.HasLinkTo) { - newResProp.objectType = prop.guiAttr; - // newResProp.subjectType = classIri; - } else { - newResProp.objectType = prop.type.objectType; - } - - onto.entity = newResProp; - - this._dspApiConnection.v2.onto.createResourceProperty(onto).subscribe( - (response: ResourcePropertyDefinitionWithAllLanguages) => { - this.lastModificationDate = response.lastModificationDate; - // prepare prop for cardinality - prop.iri = response.id; - this.propsForCard.push(prop); - }, - (error: ApiResponseError) => { - this._errorHandler.showMessage(error); - } - ); - }); - } - - setCardinality(props: Property[], classIri: string) { - const onto = new UpdateOntology(); - - onto.lastModificationDate = this.lastModificationDate; - - onto.id = this.ontology.id; - - const addCard = new UpdateResourceClassCardinality(); - - addCard.id = classIri; - - addCard.cardinalities = []; - - 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; - - onto.entity = addCard; - - 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); - } - ); - - this.loading = false; - this.closeDialog.emit(); - } } diff --git a/src/app/project/ontology/resource-class-form/resource-class-form.service.spec.ts b/src/app/project/ontology/resource-class-form/resource-class-form.service.spec.ts deleted file mode 100644 index 702a5cf98b..0000000000 --- a/src/app/project/ontology/resource-class-form/resource-class-form.service.spec.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { TestBed } from '@angular/core/testing'; -import { ReactiveFormsModule } from '@angular/forms'; -import { ResourceClassFormService } from './resource-class-form.service'; - -describe('ResourceClassFormService', () => { - beforeEach(() => TestBed.configureTestingModule({ - imports: [ - ReactiveFormsModule - ] - })); - - it('should be created', () => { - const service: ResourceClassFormService = TestBed.inject(ResourceClassFormService); - expect(service).toBeTruthy(); - }); -}); 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 deleted file mode 100644 index 1ddf9a3057..0000000000 --- a/src/app/project/ontology/resource-class-form/resource-class-form.service.ts +++ /dev/null @@ -1,269 +0,0 @@ -import { Injectable } from '@angular/core'; -import { FormArray, FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms'; -import { Cardinality, IHasProperty, PropertyDefinition, ResourceClassDefinition, ResourcePropertyDefinition } from '@dasch-swiss/dsp-js'; -import { BehaviorSubject, Observable } from 'rxjs'; -import { DefaultProperty } from '../default-data/default-properties'; - -// property data structure -export class Property { - iri: string; - label: string; - type: DefaultProperty; - multiple: boolean; - required: boolean; - guiAttr: string; - // permission: string; - - constructor( - iri?: string, - label?: string, - type?: any, - multiple?: boolean, - required?: boolean, - guiAttr?: string - // permission?: string - ) { - this.iri = iri; - this.label = label; - this.type = type; - this.multiple = multiple; - this.required = required; - this.guiAttr = guiAttr; - // this.permission = permission; - } -} - -// property form controls -export class PropertyForm { - iri = new FormControl(); - label = new FormControl(); - type = new FormControl(); - multiple = new FormControl(); - required = new FormControl(); - guiAttr = new FormControl(); - // permission = new FormControl(); - - constructor( - property: Property - ) { - this.iri.setValue(property.iri); - - this.label.setValue(property.label); - this.label.setValidators([Validators.required]); - - this.type.setValue(property.type); - this.type.setValidators([Validators.required]); - - this.multiple.setValue(property.multiple); - - this.required.setValue(property.required); - - this.guiAttr.setValue(property.guiAttr); - - // this.permission.setValue(property.permission); - // --> TODO permission is not implemented yet - // this.permission.setValidators([Validators.required]); - } -} - -// resource class data structure -export class ResourceClass { - language: string; - properties: Property[]; - - constructor(language: 'en' | 'de' | 'fr' | 'it' = 'en', properties?: Property[]) { - this.language = language; - this.properties = properties; - } -} - -// resource class form controls -export class ResourceClassForm { - language = new FormControl(); - properties = new FormArray([]); - - constructor(resourceClass: ResourceClass) { - this.language.setValue('en'); - if (resourceClass.properties) { - let i = 0; - // this.properties.setControl; - resourceClass.properties.forEach(prop => { - this.properties[i] = new FormControl(prop); - i++; - }); - } - } -} - -@Injectable({ - providedIn: 'root' -}) -export class ResourceClassFormService { - - - resourceClassForm: BehaviorSubject = new BehaviorSubject(this._fb.group( - new ResourceClassForm(new ResourceClass()) - )); - - resourceClassForm$: Observable = this.resourceClassForm.asObservable(); - - constructor(private _fb: FormBuilder) { } - - /** - * reset all properties - */ - resetProperties() { - - const currentResourceClass = this._fb.group( - new ResourceClassForm(new ResourceClass()) - ); - - 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) => { - - // 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); - - } - }); - - }); - - } - - /** - * add new property line - */ - addProperty(prop?: Property) { - const currentResourceClass = this.resourceClassForm.getValue(); - const currentProperties = currentResourceClass.get('properties') as FormArray; - - currentProperties.push( - this._fb.group( - new PropertyForm((prop ? prop : new Property('', '', {}, false, false))) - ) - ); - - this.resourceClassForm.next(currentResourceClass); - } - - /** - * delete property line by index i - * - * @param {number} i - */ - removeProperty(i: number) { - const currentResourceClass = this.resourceClassForm.getValue(); - const currentProperties = currentResourceClass.get('properties') as FormArray; - - currentProperties.removeAt(i); - this.resourceClassForm.next(currentResourceClass); - } - - /** - * create a unique name (id) for resource classes or properties; - * - * @param ontologyIri - * @param [label] - * @returns unique name - */ - setUniqueName(ontologyIri: string, label?: string, type?: 'class' | 'prop'): string { - - if (label && type) { - // build name from label - // normalize and replace spaces and special chars - 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 - return this.getOntologyName(ontologyIri).substring(0, 3) + Math.random().toString(36).substring(2, 5) + Math.random().toString(36).substring(2, 5); - } - } - - /** - * get the ontolgoy name from ontology iri - * - * @param {string} ontologyIri - * @returns string - */ - getOntologyName(ontologyIri: string): string { - - const array = ontologyIri.split('/'); - - const pos = array.length - 2; - - return array[pos].toLowerCase(); - } - - /** - * convert cardinality values (multiple? & required?) from form to DSP-JS cardinality enum 1-n, 0-n, 1, 0-1 - * @param {boolean} multiple - * @param {boolean} required - * @returns Cardinality - */ - translateCardinality(multiple: boolean, required: boolean): Cardinality { - - if (multiple && required) { - // cardinality 1-n (at least one) - return Cardinality._1_n; - } else if (multiple && !required) { - // cardinality 0-n (may have many) - return Cardinality._0_n; - } else if (!multiple && required) { - // cardinality 1 (required) - return Cardinality._1; - } else { - // cardinality 0-1 (optional) - return Cardinality._0_1; - } - } - -} diff --git a/src/app/project/ontology/resource-class-info/resource-class-info.component.html b/src/app/project/ontology/resource-class-info/resource-class-info.component.html index ce77e96f18..6d9702f76f 100644 --- a/src/app/project/ontology/resource-class-info/resource-class-info.component.html +++ b/src/app/project/ontology/resource-class-info/resource-class-info.component.html @@ -28,9 +28,9 @@ - + + + + + + + + + + + + + + - - diff --git a/src/app/project/ontology/resource-class-info/resource-class-info.component.scss b/src/app/project/ontology/resource-class-info/resource-class-info.component.scss index 03d4deb7c4..b3e9cb9da4 100644 --- a/src/app/project/ontology/resource-class-info/resource-class-info.component.scss +++ b/src/app/project/ontology/resource-class-info/resource-class-info.component.scss @@ -24,9 +24,9 @@ } .resource-class-footer { - position: absolute; - bottom: 14px; - right: 14px; + text-align: left; + padding-left: 32px; + margin-top: 16px; } .resource-class-properties { @@ -52,7 +52,7 @@ transition: transform 250ms cubic-bezier(0, 0, 0.2, 1); } -.drag-n-drop-property { +.property { &:hover, &:active, &:focus { @@ -70,9 +70,6 @@ } .gui-order { - font-size: 16px; - color: $primary_700; - margin-left: -16px; .display-on-hover { display: none; } @@ -82,6 +79,17 @@ } } + .list-icon { + font-size: 16px; + color: $primary_700; + margin-left: -16px; + + } + + &.link { + border-top: 1px solid $black-12-opacity; + } + } .cdk-drag-preview { diff --git a/src/app/project/ontology/resource-class-info/resource-class-info.component.spec.ts b/src/app/project/ontology/resource-class-info/resource-class-info.component.spec.ts index 3976ccf9aa..fbe60a7293 100644 --- a/src/app/project/ontology/resource-class-info/resource-class-info.component.spec.ts +++ b/src/app/project/ontology/resource-class-info/resource-class-info.component.spec.ts @@ -1,17 +1,14 @@ -import { DragDropModule } from '@angular/cdk/drag-drop'; import { Component, DebugElement, OnInit, ViewChild } from '@angular/core'; import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { MatCardModule } from '@angular/material/card'; import { MatIconModule } from '@angular/material/icon'; import { MatMenuModule } from '@angular/material/menu'; -import { MatSnackBarModule } from '@angular/material/snack-bar'; import { MatTooltipModule } from '@angular/material/tooltip'; import { By } from '@angular/platform-browser'; import { ClassDefinition, Constants, MockOntology, ReadOntology } from '@dasch-swiss/dsp-js'; import { DspActionModule, DspApiConnectionToken, SortingService } from '@dasch-swiss/dsp-ui'; import { of } from 'rxjs'; import { CacheService } from 'src/app/main/cache/cache.service'; -import { PropertyInfoComponent } from '../property-info/property-info.component'; import { ResourceClassInfoComponent } from './resource-class-info.component'; /** diff --git a/src/app/project/ontology/resource-class-info/resource-class-info.component.ts b/src/app/project/ontology/resource-class-info/resource-class-info.component.ts index 0f56d8d6c7..b26493b5e4 100644 --- a/src/app/project/ontology/resource-class-info/resource-class-info.component.ts +++ b/src/app/project/ontology/resource-class-info/resource-class-info.component.ts @@ -1,20 +1,25 @@ import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop'; import { Component, EventEmitter, Inject, Input, OnInit, Output } from '@angular/core'; -import { MatSnackBar } from '@angular/material/snack-bar'; +import { MatDialog, MatDialogConfig } from '@angular/material/dialog'; import { ApiResponseError, ClassDefinition, IHasProperty, KnoraApiConnection, + PropertyDefinition, ReadOntology, ResourceClassDefinitionWithAllLanguages, + ResourcePropertyDefinitionWithAllLanguages, UpdateOntology, UpdateResourceClassCardinality } from '@dasch-swiss/dsp-js'; -import { DspApiConnectionToken } from '@dasch-swiss/dsp-ui'; +import { DspApiConnectionToken, NotificationService } from '@dasch-swiss/dsp-ui'; import { CacheService } from 'src/app/main/cache/cache.service'; +import { DialogComponent } from 'src/app/main/dialog/dialog.component'; import { ErrorHandlerService } from 'src/app/main/error/error-handler.service'; +import { DefaultProperties, DefaultProperty, PropertyCategory, PropertyInfoObject } from '../default-data/default-properties'; import { DefaultClass, DefaultResourceClasses } from '../default-data/default-resource-classes'; +import { CardinalityInfo } from '../ontology.component'; @Component({ selector: 'app-resource-class-info', @@ -30,13 +35,23 @@ export class ResourceClassInfoComponent implements OnInit { @Input() projectCode: string; + @Input() ontoProperties: ResourcePropertyDefinitionWithAllLanguages[] = []; + @Input() lastModificationDate?: string; + // event emitter when the lastModificationDate changed; bidirectional binding with lastModificationDate parameter + @Output() lastModificationDateChange: EventEmitter = new EventEmitter(); + + // event emitter when the lastModificationDate changed; bidirectional binding with lastModificationDate parameter + @Output() ontoPropertiesChange: EventEmitter = new EventEmitter(); + + // to update the resource class itself (edit or delete) @Output() editResourceClass: EventEmitter = new EventEmitter(); - @Output() updateCardinality: EventEmitter = new EventEmitter(); @Output() deleteResourceClass: EventEmitter = new EventEmitter(); - @Output() updateParent: EventEmitter = new EventEmitter(); + // to update the cardinality we need the information about property (incl. propType) and resource class + @Output() updateCardinality: EventEmitter = new EventEmitter(); + ontology: ReadOntology; @@ -47,17 +62,25 @@ export class ResourceClassInfoComponent implements OnInit { subClassOfLabel = ''; - // list of default classes + // list of default resource classes defaultClasses: DefaultClass[] = DefaultResourceClasses.data; + // list of default property types + defaultProperties: PropertyCategory[] = DefaultProperties.data; + + // list of existing ontology properties, which are not in this resource class + existingProperties: PropertyInfoObject[]; + constructor( @Inject(DspApiConnectionToken) private _dspApiConnection: KnoraApiConnection, private _cache: CacheService, + private _dialog: MatDialog, private _errorHandler: ErrorHandlerService, - private _snackBar: MatSnackBar + private _notification: NotificationService ) { } ngOnInit(): void { + this._cache.get('currentOntology').subscribe( (response: ReadOntology) => { this.ontology = response; @@ -81,6 +104,9 @@ export class ResourceClassInfoComponent implements OnInit { */ translateSubClassOfIri(classIris: string[]) { + // reset the label + this.subClassOfLabel = ''; + classIris.forEach((iri, index) => { // get ontology iri from class iri const splittedIri = iri.split('#'); @@ -132,8 +158,11 @@ export class ResourceClassInfoComponent implements OnInit { // reset properties to display this.propsToDisplay = []; + // reset existing properties to select from + this.existingProperties = []; + + classProps.forEach((hasProp: IHasProperty) => { - classProps.forEach((hasProp) => { const propToDisplay = ontoProps.find(obj => obj.id === hasProp.propertyIndex && (obj.objectType !== 'http://api.knora.org/ontology/knora-api/v2#LinkValue' || @@ -142,13 +171,190 @@ export class ResourceClassInfoComponent implements OnInit { ); if (propToDisplay) { + // add to list of properties to display in res class this.propsToDisplay.push(hasProp); + // and remove from list of existing properties which can be added + this.ontoProperties = this.ontoProperties.filter(prop => !(prop.id === propToDisplay.id)); } }); + this.ontoProperties.forEach((availableProp: ResourcePropertyDefinitionWithAllLanguages) => { + let propType: DefaultProperty; + // find corresponding default property to have more prop info + if (availableProp.guiElement) { + for (const group of this.defaultProperties) { + propType = group.elements.find(i => + i.guiEle === availableProp.guiElement && + (i.objectType === availableProp.objectType || i.subPropOf === availableProp.subPropertyOf[0]) + ); + + if (propType) { + break; + } + } + } + this.existingProperties.push( + { + propType: propType, + propDef: availableProp + } + ); + }); + + } + + addNewProperty(propType: DefaultProperty) { + const cardinality: CardinalityInfo = { + resClass: this.resourceClass, + property: { + propType: propType + } + }; + this.updateCard(cardinality); + } + + addExistingProperty(propDef: ResourcePropertyDefinitionWithAllLanguages) { + let propType: DefaultProperty; + for (const group of this.defaultProperties) { + propType = group.elements.find(i => + i.guiEle === propDef.guiElement && + (i.objectType === propDef.objectType || i.subPropOf === propDef.subPropertyOf[0]) + ); + + if (propType) { + break; + } + } + const cardinality: CardinalityInfo = { + resClass: this.resourceClass, + property: { + propType: propType, + propDef: propDef, + } + }; + + this.updateCard(cardinality); } + /** + * removes property from resource class + * @param property + */ + removeProperty(property: DefaultClass) { + + const onto = new UpdateOntology(); + + onto.lastModificationDate = this.lastModificationDate; + + onto.id = this.ontology.id; + + const addCard = new UpdateResourceClassCardinality(); + + addCard.id = this.resourceClass.id; + + addCard.cardinalities = []; + + this.propsToDisplay = this.propsToDisplay.filter(prop => !(prop.propertyIndex === property.iri)); + + addCard.cardinalities = this.propsToDisplay; + onto.entity = addCard; + + this._dspApiConnection.v2.onto.replaceCardinalityOfResourceClass(onto).subscribe( + (res: ResourceClassDefinitionWithAllLanguages) => { + this.lastModificationDate = res.lastModificationDate; + this.lastModificationDateChange.emit(this.lastModificationDate); + this.preparePropsToDisplay(this.propsToDisplay); + + this.updateCardinality.emit(this.ontology.id); + // display success message + this._notification.openSnackBar(`You have successfully removed "${property.label}" from "${this.resourceClass.label}".`); + }, + (error: ApiResponseError) => { + this._errorHandler.showMessage(error); + } + ); + + } + + /** + * updates cardinality + * @param card cardinality info object + */ + updateCard(card: CardinalityInfo) { + + if (card) { + const classLabel = card.resClass.label; + + let mode: 'createProperty' | 'updateCardinality' = 'createProperty'; + let propLabel = card.property.propType.group + ': ' + card.property.propType.label; + let title = 'Add new property of type "' + propLabel + '" to class "' + classLabel + '"'; + + if (card.property.propDef) { + // the property exists already + mode = 'updateCardinality'; + propLabel = card.property.propDef.label; + title = 'Add existing property "' + propLabel + '" to class "' + classLabel + '"'; + } + + const dialogConfig: MatDialogConfig = { + width: '640px', + maxHeight: '80vh', + position: { + top: '112px' + }, + data: { propInfo: card.property, title: title, subtitle: 'Customize property and cardinality', mode: mode, parentIri: card.resClass.id, position: this.propsToDisplay.length } + }; + + const dialogRef = this._dialog.open( + DialogComponent, + dialogConfig + ); + + dialogRef.afterClosed().subscribe(result => { + // update the view: list of properties in resource class + this.updateCardinality.emit(this.ontology.id); + }); + } else { + + } + + } + + /** + * opens property form + * @param mode + * @param propertyInfo (could be subClassOf (create mode) or resource class itself (edit mode)) + */ + openPropertyForm(mode: 'createProperty' | 'editProperty', propertyInfo: PropertyInfoObject): void { + + const title = (propertyInfo.propDef ? propertyInfo.propDef.label : propertyInfo.propType.group + ': ' + propertyInfo.propType.label); + + const dialogConfig: MatDialogConfig = { + width: '640px', + maxHeight: '80vh', + position: { + top: '112px' + }, + data: { propInfo: propertyInfo, title: title, subtitle: 'Customize property as part of ' + this.resourceClass.label, mode: mode, parentIri: this.resourceClass.id } + }; + + const dialogRef = this._dialog.open( + DialogComponent, + dialogConfig + ); + + dialogRef.afterClosed().subscribe(result => { + // update the view + // this.initOntologiesList(); + this.ngOnInit(); + }); + } + // open dialog box with property-form + // create new property or add existing property + // form includes cardinality and gui-attribute + + /** * drag and drop property line */ @@ -193,15 +399,10 @@ export class ResourceClassInfoComponent implements OnInit { // successful request: update the view this.preparePropsToDisplay(this.propsToDisplay); - this.updateParent.emit(this.lastModificationDate); + this.lastModificationDateChange.emit(this.lastModificationDate); // display success message - this._snackBar.open(`You have successfully changed the order of properties in the resource class "${this.resourceClass.label}".`, '', { - horizontalPosition: 'center', - verticalPosition: 'top', - duration: 2500, - panelClass: 'success' - }); + this._notification.openSnackBar(`You have successfully changed the order of properties in the resource class "${this.resourceClass.label}".`); }, (error: ApiResponseError) => { diff --git a/src/app/project/ontology/resource-class-property-form/resource-class-property-form.component.html b/src/app/project/ontology/resource-class-property-form/resource-class-property-form.component.html deleted file mode 100644 index 4bdfea7ce9..0000000000 --- a/src/app/project/ontology/resource-class-property-form/resource-class-property-form.component.html +++ /dev/null @@ -1,115 +0,0 @@ -
- {{index + 1}})   - - - - - - - - - - This property already exists. - - - - - - - {{prop.label}} {{ prop?.labels | dspStringifyStringLiteral:'all' }} - - - - - - Property type * - - - - {{propertyForm.controls['type'].value.group}}:  - {{propertyForm.controls['type'].value.label}} - - - - {{ele.icon}} {{ele.label}} - - - - - You can re-use but not edit. - - - - -
-
-
- - Select list - - - {{item.labels[0].value}} - - - -
- -
- - Select resource class - - - {{item.label}} - - - -
- -
- - Define range - - - - -
- -
-
- -
- - - - - -
- - Multiple values? - -
- -
- - Required field? - -
- - -
- -
- -
- -
diff --git a/src/app/project/ontology/resource-class-property-form/resource-class-property-form.component.scss b/src/app/project/ontology/resource-class-property-form/resource-class-property-form.component.scss deleted file mode 100644 index 8ff2ec6949..0000000000 --- a/src/app/project/ontology/resource-class-property-form/resource-class-property-form.component.scss +++ /dev/null @@ -1,38 +0,0 @@ -:host { - margin: 12px 0 0 20px; - - form { - margin: 0 0 16px 0; - width: 100%; - } -} - -.large-field, -.medium-field, -.small-field, -.x-small-field { - margin: 0 2px; - display: inline-flex; -} - -.index { - color: rgba(0, 0, 0, 0.54); - font-size: small; - margin-left: -12px; - margin-right: 6px; -} - -.empty-index { - display: inline-block; - width: 12px; -} - -.reset-button { - position: absolute; - right: 0; - bottom: 12px; -} - -.hidden { - display: none; -} diff --git a/src/app/project/ontology/resource-class-property-form/resource-class-property-form.component.spec.ts b/src/app/project/ontology/resource-class-property-form/resource-class-property-form.component.spec.ts deleted file mode 100644 index 4bc4c107e2..0000000000 --- a/src/app/project/ontology/resource-class-property-form/resource-class-property-form.component.spec.ts +++ /dev/null @@ -1,185 +0,0 @@ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { Component, OnInit, ViewChild } from '@angular/core'; -import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing'; -import { FormArray, FormBuilder, FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms'; -import { MatAutocompleteModule } from '@angular/material/autocomplete'; -import { MatOptionModule } from '@angular/material/core'; -import { MatDialogModule } from '@angular/material/dialog'; -import { MatFormFieldModule } from '@angular/material/form-field'; -import { MatIconModule } from '@angular/material/icon'; -import { MatInputModule } from '@angular/material/input'; -import { MatSelectModule } from '@angular/material/select'; -import { MatSlideToggleModule } from '@angular/material/slide-toggle'; -import { MatSnackBarModule } from '@angular/material/snack-bar'; -import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { Constants, IHasProperty, KnoraApiConnection, ListNodeInfo, MockOntology, PropertyDefinition, ReadOntology, ResourcePropertyDefinitionWithAllLanguages } from '@dasch-swiss/dsp-js'; -import { AppInitService, DspApiConfigToken, DspApiConnectionToken } from '@dasch-swiss/dsp-ui'; -import { of, Subscription } from 'rxjs'; -import { CacheService } from 'src/app/main/cache/cache.service'; -import { TestConfig } from 'test.config'; -import { Property, ResourceClassFormService } from '../resource-class-form/resource-class-form.service'; -import { ResourceClassPropertyFormComponent } from './resource-class-property-form.component'; - - -/** - * test host component to simulate parent component - * Property is of type simple text - */ -@Component({ - template: '' -}) -class HostComponent implements OnInit { - - @ViewChild('propertyForm') resClassPropertyFormComponent: ResourceClassPropertyFormComponent; - - resourceClassForm: FormGroup; - - resourceClassFormSub: Subscription; - - properties: FormArray; - - resClassIri = Constants.Resource; - - ontology: ReadOntology = MockOntology.mockReadOntology('http://0.0.0.0:3333/ontology/0001/anything/v2'); - - constructor( - private _resourceClassFormService: ResourceClassFormService - ) { - - this._resourceClassFormService.addProperty(); - } - - ngOnInit() { - const ontoProperties: PropertyDefinition[] = this.ontology.getAllPropertyDefinitions(); - - this.resourceClassFormSub = this._resourceClassFormService.resourceClassForm$ - .subscribe(resourceClass => { - this.resourceClassForm = resourceClass; - this.properties = this.resourceClassForm.get('properties') as FormArray; - }); - - } - -} - -xdescribe('ResourceClassPropertyFormComponent', () => { - let component: HostComponent; - let fixture: ComponentFixture; - - const formBuilder: FormBuilder = new FormBuilder(); - - const cacheServiceSpy = jasmine.createSpyObj('CacheService', ['get']); - - beforeEach(waitForAsync(() => { - TestBed.configureTestingModule({ - declarations: [ - HostComponent, - ResourceClassPropertyFormComponent - ], - imports: [ - BrowserAnimationsModule, - HttpClientTestingModule, - MatAutocompleteModule, - MatDialogModule, - MatFormFieldModule, - MatIconModule, - MatInputModule, - MatOptionModule, - MatSelectModule, - MatSlideToggleModule, - MatSnackBarModule, - ReactiveFormsModule - ], - providers: [ - // reference the new instance of formBuilder from above - { provide: FormBuilder, useValue: formBuilder }, - AppInitService, - { - provide: DspApiConfigToken, - useValue: TestConfig.ApiConfig - }, - { - provide: DspApiConnectionToken, - useValue: new KnoraApiConnection(TestConfig.ApiConfig) - }, - { - provide: CacheService, - useValue: cacheServiceSpy - }, - ] - }) - .compileComponents(); - })); - - beforeEach(() => { - - // mock cache service for currentOntologyLists - const cacheSpy = TestBed.inject(CacheService); - - (cacheSpy as jasmine.SpyObj).get.and.callFake( - () => { - const response: ListNodeInfo[] = [{ - 'comments': [], - 'id': 'http://rdfh.ch/lists/0001/otherTreeList', - 'isRootNode': true, - 'labels': [{ - 'language': 'en', - 'value': 'Tree list root' - }], - 'projectIri': 'http://rdfh.ch/projects/0001' - }, { - 'comments': [{ - 'language': 'en', - 'value': 'a list that is not in used in ontology or data' - }], - 'id': 'http://rdfh.ch/lists/0001/notUsedList', - 'isRootNode': true, - 'labels': [{ - 'language': 'de', - 'value': 'unbenutzte Liste' - }, { - 'language': 'en', - 'value': 'a list that is not used' - }], - 'name': 'notUsedList', - 'projectIri': 'http://rdfh.ch/projects/0001' - }, { - 'comments': [{ - 'language': 'en', - 'value': 'Anything Tree List' - }], - 'id': 'http://rdfh.ch/lists/0001/treeList', - 'isRootNode': true, - 'labels': [{ - 'language': 'de', - 'value': 'Listenwurzel' - }, { - 'language': 'en', - 'value': 'Tree list root' - }], - 'name': 'treelistroot', - 'projectIri': 'http://rdfh.ch/projects/0001' - }]; - return of(response); - } - ); - - fixture = TestBed.createComponent(HostComponent); - component = fixture.componentInstance; - - // // pass in the form dynamically - // component.propertyForm = formBuilder.group({ - // type: null, - // label: null, - // multiple: null, - // required: null, - // permission: null - // }); - - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/src/app/project/ontology/resource-class-property-form/resource-class-property-form.component.ts b/src/app/project/ontology/resource-class-property-form/resource-class-property-form.component.ts deleted file mode 100644 index 35dbfcedc2..0000000000 --- a/src/app/project/ontology/resource-class-property-form/resource-class-property-form.component.ts +++ /dev/null @@ -1,245 +0,0 @@ -import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; -import { FormControl, FormGroup, Validators } from '@angular/forms'; -import { MatSelectChange } from '@angular/material/select'; -import { - ApiResponseError, - ClassDefinition, - Constants, - ListNodeInfo, - ReadOntology, - ResourcePropertyDefinition -} from '@dasch-swiss/dsp-js'; -import { AutocompleteItem } from '@dasch-swiss/dsp-ui'; -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, DefaultProperty, PropertyCategory } from '../default-data/default-properties'; - -@Component({ - selector: 'app-resource-class-property-form', - templateUrl: './resource-class-property-form.component.html', - styleUrls: ['./resource-class-property-form.component.scss'] -}) -export class ResourceClassPropertyFormComponent implements OnInit { - - @Input() propertyForm: FormGroup; - - @Input() index: number; - - @Input() resClassIri?: string; - - @Output() deleteProperty: EventEmitter = new EventEmitter(); - - ontology: ReadOntology; - // selection of default property types - propertyTypes: PropertyCategory[] = DefaultProperties.data; - - showGuiAttr = false; - - // list of project specific lists (TODO: probably we have to add default knora lists?!) - lists: ListNodeInfo[]; - - // resource classes in this ontology - resourceClass: ClassDefinition[] = []; - - // list of existing properties - properties: AutocompleteItem[] = []; - - filteredProperties: Observable; - - selectTypeLabel: string; // = this.propertyTypes[0].group + ': ' + this.propertyTypes[0].elements[0].label; - selectedGroup: string; - - existingProperty: boolean; - - loading = false; - - dspConstants = Constants; - - constructor( - private _cache: CacheService, - private _errorHandler: ErrorHandlerService - ) { } - - ngOnInit() { - - this._cache.get('currentOntology').subscribe( - (response: ReadOntology) => { - this.ontology = response; - - // set various lists to select from - // a) in case of link value: - // set list of resource classes from response; needed for linkValue - const classKeys: string[] = Object.keys(response.classes); - for (const c of classKeys) { - this.resourceClass.push(this.ontology.classes[c]); - } - - // b) in case of already existing label: - // 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) { - 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], - label: this.ontology.properties[p].label - }; - this.properties.push(existingProperty); - } - } - - 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); - } - } - }, - (error: ApiResponseError) => { - this._errorHandler.showMessage(error); - } - ); - - // c) in case of list value: - // set list of lists; needed for listValue - this._cache.get('currentOntologyLists').subscribe( - (response: ListNodeInfo[]) => { - this.lists = response; - } - ); - - this.filteredProperties = this.propertyForm.controls['label'].valueChanges - .pipe( - startWith(''), - map(prop => prop.length >= 0 ? this.filter(this.properties, prop) : []) - ); - } - - /** - * filter a list while typing in auto complete input field - * @param list List of options - * @param label Value to filter by - * @returns Filtered list of options - */ - filter(list: AutocompleteItem[], label: string) { - return list.filter(prop => - prop.label?.toLowerCase().includes(label.toLowerCase()) - ); - } - - updateAttributeField(event: MatSelectChange) { - - // reset value of guiAttr - 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 resource class - switch (event.value.objectType) { - case Constants.ListValue: - case Constants.LinkValue: - this.showGuiAttr = true; - this.propertyForm.controls['guiAttr'].setValidators([ - Validators.required - ]); - this.propertyForm.controls['guiAttr'].updateValueAndValidity(); - break; - - default: - this.propertyForm.controls['guiAttr'].clearValidators(); - this.propertyForm.controls['guiAttr'].updateValueAndValidity(); - this.showGuiAttr = false; - } - - } - - /** - * @param {MatOption} option - */ - updateFieldsDependingOnLabel(option: AutocompleteItem) { - this.propertyForm.controls['iri'].setValue(option.iri); - - // set label and disable the input - this.propertyForm.controls['label'].setValue(option.label); - this.propertyForm.controls['label'].disable(); - - // find corresponding property type - - if (this.ontology.properties[option.iri] instanceof ResourcePropertyDefinition) { - const tempProp: any | ResourcePropertyDefinition = this.ontology.properties[option.iri]; - - let obj: DefaultProperty; - // find gui ele from list of default property-types to set type value - for (const group of this.propertyTypes) { - obj = group.elements.find(i => i.guiEle === tempProp.guiElement && (i.objectType === tempProp.objectType || i.subPropOf === tempProp.subPropertyOf[0])); - - if (obj) { - this.propertyForm.controls['type'].setValue(obj); - break; - } - } - - switch (tempProp.guiElement) { - // prop type is a list - case Constants.SalsahGui + Constants.HashDelimiter + 'List': - case Constants.SalsahGui + Constants.HashDelimiter + 'Radio': - // gui attribute value for lists looks as follow: hlist= - // get index from guiAttr array where value starts with hlist= - const i = tempProp.guiAttributes.findIndex(element => element.includes('hlist')); - - // find content beteween pointy brackets to get list iri - const re = /\<([^)]+)\>/; - const listIri = tempProp.guiAttributes[i].match(re)[1]; - - this.showGuiAttr = true; - this.propertyForm.controls['guiAttr'].setValue(listIri); - this.propertyForm.controls['guiAttr'].disable(); - break; - - // prop type is resource pointer - case Constants.SalsahGui + Constants.HashDelimiter + 'Searchbox': - - this.showGuiAttr = true; - this.propertyForm.controls['guiAttr'].setValue(tempProp.objectType); - this.propertyForm.controls['guiAttr'].disable(); - break; - - default: - this.showGuiAttr = false; - } - } - this.propertyForm.controls['type'].disable(); - this.existingProperty = true; - } - - resetProperty(ev: Event) { - ev.preventDefault(); - this.existingProperty = false; - - this.propertyForm.controls['iri'].reset(); - this.propertyForm.controls['label'].setValue(''); - this.propertyForm.controls['label'].enable(); - this.propertyForm.controls['type'].setValue(this.propertyTypes[0].elements[0]); - this.propertyForm.controls['type'].enable(); - this.propertyForm.controls['guiAttr'].setValue(undefined); - this.propertyForm.controls['guiAttr'].enable(); - - this.propertyForm.controls['multiple'].reset(); - this.propertyForm.controls['required'].reset(); - } - - -} diff --git a/src/app/workspace/resource/resource-instance-form/resource-instance-form.component.ts b/src/app/workspace/resource/resource-instance-form/resource-instance-form.component.ts index 034cd8a10b..35a6686054 100644 --- a/src/app/workspace/resource/resource-instance-form/resource-instance-form.component.ts +++ b/src/app/workspace/resource/resource-instance-form/resource-instance-form.component.ts @@ -165,7 +165,7 @@ export class ResourceInstanceFormComponent implements OnInit, OnDestroy { this.resource = res; // navigate to the resource viewer page - this._router.navigateByUrl('/resource', { skipLocationChange: true }).then(() => + this._router.navigateByUrl('/refresh', { skipLocationChange: true }).then(() => this._router.navigate(['/resource/' + encodeURIComponent(this.resource.id)]) ); diff --git a/src/assets/style/_elements.scss b/src/assets/style/_elements.scss index 0ae606b248..77f61b0be2 100644 --- a/src/assets/style/_elements.scss +++ b/src/assets/style/_elements.scss @@ -50,6 +50,11 @@ flex-direction: row; align-items: center; white-space: nowrap; + + .mat-title { + word-wrap: normal; + white-space: normal; + } } .app-toolbar-action.button { @@ -435,6 +440,13 @@ $gc-small: $form-width - $gc-large - 4; } } +// progress indicator icon on submit button +.submit-progress { + display: inline-block; + margin-right: 6px; + margin-bottom: 2px; +} + // -------------------------------------- // @@ -618,7 +630,9 @@ $gc-small: $form-width - $gc-large - 4; .switch-nested-sub-menu { width: 180px; margin-left: -360px !important; - +} +.switch-nested-sub-menu, +.default-nested-sub-menu { &.mat-menu-panel { margin-top: 8px; min-height: 48px !important;