From 4c431e3e91be7a678cd11e4930ef5cc572e05dad Mon Sep 17 00:00:00 2001 From: CircleCI Date: Wed, 4 Dec 2019 13:32:37 +0000 Subject: [PATCH] Include changes from v0.11.0 --- CHANGELOG.md | 45 +- package.json | 10 +- src/examples/resources/classes.json | 212 ++-- src/examples/resources/elements.json | 972 +++++++-------- src/examples/resources/exampleMetadataApi.ts | 20 +- src/examples/resources/linkTypes.json | 36 +- src/index.ts | 2 +- src/internalApi.ts | 1 + .../customization/defaultLinkStyles.ts | 4 +- src/ontodia/customization/props.ts | 1 + .../customization/templates/standard.tsx | 15 +- src/ontodia/customization/templates/utils.ts | 2 +- src/ontodia/data/composite/composite.ts | 11 +- src/ontodia/data/composite/mergeUtils.ts | 5 +- src/ontodia/data/demo/provider.ts | 2 +- src/ontodia/data/metadataApi.ts | 5 + src/ontodia/data/model.ts | 17 +- src/ontodia/data/rdf/rdfCacheableStore.ts | 12 - src/ontodia/data/rdf/rdfDataProvider.ts | 15 +- src/ontodia/data/sparql/blankNodes.ts | 27 +- src/ontodia/data/sparql/responseHandler.ts | 356 ++++-- src/ontodia/data/sparql/sparqlDataProvider.ts | 813 ++++++++----- .../data/sparql/sparqlDataProviderSettings.ts | 318 ++++- src/ontodia/data/sparql/sparqlModels.ts | 13 +- src/ontodia/diagram/commands.ts | 17 +- src/ontodia/diagram/elementLayer.tsx | 2 +- src/ontodia/diagram/elements.ts | 22 +- src/ontodia/diagram/linkLayer.tsx | 14 +- src/ontodia/diagram/view.ts | 8 +- src/ontodia/editor/authoredEntity.tsx | 13 +- src/ontodia/editor/authoringState.ts | 318 +++-- src/ontodia/editor/editLayer.tsx | 4 +- src/ontodia/editor/editorController.tsx | 57 +- src/ontodia/editor/elementDecorator.tsx | 29 +- src/ontodia/editor/linkStateWidget.tsx | 24 +- src/ontodia/editor/validation.ts | 14 +- src/ontodia/forms/editEntityForm.tsx | 42 +- src/ontodia/forms/editLinkLabelForm.tsx | 2 +- src/ontodia/forms/elementTypeSelector.tsx | 2 +- src/ontodia/viewUtils/collections.ts | 27 + src/ontodia/widgets/connectionsMenu.tsx | 4 +- src/ontodia/widgets/halo.tsx | 4 +- src/ontodia/widgets/haloLink.tsx | 17 +- src/ontodia/widgets/linksToolbox.tsx | 2 +- src/ontodia/widgets/searchResults.tsx | 73 +- src/ontodia/workspace/workspace.ts | 3 +- src/ontodia/workspace/workspaceMarkup.tsx | 6 +- yarn.lock | 1041 +++++++++++++++-- 48 files changed, 3135 insertions(+), 1524 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f40a8d8d..d2f4123f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,13 +6,42 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## [Latest] +## [0.11.0] - 2019-12-04 +### Added +- Ability to externally fetch labels for resources in `SparqlDataProvider` +using `prepareLabels` option. +- Options to override schema info queries (classes, link types, datatype properties) +in `SparqlDataProviderSettings`. +- Support for "direct" link and property configurations in `SparqlDataProviderSettings`, +explicit domain types matching and experimental "open world" mode for link and property +configurations. +- Support for changing IRI of existing entities in authoring mode. +- Display link type IRI as default title for link labels. + +### Changed +- **[Breaking]** Made `LocalizedString` compatible with `Literal` interface +from [RDF/JS Data Model](https://rdf.js.org/data-model-spec/) specification. +- **[Breaking]** Replaced separate authoring event types for deleting entities +and links by `deleted` flag in corresponding events, replaced `AuthoringState` +representation by only using authoring event index. +- **[Breaking]** Renamed `SparqlDataProviderSettings.filterTypePattern` binding +`${elementTypeIri}` -> `?class`. +- **[Breaking]** Removed `LinkConfiguration.inverseId`. + +### Fixed +- Stale `ElementLayer` rendering after calling `importLayout()` if diagram +already contains elements with the same IDs. +- React 16.x warning about uppercase characters in `data-linkTypeId` attribute name. +- `SparqlDataProvider` only adds `extractLabel` pattern to queries if text token is +provided when searching/filtering elements. + ## [0.10.0] - 2019-09-30 ### Added - Custom element state in the serialized diagram layout via `elementTemplateState` property. - Enhance standard template with ability to "pin" properties to display them even in collapsed state. -- Add options to override `selectLabelLanguage` to customize label language +- Options to override `selectLabelLanguage` to customize label language selection based on user-preferred language. ### Changed @@ -33,13 +62,14 @@ scrolling on a page (`Window.pageYOffset` is not zero). ## [0.9.12] - 2019-08-27 ### Added -- Bringing selected elements to front -- Added ability to collapse navigator by default -- Implemented lazy class tree loading -- Implemented copyright changes due to transfer IP to metaphacts GmbH +- Option to collapse diagram navigator by default. ### Changed -- **[Breaking]** decoding href value of anchor before calling UI callback +- **[Breaking]** decoding href value of anchor before calling UI callback. +- Selected elements on a diagram are always brought to front. +- Loading class tree is now done in a lazy way and separately from importing +diagram layout. +- Changed copyright due to IP transfer to metaphacts GmbH. ## [0.9.11] - 2019-07-25 ### Fixed @@ -475,7 +505,8 @@ info loaded from `DataProvider`. ### Added - Ontodia published on GitHub as OSS project. -[Latest]: https://github.com/metaphacts/ontodia/compare/v0.10.0...HEAD +[Latest]: https://github.com/metaphacts/ontodia/compare/v0.11.0...HEAD +[0.11.0]: https://github.com/metaphacts/ontodia/compare/v0.10.0...v0.11.0 [0.10.0]: https://github.com/metaphacts/ontodia/compare/v0.9.12...v0.10.0 [0.9.12]: https://github.com/metaphacts/ontodia/compare/v0.9.11...v0.9.12 [0.9.11]: https://github.com/metaphacts/ontodia/compare/v0.9.10...v0.9.11 diff --git a/package.json b/package.json index c33ebb9a..c5720e08 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ontodia", - "version": "0.9.12", + "version": "0.10.0", "description": "Ontodia Library", "repository": { "type": "git", @@ -44,8 +44,8 @@ "whatwg-fetch": "~2.0.2" }, "peerDependencies": { - "react": "^15.0.0", - "react-dom": "^15.0.0" + "react": "^15.0.0 || ^16.0.0", + "react-dom": "^15.0.0 || ^16.0.0" }, "devDependencies": { "@types/d3-color": "1.0.4", @@ -66,10 +66,10 @@ "react-dom": "15.6.2", "sass-loader": "7.1.0", "style-loader": "0.23.0", - "ts-loader": "5.1.1", + "ts-loader": "6.2.1", "tslib": "1.10.0", "tslint": "5.20.0", - "typescript": "3.0.3", + "typescript": "3.7.2", "url-loader": "1.1.1", "webpack": "4.19.0", "webpack-cli": "3.1.0", diff --git a/src/examples/resources/classes.json b/src/examples/resources/classes.json index 4499ea4f..e4959182 100644 --- a/src/examples/resources/classes.json +++ b/src/examples/resources/classes.json @@ -4,12 +4,12 @@ "label": { "values": [ { - "text": "Thing", - "lang": "en" + "value": "Thing", + "language": "en" }, { - "text": "Вещь", - "lang": "ru" + "value": "Вещь", + "language": "ru" } ] }, @@ -20,8 +20,8 @@ "label": { "values": [ { - "text": "ServiceProvider", - "lang": "" + "value": "ServiceProvider", + "language": "" } ] }, @@ -33,8 +33,8 @@ "label": { "values": [ { - "text": "SimpleProperty", - "lang": "" + "value": "SimpleProperty", + "language": "" } ] }, @@ -45,8 +45,8 @@ "label": { "values": [ { - "text": "Image", - "lang": "" + "value": "Image", + "language": "" } ] }, @@ -58,8 +58,8 @@ "label": { "values": [ { - "text": "Speed", - "lang": "" + "value": "Speed", + "language": "" } ] }, @@ -73,8 +73,8 @@ "label": { "values": [ { - "text": "RTService", - "lang": "" + "value": "RTService", + "language": "" } ] }, @@ -86,8 +86,8 @@ "label": { "values": [ { - "text": "Operation", - "lang": "" + "value": "Operation", + "language": "" } ] }, @@ -98,8 +98,8 @@ "label": { "values": [ { - "text": "Subscription", - "lang": "" + "value": "Subscription", + "language": "" } ] }, @@ -110,8 +110,8 @@ "label": { "values": [ { - "text": "Unsubscribe", - "lang": "" + "value": "Unsubscribe", + "language": "" } ] }, @@ -123,8 +123,8 @@ "label": { "values": [ { - "text": "Subscribe", - "lang": "" + "value": "Subscribe", + "language": "" } ] }, @@ -138,8 +138,8 @@ "label": { "values": [ { - "text": "Contact", - "lang": "" + "value": "Contact", + "language": "" } ] }, @@ -150,8 +150,8 @@ "label": { "values": [ { - "text": "TurnOff", - "lang": "" + "value": "TurnOff", + "language": "" } ] }, @@ -163,8 +163,8 @@ "label": { "values": [ { - "text": "Соединение", - "lang": "" + "value": "Соединение", + "language": "" } ] }, @@ -176,8 +176,8 @@ "label": { "values": [ { - "text": "Disconnect", - "lang": "" + "value": "Disconnect", + "language": "" } ] }, @@ -189,8 +189,8 @@ "label": { "values": [ { - "text": "TurnOn", - "lang": "" + "value": "TurnOn", + "language": "" } ] }, @@ -204,8 +204,8 @@ "label": { "values": [ { - "text": "Observe", - "lang": "" + "value": "Observe", + "language": "" } ] }, @@ -216,8 +216,8 @@ "label": { "values": [ { - "text": "Disappear", - "lang": "" + "value": "Disappear", + "language": "" } ] }, @@ -229,8 +229,8 @@ "label": { "values": [ { - "text": "Appear", - "lang": "" + "value": "Appear", + "language": "" } ] }, @@ -246,8 +246,8 @@ "label": { "values": [ { - "text": "Service", - "lang": "" + "value": "Service", + "language": "" } ] }, @@ -258,8 +258,8 @@ "label": { "values": [ { - "text": "TVChannel", - "lang": "" + "value": "TVChannel", + "language": "" } ] }, @@ -270,8 +270,8 @@ "label": { "values": [ { - "text": "DigitalTVChannel", - "lang": "" + "value": "DigitalTVChannel", + "language": "" } ] }, @@ -283,8 +283,8 @@ "label": { "values": [ { - "text": "AnalogueTVChannel", - "lang": "" + "value": "AnalogueTVChannel", + "language": "" } ] }, @@ -298,8 +298,8 @@ "label": { "values": [ { - "text": "ServiceDevice", - "lang": "" + "value": "ServiceDevice", + "language": "" } ] }, @@ -310,8 +310,8 @@ "label": { "values": [ { - "text": "Battery", - "lang": "" + "value": "Battery", + "language": "" } ] }, @@ -323,8 +323,8 @@ "label": { "values": [ { - "text": "Router", - "lang": "" + "value": "Router", + "language": "" } ] }, @@ -335,8 +335,8 @@ "label": { "values": [ { - "text": "WiFiRouter", - "lang": "" + "value": "WiFiRouter", + "language": "" } ] }, @@ -350,8 +350,8 @@ "label": { "values": [ { - "text": "CamModule", - "lang": "" + "value": "CamModule", + "language": "" } ] }, @@ -363,8 +363,8 @@ "label": { "values": [ { - "text": "TVSet", - "lang": "" + "value": "TVSet", + "language": "" } ] }, @@ -376,8 +376,8 @@ "label": { "values": [ { - "text": "ControlPanel", - "lang": "" + "value": "ControlPanel", + "language": "" } ] }, @@ -389,8 +389,8 @@ "label": { "values": [ { - "text": "Cable", - "lang": "" + "value": "Cable", + "language": "" } ] }, @@ -402,8 +402,8 @@ "label": { "values": [ { - "text": "SetTopBox", - "lang": "" + "value": "SetTopBox", + "language": "" } ] }, @@ -417,8 +417,8 @@ "label": { "values": [ { - "text": "All", - "lang": "" + "value": "All", + "language": "" } ] }, @@ -430,8 +430,8 @@ "label": { "values": [ { - "text": "SubscribableService", - "lang": "" + "value": "SubscribableService", + "language": "" } ] }, @@ -442,8 +442,8 @@ "label": { "values": [ { - "text": "TVService", - "lang": "" + "value": "TVService", + "language": "" } ] }, @@ -454,8 +454,8 @@ "label": { "values": [ { - "text": "DigitalTVService", - "lang": "" + "value": "DigitalTVService", + "language": "" } ] }, @@ -469,8 +469,8 @@ "label": { "values": [ { - "text": "Internet", - "lang": "" + "value": "Internet", + "language": "" } ] }, @@ -482,8 +482,8 @@ "label": { "values": [ { - "text": "ChannelPack", - "lang": "" + "value": "ChannelPack", + "language": "" } ] }, @@ -499,8 +499,8 @@ "label": { "values": [ { - "text": "ServiceDesk", - "lang": "" + "value": "ServiceDesk", + "language": "" } ] }, @@ -512,8 +512,8 @@ "label": { "values": [ { - "text": "PropertyAsGoods", - "lang": "" + "value": "PropertyAsGoods", + "language": "" } ] }, @@ -524,8 +524,8 @@ "label": { "values": [ { - "text": "Home", - "lang": "" + "value": "Home", + "language": "" } ] }, @@ -536,8 +536,8 @@ "label": { "values": [ { - "text": "Flat", - "lang": "" + "value": "Flat", + "language": "" } ] }, @@ -551,8 +551,8 @@ "label": { "values": [ { - "text": "Phone", - "lang": "" + "value": "Phone", + "language": "" } ] }, @@ -563,8 +563,8 @@ "label": { "values": [ { - "text": "HomePhone", - "lang": "" + "value": "HomePhone", + "language": "" } ] }, @@ -580,8 +580,8 @@ "label": { "values": [ { - "text": "User", - "lang": "" + "value": "User", + "language": "" } ] }, @@ -593,8 +593,8 @@ "label": { "values": [ { - "text": "ServiceMan", - "lang": "" + "value": "ServiceMan", + "language": "" } ] }, @@ -608,8 +608,8 @@ "label": { "values": [ { - "text": "Property", - "lang": "" + "value": "Property", + "language": "" } ] }, @@ -621,8 +621,8 @@ "label": { "values": [ { - "text": "Datatype", - "lang": "" + "value": "Datatype", + "language": "" } ] }, @@ -634,8 +634,8 @@ "label": { "values": [ { - "text": "Class", - "lang": "" + "value": "Class", + "language": "" } ] }, @@ -647,8 +647,8 @@ "label": { "values": [ { - "text": "Ontology", - "lang": "" + "value": "Ontology", + "language": "" } ] }, @@ -660,8 +660,8 @@ "label": { "values": [ { - "text": "AnnotationProperty", - "lang": "" + "value": "AnnotationProperty", + "language": "" } ] }, @@ -673,8 +673,8 @@ "label": { "values": [ { - "text": "ObjectProperty", - "lang": "" + "value": "ObjectProperty", + "language": "" } ] }, @@ -686,8 +686,8 @@ "label": { "values": [ { - "text": "DatatypeProperty", - "lang": "" + "value": "DatatypeProperty", + "language": "" } ] }, diff --git a/src/examples/resources/elements.json b/src/examples/resources/elements.json index 4103f5ba..8cea291b 100644 --- a/src/examples/resources/elements.json +++ b/src/examples/resources/elements.json @@ -7,8 +7,8 @@ "label": { "values": [ { - "text": "schema", - "lang": "" + "value": "schema", + "language": "" } ] }, @@ -17,8 +17,8 @@ "type": "string", "values": [ { - "text": "Created with TopBraid Composer", - "lang": "" + "value": "Created with TopBraid Composer", + "language": "" } ] } @@ -32,8 +32,8 @@ "label": { "values": [ { - "text": "ortChannel", - "lang": "" + "value": "ortChannel", + "language": "" } ] }, @@ -42,8 +42,8 @@ "type": "string", "values": [ { - "text": "орт", - "lang": "" + "value": "орт", + "language": "" } ] } @@ -57,8 +57,8 @@ "label": { "values": [ { - "text": "Battery", - "lang": "" + "value": "Battery", + "language": "" } ] }, @@ -67,8 +67,8 @@ "type": "string", "values": [ { - "text": "батарейка", - "lang": "" + "value": "батарейка", + "language": "" } ] } @@ -82,8 +82,8 @@ "label": { "values": [ { - "text": "rostelekom", - "lang": "" + "value": "rostelekom", + "language": "" } ] }, @@ -92,8 +92,8 @@ "type": "string", "values": [ { - "text": "ростелеком", - "lang": "" + "value": "ростелеком", + "language": "" } ] } @@ -107,8 +107,8 @@ "label": { "values": [ { - "text": "problemStated", - "lang": "" + "value": "problemStated", + "language": "" } ] }, @@ -122,8 +122,8 @@ "label": { "values": [ { - "text": "hdPremiumChannelPack", - "lang": "" + "value": "hdPremiumChannelPack", + "language": "" } ] }, @@ -132,8 +132,8 @@ "type": "string", "values": [ { - "text": "эйчди-премиум", - "lang": "" + "value": "эйчди-премиум", + "language": "" } ] } @@ -147,8 +147,8 @@ "label": { "values": [ { - "text": "subscriberName", - "lang": "" + "value": "subscriberName", + "language": "" } ] }, @@ -157,8 +157,8 @@ "type": "string", "values": [ { - "text": "Имя человека, на которого оформлен договор.", - "lang": "" + "value": "Имя человека, на которого оформлен договор.", + "language": "" } ] }, @@ -166,8 +166,8 @@ "type": "string", "values": [ { - "text": "оформлять", - "lang": "" + "value": "оформлять", + "language": "" } ] } @@ -181,8 +181,8 @@ "label": { "values": [ { - "text": "encryptedSignal", - "lang": "" + "value": "encryptedSignal", + "language": "" } ] }, @@ -191,8 +191,8 @@ "type": "string", "values": [ { - "text": "На экаране показывается, что сигнал зашифрован ил нет доступа", - "lang": "" + "value": "На экаране показывается, что сигнал зашифрован ил нет доступа", + "language": "" } ] } @@ -206,8 +206,8 @@ "label": { "values": [ { - "text": "forwardedToAccounts", - "lang": "" + "value": "forwardedToAccounts", + "language": "" } ] }, @@ -221,8 +221,8 @@ "label": { "values": [ { - "text": "TVService", - "lang": "" + "value": "TVService", + "language": "" } ] }, @@ -231,8 +231,8 @@ "type": "string", "values": [ { - "text": "Услуга телевидения. Пока нахождение на этом уровне определяется только необходимостью преобразования в онто-факты.", - "lang": "" + "value": "Услуга телевидения. Пока нахождение на этом уровне определяется только необходимостью преобразования в онто-факты.", + "language": "" } ] }, @@ -240,8 +240,8 @@ "type": "string", "values": [ { - "text": "телевидение", - "lang": "" + "value": "телевидение", + "language": "" } ] } @@ -255,8 +255,8 @@ "label": { "values": [ { - "text": "Address", - "lang": "" + "value": "Address", + "language": "" } ] }, @@ -270,8 +270,8 @@ "label": { "values": [ { - "text": "TurnOff", - "lang": "" + "value": "TurnOff", + "language": "" } ] }, @@ -280,8 +280,8 @@ "type": "string", "values": [ { - "text": "выключать", - "lang": "" + "value": "выключать", + "language": "" } ] } @@ -295,8 +295,8 @@ "label": { "values": [ { - "text": "TVChannel", - "lang": "" + "value": "TVChannel", + "language": "" } ] }, @@ -305,8 +305,8 @@ "type": "string", "values": [ { - "text": "Телевизионный канал.", - "lang": "" + "value": "Телевизионный канал.", + "language": "" } ] }, @@ -314,8 +314,8 @@ "type": "string", "values": [ { - "text": "канал", - "lang": "" + "value": "канал", + "language": "" } ] } @@ -329,8 +329,8 @@ "label": { "values": [ { - "text": "ServiceDevice", - "lang": "" + "value": "ServiceDevice", + "language": "" } ] }, @@ -339,8 +339,8 @@ "type": "string", "values": [ { - "text": "Устройство у пользователя, отвечающее за предоставление услуг", - "lang": "" + "value": "Устройство у пользователя, отвечающее за предоставление услуг", + "language": "" } ] } @@ -354,8 +354,8 @@ "label": { "values": [ { - "text": "Соединение", - "lang": "" + "value": "Соединение", + "language": "" } ] }, @@ -364,12 +364,12 @@ "type": "string", "values": [ { - "text": "втыкать", - "lang": "" + "value": "втыкать", + "language": "" }, { - "text": "вставлять", - "lang": "" + "value": "вставлять", + "language": "" } ] } @@ -383,8 +383,8 @@ "label": { "values": [ { - "text": "falseExpressedBy", - "lang": "" + "value": "falseExpressedBy", + "language": "" } ] }, @@ -398,8 +398,8 @@ "label": { "values": [ { - "text": "changedSpeed", - "lang": "" + "value": "changedSpeed", + "language": "" } ] }, @@ -408,24 +408,24 @@ "type": "string", "values": [ { - "text": "уменьшать", - "lang": "" + "value": "уменьшать", + "language": "" }, { - "text": "увеличивать", - "lang": "" + "value": "увеличивать", + "language": "" }, { - "text": "снижать", - "lang": "" + "value": "снижать", + "language": "" }, { - "text": "поднимать", - "lang": "" + "value": "поднимать", + "language": "" }, { - "text": "изменять", - "lang": "" + "value": "изменять", + "language": "" } ] } @@ -439,8 +439,8 @@ "label": { "values": [ { - "text": "kinoChannelPack", - "lang": "" + "value": "kinoChannelPack", + "language": "" } ] }, @@ -449,8 +449,8 @@ "type": "string", "values": [ { - "text": "кино", - "lang": "" + "value": "кино", + "language": "" } ] } @@ -464,8 +464,8 @@ "label": { "values": [ { - "text": "Subscription", - "lang": "" + "value": "Subscription", + "language": "" } ] }, @@ -479,8 +479,8 @@ "label": { "values": [ { - "text": "switchedOn", - "lang": "" + "value": "switchedOn", + "language": "" } ] }, @@ -489,8 +489,8 @@ "type": "string", "values": [ { - "text": "Включено ли устройство.", - "lang": "" + "value": "Включено ли устройство.", + "language": "" } ] }, @@ -498,8 +498,8 @@ "type": "string", "values": [ { - "text": "tv:TurnOff", - "lang": "" + "value": "tv:TurnOff", + "language": "" } ] }, @@ -507,8 +507,8 @@ "type": "string", "values": [ { - "text": "tv:TurnOn", - "lang": "" + "value": "tv:TurnOn", + "language": "" } ] } @@ -522,8 +522,8 @@ "label": { "values": [ { - "text": "Contact", - "lang": "" + "value": "Contact", + "language": "" } ] }, @@ -537,8 +537,8 @@ "label": { "values": [ { - "text": "canHelp", - "lang": "" + "value": "canHelp", + "language": "" } ] }, @@ -547,8 +547,8 @@ "type": "string", "values": [ { - "text": "Система спрашивает 'Я могу еще чем-либо помочь?'", - "lang": "" + "value": "Система спрашивает 'Я могу еще чем-либо помочь?'", + "language": "" } ] } @@ -562,8 +562,8 @@ "label": { "values": [ { - "text": "trueExpressedBy", - "lang": "" + "value": "trueExpressedBy", + "language": "" } ] }, @@ -577,8 +577,8 @@ "label": { "values": [ { - "text": "inserted", - "lang": "" + "value": "inserted", + "language": "" } ] }, @@ -587,8 +587,8 @@ "type": "string", "values": [ { - "text": "Вставлен ли Cam-модуль в ТВ или приставку", - "lang": "" + "value": "Вставлен ли Cam-модуль в ТВ или приставку", + "language": "" } ] }, @@ -596,8 +596,8 @@ "type": "string", "values": [ { - "text": "tv:Disconnect", - "lang": "" + "value": "tv:Disconnect", + "language": "" } ] }, @@ -605,8 +605,8 @@ "type": "string", "values": [ { - "text": "tv:Connect", - "lang": "" + "value": "tv:Connect", + "language": "" } ] } @@ -620,8 +620,8 @@ "label": { "values": [ { - "text": "operational", - "lang": "" + "value": "operational", + "language": "" } ] }, @@ -630,8 +630,8 @@ "type": "string", "values": [ { - "text": "Работает в смысле 'Не сломано и может выполняет основные фунции'", - "lang": "" + "value": "Работает в смысле 'Не сломано и может выполняет основные фунции'", + "language": "" } ] }, @@ -639,8 +639,8 @@ "type": "string", "values": [ { - "text": "tv:Disappear", - "lang": "" + "value": "tv:Disappear", + "language": "" } ] }, @@ -648,20 +648,20 @@ "type": "string", "values": [ { - "text": "работать", - "lang": "" + "value": "работать", + "language": "" }, { - "text": "показывать", - "lang": "" + "value": "показывать", + "language": "" }, { - "text": "включаться", - "lang": "" + "value": "включаться", + "language": "" }, { - "text": "включать", - "lang": "" + "value": "включать", + "language": "" } ] } @@ -675,8 +675,8 @@ "label": { "values": [ { - "text": "Name", - "lang": "" + "value": "Name", + "language": "" } ] }, @@ -690,8 +690,8 @@ "label": { "values": [ { - "text": "Router", - "lang": "" + "value": "Router", + "language": "" } ] }, @@ -700,8 +700,8 @@ "type": "string", "values": [ { - "text": "Роутер - устройство маршрутизации между сетями, компонент услуги предоставление доступа в интернет или интерактивного телевидения.", - "lang": "" + "value": "Роутер - устройство маршрутизации между сетями, компонент услуги предоставление доступа в интернет или интерактивного телевидения.", + "language": "" } ] }, @@ -709,8 +709,8 @@ "type": "string", "values": [ { - "text": "роутер", - "lang": "" + "value": "роутер", + "language": "" } ] } @@ -724,8 +724,8 @@ "label": { "values": [ { - "text": "Disconnect", - "lang": "" + "value": "Disconnect", + "language": "" } ] }, @@ -734,16 +734,16 @@ "type": "string", "values": [ { - "text": "извлекать", - "lang": "" + "value": "извлекать", + "language": "" }, { - "text": "вытаскивать", - "lang": "" + "value": "вытаскивать", + "language": "" }, { - "text": "вынимать", - "lang": "" + "value": "вынимать", + "language": "" } ] } @@ -757,8 +757,8 @@ "label": { "values": [ { - "text": "Unsubscribe", - "lang": "" + "value": "Unsubscribe", + "language": "" } ] }, @@ -767,16 +767,16 @@ "type": "string", "values": [ { - "text": "отписываться", - "lang": "" + "value": "отписываться", + "language": "" }, { - "text": "отключать", - "lang": "" + "value": "отключать", + "language": "" }, { - "text": "отказываться", - "lang": "" + "value": "отказываться", + "language": "" } ] } @@ -790,8 +790,8 @@ "label": { "values": [ { - "text": "CamModule", - "lang": "" + "value": "CamModule", + "language": "" } ] }, @@ -800,8 +800,8 @@ "type": "string", "values": [ { - "text": "Cam-модуль, отвечает за декодирование платных каналов DVB. Может вставляться в телевизор или в теле-приставку.", - "lang": "" + "value": "Cam-модуль, отвечает за декодирование платных каналов DVB. Может вставляться в телевизор или в теле-приставку.", + "language": "" } ] }, @@ -809,12 +809,12 @@ "type": "string", "values": [ { - "text": "к-модуль", - "lang": "" + "value": "к-модуль", + "language": "" }, { - "text": "cam-модуль", - "lang": "" + "value": "cam-модуль", + "language": "" } ] } @@ -828,8 +828,8 @@ "label": { "values": [ { - "text": "Subscribe", - "lang": "" + "value": "Subscribe", + "language": "" } ] }, @@ -838,20 +838,20 @@ "type": "string", "values": [ { - "text": "устанавливать", - "lang": "" + "value": "устанавливать", + "language": "" }, { - "text": "подключать", - "lang": "" + "value": "подключать", + "language": "" }, { - "text": "переходить", - "lang": "" + "value": "переходить", + "language": "" }, { - "text": "настраивать", - "lang": "" + "value": "настраивать", + "language": "" } ] } @@ -865,8 +865,8 @@ "label": { "values": [ { - "text": "Internet", - "lang": "" + "value": "Internet", + "language": "" } ] }, @@ -875,8 +875,8 @@ "type": "string", "values": [ { - "text": "интернет", - "lang": "" + "value": "интернет", + "language": "" } ] } @@ -890,8 +890,8 @@ "label": { "values": [ { - "text": "sufficientFunds", - "lang": "" + "value": "sufficientFunds", + "language": "" } ] }, @@ -905,8 +905,8 @@ "label": { "values": [ { - "text": "ServiceProvider", - "lang": "" + "value": "ServiceProvider", + "language": "" } ] }, @@ -915,8 +915,8 @@ "type": "string", "values": [ { - "text": "Поставщик услуг.", - "lang": "" + "value": "Поставщик услуг.", + "language": "" } ] } @@ -930,8 +930,8 @@ "label": { "values": [ { - "text": "suspended", - "lang": "" + "value": "suspended", + "language": "" } ] }, @@ -940,8 +940,8 @@ "type": "string", "values": [ { - "text": "Приостановлено ли предоставление услуги из-за отсутствия оплаты или еще каких причин.", - "lang": "" + "value": "Приостановлено ли предоставление услуги из-за отсутствия оплаты или еще каких причин.", + "language": "" } ] }, @@ -949,12 +949,12 @@ "type": "string", "values": [ { - "text": "отключать", - "lang": "" + "value": "отключать", + "language": "" }, { - "text": "заблокировать", - "lang": "" + "value": "заблокировать", + "language": "" } ] } @@ -968,8 +968,8 @@ "label": { "values": [ { - "text": "SimpleProperty", - "lang": "" + "value": "SimpleProperty", + "language": "" } ] }, @@ -983,8 +983,8 @@ "label": { "values": [ { - "text": "RTService", - "lang": "" + "value": "RTService", + "language": "" } ] }, @@ -993,8 +993,8 @@ "type": "string", "values": [ { - "text": "Услуга ростелекома как элемент оплаты, биллинга, части договора.", - "lang": "" + "value": "Услуга ростелекома как элемент оплаты, биллинга, части договора.", + "language": "" } ] }, @@ -1002,8 +1002,8 @@ "type": "string", "values": [ { - "text": "услуга", - "lang": "" + "value": "услуга", + "language": "" } ] } @@ -1017,8 +1017,8 @@ "label": { "values": [ { - "text": "serviceNumber", - "lang": "" + "value": "serviceNumber", + "language": "" } ] }, @@ -1027,8 +1027,8 @@ "type": "string", "values": [ { - "text": "Номер услуги клиента. Используется для идентификации клиента.", - "lang": "" + "value": "Номер услуги клиента. Используется для идентификации клиента.", + "language": "" } ] }, @@ -1036,8 +1036,8 @@ "type": "string", "values": [ { - "text": "номер", - "lang": "" + "value": "номер", + "language": "" } ] } @@ -1051,8 +1051,8 @@ "label": { "values": [ { - "text": "Disappear", - "lang": "" + "value": "Disappear", + "language": "" } ] }, @@ -1061,16 +1061,16 @@ "type": "string", "values": [ { - "text": "отключаться", - "lang": "" + "value": "отключаться", + "language": "" }, { - "text": "пропадать", - "lang": "" + "value": "пропадать", + "language": "" }, { - "text": "исчезать", - "lang": "" + "value": "исчезать", + "language": "" } ] } @@ -1084,8 +1084,8 @@ "label": { "values": [ { - "text": "Operation", - "lang": "" + "value": "Operation", + "language": "" } ] }, @@ -1099,8 +1099,8 @@ "label": { "values": [ { - "text": "WiFiRouter", - "lang": "" + "value": "WiFiRouter", + "language": "" } ] }, @@ -1109,8 +1109,8 @@ "type": "string", "values": [ { - "text": "WiFi", - "lang": "" + "value": "WiFi", + "language": "" } ] } @@ -1124,8 +1124,8 @@ "label": { "values": [ { - "text": "Service", - "lang": "" + "value": "Service", + "language": "" } ] }, @@ -1134,8 +1134,8 @@ "type": "string", "values": [ { - "text": "Сервис, класс всех сущностей, ответственных за предоставление услуг клиентам.", - "lang": "" + "value": "Сервис, класс всех сущностей, ответственных за предоставление услуг клиентам.", + "language": "" } ] } @@ -1149,8 +1149,8 @@ "label": { "values": [ { - "text": "DigitalTVChannel", - "lang": "" + "value": "DigitalTVChannel", + "language": "" } ] }, @@ -1159,8 +1159,8 @@ "type": "string", "values": [ { - "text": "Цифровой телевизионный канал.", - "lang": "" + "value": "Цифровой телевизионный канал.", + "language": "" } ] }, @@ -1168,8 +1168,8 @@ "type": "string", "values": [ { - "text": "цифровой", - "lang": "" + "value": "цифровой", + "language": "" } ] } @@ -1183,8 +1183,8 @@ "label": { "values": [ { - "text": "rent", - "lang": "" + "value": "rent", + "language": "" } ] }, @@ -1193,8 +1193,8 @@ "type": "string", "values": [ { - "text": "Арендовать указанное имущество", - "lang": "" + "value": "Арендовать указанное имущество", + "language": "" } ] }, @@ -1202,8 +1202,8 @@ "type": "string", "values": [ { - "text": "арендовать", - "lang": "" + "value": "арендовать", + "language": "" } ] } @@ -1217,8 +1217,8 @@ "label": { "values": [ { - "text": "ServiceDesk", - "lang": "" + "value": "ServiceDesk", + "language": "" } ] }, @@ -1227,8 +1227,8 @@ "type": "string", "values": [ { - "text": "Служба поддержки.", - "lang": "" + "value": "Служба поддержки.", + "language": "" } ] } @@ -1242,8 +1242,8 @@ "label": { "values": [ { - "text": "filedRequest", - "lang": "" + "value": "filedRequest", + "language": "" } ] }, @@ -1252,8 +1252,8 @@ "type": "string", "values": [ { - "text": "Пользователь оставил заявку", - "lang": "" + "value": "Пользователь оставил заявку", + "language": "" } ] } @@ -1267,8 +1267,8 @@ "label": { "values": [ { - "text": "isClientOf", - "lang": "" + "value": "isClientOf", + "language": "" } ] }, @@ -1277,8 +1277,8 @@ "type": "string", "values": [ { - "text": "Являться клиентом указанного оператора", - "lang": "" + "value": "Являться клиентом указанного оператора", + "language": "" } ] }, @@ -1286,8 +1286,8 @@ "type": "string", "values": [ { - "text": "клиент", - "lang": "" + "value": "клиент", + "language": "" } ] } @@ -1301,8 +1301,8 @@ "label": { "values": [ { - "text": "came", - "lang": "" + "value": "came", + "language": "" } ] }, @@ -1311,8 +1311,8 @@ "type": "string", "values": [ { - "text": "Был ли мастер у клиента.", - "lang": "" + "value": "Был ли мастер у клиента.", + "language": "" } ] }, @@ -1320,8 +1320,8 @@ "type": "string", "values": [ { - "text": "приходить", - "lang": "" + "value": "приходить", + "language": "" } ] } @@ -1335,8 +1335,8 @@ "label": { "values": [ { - "text": "Observe", - "lang": "" + "value": "Observe", + "language": "" } ] }, @@ -1350,8 +1350,8 @@ "label": { "values": [ { - "text": "PropertyAsGoods", - "lang": "" + "value": "PropertyAsGoods", + "language": "" } ] }, @@ -1360,8 +1360,8 @@ "type": "string", "values": [ { - "text": "Собственность пользователя в виде материальных или нематериальных объектов.", - "lang": "" + "value": "Собственность пользователя в виде материальных или нематериальных объектов.", + "language": "" } ] } @@ -1375,8 +1375,8 @@ "label": { "values": [ { - "text": "expressedBy", - "lang": "" + "value": "expressedBy", + "language": "" } ] }, @@ -1390,8 +1390,8 @@ "label": { "values": [ { - "text": "modified", - "lang": "" + "value": "modified", + "language": "" } ] }, @@ -1400,8 +1400,8 @@ "type": "string", "values": [ { - "text": "Перенастроено ли устройство, были ли изменены настройки.", - "lang": "" + "value": "Перенастроено ли устройство, были ли изменены настройки.", + "language": "" } ] }, @@ -1409,8 +1409,8 @@ "type": "string", "values": [ { - "text": "настраивать", - "lang": "" + "value": "настраивать", + "language": "" } ] } @@ -1424,8 +1424,8 @@ "label": { "values": [ { - "text": "ChannelPack", - "lang": "" + "value": "ChannelPack", + "language": "" } ] }, @@ -1434,16 +1434,16 @@ "type": "string", "values": [ { - "text": "тариф", - "lang": "" + "value": "тариф", + "language": "" }, { - "text": "план", - "lang": "" + "value": "план", + "language": "" }, { - "text": "пакет", - "lang": "" + "value": "пакет", + "language": "" } ] } @@ -1457,8 +1457,8 @@ "label": { "values": [ { - "text": "deviceModel", - "lang": "" + "value": "deviceModel", + "language": "" } ] }, @@ -1467,8 +1467,8 @@ "type": "string", "values": [ { - "text": "Название модели устройства.", - "lang": "" + "value": "Название модели устройства.", + "language": "" } ] }, @@ -1476,8 +1476,8 @@ "type": "string", "values": [ { - "text": "модель", - "lang": "" + "value": "модель", + "language": "" } ] } @@ -1491,8 +1491,8 @@ "label": { "values": [ { - "text": "popularChannelPack", - "lang": "" + "value": "popularChannelPack", + "language": "" } ] }, @@ -1501,8 +1501,8 @@ "type": "string", "values": [ { - "text": "популярный", - "lang": "" + "value": "популярный", + "language": "" } ] } @@ -1516,8 +1516,8 @@ "label": { "values": [ { - "text": "canHangUp", - "lang": "" + "value": "canHangUp", + "language": "" } ] }, @@ -1526,8 +1526,8 @@ "type": "string", "values": [ { - "text": "Можно ли клиенту повесить трубку и продложить решать проблему автономно.", - "lang": "" + "value": "Можно ли клиенту повесить трубку и продложить решать проблему автономно.", + "language": "" } ] } @@ -1541,8 +1541,8 @@ "label": { "values": [ { - "text": "canSolve", - "lang": "" + "value": "canSolve", + "language": "" } ] }, @@ -1551,8 +1551,8 @@ "type": "string", "values": [ { - "text": "Может ли служба поддержки решить проблему пользователя. Нет - в случае неработающего телевизора, например.", - "lang": "" + "value": "Может ли служба поддержки решить проблему пользователя. Нет - в случае неработающего телевизора, например.", + "language": "" } ] } @@ -1566,8 +1566,8 @@ "label": { "values": [ { - "text": "userCanSwitchChannel", - "lang": "" + "value": "userCanSwitchChannel", + "language": "" } ] }, @@ -1576,12 +1576,12 @@ "type": "string", "values": [ { - "text": "переключаться", - "lang": "" + "value": "переключаться", + "language": "" }, { - "text": "переключать", - "lang": "" + "value": "переключать", + "language": "" } ] } @@ -1595,8 +1595,8 @@ "label": { "values": [ { - "text": "rebooted", - "lang": "" + "value": "rebooted", + "language": "" } ] }, @@ -1605,8 +1605,8 @@ "type": "string", "values": [ { - "text": "Перезагружено ли устройство", - "lang": "" + "value": "Перезагружено ли устройство", + "language": "" } ] }, @@ -1614,8 +1614,8 @@ "type": "string", "values": [ { - "text": "перезагружать", - "lang": "" + "value": "перезагружать", + "language": "" } ] } @@ -1629,8 +1629,8 @@ "label": { "values": [ { - "text": "own", - "lang": "" + "value": "own", + "language": "" } ] }, @@ -1639,8 +1639,8 @@ "type": "string", "values": [ { - "text": "Владеть указанным имуществом", - "lang": "" + "value": "Владеть указанным имуществом", + "language": "" } ] }, @@ -1648,16 +1648,16 @@ "type": "string", "values": [ { - "text": "есть", - "lang": "" + "value": "есть", + "language": "" }, { - "text": "иметь", - "lang": "" + "value": "иметь", + "language": "" }, { - "text": "владеть", - "lang": "" + "value": "владеть", + "language": "" } ] } @@ -1671,8 +1671,8 @@ "label": { "values": [ { - "text": "DigitalTVService", - "lang": "" + "value": "DigitalTVService", + "language": "" } ] }, @@ -1681,8 +1681,8 @@ "type": "string", "values": [ { - "text": "Услуга цифрового телевидения.", - "lang": "" + "value": "Услуга цифрового телевидения.", + "language": "" } ] }, @@ -1690,8 +1690,8 @@ "type": "string", "values": [ { - "text": "цифровой", - "lang": "" + "value": "цифровой", + "language": "" } ] } @@ -1705,8 +1705,8 @@ "label": { "values": [ { - "text": "PhoneNumber", - "lang": "" + "value": "PhoneNumber", + "language": "" } ] }, @@ -1720,8 +1720,8 @@ "label": { "values": [ { - "text": "User", - "lang": "" + "value": "User", + "language": "" } ] }, @@ -1730,8 +1730,8 @@ "type": "string", "values": [ { - "text": "Пользователь. Пока и клиент Ростелекома и позвонивший человек в одном лице.", - "lang": "" + "value": "Пользователь. Пока и клиент Ростелекома и позвонивший человек в одном лице.", + "language": "" } ] }, @@ -1739,12 +1739,12 @@ "type": "string", "values": [ { - "text": "мы", - "lang": "" + "value": "мы", + "language": "" }, { - "text": "я", - "lang": "" + "value": "я", + "language": "" } ] } @@ -1758,8 +1758,8 @@ "label": { "values": [ { - "text": "subscribed", - "lang": "" + "value": "subscribed", + "language": "" } ] }, @@ -1768,8 +1768,8 @@ "type": "string", "values": [ { - "text": "Подписан ли пользователь на эту услугу.", - "lang": "" + "value": "Подписан ли пользователь на эту услугу.", + "language": "" } ] }, @@ -1777,8 +1777,8 @@ "type": "string", "values": [ { - "text": "tv:Unsubscribe", - "lang": "" + "value": "tv:Unsubscribe", + "language": "" } ] }, @@ -1786,8 +1786,8 @@ "type": "string", "values": [ { - "text": "tv:Subscribe", - "lang": "" + "value": "tv:Subscribe", + "language": "" } ] } @@ -1801,8 +1801,8 @@ "label": { "values": [ { - "text": "baseChannelPack", - "lang": "" + "value": "baseChannelPack", + "language": "" } ] }, @@ -1811,16 +1811,16 @@ "type": "string", "values": [ { - "text": "стартовый", - "lang": "" + "value": "стартовый", + "language": "" }, { - "text": "начальный", - "lang": "" + "value": "начальный", + "language": "" }, { - "text": "базовый", - "lang": "" + "value": "базовый", + "language": "" } ] } @@ -1834,8 +1834,8 @@ "label": { "values": [ { - "text": "TVSet", - "lang": "" + "value": "TVSet", + "language": "" } ] }, @@ -1844,8 +1844,8 @@ "type": "string", "values": [ { - "text": "Телевизор. Может иметь возможность декодирования платных каналов услуги цифрового телевидения с использованием CAM-модуля.", - "lang": "" + "value": "Телевизор. Может иметь возможность декодирования платных каналов услуги цифрового телевидения с использованием CAM-модуля.", + "language": "" } ] }, @@ -1853,8 +1853,8 @@ "type": "string", "values": [ { - "text": "телевизор", - "lang": "" + "value": "телевизор", + "language": "" } ] } @@ -1868,8 +1868,8 @@ "label": { "values": [ { - "text": "forwardedToInfoSupport", - "lang": "" + "value": "forwardedToInfoSupport", + "language": "" } ] }, @@ -1883,8 +1883,8 @@ "label": { "values": [ { - "text": "lemma", - "lang": "" + "value": "lemma", + "language": "" } ] }, @@ -1898,8 +1898,8 @@ "label": { "values": [ { - "text": "identified", - "lang": "" + "value": "identified", + "language": "" } ] }, @@ -1913,8 +1913,8 @@ "label": { "values": [ { - "text": "forwardedToSecondSupport", - "lang": "" + "value": "forwardedToSecondSupport", + "language": "" } ] }, @@ -1928,8 +1928,8 @@ "label": { "values": [ { - "text": "callsAgain", - "lang": "" + "value": "callsAgain", + "language": "" } ] }, @@ -1938,8 +1938,8 @@ "type": "string", "values": [ { - "text": "Пользователь обращается повторно", - "lang": "" + "value": "Пользователь обращается повторно", + "language": "" } ] } @@ -1953,8 +1953,8 @@ "label": { "values": [ { - "text": "ControlPanel", - "lang": "" + "value": "ControlPanel", + "language": "" } ] }, @@ -1963,8 +1963,8 @@ "type": "string", "values": [ { - "text": "Пульт от телевизора", - "lang": "" + "value": "Пульт от телевизора", + "language": "" } ] }, @@ -1972,8 +1972,8 @@ "type": "string", "values": [ { - "text": "пульт", - "lang": "" + "value": "пульт", + "language": "" } ] } @@ -1987,8 +1987,8 @@ "label": { "values": [ { - "text": "hasNetworkConnection", - "lang": "" + "value": "hasNetworkConnection", + "language": "" } ] }, @@ -1997,8 +1997,8 @@ "type": "string", "values": [ { - "text": "Есть ли подключение к роутеру со стороны провайдера услуг.", - "lang": "" + "value": "Есть ли подключение к роутеру со стороны провайдера услуг.", + "language": "" } ] } @@ -2012,8 +2012,8 @@ "label": { "values": [ { - "text": "Image", - "lang": "" + "value": "Image", + "language": "" } ] }, @@ -2022,20 +2022,20 @@ "type": "string", "values": [ { - "text": "сигнал", - "lang": "" + "value": "сигнал", + "language": "" }, { - "text": "картинка", - "lang": "" + "value": "картинка", + "language": "" }, { - "text": "изображение", - "lang": "" + "value": "изображение", + "language": "" }, { - "text": "видео", - "lang": "" + "value": "видео", + "language": "" } ] } @@ -2049,8 +2049,8 @@ "label": { "values": [ { - "text": "phoneNumber", - "lang": "" + "value": "phoneNumber", + "language": "" } ] }, @@ -2059,8 +2059,8 @@ "type": "string", "values": [ { - "text": "Номер телефона", - "lang": "" + "value": "Номер телефона", + "language": "" } ] }, @@ -2068,8 +2068,8 @@ "type": "string", "values": [ { - "text": "номер", - "lang": "" + "value": "номер", + "language": "" } ] } @@ -2083,8 +2083,8 @@ "label": { "values": [ { - "text": "deviceAppearance", - "lang": "" + "value": "deviceAppearance", + "language": "" } ] }, @@ -2093,8 +2093,8 @@ "type": "string", "values": [ { - "text": "Описание внешнего вида устройства.", - "lang": "" + "value": "Описание внешнего вида устройства.", + "language": "" } ] }, @@ -2102,8 +2102,8 @@ "type": "string", "values": [ { - "text": "внешний вид", - "lang": "" + "value": "внешний вид", + "language": "" } ] } @@ -2117,8 +2117,8 @@ "label": { "values": [ { - "text": "TurnOn", - "lang": "" + "value": "TurnOn", + "language": "" } ] }, @@ -2127,8 +2127,8 @@ "type": "string", "values": [ { - "text": "включать", - "lang": "" + "value": "включать", + "language": "" } ] } @@ -2142,8 +2142,8 @@ "label": { "values": [ { - "text": "tvoeTV", - "lang": "" + "value": "tvoeTV", + "language": "" } ] }, @@ -2152,12 +2152,12 @@ "type": "string", "values": [ { - "text": "твой интернет", - "lang": "" + "value": "твой интернет", + "language": "" }, { - "text": "твое тв", - "lang": "" + "value": "твое тв", + "language": "" } ] } @@ -2171,8 +2171,8 @@ "label": { "values": [ { - "text": "HomePhone", - "lang": "" + "value": "HomePhone", + "language": "" } ] }, @@ -2181,8 +2181,8 @@ "type": "string", "values": [ { - "text": "Домашний телефон. Один из способов идентифицировать клиента. Другой - по номеру услуги.", - "lang": "" + "value": "Домашний телефон. Один из способов идентифицировать клиента. Другой - по номеру услуги.", + "language": "" } ] }, @@ -2190,12 +2190,12 @@ "type": "string", "values": [ { - "text": "домашний", - "lang": "" + "value": "домашний", + "language": "" }, { - "text": "городской", - "lang": "" + "value": "городской", + "language": "" } ] } @@ -2209,8 +2209,8 @@ "label": { "values": [ { - "text": "All", - "lang": "" + "value": "All", + "language": "" } ] }, @@ -2219,8 +2219,8 @@ "type": "string", "values": [ { - "text": "Концепт для обработки 'а потом все отвалилось'", - "lang": "" + "value": "Концепт для обработки 'а потом все отвалилось'", + "language": "" } ] }, @@ -2228,8 +2228,8 @@ "type": "string", "values": [ { - "text": "всё", - "lang": "" + "value": "всё", + "language": "" } ] } @@ -2243,8 +2243,8 @@ "label": { "values": [ { - "text": "Home", - "lang": "" + "value": "Home", + "language": "" } ] }, @@ -2253,8 +2253,8 @@ "type": "string", "values": [ { - "text": "Дом клиента в смысле места, где установлено такое оборудование, как роутер.", - "lang": "" + "value": "Дом клиента в смысле места, где установлено такое оборудование, как роутер.", + "language": "" } ] }, @@ -2262,8 +2262,8 @@ "type": "string", "values": [ { - "text": "дом", - "lang": "" + "value": "дом", + "language": "" } ] } @@ -2277,8 +2277,8 @@ "label": { "values": [ { - "text": "Appear", - "lang": "" + "value": "Appear", + "language": "" } ] }, @@ -2287,8 +2287,8 @@ "type": "string", "values": [ { - "text": "появляться", - "lang": "" + "value": "появляться", + "language": "" } ] } @@ -2302,8 +2302,8 @@ "label": { "values": [ { - "text": "Cable", - "lang": "" + "value": "Cable", + "language": "" } ] }, @@ -2312,8 +2312,8 @@ "type": "string", "values": [ { - "text": "Кабель - нужен для подключения кабельного телевидения.", - "lang": "" + "value": "Кабель - нужен для подключения кабельного телевидения.", + "language": "" } ] } @@ -2327,8 +2327,8 @@ "label": { "values": [ { - "text": "homeAddress", - "lang": "" + "value": "homeAddress", + "language": "" } ] }, @@ -2337,8 +2337,8 @@ "type": "string", "values": [ { - "text": "Домашний адрес", - "lang": "" + "value": "Домашний адрес", + "language": "" } ] }, @@ -2346,8 +2346,8 @@ "type": "string", "values": [ { - "text": "адрес", - "lang": "" + "value": "адрес", + "language": "" } ] } @@ -2361,8 +2361,8 @@ "label": { "values": [ { - "text": "Flat", - "lang": "" + "value": "Flat", + "language": "" } ] }, @@ -2371,8 +2371,8 @@ "type": "string", "values": [ { - "text": "Квартира (уточнение дома).", - "lang": "" + "value": "Квартира (уточнение дома).", + "language": "" } ] }, @@ -2380,8 +2380,8 @@ "type": "string", "values": [ { - "text": "квартира", - "lang": "" + "value": "квартира", + "language": "" } ] } @@ -2395,8 +2395,8 @@ "label": { "values": [ { - "text": "operationalBad", - "lang": "" + "value": "operationalBad", + "language": "" } ] }, @@ -2405,8 +2405,8 @@ "type": "string", "values": [ { - "text": "tv:Disappear", - "lang": "" + "value": "tv:Disappear", + "language": "" } ] }, @@ -2414,20 +2414,20 @@ "type": "string", "values": [ { - "text": "тормозить", - "lang": "" + "value": "тормозить", + "language": "" }, { - "text": "расползаться", - "lang": "" + "value": "расползаться", + "language": "" }, { - "text": "портиться", - "lang": "" + "value": "портиться", + "language": "" }, { - "text": "барахлить", - "lang": "" + "value": "барахлить", + "language": "" } ] } @@ -2441,8 +2441,8 @@ "label": { "values": [ { - "text": "AnalogueTVChannel", - "lang": "" + "value": "AnalogueTVChannel", + "language": "" } ] }, @@ -2451,8 +2451,8 @@ "type": "string", "values": [ { - "text": "Аналоговый телевизионный канал.", - "lang": "" + "value": "Аналоговый телевизионный канал.", + "language": "" } ] }, @@ -2460,8 +2460,8 @@ "type": "string", "values": [ { - "text": "аналоговый", - "lang": "" + "value": "аналоговый", + "language": "" } ] } @@ -2475,8 +2475,8 @@ "label": { "values": [ { - "text": "ServiceMan", - "lang": "" + "value": "ServiceMan", + "language": "" } ] }, @@ -2485,8 +2485,8 @@ "type": "string", "values": [ { - "text": "Квалифицированный работник", - "lang": "" + "value": "Квалифицированный работник", + "language": "" } ] }, @@ -2494,8 +2494,8 @@ "type": "string", "values": [ { - "text": "мастер", - "lang": "" + "value": "мастер", + "language": "" } ] } @@ -2509,8 +2509,8 @@ "label": { "values": [ { - "text": "Phone", - "lang": "" + "value": "Phone", + "language": "" } ] }, @@ -2519,8 +2519,8 @@ "type": "string", "values": [ { - "text": "Телефон. Здесь пока только для образование сущности 'домашний телефон'", - "lang": "" + "value": "Телефон. Здесь пока только для образование сущности 'домашний телефон'", + "language": "" } ] }, @@ -2528,8 +2528,8 @@ "type": "string", "values": [ { - "text": "телефон", - "lang": "" + "value": "телефон", + "language": "" } ] } @@ -2543,8 +2543,8 @@ "label": { "values": [ { - "text": "Speed", - "lang": "" + "value": "Speed", + "language": "" } ] }, @@ -2553,12 +2553,12 @@ "type": "string", "values": [ { - "text": "скорость", - "lang": "" + "value": "скорость", + "language": "" }, { - "text": "производительность", - "lang": "" + "value": "производительность", + "language": "" } ] } @@ -2572,8 +2572,8 @@ "label": { "values": [ { - "text": "SubscribableService", - "lang": "" + "value": "SubscribableService", + "language": "" } ] }, @@ -2587,8 +2587,8 @@ "label": { "values": [ { - "text": "hdChannelPack", - "lang": "" + "value": "hdChannelPack", + "language": "" } ] }, @@ -2597,8 +2597,8 @@ "type": "string", "values": [ { - "text": "эйчди", - "lang": "" + "value": "эйчди", + "language": "" } ] } @@ -2612,8 +2612,8 @@ "label": { "values": [ { - "text": "SetTopBox", - "lang": "" + "value": "SetTopBox", + "language": "" } ] }, @@ -2622,8 +2622,8 @@ "type": "string", "values": [ { - "text": "Телеприставка, осуществляющая декодирование цифрового сигнала и подключаемая к телевизору.", - "lang": "" + "value": "Телеприставка, осуществляющая декодирование цифрового сигнала и подключаемая к телевизору.", + "language": "" } ] }, @@ -2631,8 +2631,8 @@ "type": "string", "values": [ { - "text": "приставка", - "lang": "" + "value": "приставка", + "language": "" } ] } diff --git a/src/examples/resources/exampleMetadataApi.ts b/src/examples/resources/exampleMetadataApi.ts index 8eddfd02..f32e7bb0 100644 --- a/src/examples/resources/exampleMetadataApi.ts +++ b/src/examples/resources/exampleMetadataApi.ts @@ -1,9 +1,7 @@ import { - ElementModel, LinkModel, ElementTypeIri, LinkTypeIri, PropertyTypeIri, MetadataApi, CancellationToken, - AuthoringKind, LinkChange, ValidationApi, ValidationEvent, ElementError, LinkError, - LinkDirection, ElementIri, + ElementModel, LinkModel, ElementIri, ElementTypeIri, LinkTypeIri, PropertyTypeIri, LinkDirection, + MetadataApi, ValidationApi, ValidationEvent, ElementError, LinkError, DirectedLinkType, CancellationToken, } from '../../index'; -import { DirectedLinkType } from '../../ontodia/diagram/elements'; const OWL_PREFIX = 'http://www.w3.org/2002/07/owl#'; const RDFS_PREFIX = 'http://www.w3.org/2000/01/rdf-schema#'; @@ -126,23 +124,21 @@ export class ExampleValidationApi implements ValidationApi { async validate(event: ValidationEvent): Promise> { const errors: Array = []; if (event.target.types.indexOf(owl.class) >= 0) { - event.state.events - .filter((e): e is LinkChange => - e.type === AuthoringKind.ChangeLink && - !e.before && e.after.sourceId === event.target.id - ).forEach(newLinkEvent => { + event.state.links.forEach(e => { + if (!e.before && e.after.sourceId === event.target.id) { errors.push({ type: 'link', - target: newLinkEvent.after, + target: e.after, message: 'Cannot add any new link from a Class', }); - const linkType = event.model.createLinkType(newLinkEvent.after.linkTypeId); + const linkType = event.model.createLinkType(e.after.linkTypeId); errors.push({ type: 'element', target: event.target.id, message: `Cannot create <${linkType.id}> link from a Class`, }); - }); + } + }); } await delay(); diff --git a/src/examples/resources/linkTypes.json b/src/examples/resources/linkTypes.json index 857a236b..f76815a0 100644 --- a/src/examples/resources/linkTypes.json +++ b/src/examples/resources/linkTypes.json @@ -4,8 +4,8 @@ "label": { "values": [ { - "text": "own", - "lang": "" + "value": "own", + "language": "" } ] }, @@ -16,8 +16,8 @@ "label": { "values": [ { - "text": "disjointWith", - "lang": "" + "value": "disjointWith", + "language": "" } ] }, @@ -28,8 +28,8 @@ "label": { "values": [ { - "text": "type", - "lang": "" + "value": "type", + "language": "" } ] }, @@ -40,8 +40,8 @@ "label": { "values": [ { - "text": "isClientOf", - "lang": "" + "value": "isClientOf", + "language": "" } ] }, @@ -52,8 +52,8 @@ "label": { "values": [ { - "text": "domain", - "lang": "" + "value": "domain", + "language": "" } ] }, @@ -64,8 +64,8 @@ "label": { "values": [ { - "text": "subPropertyOf", - "lang": "" + "value": "subPropertyOf", + "language": "" } ] }, @@ -76,8 +76,8 @@ "label": { "values": [ { - "text": "rent", - "lang": "" + "value": "rent", + "language": "" } ] }, @@ -88,8 +88,8 @@ "label": { "values": [ { - "text": "subClassOf", - "lang": "" + "value": "subClassOf", + "language": "" } ] }, @@ -100,8 +100,8 @@ "label": { "values": [ { - "text": "range", - "lang": "" + "value": "range", + "language": "" } ] }, diff --git a/src/index.ts b/src/index.ts index 4968ef57..6b5d3387 100644 --- a/src/index.ts +++ b/src/index.ts @@ -21,7 +21,7 @@ export * from './ontodia/data/sparql/graphBuilder'; export * from './ontodia/data/sparql/sparqlGraphBuilder'; export { DIAGRAM_CONTEXT_URL_V1 } from './ontodia/data/schema'; -export { RestoreGeometry } from './ontodia/diagram/commands'; +export { RestoreGeometry, setElementExpanded, setElementData, setLinkData } from './ontodia/diagram/commands'; export { Element, ElementEvents, ElementTemplateState, Link, LinkEvents, LinkTemplateState, LinkVertex, Cell, LinkDirection } from './ontodia/diagram/elements'; diff --git a/src/internalApi.ts b/src/internalApi.ts index a93d9040..07fead53 100644 --- a/src/internalApi.ts +++ b/src/internalApi.ts @@ -11,6 +11,7 @@ export * from './ontodia/viewUtils/keyedObserver'; export * from './ontodia/viewUtils/spinner'; export * from './ontodia/widgets/listElementView'; +export * from './ontodia/widgets/searchResults'; export { WorkspaceContext, WorkspaceContextWrapper, WorkspaceContextTypes, diff --git a/src/ontodia/customization/defaultLinkStyles.ts b/src/ontodia/customization/defaultLinkStyles.ts index 12c7d274..9d7dfeed 100644 --- a/src/ontodia/customization/defaultLinkStyles.ts +++ b/src/ontodia/customization/defaultLinkStyles.ts @@ -8,8 +8,8 @@ export const LINK_SHOW_IRI: LinkTemplate = { attrs: { text: { text: [{ - text: link.typeId, - lang: '', + value: link.typeId, + language: '', }], fill: 'gray', 'font-size': 12, diff --git a/src/ontodia/customization/props.ts b/src/ontodia/customization/props.ts index ebcba383..efc58944 100644 --- a/src/ontodia/customization/props.ts +++ b/src/ontodia/customization/props.ts @@ -82,6 +82,7 @@ export interface LinkMarkerStyle { export interface LinkLabel { position?: number; + title?: string; attrs?: { rect?: { fill?: string; diff --git a/src/ontodia/customization/templates/standard.tsx b/src/ontodia/customization/templates/standard.tsx index 94472a4e..22154c04 100644 --- a/src/ontodia/customization/templates/standard.tsx +++ b/src/ontodia/customization/templates/standard.tsx @@ -59,7 +59,7 @@ export class StandardTemplate extends Component {
{this.renderPhoto()}
- {this.renderIri()} + {this.renderIri(context)} {this.renderProperties(propsAsList)} {editor.inAuthoringMode ?
: null} {editor.inAuthoringMode ? this.renderActions(context) : null} @@ -119,18 +119,23 @@ export class StandardTemplate extends Component { ); } - private renderIri() { + private renderIri(context: AuthoredEntityContext) { const {iri} = this.props; + const finalIri = context.editedIri === undefined ? iri : context.editedIri; return (
- IRI: + IRI{context.editedIri ? '\u00A0(edited)' : ''}:
- {isEncodedBlank(iri) + {isEncodedBlank(finalIri) ? (blank node) - : {iri}} + : + {finalIri} + }

diff --git a/src/ontodia/customization/templates/utils.ts b/src/ontodia/customization/templates/utils.ts index 1c4f0234..56567e47 100644 --- a/src/ontodia/customization/templates/utils.ts +++ b/src/ontodia/customization/templates/utils.ts @@ -12,7 +12,7 @@ export function getPropertyValues(property: Property): string[] { if (isIriProperty(property)) { return property.values.map(({value}) => value); } else if (isLiteralProperty(property)) { - return property.values.map(({text}) => text); + return property.values.map(({value}) => value); } return []; } diff --git a/src/ontodia/data/composite/composite.ts b/src/ontodia/data/composite/composite.ts index 8a086c05..3377d9e7 100644 --- a/src/ontodia/data/composite/composite.ts +++ b/src/ontodia/data/composite/composite.ts @@ -237,10 +237,7 @@ export class CompositeDataProvider implements DataProvider { } } -type OperationParams = any; -type OperationResult = any; -// TODO: replace on compiler update -// type OperationParams = -// DataProvider[K] extends (params: infer P) => any ? P : never; -// type OperationResult = -// ReturnType extends Promise ? R : never; +type OperationParams = + DataProvider[K] extends (params: infer P) => any ? P : never; +type OperationResult = + ReturnType extends Promise ? R : never; diff --git a/src/ontodia/data/composite/mergeUtils.ts b/src/ontodia/data/composite/mergeUtils.ts index dfaa4343..6cdc7fbc 100644 --- a/src/ontodia/data/composite/mergeUtils.ts +++ b/src/ontodia/data/composite/mergeUtils.ts @@ -169,7 +169,8 @@ export function mergeElementInfo(response: CompositeResponse label.text.toLowerCase().indexOf(text) >= 0); + label => label.value.toLowerCase().indexOf(text) >= 0); } if (found) { filteredByText[element.id] = element; diff --git a/src/ontodia/data/metadataApi.ts b/src/ontodia/data/metadataApi.ts index bc29d35b..d66ce159 100644 --- a/src/ontodia/data/metadataApi.ts +++ b/src/ontodia/data/metadataApi.ts @@ -46,3 +46,8 @@ export interface MetadataApi { generateNewElementIri(types: ReadonlyArray, ct: CancellationToken): Promise; } + +export interface DirectedLinkType { + readonly linkTypeIri: LinkTypeIri; + readonly direction: LinkDirection; +} diff --git a/src/ontodia/data/model.ts b/src/ontodia/data/model.ts index ac79b41b..96b03ccd 100644 --- a/src/ontodia/data/model.ts +++ b/src/ontodia/data/model.ts @@ -4,13 +4,12 @@ import { RdfIri } from './sparql/sparqlModels'; export interface Dictionary { [key: string]: T; } export interface LocalizedString { - text: string; - lang: string; + readonly value: string; + readonly language: string; /** Equals `xsd:string` if not defined. */ - datatype?: string; + readonly datatype?: { readonly value: string }; } -// tslint:disable-next-line:interface-over-type-literal export interface IriProperty { type: 'uri'; values: ReadonlyArray; @@ -65,7 +64,7 @@ export interface LinkCount { export interface LinkType { id: LinkTypeIri; label: { values: LocalizedString[] }; - count: number; + count?: number; } export interface PropertyModel { @@ -93,7 +92,7 @@ export function sameElement(left: ElementModel, right: ElementModel): boolean { return ( left.id === right.id && isArraysEqual(left.types, right.types) && - isLocalizedStringsEqual(left.label.values, right.label.values) && + isLiteralsEqual(left.label.values, right.label.values) && left.image === right.image && isPropertiesEqual(left.properties, right.properties) && ( @@ -111,12 +110,12 @@ function isArraysEqual(left: string[], right: string[]): boolean { return true; } -function isLocalizedStringsEqual(left: ReadonlyArray, right: ReadonlyArray): boolean { +function isLiteralsEqual(left: ReadonlyArray, right: ReadonlyArray): boolean { if (left.length !== right.length) { return false; } for (let i = 0; i < left.length; i++) { const leftValue = left[i]; const rightValue = right[i]; - if (leftValue.text !== rightValue.text || leftValue.lang !== rightValue.lang) { + if (leftValue.value !== rightValue.value || leftValue.language !== rightValue.language) { return false; } } @@ -138,7 +137,7 @@ function isIriPropertiesEqual(left: Property, right: Property): boolean { function isLiteralPropertiesEqual(left: Property, right: Property): boolean { if (!isLiteralProperty(left) || !isLiteralProperty(right)) { return false; } - return isLocalizedStringsEqual(left.values, right.values); + return isLiteralsEqual(left.values, right.values); } function isPropertiesEqual(left: { [id: string]: Property }, right: { [id: string]: Property }) { diff --git a/src/ontodia/data/rdf/rdfCacheableStore.ts b/src/ontodia/data/rdf/rdfCacheableStore.ts index b1a605de..fb712d54 100644 --- a/src/ontodia/data/rdf/rdfCacheableStore.ts +++ b/src/ontodia/data/rdf/rdfCacheableStore.ts @@ -15,18 +15,6 @@ export function prefixFactory(prefix: string): ((id: string) => string) { }; } -export interface RDFStore { - add: (id: string, graph: RDFGraph) => void; - match: ( - subject?: string, - predicate?: string, - object?: string, - iri?: string, - callback?: (result: any) => void, - limit?: number, - ) => Promise; -} - export function isLiteral(el: Node): el is Literal { return el.interfaceName === 'Literal'; } diff --git a/src/ontodia/data/rdf/rdfDataProvider.ts b/src/ontodia/data/rdf/rdfDataProvider.ts index 8aea6f38..6c36400c 100644 --- a/src/ontodia/data/rdf/rdfDataProvider.ts +++ b/src/ontodia/data/rdf/rdfDataProvider.ts @@ -33,6 +33,9 @@ export interface RDFDataProviderOptions { proxy?: string; } +/** An opaque reference to RDFGraph type from `rdf-ext` library */ +export type RDFExtGraph = object; + export class RDFDataProvider implements DataProvider { public dataFetching: boolean; private initStatement: Promise | undefined; @@ -69,8 +72,8 @@ export class RDFDataProvider implements DataProvider { this.options = options; } - addGraph(graph: RDFGraph) { - this.rdfStorage.add(graph); + addGraph(graph: RDFExtGraph) { + this.rdfStorage.add(graph as RDFGraph); } private waitInitCompleted(): Promise { @@ -553,7 +556,7 @@ export class RDFDataProvider implements DataProvider { for (const el of elements) { let acceptableKey = false; for (const label of el.label.values) { - acceptableKey = acceptableKey || label.text.toLowerCase().indexOf(key) !== -1; + acceptableKey = acceptableKey || label.value.toLowerCase().indexOf(key) !== -1; if (acceptableKey) { break; } } acceptableKey = acceptableKey || el.id.toLowerCase().indexOf(key) !== -1; @@ -605,9 +608,9 @@ export class RDFDataProvider implements DataProvider { private getLabels(id: string): Promise { return this.rdfStorage.getLabels(id).then(labelTriples => { - return labelTriples.toArray().map(l => ({ - text: l.object.nominalValue, - lang: isLiteral(l.object) ? l.object.language || '' : '', + return labelTriples.toArray().map((l): LocalizedString => ({ + value: l.object.nominalValue, + language: isLiteral(l.object) ? l.object.language || '' : '', })); }); } diff --git a/src/ontodia/data/sparql/blankNodes.ts b/src/ontodia/data/sparql/blankNodes.ts index afcda223..bcfa2a7e 100644 --- a/src/ontodia/data/sparql/blankNodes.ts +++ b/src/ontodia/data/sparql/blankNodes.ts @@ -4,8 +4,8 @@ import { getUriLocalName } from '../utils'; import { SparqlDataProviderSettings } from './sparqlDataProviderSettings'; import { - ElementBinding, LinkBinding, BlankBinding, isRdfIri, isRdfBlank, - LinkCountBinding, SparqlResponse, RdfLiteral, isBlankBinding, + ElementBinding, LinkBinding, BlankBinding, FilterBinding, LinkCountBinding, ElementTypeBinding, + SparqlResponse, RdfLiteral, isRdfIri, isRdfBlank, isBlankBinding, } from './sparqlModels'; export const MAX_RECURSION_DEEP = 3; @@ -67,12 +67,12 @@ export class QueryExecutor { } export function updateFilterResults( - result: SparqlResponse, + result: SparqlResponse, queryFunction: (query: string) => Promise>, settings: SparqlDataProviderSettings, -): Promise> { - const completeBindings: ElementBinding[] = []; - const blankBindings: BlankBinding[] = []; +): Promise> { + const completeBindings: Array = []; + const blankBindings: Array = []; for (const binding of result.results.bindings) { if (isBlankBinding(binding)) { @@ -394,6 +394,21 @@ export function filter(params: FilterParams): SparqlResponse { return filterResponse; } +export function getElementTypes(elementIds: ReadonlyArray): SparqlResponse { + const bindings: ElementTypeBinding[] = []; + for (const id of elementIds) { + const blankBindings = decodeId(id); + if (blankBindings) { + for (const be of blankBindings) { + if (isRdfIri(be.inst) && be.class) { + bindings.push({inst: be.inst, class: be.class}); + } + } + } + } + return {head: undefined, results: {bindings}}; +} + function getAllRelatedByLinkTypeElements( refElementId: string, refElementLinkId: string, linkDirection: string, ): ElementBinding[] { diff --git a/src/ontodia/data/sparql/responseHandler.ts b/src/ontodia/data/sparql/responseHandler.ts index 6d321355..f611c103 100644 --- a/src/ontodia/data/sparql/responseHandler.ts +++ b/src/ontodia/data/sparql/responseHandler.ts @@ -1,19 +1,22 @@ +import { LinkConfiguration, PropertyConfiguration } from './sparqlDataProviderSettings'; import { RdfLiteral, isRdfLiteral, SparqlResponse, ClassBinding, ElementBinding, LinkBinding, isRdfIri, isRdfBlank, RdfIri, ElementImageBinding, LinkCountBinding, LinkTypeBinding, - PropertyBinding, Triple, RdfNode, + PropertyBinding, ElementTypeBinding, FilterBinding, Triple, } from './sparqlModels'; import { Dictionary, LocalizedString, LinkType, ClassModel, ElementModel, LinkModel, Property, PropertyModel, LinkCount, - ElementIri, ElementTypeIri, LinkTypeIri, PropertyTypeIri, isIriProperty, isLiteralProperty, + ElementIri, ElementTypeIri, LinkTypeIri, PropertyTypeIri, isIriProperty, isLiteralProperty, sameLink, hashLink } from '../model'; -import * as _ from 'lodash'; +import { HashMap, getOrCreateSetInMap } from '../../viewUtils/collections'; const LABEL_URI = 'http://www.w3.org/2000/01/rdf-schema#label'; const RDF_TYPE_URI = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type'; +const EMPTY_MAP: ReadonlyMap = new Map(); + export function getClassTree(response: SparqlResponse): ClassModel[] { const treeNodes = createClassMap(response.results.bindings); const allNodes: ClassModel[] = []; @@ -39,6 +42,18 @@ export function getClassTree(response: SparqlResponse): ClassModel return tree; } +export function flattenClassTree(classTree: ReadonlyArray) { + const all: ClassModel[] = []; + const visitClasses = (classes: ReadonlyArray) => { + for (const model of classes) { + all.push(model); + visitClasses(model.children); + } + }; + visitClasses(classTree); + return all; +} + /** * This extension of ClassModel is used only in processing, parent links are not needed in UI (yet?) */ @@ -166,11 +181,11 @@ export function getPropertyInfo(response: SparqlResponse): Dict const models: Dictionary = {}; for (const sProperty of response.results.bindings) { - const sPropertyTypeId = sProperty.prop.value as PropertyTypeIri; + const sPropertyTypeId = sProperty.property.value as PropertyTypeIri; if (models[sPropertyTypeId]) { if (sProperty.label) { const label = models[sPropertyTypeId].label; - if (label.values.length === 1 && !label.values[0].lang) { + if (label.values.length === 1 && !label.values[0].language) { label.values = []; } label.values.push(getLocalizedString(sProperty.label)); @@ -192,7 +207,7 @@ export function getLinkTypes(response: SparqlResponse): LinkTyp if (linkTypesMap[sInstTypeId]) { if (sLink.label) { const label = linkTypesMap[sInstTypeId].label; - if (label.values.length === 1 && !label.values[0].lang) { + if (label.values.length === 1 && !label.values[0].language) { label.values = []; } label.values.push(getLocalizedString(sLink.label)); @@ -207,7 +222,7 @@ export function getLinkTypes(response: SparqlResponse): LinkTyp } export function triplesToElementBinding( - tripples: Triple[], + triples: Triple[], ): SparqlResponse { const map: Dictionary = {}; const convertedResponse: SparqlResponse = { @@ -218,31 +233,31 @@ export function triplesToElementBinding( bindings: [], }, }; - for (const tripple of tripples) { - const trippleId = tripple.subject.value; + for (const triple of triples) { + const trippleId = triple.subject.value; if (!map[trippleId]) { - map[trippleId] = createAndPushBinding(tripple); + map[trippleId] = createAndPushBinding(triple); } - if (tripple.predicate.value === LABEL_URI && isRdfLiteral(tripple.object)) { // Label + if (triple.predicate.value === LABEL_URI && isRdfLiteral(triple.object)) { // Label if (map[trippleId].label) { - map[trippleId] = createAndPushBinding(tripple); + map[trippleId] = createAndPushBinding(triple); } - map[trippleId].label = tripple.object; + map[trippleId].label = triple.object; } else if ( // Class - tripple.predicate.value === RDF_TYPE_URI && - isRdfIri(tripple.object) && isRdfIri(tripple.predicate) + triple.predicate.value === RDF_TYPE_URI && + isRdfIri(triple.object) && isRdfIri(triple.predicate) ) { if (map[trippleId].class) { - map[trippleId] = createAndPushBinding(tripple); + map[trippleId] = createAndPushBinding(triple); } - map[trippleId].class = tripple.object; - } else if (!isRdfBlank(tripple.object) && isRdfIri(tripple.predicate)) { // Property + map[trippleId].class = triple.object; + } else if (!isRdfBlank(triple.object) && isRdfIri(triple.predicate)) { // Property if (map[trippleId].propType) { - map[trippleId] = createAndPushBinding(tripple); + map[trippleId] = createAndPushBinding(triple); } - map[trippleId].propType = tripple.predicate; - map[trippleId].propValue = tripple.object; + map[trippleId].propType = triple.predicate; + map[trippleId].propValue = triple.object; } } @@ -257,25 +272,66 @@ export function triplesToElementBinding( return convertedResponse; } -export function getElementsInfo(response: SparqlResponse, ids: string[]): Dictionary { - const sInstances = response.results.bindings; +export function getElementsInfo( + response: SparqlResponse, + types: ReadonlyMap> = EMPTY_MAP, + propertyByPredicate: ReadonlyMap = EMPTY_MAP, + openWorldProperties = true, +): Dictionary { const instancesMap: Dictionary = {}; - for (const sInst of sInstances) { - const sInstTypeId = sInst.inst.value as ElementIri; - if (!instancesMap[sInstTypeId]) { - instancesMap[sInstTypeId] = emptyElementInfo(sInstTypeId); + for (const binding of response.results.bindings) { + if (!isRdfIri(binding.inst)) { continue; } + const iri = binding.inst.value as ElementIri; + let model = instancesMap[iri]; + if (!model) { + model = emptyElementInfo(iri); + instancesMap[iri] = model; + } + enrichElement(model, binding); + } + + if (!openWorldProperties || propertyByPredicate.size > 0) { + for (const iri in instancesMap) { + if (!Object.hasOwnProperty.call(instancesMap, iri)) { continue; } + const model = instancesMap[iri]; + const modelTypes = types.get(model.id); + model.properties = mapPropertiesByConfig( + model, modelTypes, propertyByPredicate, openWorldProperties + ); } - enrichElement(instancesMap[sInstTypeId], sInst); } return instancesMap; } -export function getEnrichedElementsInfo( +function mapPropertiesByConfig( + model: ElementModel, + modelTypes: ReadonlySet | undefined, + propertyByPredicate: ReadonlyMap, + openWorldProperties: boolean +): ElementModel['properties'] { + const mapped: ElementModel['properties'] = {}; + for (const propertyIri in model.properties) { + if (!Object.hasOwnProperty.call(model.properties, propertyIri)) { continue; } + const properties = propertyByPredicate.get(propertyIri); + if (properties && properties.length > 0) { + for (const property of properties) { + if (typeMatchesDomain(property, modelTypes)) { + mapped[property.id] = model.properties[propertyIri]; + } + } + } else if (openWorldProperties) { + mapped[propertyIri] = model.properties[propertyIri]; + } + } + return mapped; +} + +export function enrichElementsWithImages( response: SparqlResponse, elementsInfo: Dictionary, -): Dictionary { +): void { const respElements = response.results.bindings; for (const respEl of respElements) { const elementInfo = elementsInfo[respEl.inst.value]; @@ -283,27 +339,66 @@ export function getEnrichedElementsInfo( elementInfo.image = respEl.image.value; } } - return elementsInfo; } -export function getLinksInfo(response: SparqlResponse): LinkModel[] { - const sparqlLinks = response.results.bindings; - const linksMap: Dictionary = {}; - - for (const sLink of sparqlLinks) { - const linkKey = `${sLink.source.value} ${sLink.type.value} ${sLink.target.value}`; +export function getElementTypes( + response: SparqlResponse +): Map> { + const types = new Map>(); + for (const binding of response.results.bindings) { + if (isRdfIri(binding.inst) && isRdfIri(binding.class)) { + const element = binding.inst.value as ElementIri; + const type = binding.class.value as ElementTypeIri; + getOrCreateSetInMap(types, element).add(type); + } + } + return types; +} - if (linksMap[linkKey]) { +export function getLinksInfo( + response: SparqlResponse, + types: ReadonlyMap> = EMPTY_MAP, + linkByPredicateType: ReadonlyMap = EMPTY_MAP, + openWorldLinks: boolean = true +): LinkModel[] { + const sparqlLinks = response.results.bindings; + const links = new HashMap(hashLink, sameLink); + + for (const binding of sparqlLinks) { + const model: LinkModel = { + sourceId: binding.source.value as ElementIri, + linkTypeId: binding.type.value as LinkTypeIri, + targetId: binding.target.value as ElementIri, + properties: {}, + }; + if (links.has(model)) { // this can only happen due to error in sparql or when merging properties - if (sLink.propType) { - mergeProperties(linksMap[linkKey].properties, sLink.propType, sLink.propValue); + if (binding.propType) { + const existing = links.get(model); + mergeProperties(existing.properties, binding.propType, binding.propValue); } } else { - linksMap[linkKey] = getLinkInfo(sLink); + if (binding.propType) { + mergeProperties(model.properties, binding.propType, binding.propValue); + } + const linkConfigs = linkByPredicateType.get(model.linkTypeId); + if (linkConfigs && linkConfigs.length > 0) { + for (const linkConfig of linkConfigs) { + if (typeMatchesDomain(linkConfig, types.get(model.sourceId))) { + const mappedModel: LinkModel = isDirectLink(linkConfig) + ? {...model, linkTypeId: linkConfig.id as LinkTypeIri} : model; + links.set(mappedModel, mappedModel); + } + } + } else if (openWorldLinks) { + links.set(model, model); + } } } - return _.values(linksMap); + const linkArray: LinkModel[] = []; + links.forEach(value => linkArray.push(value)); + return linkArray; } export function getLinksTypesOf(response: SparqlResponse): LinkCount[] { @@ -311,33 +406,139 @@ export function getLinksTypesOf(response: SparqlResponse): Lin return sparqlLinkTypes.map((sLink: LinkCountBinding) => getLinkCount(sLink)); } -export function getLinksTypeIds(response: SparqlResponse): LinkTypeIri[] { - const sparqlLinkTypes = response.results.bindings.filter(b => !isRdfBlank(b.link)); - return sparqlLinkTypes.map((sLink: LinkTypeBinding) => sLink.link.value as LinkTypeIri); +export function getLinksTypeIds( + response: SparqlResponse, + linkByPredicateType: ReadonlyMap = EMPTY_MAP, + openWorldLinks: boolean = true +): LinkTypeIri[] { + const linkTypes: LinkTypeIri[] = []; + for (const binding of response.results.bindings) { + if (!isRdfIri(binding.link)) { continue; } + const linkConfigs = linkByPredicateType.get(binding.link.value); + if (linkConfigs && linkConfigs.length > 0) { + for (const linkConfig of linkConfigs) { + const mappedLinkType = isDirectLink(linkConfig) + ? linkConfig.id : binding.link.value; + linkTypes.push(mappedLinkType as LinkTypeIri); + } + } else if (openWorldLinks) { + linkTypes.push(binding.link.value as LinkTypeIri); + } + } + return linkTypes; } -export function getLinkStatistics(response: SparqlResponse): LinkCount { - const sparqlLinkCount = response.results.bindings.filter(b => !isRdfBlank(b.link))[0]; - return getLinkCount(sparqlLinkCount); +export function getLinkStatistics(response: SparqlResponse): LinkCount | undefined { + for (const binding of response.results.bindings) { + if (isRdfIri(binding.link)) { + return getLinkCount(binding); + } + } + return undefined; } -export function getFilteredData(response: SparqlResponse): Dictionary { - const sInstances = response.results.bindings; +export function getFilteredData( + response: SparqlResponse, + sourceTypes?: ReadonlySet, + linkByPredicateType: ReadonlyMap = EMPTY_MAP, + openWorldLinks: boolean = true +): Dictionary { const instancesMap: Dictionary = {}; + const resultTypes = new Map>(); + const outPredicates = new Map>(); + const inPredicates = new Map>(); - for (const sInst of sInstances) { - if (!isRdfIri(sInst.inst) && !isRdfBlank(sInst.inst)) { + for (const binding of response.results.bindings) { + if (!isRdfIri(binding.inst) && !isRdfBlank(binding.inst)) { continue; } - const iri = sInst.inst.value as ElementIri; - if (!instancesMap[iri]) { - instancesMap[iri] = emptyElementInfo(iri); + + const iri = binding.inst.value as ElementIri; + let model = instancesMap[iri]; + if (!model) { + model = emptyElementInfo(iri); + instancesMap[iri] = model; + } + enrichElement(model, binding); + + if (isRdfIri(binding.classAll)) { + getOrCreateSetInMap(resultTypes, iri).add(binding.classAll.value as ElementTypeIri); + } + + if (!openWorldLinks && binding.link && binding.direction) { + const predicates = binding.direction.value === 'out' ? outPredicates : inPredicates; + getOrCreateSetInMap(predicates, model.id).add(binding.link.value); + } + } + + if (!openWorldLinks) { + for (const id of Object.keys(instancesMap)) { + const model = instancesMap[id]; + const targetTypes = resultTypes.get(model.id); + const doesMatchesDomain = ( + matchesDomainForLink(sourceTypes, outPredicates.get(model.id), linkByPredicateType) && + matchesDomainForLink(targetTypes, inPredicates.get(model.id), linkByPredicateType) + ); + if (!doesMatchesDomain) { + delete instancesMap[id]; + } } - enrichElement(instancesMap[iri], sInst); } + return instancesMap; } +function matchesDomainForLink( + types: ReadonlySet | undefined, + predicates: Set | undefined, + linkByPredicateType: ReadonlyMap +) { + if (!predicates) { return true; } + + let hasMatch = false; + predicates.forEach(predicate => { + const matched = linkByPredicateType.get(predicate); + if (matched) { + for (const link of matched) { + if (typeMatchesDomain(link, types)) { + hasMatch = true; + } + } + } + }); + return hasMatch; +} + +export function isDirectLink(link: LinkConfiguration) { + // link configuration is path-based if includes any variables + const pathBased = /[?$][a-zA-Z]+\b/.test(link.path); + return !pathBased; +} + +export function isDirectProperty(property: PropertyConfiguration) { + // property configuration is path-based if includes any variables + const pathBased = /[?$][a-zA-Z]+\b/.test(property.path); + return !pathBased; +} + +function typeMatchesDomain( + config: { readonly domain?: ReadonlyArray }, + types: ReadonlySet | undefined +): boolean { + if (!config.domain || config.domain.length === 0) { + return true; + } else if (!types) { + return false; + } else { + for (const type of config.domain) { + if (types.has(type as ElementTypeIri)) { + return true; + } + } + return false; + } +} + /** * Modifies properties with merging with new values, couls be new peroperty or new value for existing properties. * @param properties @@ -369,7 +570,6 @@ export function enrichElement(element: ElementModel, sInst: ElementBinding) { if (!element) { return; } if (sInst.label) { const label = getLocalizedString(sInst.label); - const currentLabels = element.label.values; if (element.label.values.every(value => !isLocalizedEqual(value, label))) { element.label.values.push(label); @@ -392,15 +592,15 @@ function appendLabel(container: { values: LocalizedString[] }, newLabel: Localiz } function isLocalizedEqual(left: LocalizedString, right: LocalizedString) { - return left.lang === right.lang && left.text === right.text; + return left.language === right.language && left.value === right.value; } export function getLocalizedString(label: RdfLiteral): LocalizedString | undefined { if (label) { return { - text: label.value, - lang: label['xml:lang'], - datatype: label.datatype, + value: label.value, + language: label['xml:lang'], + datatype: label.datatype ? {value: label.datatype} : undefined, }; } else { return undefined; @@ -414,7 +614,7 @@ export function getInstCount(instcount: RdfLiteral): number | undefined { export function getPropertyModel(node: PropertyBinding): PropertyModel { const label = getLocalizedString(node.label); return { - id: node.prop.value as PropertyTypeIri, + id: node.property.value as PropertyTypeIri, label: { values: label ? [label] : [] }, }; } @@ -437,23 +637,6 @@ export function emptyElementInfo(id: ElementIri): ElementModel { return elementInfo; } -export function getLinkInfo(sLinkInfo: LinkBinding): LinkModel { - if (!sLinkInfo) { return undefined; } - const linkModel: LinkModel = { - linkTypeId: sLinkInfo.type.value as LinkTypeIri, - sourceId: sLinkInfo.source.value as ElementIri, - targetId: sLinkInfo.target.value as ElementIri, - properties: {}, - }; - if (sLinkInfo.propType && sLinkInfo.propValue) { - linkModel.properties[sLinkInfo.propType.value] = { - type: 'string', - values: [getLocalizedString(sLinkInfo.propValue)], - }; - } - return linkModel; -} - export function getLinkTypeInfo(sLinkInfo: LinkTypeBinding): LinkType { if (!sLinkInfo) { return undefined; } const label = getLocalizedString(sLinkInfo.label); @@ -463,3 +646,18 @@ export function getLinkTypeInfo(sLinkInfo: LinkTypeBinding): LinkType { count: getInstCount(sLinkInfo.instcount), }; } + +export function prependAdditionalBindings( + base: SparqlResponse, + additional: SparqlResponse | undefined, +): SparqlResponse { + if (!additional) { + return base; + } + return { + head: {vars: base.head.vars}, + results: { + bindings: [...additional.results.bindings, ...base.results.bindings] + }, + }; +} diff --git a/src/ontodia/data/sparql/sparqlDataProvider.ts b/src/ontodia/data/sparql/sparqlDataProvider.ts index aeb9e129..344dd5c2 100644 --- a/src/ontodia/data/sparql/sparqlDataProvider.ts +++ b/src/ontodia/data/sparql/sparqlDataProvider.ts @@ -1,28 +1,33 @@ -import * as N3 from 'n3'; +import { objectValues, getOrCreateArrayInMap } from '../../viewUtils/collections'; import { DataProvider, LinkElementsParams, FilterParams } from '../provider'; import { Dictionary, ClassModel, LinkType, ElementModel, LinkModel, LinkCount, PropertyModel, - ElementIri, ElementTypeIri, LinkTypeIri, PropertyTypeIri, + ElementIri, ElementTypeIri, LinkTypeIri, PropertyTypeIri, LocalizedString } from '../model'; import { - triplesToElementBinding, + prependAdditionalBindings, + enrichElementsWithImages, + flattenClassTree, getClassTree, getClassInfo, getPropertyInfo, getLinkTypes, getElementsInfo, + getElementTypes, getLinksInfo, getLinksTypeIds, getFilteredData, - getEnrichedElementsInfo, getLinksTypesOf, getLinkStatistics, + triplesToElementBinding, + isDirectLink, + isDirectProperty, } from './responseHandler'; import { - ClassBinding, ElementBinding, LinkBinding, PropertyBinding, BlankBinding, - LinkCountBinding, LinkTypeBinding, ElementImageBinding, SparqlResponse, Triple, RdfNode, + ClassBinding, ElementBinding, LinkBinding, PropertyBinding, BlankBinding, FilterBinding, + LinkCountBinding, LinkTypeBinding, ElementImageBinding, ElementTypeBinding, SparqlResponse, Triple, } from './sparqlModels'; -import { SparqlDataProviderSettings, OWLStatsSettings } from './sparqlDataProviderSettings'; +import { SparqlDataProviderSettings, OWLStatsSettings, LinkConfiguration, PropertyConfiguration } from './sparqlDataProviderSettings'; import * as BlankNodes from './blankNodes'; import { parseTurtleText } from './turtle'; @@ -60,10 +65,16 @@ export interface SparqlDataProviderOptions { imagePropertyUris?: string[]; /** - * you can specify prepareImages function to extract image URL from element model + * Allows to extract/fetch image URLs externally instead of using `imagePropertyUris` option. */ prepareImages?: (elementInfo: Dictionary) => Promise>; + /** + * Allows to extract/fetch labels separately from SPARQL query as an alternative or + * in addition to `label` output binding. + */ + prepareLabels?: (resources: Set) => Promise>; + /** * wether to use GET (more compatible (Virtuozo), more error-prone due to large request URLs) * or POST(less compatible, better on large data sets) @@ -80,6 +91,13 @@ export class SparqlDataProvider implements DataProvider { readonly options: SparqlDataProviderOptions; readonly settings: SparqlDataProviderSettings; + private linkByPredicate = new Map(); + private linkById = new Map(); + private openWorldLinks: boolean; + + private propertyByPredicate = new Map(); + private openWorldProperties: boolean; + constructor( options: SparqlDataProviderOptions, settings: SparqlDataProviderSettings = OWLStatsSettings, @@ -87,97 +105,184 @@ export class SparqlDataProvider implements DataProvider { const {queryFunction = queryInternal} = options; this.options = {...options, queryFunction}; this.settings = settings; - } - classTree(): Promise { - const query = this.settings.defaultPrefix + this.settings.classTreeQuery; - return this.executeSparqlQuery(query).then(getClassTree); + for (const link of settings.linkConfigurations) { + this.linkById.set(link.id as LinkTypeIri, link); + const predicate = isDirectLink(link) ? link.path : link.id; + getOrCreateArrayInMap(this.linkByPredicate, predicate).push(link); + } + this.openWorldLinks = settings.linkConfigurations.length === 0 || + Boolean(settings.openWorldLinks); + + for (const property of settings.propertyConfigurations) { + const predicate = isDirectProperty(property) ? property.path : property.id; + getOrCreateArrayInMap(this.propertyByPredicate, predicate).push(property); + } + this.openWorldProperties = settings.propertyConfigurations.length === 0 || + Boolean(settings.openWorldProperties); } - propertyInfo(params: { propertyIds: PropertyTypeIri[] }): Promise> { - const ids = params.propertyIds.map(escapeIri).map(id => ` ( ${id} )`).join(' '); - const query = this.settings.defaultPrefix + ` - SELECT ?prop ?label - WHERE { - VALUES (?prop) {${ids}}. - OPTIONAL { ?prop ${this.settings.schemaLabelProperty} ?label. } - } - `; - return this.executeSparqlQuery(query).then(getPropertyInfo); + async classTree(): Promise { + const {defaultPrefix, schemaLabelProperty, classTreeQuery} = this.settings; + if (!classTreeQuery) { + return []; + } + + const query = defaultPrefix + resolveTemplate(classTreeQuery, { + schemaLabelProperty, + }); + const result = await this.executeSparqlQuery(query); + const classTree = getClassTree(result); + + if (this.options.prepareLabels) { + await attachLabels(flattenClassTree(classTree), this.options.prepareLabels); + } + + return classTree; } - classInfo(params: { classIds: ElementTypeIri[] }): Promise { - const ids = params.classIds.map(escapeIri).map(id => ` ( ${id} )`).join(' '); - const query = this.settings.defaultPrefix + ` - SELECT ?class ?label ?instcount - WHERE { - VALUES (?class) {${ids}}. - OPTIONAL { ?class ${this.settings.schemaLabelProperty} ?label. } - BIND("" as ?instcount) + async propertyInfo(params: { propertyIds: PropertyTypeIri[] }): Promise> { + const {defaultPrefix, schemaLabelProperty, propertyInfoQuery} = this.settings; + + let properties: Dictionary; + if (propertyInfoQuery) { + const ids = params.propertyIds.map(escapeIri).map(id => ` ( ${id} )`).join(' '); + const query = defaultPrefix + resolveTemplate(propertyInfoQuery, { + ids, + schemaLabelProperty, + }); + const result = await this.executeSparqlQuery(query); + properties = getPropertyInfo(result); + } else { + properties = {}; + for (const id of params.propertyIds) { + properties[id] = {id, label: {values: []}}; } - `; - return this.executeSparqlQuery(query).then(getClassInfo); + } + + if (this.options.prepareLabels) { + await attachLabels(objectValues(properties), this.options.prepareLabels); + } + + return properties; } - linkTypesInfo(params: { linkTypeIds: LinkTypeIri[] }): Promise { - const ids = params.linkTypeIds.map(escapeIri).map(id => ` ( ${id} )`).join(' '); - const query = this.settings.defaultPrefix + ` - SELECT ?link ?label ?instcount - WHERE { - VALUES (?link) {${ids}}. - OPTIONAL { ?link ${this.settings.schemaLabelProperty} ?label. } - BIND("" as ?instcount) - } - `; - return this.executeSparqlQuery(query).then(getLinkTypes); + async classInfo(params: { classIds: ElementTypeIri[] }): Promise { + const {defaultPrefix, schemaLabelProperty, classInfoQuery} = this.settings; + + let classes: ClassModel[]; + if (classInfoQuery) { + const ids = params.classIds.map(escapeIri).map(id => ` ( ${id} )`).join(' '); + const query = defaultPrefix + resolveTemplate(classInfoQuery, { + ids, + schemaLabelProperty, + }); + const result = await this.executeSparqlQuery(query); + classes = getClassInfo(result); + } else { + classes = params.classIds.map((id): ClassModel => ( + {id, label: {values: []}, children: []} + )); + } + + if (this.options.prepareLabels) { + await attachLabels(classes, this.options.prepareLabels); + } + + return classes; } - linkTypes(): Promise { - const query = this.settings.defaultPrefix + ` - SELECT DISTINCT ?link ?instcount ?label - WHERE { - ${this.settings.linkTypesPattern} - OPTIONAL { ?link ${this.settings.schemaLabelProperty} ?label. } - } - `; - return this.executeSparqlQuery(query).then(getLinkTypes); + async linkTypesInfo(params: { linkTypeIds: LinkTypeIri[] }): Promise { + const {defaultPrefix, schemaLabelProperty, linkTypesInfoQuery} = this.settings; + + let linkTypes: LinkType[]; + if (linkTypesInfoQuery) { + const ids = params.linkTypeIds.map(escapeIri).map(id => ` ( ${id} )`).join(' '); + const query = defaultPrefix + resolveTemplate(linkTypesInfoQuery, { + ids, + schemaLabelProperty, + }); + const result = await this.executeSparqlQuery(query); + linkTypes = getLinkTypes(result); + } else { + linkTypes = params.linkTypeIds.map((id): LinkType => ( + {id, label: {values: []}} + )); + } + + if (this.options.prepareLabels) { + await attachLabels(linkTypes, this.options.prepareLabels); + } + + return linkTypes; } - elementInfo(params: { elementIds: ElementIri[] }): Promise> { - const blankIds: string[] = []; + async linkTypes(): Promise { + const {defaultPrefix, schemaLabelProperty, linkTypesQuery, linkTypesPattern} = this.settings; + if (!linkTypesQuery) { + return []; + } + + const query = defaultPrefix + resolveTemplate(linkTypesQuery, { + linkTypesPattern, + schemaLabelProperty, + }); + const result = await this.executeSparqlQuery(query); + const linkTypes = getLinkTypes(result); + + if (this.options.prepareLabels) { + await attachLabels(linkTypes, this.options.prepareLabels); + } + + return linkTypes; + } - const elementIds = params.elementIds.filter(id => !BlankNodes.isEncodedBlank(id)); + async elementInfo(params: { elementIds: ElementIri[] }): Promise> { + const nonBlankResources = params.elementIds.filter(id => !BlankNodes.isEncodedBlank(id)); const blankNodeResponse = this.options.acceptBlankNodes ? BlankNodes.elementInfo(params.elementIds) : undefined; - if (elementIds.length === 0 && this.options.acceptBlankNodes) { - return Promise.resolve(getElementsInfo(blankNodeResponse, params.elementIds)); - } - - const ids = elementIds.map(escapeIri).map(id => ` (${id})`).join(' '); - const {defaultPrefix, dataLabelProperty, elementInfoQuery, propertyConfigurations} = this.settings; - const query = defaultPrefix + resolveTemplate( - elementInfoQuery, {ids, dataLabelProperty, propertyConfigurations: this.formatPropertyInfo()}); - - return this.executeSparqlConstruct(query) - .then(triplesToElementBinding) - .then(result => this.concatWithBlankNodeResponse(result, blankNodeResponse)) - .then(elementsInfo => getElementsInfo(elementsInfo, params.elementIds)) - .then(elementModels => { - if (this.options.prepareImages) { - return this.prepareElementsImage(elementModels); - } else if (this.options.imagePropertyUris && this.options.imagePropertyUris.length) { - return this.enrichedElementsInfo(elementModels, this.options.imagePropertyUris); - } else { - return elementModels; - } + let triples: Triple[]; + if (nonBlankResources.length > 0) { + const ids = nonBlankResources.map(escapeIri).map(id => ` (${id})`).join(' '); + const {defaultPrefix, dataLabelProperty, elementInfoQuery} = this.settings; + const query = defaultPrefix + resolveTemplate(elementInfoQuery, { + ids, + dataLabelProperty, + propertyConfigurations: this.formatPropertyInfo(), }); + triples = await this.executeSparqlConstruct(query); + } else { + triples = []; + } + + const types = this.queryManyElementTypes( + this.settings.propertyConfigurations.length > 0 ? params.elementIds : [] + ); + + const bindings = triplesToElementBinding(triples); + const bindingsWithBlanks = prependAdditionalBindings(bindings, blankNodeResponse); + const elementModels = getElementsInfo( + bindingsWithBlanks, + await types, + this.propertyByPredicate, + this.openWorldProperties + ); + + if (this.options.prepareLabels) { + await attachLabels(objectValues(elementModels), this.options.prepareLabels); + } + + if (this.options.prepareImages) { + await prepareElementImages(this.options.prepareImages, elementModels); + } else if (this.options.imagePropertyUris && this.options.imagePropertyUris.length) { + await this.attachImages(elementModels, this.options.imagePropertyUris); + } + + return elementModels; } - private enrichedElementsInfo( - elementsInfo: Dictionary, - types: string[], - ): Promise> { + private async attachImages(elementsInfo: Dictionary, types: string[]): Promise { const ids = Object.keys(elementsInfo).filter(id => !BlankNodes.isEncodedBlank(id)) .map(escapeIri).map(id => ` ( ${id} )`).join(' '); const typesString = types.map(escapeIri).map(id => ` ( ${id} )`).join(' '); @@ -190,87 +295,122 @@ export class SparqlDataProvider implements DataProvider { ${this.settings.imageQueryPattern} }} `; - return this.executeSparqlQuery(query) - .then(imageResponse => getEnrichedElementsInfo(imageResponse, elementsInfo)) - .catch(err => { - // tslint:disable-next-line:no-console - console.error(err); - return elementsInfo; - }); - } - - private prepareElementsImage( - elementsInfo: Dictionary, - ): Promise> { - return this.options.prepareImages(elementsInfo).then(images => { - for (const key in images) { - if (images.hasOwnProperty(key) && elementsInfo[key]) { - elementsInfo[key].image = images[key]; - } - } - return elementsInfo; - }); + try { + const bindings = await this.executeSparqlQuery(query); + enrichElementsWithImages(bindings, elementsInfo); + } catch (err) { + // tslint:disable-next-line:no-console + console.error(err); + } } - linksInfo(params: { + async linksInfo(params: { elementIds: ElementIri[]; linkTypeIds: LinkTypeIri[]; }): Promise { - const elementIds = params.elementIds.filter(id => !BlankNodes.isEncodedBlank(id)); - + const nonBlankResources = params.elementIds.filter(id => !BlankNodes.isEncodedBlank(id)); const blankNodeResponse = this.options.acceptBlankNodes ? BlankNodes.linksInfo(params.elementIds) : undefined; - if (elementIds.length === 0 && this.options.acceptBlankNodes) { - return Promise.resolve(getLinksInfo(blankNodeResponse)); + const linkConfigurations = this.formatLinkLinks(); + + let bindings: Promise>; + let types: Promise>>; + if (nonBlankResources.length > 0) { + const ids = nonBlankResources.map(escapeIri).map(id => ` ( ${id} )`).join(' '); + const linksInfoQuery = this.settings.defaultPrefix + resolveTemplate(this.settings.linksInfoQuery, { + ids, + linkConfigurations, + }); + bindings = this.executeSparqlQuery(linksInfoQuery); + types = this.queryManyElementTypes(params.elementIds); + } else { + bindings = Promise.resolve({ + head: {vars: []}, + results: {bindings: []}, + }); + types = this.queryManyElementTypes([]); } - const ids = elementIds.map(escapeIri).map(id => ` ( ${id} )`).join(' '); - const linksInfoQuery = resolveTemplate( - this.settings.linksInfoQuery, - {ids: ids, linkConfigurations: this.formatLinkLinks()}, + const bindingsWithBlanks = prependAdditionalBindings(await bindings, blankNodeResponse); + const linksInfo = getLinksInfo( + bindingsWithBlanks, + await types, + this.linkByPredicate, + this.openWorldLinks ); - const query = this.settings.defaultPrefix + linksInfoQuery; - return this.executeSparqlQuery(query) - .then(result => this.concatWithBlankNodeResponse(result, blankNodeResponse)) - .then(getLinksInfo); + return linksInfo; } - linkTypesOf(params: { elementId: ElementIri }): Promise { + async linkTypesOf(params: { elementId: ElementIri }): Promise { if (this.options.acceptBlankNodes && BlankNodes.isEncodedBlank(params.elementId)) { return Promise.resolve(getLinksTypesOf(BlankNodes.linkTypesOf(params))); } + const {defaultPrefix, linkTypesOfQuery, linkTypesStatisticsQuery, filterTypePattern} = this.settings; + const elementIri = escapeIri(params.elementId); - // Ask for linkTypes - const query = this.settings.defaultPrefix - + resolveTemplate(this.settings.linkTypesOfQuery, - {elementIri, linkConfigurations: this.formatLinkTypesOf(params.elementId)}, - ); - return this.executeSparqlQuery(query) - .then(linkTypeBinding => { - const linkTypeIds = getLinksTypeIds(linkTypeBinding); - const requests: Promise[] = []; - - const navigateElementFilterOut = this.options.acceptBlankNodes ? - `FILTER (IsIri(?outObject) || IsBlank(?outObject))` : `FILTER IsIri(?outObject)`; - const navigateElementFilterIn = this.options.acceptBlankNodes ? - `FILTER (IsIri(?inObject) || IsBlank(?inObject))` : `FILTER IsIri(?inObject)`; - - for (const id of linkTypeIds) { - const q = this.settings.defaultPrefix - + resolveTemplate(this.settings.linkTypesStatisticsQuery, { - linkId: escapeIri(id), - elementIri, - linkConfigurations: this.formatLinkTypesStatistics(params.elementId, id), - navigateElementFilterOut, - navigateElementFilterIn, - }); - requests.push( - this.executeSparqlQuery(q).then(getLinkStatistics) - ); - } - return Promise.all(requests); + const forAll = this.formatLinkUnion( + params.elementId, undefined, undefined, '?outObject', '?inObject', false + ); + if (forAll.usePredicatePart) { + forAll.unionParts.push(`{ ${elementIri} ?link ?outObject }`); + forAll.unionParts.push(`{ ?inObject ?link ${elementIri} }`); + } + + const query = defaultPrefix + resolveTemplate(linkTypesOfQuery, { + elementIri, + linkConfigurations: forAll.unionParts.join('\nUNION\n'), + }); + + const linkTypeBindings = await this.executeSparqlQuery(query); + const linkTypeIds = getLinksTypeIds(linkTypeBindings, this.linkByPredicate, this.openWorldLinks); + + const navigateElementFilterOut = this.options.acceptBlankNodes + ? `FILTER (IsIri(?outObject) || IsBlank(?outObject))` + : `FILTER IsIri(?outObject)`; + const navigateElementFilterIn = this.options.acceptBlankNodes + ? `FILTER (IsIri(?inObject) || IsBlank(?inObject))` + : `FILTER IsIri(?inObject)`; + + const foundLinkStats: LinkCount[] = []; + await Promise.all(linkTypeIds.map(async linkId => { + const linkConfig = this.linkById.get(linkId); + let linkConfigurationOut: string; + let linkConfigurationIn: string; + + if (!linkConfig || isDirectLink(linkConfig)) { + const predicate = escapeIri(linkConfig && isDirectLink(linkConfig) ? linkConfig.path : linkId); + linkConfigurationOut = `${elementIri} ${predicate} ?outObject`; + linkConfigurationIn = `?inObject ${predicate} ${elementIri}`; + } else { + linkConfigurationOut = this.formatLinkPath(linkConfig.path, elementIri, '?outObject'); + linkConfigurationIn = this.formatLinkPath(linkConfig.path, '?inObject', elementIri); + } + + if (linkConfig && linkConfig.domain?.length > 0) { + const commaSeparatedDomains = linkConfig.domain.map(escapeIri).join(', '); + const restrictionOut = filterTypePattern.replace(/[?$]inst\b/g, elementIri); + const restrictionIn = filterTypePattern.replace(/[?$]inst\b/g, '?inObject'); + linkConfigurationOut += ` { ${restrictionOut} FILTER(?class IN (${commaSeparatedDomains})) }`; + linkConfigurationIn += ` { ${restrictionIn} FILTER(?class IN (${commaSeparatedDomains})) }`; + } + + const statsQuery = defaultPrefix + resolveTemplate(linkTypesStatisticsQuery, { + linkId: escapeIri(linkId), + elementIri, + linkConfigurationOut, + linkConfigurationIn, + navigateElementFilterOut, + navigateElementFilterIn, }); + + const bindings = await this.executeSparqlQuery(statsQuery); + const linkStats = getLinkStatistics(bindings); + if (linkStats) { + foundLinkStats.push(linkStats); + } + })); + return foundLinkStats; } linkElements(params: LinkElementsParams): Promise> { @@ -281,74 +421,119 @@ export class SparqlDataProvider implements DataProvider { linkDirection: params.direction, limit: params.limit, offset: params.offset, - languageCode: ''}); + languageCode: '' + }); } - filter(params: FilterParams): Promise> { - if (params.limit === undefined) { params.limit = 100; } - const blankFiltration = this.options.acceptBlankNodes - ? BlankNodes.filter(params) : undefined; + async filter(baseParams: FilterParams): Promise> { + const params: FilterParams = {...baseParams}; + if (params.limit === undefined) { + params.limit = 100; + } + + // query types to match link configuration domains + const types = this.querySingleElementTypes( + params.refElementId && this.settings.linkConfigurations.length > 0 + ? params.refElementId : undefined + ); + + const blankFiltration = this.options.acceptBlankNodes ? BlankNodes.filter(params) : undefined; + if (blankFiltration && blankFiltration.results.bindings.length > 0) { + return getFilteredData(blankFiltration, await types, this.linkByPredicate, this.openWorldLinks); + } + + const filterQuery = this.createFilterQuery(params); + const bindings = await this.executeSparqlQuery(filterQuery); + + let bindingsWithBlanks: SparqlResponse; + if (this.options.acceptBlankNodes) { + bindingsWithBlanks = await BlankNodes.updateFilterResults( + bindings, + blankQuery => this.executeSparqlQuery(blankQuery), + this.settings + ); + } else { + bindingsWithBlanks = bindings as SparqlResponse; + } + + const elementModels = getFilteredData( + bindingsWithBlanks, await types, this.linkByPredicate, this.openWorldLinks + ); - if (this.options.acceptBlankNodes && blankFiltration.results.bindings.length > 0) { - return Promise.resolve(getFilteredData(blankFiltration)); + if (this.options.prepareLabels) { + await attachLabels(objectValues(elementModels), this.options.prepareLabels); } + return elementModels; + } + + private createFilterQuery(params: FilterParams): string { if (!params.refElementId && params.refElementLinkId) { - throw new Error(`Can't execute refElementLink filter without refElement`); + throw new Error('Cannot execute refElementLink filter without refElement'); } + let outerProjection = '?inst ?class ?label ?blankType'; + let innerProjection = '?inst'; + let refQueryPart = ''; + let refQueryTypes = ''; if (params.refElementId) { + outerProjection += ' ?link ?direction'; + innerProjection += ' ?link ?direction'; refQueryPart = this.createRefQueryPart({ elementId: params.refElementId, linkId: params.refElementLinkId, direction: params.linkDirection, }); + + if (this.settings.linkConfigurations.length > 0) { + outerProjection += ' ?classAll'; + refQueryTypes = this.settings.filterTypePattern.replace(/[?$]class\b/g, '?classAll'); + } } let elementTypePart = ''; if (params.elementTypeId) { const elementTypeIri = escapeIri(params.elementTypeId); - elementTypePart = resolveTemplate(this.settings.filterTypePattern, {elementTypeIri}); + elementTypePart = this.settings.filterTypePattern.replace(/[?$]class\b/g, elementTypeIri); } const {defaultPrefix, fullTextSearch, dataLabelProperty} = this.settings; let textSearchPart = ''; if (params.text) { + innerProjection += ' ?score'; + if (this.settings.fullTextSearch.extractLabel) { + textSearchPart += sparqlExtractLabel('?inst', '?extractedLabel'); + } textSearchPart = resolveTemplate(fullTextSearch.queryPattern, {text: params.text, dataLabelProperty}); } const blankNodes = this.options.acceptBlankNodes; - const query = `${defaultPrefix} + if (blankNodes) { + outerProjection += ` ${BlankNodes.BLANK_NODE_QUERY_PARAMETERS}`; + } + + return `${defaultPrefix} ${fullTextSearch.prefix} - SELECT ?inst ?class ?label ?blankType ${blankNodes ? BlankNodes.BLANK_NODE_QUERY_PARAMETERS : ''} + SELECT ${outerProjection} WHERE { { - SELECT DISTINCT ?inst ${textSearchPart ? '?score' : ''} WHERE { + SELECT DISTINCT ${innerProjection} WHERE { ${elementTypePart} ${refQueryPart} - ${this.settings.fullTextSearch.extractLabel ? sparqlExtractLabel('?inst', '?extractedLabel') : ''} ${textSearchPart} ${this.settings.filterAdditionalRestriction} } ${textSearchPart ? 'ORDER BY DESC(?score)' : ''} LIMIT ${params.limit} OFFSET ${params.offset} } + ${refQueryTypes} ${resolveTemplate(this.settings.filterElementInfoPattern, {dataLabelProperty})} ${blankNodes ? BlankNodes.BLANK_NODE_QUERY : ''} } ${textSearchPart ? 'ORDER BY DESC(?score)' : ''} `; - - return this.executeSparqlQuery(query) - .then(result => { - if (this.options.acceptBlankNodes) { - return BlankNodes.updateFilterResults(result, blankQuery => - this.executeSparqlQuery(blankQuery), this.settings); - } - return result; - }).then(getFilteredData); } executeSparqlQuery(query: string) { @@ -356,21 +541,6 @@ export class SparqlDataProvider implements DataProvider { return executeSparqlQuery(this.options.endpointUrl, query, method, this.options.queryFunction); } - concatWithBlankNodeResponse( - response: SparqlResponse, - blankNodeResponse: SparqlResponse, - ): SparqlResponse { - if (!this.options.acceptBlankNodes) { - return response; - } - return { - head: { vars: response.head.vars }, - results: { - bindings: blankNodeResponse.results.bindings.concat(response.results.bindings) - }, - }; - } - executeSparqlConstruct(query: string): Promise { const method = this.options.queryMethod ? this.options.queryMethod : SparqlQueryMethod.GET; return executeSparqlConstruct(this.options.endpointUrl, query, method, this.options.queryFunction); @@ -378,155 +548,210 @@ export class SparqlDataProvider implements DataProvider { protected createRefQueryPart(params: { elementId: ElementIri; linkId?: LinkTypeIri; direction?: 'in' | 'out' }) { const {elementId, linkId, direction} = params; - const refElementIRI = escapeIri(params.elementId); - // If no link configuration is passed, use rdf predicates as links - if (this.settings.linkConfigurations.length === 0) { - const linkPattern = linkId ? escapeIri(params.linkId) : '?link'; + const {unionParts, usePredicatePart} = this.formatLinkUnion( + elementId, linkId, direction, '?inst', '?inst', true + ); + + if (usePredicatePart) { + const refElementIRI = escapeIri(params.elementId); + let refLinkType: string | undefined; + if (linkId) { + const link = this.linkById.get(linkId); + refLinkType = link && isDirectLink(link) ? escapeIri(link.path) : escapeIri(linkId); + } + + const linkPattern = refLinkType || '?link'; + const bindType = refLinkType ? `BIND(${refLinkType} as ?link)` : ''; + // FILTER ISIRI is used to prevent blank nodes appearing in results const blankFilter = this.options.acceptBlankNodes ? 'FILTER(isIri(?inst) || isBlank(?inst))' : 'FILTER(isIri(?inst))'; - // link to element with specified link type - // if direction is not specified, provide both patterns and union them - // FILTER ISIRI is used to prevent blank nodes appearing in results - let part = ''; - if (params.direction !== 'in') { - part += `{ ${refElementIRI} ${linkPattern} ?inst . ${blankFilter} }`; - } - if (!params.direction) { part += ' UNION '; } - if (params.direction !== 'out') { - part += `{ ?inst ${linkPattern} ${refElementIRI} . ${blankFilter} }`; + + if (!direction || direction === 'out') { + unionParts.push(`{ ${refElementIRI} ${linkPattern} ?inst BIND("out" as ?direction) ${bindType} ${blankFilter} }`); } - if (this.settings.filterRefElementLinkPattern.length && !linkId) { - part += `\n${this.settings.filterRefElementLinkPattern}`; + if (!direction || direction === 'in') { + unionParts.push(`{ ?inst ${linkPattern} ${refElementIRI} BIND("in" as ?direction) ${bindType} ${blankFilter} }`); } - return part; - } else { - // use link configuration in filter. If you need more or somehow mix it with rdf predicates, override - // this function and provide nessesary sparql for this. - const linkConfigurations = this.formatLinkElements(params.elementId, params.linkId, params.direction); - return linkConfigurations; } - } + let resultPattern = unionParts.length === 0 ? 'FILTER(false)' : unionParts.join(`\nUNION\n`); - formatLinkTypesOf(elementIri: ElementIri): string { - const elementIriConst = escapeIri(elementIri); - return this.settings.linkConfigurations.map(linkConfig => { - const links: string[] = []; - links.push(`{ ${this.formatLinkPath(linkConfig.path, elementIriConst, '?outObject')} - BIND(<${linkConfig.id}> as ?link ) - }`); - links.push(`{ ${this.formatLinkPath(linkConfig.path, '?inObject', elementIriConst)} - BIND(<${linkConfig.id}> as ?link ) - }`); - if (linkConfig.inverseId) { - links.push(`{ ${this.formatLinkPath(linkConfig.path, elementIriConst, '?inObject')} - BIND(<${linkConfig.inverseId}> as ?link ) - }`); - links.push(`{ ${this.formatLinkPath(linkConfig.path, '?outObject', elementIriConst)} - BIND(<${linkConfig.inverseId}> as ?link ) - }`); - } - return links; - }).map(links => links.join(` - UNION - `)).join(` - UNION - `); - } + const useAllLinksPattern = !linkId && this.settings.filterRefElementLinkPattern.length > 0; + if (useAllLinksPattern) { + resultPattern += `\n${this.settings.filterRefElementLinkPattern}`; + } - formatLinkTypesStatistics(elementIri: ElementIri, linkIri: LinkTypeIri): string { - const elementIriConst = escapeIri(elementIri); - - const links: string[] = []; - this.settings.linkConfigurations.filter(link => link.id === linkIri).forEach(link => { - links.push(`{ ${this.formatLinkPath(link.path, elementIriConst, '?outObject')} - BIND(<${linkIri}> as ?link) - }`); - links.push(`{ ${this.formatLinkPath(link.path, '?inObject', elementIriConst)} - BIND(<${linkIri}> as ?link) - }`); - }); - this.settings.linkConfigurations.filter(link => link.inverseId === linkIri).forEach(link => { - links.push(`{ ${this.formatLinkPath(link.path, elementIriConst, '?inObject')} - BIND(<${linkIri}> as ?link) - }`); - links.push(`{ ${this.formatLinkPath(link.path, '?outObject', elementIriConst)} - BIND(<${linkIri}> as ?link) - }`); - }); - return links.join(` \n UNION \n `); + return resultPattern; } - formatLinkElements(refElementIri: ElementIri, linkIri?: LinkTypeIri, direction?: 'in' | 'out'): string { + private formatLinkUnion( + refElementIri: ElementIri, + linkIri: LinkTypeIri | undefined, + direction: 'in' | 'out' | undefined, + outElementVar: string, + inElementVar: string, + bindDirection: boolean + ) { const {linkConfigurations} = this.settings; - const elementIriConst = `<${refElementIri}>`; - let parts: string[] = []; - if (!linkIri) { - if (!direction || direction === 'out') { - parts = parts.concat(linkConfigurations.map(link => - `{ ${this.formatLinkPath(link.path, elementIriConst, '?inst')} }`) - ); - } - if (!direction || direction === 'in') { - parts = parts.concat(linkConfigurations.map(link => - `{ ${this.formatLinkPath(link.path, '?inst', elementIriConst)} }`) - ); + const fixedIri = escapeIri(refElementIri); + + const unionParts: string[] = []; + let hasDirectLink = false; + + for (const link of linkConfigurations) { + if (linkIri && link.id !== linkIri) { continue; } + if (isDirectLink(link)) { + hasDirectLink = true; + } else { + const linkType = escapeIri(link.id); + if (!direction || direction === 'out') { + const path = this.formatLinkPath(link.path, fixedIri, outElementVar); + const boundedDirection = bindDirection ? `BIND("out" as ?direction) ` : ''; + unionParts.push( + `{ ${path} BIND(${linkType} as ?link) ${boundedDirection}}` + ); + } + if (!direction || direction === 'in') { + const path = this.formatLinkPath(link.path, inElementVar, fixedIri); + const boundedDirection = bindDirection ? `BIND("in" as ?direction) ` : ''; + unionParts.push( + `{ ${path} BIND(${linkType} as ?link) ${boundedDirection}}` + ); + } } - } else { - const outLinks = linkConfigurations.filter(link => link.id === linkIri); - const inLinks = linkConfigurations.filter(link => link.inverseId === linkIri); - if (!direction || direction === 'out') { - parts = parts.concat( - outLinks.map(link => `{ ${this.formatLinkPath(link.path, elementIriConst, '?inst')} }`), - inLinks.map(link => `{ ${this.formatLinkPath(link.path, '?inst', elementIriConst)} }`), + } + + const usePredicatePart = this.openWorldLinks || hasDirectLink; + return {unionParts, usePredicatePart}; + } + + formatLinkLinks(): string { + const unionParts: string[] = []; + let hasDirectLink = false; + for (const link of this.settings.linkConfigurations) { + if (isDirectLink(link)) { + hasDirectLink = true; + } else { + const linkType = escapeIri(link.id); + unionParts.push( + `{ ${this.formatLinkPath(link.path, '?source', '?target')} BIND(${linkType} as ?type) }` ); } - if (!direction || direction === 'in') { - parts = parts.concat( - inLinks.map(link => `{ ${this.formatLinkPath(link.path, elementIriConst, '?inst')} }`), - outLinks.map(link => `{ ${this.formatLinkPath(link.path, '?inst', elementIriConst)} }`), + } + + const usePredicatePart = this.openWorldLinks || hasDirectLink; + if (usePredicatePart) { + unionParts.push(`{ ?source ?type ?target }`); + } + + return unionParts.join('\nUNION\n'); + } + + formatLinkPath(path: string, source: string, target: string): string { + return path.replace(/[?$]source\b/g, source).replace(/[?$]target\b/g, target); + } + + formatPropertyInfo(): string { + const unionParts: string[] = []; + let hasDirectProperty = false; + for (const property of this.settings.propertyConfigurations) { + if (isDirectProperty(property)) { + hasDirectProperty = true; + } else { + const propType = escapeIri(property.id); + const formatted = this.formatPropertyPath(property.path, '?inst', '?propValue'); + unionParts.push( + `{ ${formatted} BIND(${propType} as ?propType) }` ); } } - return parts.join(` \n UNION \n `); + + const usePredicatePart = this.openWorldProperties || hasDirectProperty; + if (usePredicatePart) { + unionParts.push(`{ ?inst ?propType ?propValue }`); + } + + return unionParts.join('\nUNION\n'); } - formatLinkLinks(): string { - return this.settings.linkConfigurations.map(linkConfig => - `{ ${this.formatLinkPath(linkConfig.path, '?source', '?target')} - BIND(<${linkConfig.id}> as ?type ) - ${linkConfig.properties ? this.formatLinkPath(linkConfig.properties, '?source', '?target') : ''} - }`).join(` - UNION - `); + formatPropertyPath(path: string, subject: string, value: string): string { + return path.replace(/[?$]inst\b/g, subject).replace(/[?$]value\b/g, value); } - formatLinkPath(path: string, source: string, target: string): string { - return path.replace(/\$source/g, source).replace(/\$target/g, target); + private async querySingleElementTypes(element: ElementIri | undefined): Promise | undefined> { + const types = await this.queryManyElementTypes(element ? [element] : []); + return types.get(element); } - formatPropertyInfo() { - return this.settings.propertyConfigurations.map( propConfig => - `{ ${this.formatPropertyPath(propConfig.path, '?inst', '?propValue')} - BIND(<${propConfig.id}> as ?propType ) - }`).join(` - UNION - `); + private async queryManyElementTypes( + elements: ReadonlyArray + ): Promise>> { + if (elements.length === 0) { + return new Map(); + } + const {filterTypePattern} = this.settings; + const ids = elements + .filter(iri => !BlankNodes.isEncodedBlank(iri)) + .map(iri => `(${escapeIri(iri)})`).join(' '); + + const queryTemplate = `SELECT ?inst ?class { VALUES(?inst) { \${ids} } \${filterTypePattern} }`; + const query = resolveTemplate(queryTemplate, {ids, filterTypePattern}); + let response = await this.executeSparqlQuery(query); + + if (this.options.acceptBlankNodes && elements.find(BlankNodes.isEncodedBlank)) { + const blankResponse = BlankNodes.getElementTypes(elements); + response = prependAdditionalBindings(response, blankResponse); + } + + return getElementTypes(response); } +} - formatPropertyPath(path: string, subject: string, value: string): string { - return path.replace(/\$subject/g, subject).replace(/\$value/g, value); +interface LabeledItem { + id: string; + label: { values: LocalizedString[] }; +} + +async function attachLabels( + items: ReadonlyArray, + fetchLabels: SparqlDataProviderOptions['prepareLabels'] +): Promise { + const resources = new Set(); + for (const item of items) { + if (BlankNodes.isEncodedBlank(item.id)) { continue; } + resources.add(item.id); + } + const labels = await fetchLabels(resources); + for (const item of items) { + if (labels.has(item.id)) { + item.label = {values: labels.get(item.id)}; + } } } +function prepareElementImages( + fetchImages: SparqlDataProviderOptions['prepareImages'], + elementsInfo: Dictionary +): Promise { + return fetchImages(elementsInfo).then(images => { + for (const iri in images) { + if (Object.prototype.hasOwnProperty.call(images, iri) && elementsInfo[iri]) { + elementsInfo[iri].image = images[iri]; + } + } + }); +} + function resolveTemplate(template: string, values: Dictionary) { let result = template; for (const replaceKey in values) { if (!values.hasOwnProperty(replaceKey)) { continue; } const replaceValue = values[replaceKey]; - result = result.replace(new RegExp('\\${' + replaceKey + '}', 'g'), replaceValue); + if (replaceValue) { + result = result.replace(new RegExp('\\${' + replaceKey + '}', 'g'), replaceValue); + } } return result; } diff --git a/src/ontodia/data/sparql/sparqlDataProviderSettings.ts b/src/ontodia/data/sparql/sparqlDataProviderSettings.ts index 3a3696bf..99453e3d 100644 --- a/src/ontodia/data/sparql/sparqlDataProviderSettings.ts +++ b/src/ontodia/data/sparql/sparqlDataProviderSettings.ts @@ -1,64 +1,173 @@ /** - * this is dataset-schema specific settings + * Dataset-schema specific settings for SPARQL data provider. */ export interface SparqlDataProviderSettings { /** - * default prefix to be used in every query + * Default prefix to be used in every query. */ defaultPrefix: string; /** - * property to use as label in schema (classes, properties) + * Property path for querying schema labels in schema (classes, link types, properties). */ schemaLabelProperty: string; /** - * property to use as instance label - * todo: make it an array + * Property path for querying instance data labels (elements, links). */ dataLabelProperty: string; /** - * full-text search settings + * Full-text search settings. */ fullTextSearch: FullTextSearchSettings; /** - * query to retreive class tree. Should return class, label, parent, instcount (optional) + * SELECT query to retreive class tree. + * + * Parametrized variables: + * - `${schemaLabelProperty}` `schemaLabelProperty` property from the settings + * + * Expected output bindings: + * - `?class` + * - `?label` (optional) + * - `?parent` (optional) + * - `?instcount` (optional) */ - classTreeQuery: string; + classTreeQuery?: string; /** - * link types pattern - what to consider a link on initial fetch + * SELECT query to retrieve data for each class in a set. + * + * Parametrized variables: + * - `${ids}` VALUES clause content with class IRIs + * - `${schemaLabelProperty}` `schemaLabelProperty` property from the settings + * + * Expected output bindings: + * - `?class` + * - `?label` (optional) + * - `?instcount` (optional) */ - linkTypesPattern: string; + classInfoQuery?: string; /** - * query for fetching all information on element: labels, classes, properties + * SELECT query to retrieve initial link types. + * + * Parametrized variables: + * - `${linkTypesPattern}` `linkTypesPattern` property from the settings + * - `${schemaLabelProperty}` `schemaLabelProperty` property from the settings + * + * Expected output bindings: + * - `?link` + * - `?label` (optional) + * - `?instcount` (optional) + */ + linkTypesQuery?: string; + + /** + * Overridable part of `linkTypesQuery` with same output bindings. + * + * Parametrized variables: none + */ + linkTypesPattern?: string; + + /** + * SELECT query to retrieve data for each link type in a set. + * + * Parametrized variables: + * - `${ids}` VALUES clause content with link type IRIs + * - `${schemaLabelProperty}` `schemaLabelProperty` property from the settings + * + * Expected output bindings: + * - `?link` + * - `?label` (optional) + * - `?instcount` (optional) + */ + linkTypesInfoQuery?: string; + + /** + * SELECT query to retrieve data for each datatype property in a set. + * + * Parametrized variables: + * - `${ids}` VALUES clause content with datatype property IRIs + * - `${schemaLabelProperty}` `schemaLabelProperty` property from the settings + * + * Expected output bindings: + * - `?property` + * - `?label` (optional) + */ + propertyInfoQuery?: string; + + /** + * CONSTRUCT query to retrieve data for each element (types, labels, properties). + * + * Parametrized variables: + * - `${ids}` VALUES clause content with element IRIs + * - `${dataLabelProperty}` `dataLabelProperty` property from the settings + * - `${propertyConfigurations}` + * + * Expected output format for triples: + * - `?inst rdf:type ?class` element has type + * - `?inst rdfs:label ?label` element has label + * - `?inst ?property ?value` element has value for a datatype property */ elementInfoQuery: string; /** - * Query on all links between said instances. Should return source type target + * SELECT query to retrieve all links between specified elements. + * + * Parametrized variables: + * - `${ids}` VALUES clause content with element IRIs + * - `${linkConfigurations}` + * + * Expected output bindings: + * - `?type` link type + * - `?source` link source + * - `?target` link target + * - `?propType` (optional) link property type + * - `?propValue` (optional) link property value */ linksInfoQuery: string; /** - * this should return image URL for ?inst as instance and ?linkType for image property IRI - * todo: move to runtime settings instead? proxying is runtime thing + * Query pattern to retrieve image URL for an element. + * + * Expected bindings: + * - `?inst` element IRI + * - `?linkType` image property IRI + * - `?image` result image URL */ imageQueryPattern: string; /** - * link types of returns possible link types from specified instance with statistics + * SELECT query to retrieve incoming/outgoing link types from specified element with statistics. + * + * Parametrized variables: + * - `${elementIri}` + * - `${linkConfigurations}` + * + * Expected bindings: + * - `?link` + * - `?label` (optional) + * - `?instcount` (optional) */ linkTypesOfQuery: string; /** - * link types of stats returns statistics of a link type for specified resource. - * To support blank nodes, query should use ?inObject and ?outObject variables for counting incoming and - * outgoing links, and provide ${navigateElementFilterOut} and ${navigateElementFilterIn} variables, - * see OWLRDFSSettings for example + * SELECT query to retrieve statistics of incoming/outgoing link types for specified element. + * + * Parametrized variables: + * - `${linkId}` + * - `${elementIri}` + * - `${linkConfigurationOut}` + * - `${linkConfigurationIn}` + * - `${navigateElementFilterOut}` (optional; for blank node support only) + * - `${navigateElementFilterIn}` (optional; for blank node support only) + * + * Expected bindings: + * - `?link` link type + * - `?inCount` incoming links count + * - `?outCount` outgoing links count */ linkTypesStatisticsQuery: string; @@ -68,7 +177,11 @@ export interface SparqlDataProviderSettings { filterRefElementLinkPattern: string; /** - * filter by type pattern. One could use transitive type resolution here. + * SPARQL query pattern to retrieve transitive type sets for elements. + * + * Expected output bindings: + * - `?inst` element IRI + * - `?class` element type (there may be multiple or transitive types for an element) */ filterTypePattern: string; @@ -83,16 +196,30 @@ export interface SparqlDataProviderSettings { filterAdditionalRestriction: string; /** - * Abstract links configuration - one could abstract a property path as a link on the diagram - * If you choose to set linkConfiguration, please ensure you'll have corresponding handling of linkConfiguration in - * linkTypeOf query, refElement* queries, linkInfos query. + * Abstract links configuration - one could abstract a property path as a link on the diagram. */ linkConfigurations: LinkConfiguration[]; + /** + * (Experimental) Allows data provider to find links other than specified in `linkConfigurations` + * when `linkConfigurations` has at least one value set. + * + * @default false + */ + openWorldLinks?: boolean; + /** * Abstract property configuration similar to abstract link configuration. Not type-specific yet. */ propertyConfigurations: PropertyConfiguration[]; + + /** + * (Experimental) Allows data provider to find element properties other than specified in + * `propertyConfigurations` when `propertyConfigurations` has at least one value set. + * + * @default false + */ + openWorldProperties?: boolean; } /** @@ -102,19 +229,27 @@ export interface SparqlDataProviderSettings { */ export interface FullTextSearchSettings { /** - * prefix to use in FTS queries + * Prefixes to use in full text search queries. */ prefix: string; /** - * query pattern should return ?inst and ?score for given ${text}. + * SPARQL query pattern to search/restrict results by text token. + * + * Parametrized variables: + * - `${text}` text token + * - `${dataLabelProperty}` `dataLabelProperty` property from the settings + * + * Expected bindings: + * - `?inst` link type + * - `?score` numerical score for ordering search results by relevance + * - `?extractedLabel` (optional; if `extractLabel` is enabled) */ queryPattern: string; /** - * try to extract label from IRI for usage in search purposes. - * If you have no labels in the dataset and want to search, you - * can use ?extractedLabel as something to search for. + * When enabled, adds SPARQL patterns to try to extract label from IRI and + * makes it available as `?extractedLabel` binding in `queryPattern`. */ extractLabel?: boolean; } @@ -127,16 +262,42 @@ export interface LinkConfiguration { * IRI of the "virtual" link */ id: string; + /** - * IRI of the inverse + * Optional domain constraint for source element of the link. + * If specified checks RDF type of source element to match one from this set. */ - inverseId?: string; + domain?: ReadonlyArray; + /** - * Sparql pattern connecting $source to $target. It's required to use those specific variables. + * SPARQL predicate or pattern connecting source element to target element. + * + * Expected bindings (if it is a pattern): + * - `?source` source element + * - `?target` target element + * + * @example + * Direct configuration: `ex:relatedToOther` + * + * Pattern configuration: ` + * ?source ex:hasAddress ?addr . + * ?addr ex:hasCountry ?target . + * OPTIONAL { + * BIND(ex:addressType as ?propType) + * ?addr ex:addressType ?propValue + * } + * ` */ path: string; + /** - * Additional sparql patterns can be used for getting properties of the link. (propValue propType should be bound) + * Additional SPARQL patterns can be used for getting properties of the link. + * + * Expected bindings + * - `?source` source element + * - `?target` target element + * - `?propType` link property type + * - `?propValue` link property value */ properties?: string; } @@ -151,18 +312,39 @@ export interface PropertyConfiguration { id: string; /** - * Sparql pattern connecting $subject to $value. It's required to use those specific variables. + * Optional domain constraint for source element of the property. + * If specified checks RDF type of source element to match one from this set. + */ + domain?: ReadonlyArray; + + /** + * SPARQL predicate or pattern connecting source element to property value. + * + * Expected bindings (if it is a pattern): + * - `?inst` source element + * - `?value` property value + * + * @example + * Direct configuration: `ex:firstName` + * + * Pattern configuration: ` + * ?inst ex:hasAddress ?addr . + * ?addr ex:hasApartmentNumber ?value + * ` */ path: string; } export const RDFSettings: SparqlDataProviderSettings = { linkConfigurations: [], + openWorldLinks: false, + propertyConfigurations: [], + openWorldProperties: false, linksInfoQuery: `SELECT ?source ?type ?target WHERE { - ?source ?type ?target. + \${linkConfigurations} VALUES (?source) {\${ids}} VALUES (?target) {\${ids}} }`, @@ -179,8 +361,33 @@ export const RDFSettings: SparqlDataProviderSettings = { classTreeQuery: ``, + classInfoQuery: +`SELECT ?class ?label ?instcount WHERE { + VALUES(?class) {\${ids}} + OPTIONAL { ?class \${schemaLabelProperty} ?label } + BIND("" as ?instcount) +}`, + + linkTypesQuery: +`SELECT DISTINCT ?link ?instcount ?label WHERE { + \${linkTypesPattern} + OPTIONAL { ?link \${schemaLabelProperty} ?label } +}`, + linkTypesPattern: ``, + linkTypesInfoQuery: +`SELECT ?link ?label WHERE { + VALUES(?link) {\${ids}} + OPTIONAL { ?link \${schemaLabelProperty} ?label } +}`, + + propertyInfoQuery: +`SELECT ?property ?label WHERE { + VALUES(?property) {\${ids}} + OPTIONAL { ?property \${schemaLabelProperty} ?label } +}`, + elementInfoQuery: ``, imageQueryPattern: ``, @@ -246,11 +453,11 @@ const WikidataSettingsOverride: Partial = { } WHERE { VALUES (?inst) {\${ids}} OPTIONAL { - ?inst wdt:P31 ?class . + ?inst wdt:P31 ?class } OPTIONAL {?inst rdfs:label ?label} OPTIONAL { - ?inst ?propType ?propValue . + \${propertyConfigurations} FILTER (isLiteral(?propValue)) } } @@ -261,11 +468,7 @@ const WikidataSettingsOverride: Partial = { linkTypesOfQuery: ` SELECT DISTINCT ?link WHERE { - { - \${elementIri} ?link ?outObject - } UNION { - ?inObject ?link \${elementIri} - } + \${linkConfigurations} ?claim ?link . } `, @@ -275,7 +478,7 @@ const WikidataSettingsOverride: Partial = { { { SELECT DISTINCT ?outObject WHERE { - \${elementIri} \${linkId} ?outObject. + \${linkConfigurationOut} FILTER(ISIRI(?outObject)) ?outObject ?someprop ?someobj. } @@ -284,7 +487,7 @@ const WikidataSettingsOverride: Partial = { } UNION { { SELECT DISTINCT ?inObject WHERE { - ?inObject \${linkId} \${elementIri}. + \${linkConfigurationIn} FILTER(ISIRI(?inObject)) ?inObject ?someprop ?someobj. } @@ -294,7 +497,7 @@ const WikidataSettingsOverride: Partial = { } `, filterRefElementLinkPattern: '?claim ?link .', - filterTypePattern: `?inst wdt:P31 ?instType. ?instType wdt:P279* \${elementTypeIri} . ${'\n'}`, + filterTypePattern: `?inst wdt:P31 ?instType. ?instType wdt:P279* ?class`, filterAdditionalRestriction: `FILTER ISIRI(?inst) BIND(STR(?inst) as ?strInst) FILTER exists {?inst ?someprop ?someobj} @@ -353,19 +556,19 @@ export const OWLRDFSSettingsOverride: Partial = { ?inst ?propType ?propValue. } WHERE { VALUES (?inst) {\${ids}} - OPTIONAL {?inst rdf:type ?class . } + OPTIONAL { ?inst a ?class } OPTIONAL {?inst \${dataLabelProperty} ?label} - OPTIONAL {?inst ?propType ?propValue. - FILTER (isLiteral(?propValue)) } + OPTIONAL { + \${propertyConfigurations} + FILTER (isLiteral(?propValue)) + } } `, imageQueryPattern: `{ ?inst ?linkType ?image } UNION { [] ?linkType ?inst. BIND(?inst as ?image) }`, linkTypesOfQuery: ` SELECT DISTINCT ?link WHERE { - { \${elementIri} ?link ?outObject } - UNION - { ?inObject ?link \${elementIri} } + \${linkConfigurations} } `, linkTypesStatisticsQuery: ` @@ -373,19 +576,19 @@ export const OWLRDFSSettingsOverride: Partial = { WHERE { { SELECT (\${linkId} as ?link) (count(?outObject) as ?outCount) WHERE { - \${elementIri} \${linkId} ?outObject. + \${linkConfigurationOut} \${navigateElementFilterOut} } LIMIT 101 } { SELECT (\${linkId} as ?link) (count(?inObject) as ?inCount) WHERE { - ?inObject \${linkId} \${elementIri}. + \${linkConfigurationIn} \${navigateElementFilterIn} } LIMIT 101 } } `, filterRefElementLinkPattern: '', - filterTypePattern: `?inst rdf:type \${elementTypeIri} . ${'\n'}`, + filterTypePattern: `?inst a ?instType. ?instType rdfs:subClassOf* ?class`, filterElementInfoPattern: `OPTIONAL {?inst rdf:type ?foundClass} BIND (coalesce(?foundClass, owl:Thing) as ?class) OPTIONAL {?inst \${dataLabelProperty} ?label}`, @@ -442,14 +645,17 @@ const DBPediaOverride: Partial = { ?inst ?propType ?propValue. } WHERE { VALUES (?inst) {\${ids}} - ?inst rdf:type ?class . + ?inst a ?class . ?inst rdfs:label ?label . FILTER (!contains(str(?class), 'http://dbpedia.org/class/yago')) - OPTIONAL {?inst ?propType ?propValue. - FILTER (isLiteral(?propValue)) } + OPTIONAL { + \${propertyConfigurations} + FILTER (isLiteral(?propValue)) + } } `, + filterTypePattern: `?inst a ?instType. ?instType rdfs:subClassOf* ?class`, filterElementInfoPattern: ` OPTIONAL {?inst rdf:type ?foundClass. FILTER (!contains(str(?foundClass), 'http://dbpedia.org/class/yago'))} BIND (coalesce(?foundClass, owl:Thing) as ?class) diff --git a/src/ontodia/data/sparql/sparqlModels.ts b/src/ontodia/data/sparql/sparqlModels.ts index cb2bdb9a..7a03cfd7 100644 --- a/src/ontodia/data/sparql/sparqlModels.ts +++ b/src/ontodia/data/sparql/sparqlModels.ts @@ -70,7 +70,7 @@ export interface ClassBinding { } export interface PropertyBinding { - prop: RdfIri; + property: RdfIri; label?: RdfLiteral; } @@ -100,6 +100,17 @@ export interface ElementImageBinding { image: RdfIri; } +export interface ElementTypeBinding { + inst: RdfIri; + class: RdfIri; +} + +export interface FilterBinding { + classAll?: RdfIri; + link?: RdfIri; + direction?: RdfLiteral; +} + export interface SparqlResponse { head: { vars: string[] }; results: { bindings: Binding[] }; diff --git a/src/ontodia/diagram/commands.ts b/src/ontodia/diagram/commands.ts index 7ac2ae7d..80bb0c45 100644 --- a/src/ontodia/diagram/commands.ts +++ b/src/ontodia/diagram/commands.ts @@ -103,26 +103,11 @@ export function setElementData(model: DiagramModel, target: ElementIri, data: El return Command.create('Set element data', () => { for (const element of model.elements.filter(el => el.iri === target)) { element.setData(data); - updateLinksToReferByNewIri(element, previous.id, data.id); } - return setElementData(model, target, previous); + return setElementData(model, data.id, previous); }); } -function updateLinksToReferByNewIri(element: Element, oldIri: ElementIri, newIri: ElementIri) { - if (oldIri === newIri) { return; } - for (const link of element.links) { - let data = link.data; - if (data.sourceId === oldIri) { - data = {...data, sourceId: newIri}; - } - if (data.targetId === oldIri) { - data = {...data, targetId: newIri}; - } - link.setData(data); - } -} - export function setLinkData(model: DiagramModel, oldData: LinkModel, newData: LinkModel): Command { if (!sameLink(oldData, newData)) { throw new Error('Cannot change typeId, sourceId or targetId when changing link data'); diff --git a/src/ontodia/diagram/elementLayer.tsx b/src/ontodia/diagram/elementLayer.tsx index c53fdefc..1abdd8eb 100644 --- a/src/ontodia/diagram/elementLayer.tsx +++ b/src/ontodia/diagram/elementLayer.tsx @@ -228,7 +228,7 @@ function applyRedrawRequests( const request = (batch.requests.get(elementId) || RedrawFlags.None) | batch.forAll; if (request & RedrawFlags.Render) { state = { - element: state.element, + element, templateProps: (request & RedrawFlags.RecomputeTemplate) === RedrawFlags.RecomputeTemplate ? computeTemplateProps(state.element, view) : state.templateProps, diff --git a/src/ontodia/diagram/elements.ts b/src/ontodia/diagram/elements.ts index ba7c7ea5..ee78efcf 100644 --- a/src/ontodia/diagram/elements.ts +++ b/src/ontodia/diagram/elements.ts @@ -1,5 +1,5 @@ import { - ElementModel, LinkModel, LocalizedString, ElementTypeIri, LinkTypeIri, PropertyTypeIri, + ElementModel, LinkModel, LocalizedString, ElementIri, ElementTypeIri, LinkTypeIri, PropertyTypeIri, } from '../data/model'; import { GenerateID } from '../data/schema'; @@ -14,11 +14,6 @@ export enum LinkDirection { out = 'out', } -export interface DirectedLinkType { - linkTypeIri: LinkTypeIri; - direction: LinkDirection; -} - export interface ElementEvents { changeData: PropertyChange; changePosition: PropertyChange; @@ -91,6 +86,7 @@ export class Element { if (previous === value) { return; } this._data = value; this.source.trigger('changeData', {source: this, previous}); + updateLinksToReferByNewIri(this, previous.id, value.id); } get position(): Vector { return this._position; } @@ -172,6 +168,20 @@ export interface AddToFilterRequest { direction?: 'in' | 'out'; } +function updateLinksToReferByNewIri(element: Element, oldIri: ElementIri, newIri: ElementIri) { + if (oldIri === newIri) { return; } + for (const link of element.links) { + let data = link.data; + if (data.sourceId === oldIri) { + data = {...data, sourceId: newIri}; + } + if (data.targetId === oldIri) { + data = {...data, targetId: newIri}; + } + link.setData(data); + } +} + export interface FatClassModelEvents { changeLabel: PropertyChange>; changeCount: PropertyChange; diff --git a/src/ontodia/diagram/linkLayer.tsx b/src/ontodia/diagram/linkLayer.tsx index 39a946a9..40f7b14a 100644 --- a/src/ontodia/diagram/linkLayer.tsx +++ b/src/ontodia/diagram/linkLayer.tsx @@ -338,19 +338,24 @@ function computeLinkLabels(model: DiagramLink, style: LinkStyle, view: DiagramVi const labelTexts = labelStyle.attrs && labelStyle.attrs.text ? labelStyle.attrs.text.text : undefined; let text: LocalizedString | undefined; + let title: string | undefined = labelStyle.title; if (labelTexts && labelTexts.length > 0) { text = view.selectLabel(labelTexts); } else { const type = view.model.getLinkType(model.typeId); text = view.selectLabel(type.label) || { - text: view.formatLabel(type.label, type.id), - lang: '', + value: view.formatLabel(type.label, type.id), + language: '', }; + if (title === undefined) { + title = `${text.value} ${view.formatIri(model.typeId)}`; + } } labels.push({ offset: labelStyle.position || 0.5, text, + title, attributes: { text: getLabelTextAttributes(labelStyle), rect: getLabelRectAttributes(labelStyle), @@ -365,6 +370,7 @@ function computeLinkLabels(model: DiagramLink, style: LinkStyle, view: DiagramVi labels.push({ offset: property.position || 0.5, text: view.selectLabel(property.attrs.text.text), + title: property.title, attributes: { text: getLabelTextAttributes(property), rect: getLabelRectAttributes(property), @@ -415,6 +421,7 @@ function getLabelRectAttributes(label: LinkLabelProperties): CSSProperties { interface LabelAttributes { offset: number; text: LocalizedString; + title?: string; attributes: { text: CSSProperties; rect: CSSProperties; @@ -458,6 +465,7 @@ class LinkLabel extends Component { return ( + {label.title ? {label.title} : undefined} { x={x} y={y} dy={dy} textAnchor={textAnchor} style={label.attributes.text}> - {label.text.text} + {label.text.value} ); diff --git a/src/ontodia/diagram/view.ts b/src/ontodia/diagram/view.ts index 92ff1472..5b4ee2ea 100644 --- a/src/ontodia/diagram/view.ts +++ b/src/ontodia/diagram/view.ts @@ -355,11 +355,11 @@ function defaultSelectLabel( let defaultValue: LocalizedString; let englishValue: LocalizedString; for (const text of texts) { - if (text.lang === language) { + if (text.language === language) { return text; - } else if (text.lang === '') { + } else if (text.language === '') { defaultValue = text; - } else if (text.lang === 'en') { + } else if (text.language === 'en') { englishValue = text; } } @@ -371,6 +371,6 @@ function defaultSelectLabel( } function resolveLabel(label: LocalizedString | undefined, fallbackIri: string): string { - if (label) { return label.text; } + if (label) { return label.value; } return getUriLocalName(fallbackIri) || fallbackIri; } diff --git a/src/ontodia/editor/authoredEntity.tsx b/src/ontodia/editor/authoredEntity.tsx index 13b372b9..8558ba42 100644 --- a/src/ontodia/editor/authoredEntity.tsx +++ b/src/ontodia/editor/authoredEntity.tsx @@ -12,7 +12,7 @@ import { Listener } from '../viewUtils/events'; import { WorkspaceContextTypes, WorkspaceContextWrapper } from '../workspace/workspaceContext'; -import { AuthoringState } from './authoringState'; +import { AuthoringState, AuthoringKind } from './authoringState'; import { EditorController, EditorEvents } from './editorController'; export interface AuthoredEntityProps { @@ -22,6 +22,7 @@ export interface AuthoredEntityProps { export interface AuthoredEntityContext { editor: EditorController; + editedIri?: string; view: DiagramView; canEdit: boolean | undefined; canDelete: boolean | undefined; @@ -74,7 +75,7 @@ export class AuthoredEntity extends React.Component const {source: editor, previous} = e; const iri = this.props.templateProps.data.id; const current = editor.authoringState; - if (current.index.elements.get(iri) !== previous.index.elements.get(iri)) { + if (current.elements.get(iri) !== previous.elements.get(iri)) { this.queryAllowedActions(); } } @@ -121,8 +122,14 @@ export class AuthoredEntity extends React.Component const {view} = this.context.ontodiaPaperArea; const {editor} = this.context.ontodiaWorkspace; const {canEdit, canDelete} = this.state; + + const iri = this.props.templateProps.iri; + const elementEvent = editor.authoringState.elements.get(iri); + const editedIri = elementEvent && elementEvent.type === AuthoringKind.ChangeElement ? + elementEvent.newIri : undefined; + return renderTemplate({ - editor, view, canEdit, canDelete, + editor, view, canEdit, canDelete, editedIri, onEdit: this.onEdit, onDelete: this.onDelete, }); diff --git a/src/ontodia/editor/authoringState.ts b/src/ontodia/editor/authoringState.ts index b2e4f184..ee7f993c 100644 --- a/src/ontodia/editor/authoringState.ts +++ b/src/ontodia/editor/authoringState.ts @@ -3,256 +3,234 @@ import { ElementModel, LinkModel, ElementIri, sameLink, hashLink } from '../data import { HashMap, ReadonlyHashMap, cloneMap } from '../viewUtils/collections'; export interface AuthoringState { - readonly events: ReadonlyArray; - readonly index: AuthoringIndex; + readonly elements: ReadonlyMap; + readonly links: ReadonlyHashMap; } -export type AuthoringEvent = - | ElementChange - | ElementDeletion - | LinkChange - | LinkDeletion; +export type AuthoringEvent = ElementChange | LinkChange; export enum AuthoringKind { ChangeElement = 'changeElement', - DeleteElement = 'deleteElement', ChangeLink = 'changeLink', - DeleteLink = 'deleteLink', -} - -export interface ElementDeletion { - readonly type: AuthoringKind.DeleteElement; - readonly model: ElementModel; -} - -export interface LinkDeletion { - readonly type: AuthoringKind.DeleteLink; - readonly model: LinkModel; } export interface ElementChange { readonly type: AuthoringKind.ChangeElement; readonly before?: ElementModel; readonly after: ElementModel; + readonly newIri?: ElementIri; + readonly deleted: boolean; } export interface LinkChange { readonly type: AuthoringKind.ChangeLink; readonly before?: LinkModel; readonly after: LinkModel; + readonly deleted: boolean; } -export interface AuthoringIndex { - readonly elements: ReadonlyMap; - readonly links: ReadonlyHashMap; +interface MutableAuthoringState extends AuthoringState { + readonly elements: Map; + readonly links: HashMap; } export namespace AuthoringState { export const empty: AuthoringState = { - events: [], - index: makeIndex([]), + elements: new Map(), + links: new HashMap(hashLink, sameLink), }; - export function set(state: AuthoringState, change: Pick): AuthoringState { - const events = change.events || state.events; - const index = makeIndex(events); - return {...state, events, index}; + export function isEmpty(state: AuthoringState) { + return state.elements.size === 0 && state.links.size === 0; + } + + export function clone(index: AuthoringState): MutableAuthoringState { + return { + elements: cloneMap(index.elements), + links: index.links.clone(), + }; + } + + export function has(state: AuthoringState, event: AuthoringEvent): boolean { + return event.type === AuthoringKind.ChangeElement + ? state.elements.get(event.after.id) === event + : state.links.get(event.after) === event; } export function discard(state: AuthoringState, discarded: AuthoringEvent): AuthoringState { - const index = state.events.indexOf(discarded); - if (index < 0) { + if (!has(state, discarded)) { return state; } - const newElementIri = discarded.type === AuthoringKind.ChangeElement && !discarded.before - ? discarded.after.id : undefined; - const events = state.events.filter(e => { - if (e.type === AuthoringKind.ChangeLink) { - if (newElementIri && isLinkConnectedToElement(e.after, newElementIri)) { - return false; - } + const newState = clone(state); + if (discarded.type === AuthoringKind.ChangeElement) { + newState.elements.delete(discarded.after.id); + if (!discarded.before) { + state.links.forEach(e => { + if (isLinkConnectedToElement(e.after, discarded.after.id)) { + newState.links.delete(e.after); + } + }); } - return e !== discarded; - }); - return set(state, {events}); + } else { + newState.links.delete(discarded.after); + } + return newState; } - export function addElement(state: AuthoringState, item: ElementModel) { - const event: ElementChange = {type: AuthoringKind.ChangeElement, after: item}; - return AuthoringState.set(state, {events: [...state.events, event]}); + export function addElement(state: AuthoringState, item: ElementModel): AuthoringState { + const event: ElementChange = {type: AuthoringKind.ChangeElement, after: item, deleted: false}; + const newState = clone(state); + newState.elements.set(event.after.id, event); + return newState; } - export function addLink(state: AuthoringState, item: LinkModel) { - const event: LinkChange = {type: AuthoringKind.ChangeLink, after: item}; - return AuthoringState.set(state, {events: [...state.events, event]}); + export function addLink(state: AuthoringState, item: LinkModel): AuthoringState { + const event: LinkChange = {type: AuthoringKind.ChangeLink, after: item, deleted: false}; + const newState = clone(state); + newState.links.set(event.after, event); + return newState; } - export function changeElement(state: AuthoringState, before: ElementModel, after: ElementModel) { - const iriChanged = after.id !== before.id; - if (iriChanged) { - // disallow changing IRI for existing (non-new) entities - const isNewEntity = state.events.find(e => - e.type === AuthoringKind.ChangeElement && - e.after.id === before.id && - !e.before - ); - if (!isNewEntity) { - throw new Error('Cannot change IRI of already persisted entity'); + export function changeElement(state: AuthoringState, before: ElementModel, after: ElementModel): AuthoringState { + const newState = clone(state); + // delete previous state for an entity + newState.elements.delete(before.id); + + const previous = state.elements.get(before.id); + if (previous && !previous.before) { + // adding or changing new entity + newState.elements.set(after.id, { + type: AuthoringKind.ChangeElement, + after, + deleted: false, + }); + if (before.id !== after.id) { + state.links.forEach(e => { + if (!e.before && isLinkConnectedToElement(e.after, before.id)) { + const updatedLink = updateLinkToReferByNewIri(e.after, before.id, after.id); + newState.links.delete(e.after); + newState.links.set(updatedLink, { + type: AuthoringKind.ChangeLink, + after: updatedLink, + deleted: false, + }); + } + }); } + } else { + // changing existing entity + const iriChanged = after.id !== before.id; + const previousBefore = previous ? previous.before : undefined; + newState.elements.set(before.id, { + type: AuthoringKind.ChangeElement, + // always initialize 'before', otherwise entity will be considered new + before: previousBefore || before, + after: iriChanged ? {...after, id: before.id} : after, + newIri: iriChanged ? after.id : undefined, + deleted: false, + }); } - let previousBefore: ElementModel | undefined = before; - const additional: AuthoringEvent[] = []; - const events = state.events.filter(e => { - if (e.type === AuthoringKind.DeleteElement) { - if (e.model.id === before.id) { - previousBefore = e.model; - return false; - } - } else if (e.type === AuthoringKind.ChangeElement) { - if (e.after.id === before.id) { - previousBefore = e.before; - return false; - } - } else if (e.type === AuthoringKind.ChangeLink) { - if (iriChanged && isLinkConnectedToElement(e.after, before.id)) { - additional.push({ - type: AuthoringKind.ChangeLink, - before: e.before, - after: updateLinkToReferByNewIri(e.after, before.id, after.id), - }); - return false; - } - } - return true; - }); - additional.unshift({ - type: AuthoringKind.ChangeElement, - before: previousBefore, - after: after, - }); - return AuthoringState.set(state, {events: [...events, ...additional]}); + + return newState; } - export function changeLink(state: AuthoringState, before: LinkModel, after: LinkModel) { + export function changeLink(state: AuthoringState, before: LinkModel, after: LinkModel): AuthoringState { if (!sameLink(before, after)) { throw new Error('Cannot move link to another element or change its type'); } - let previousBefore: LinkModel | undefined = before; - const events = state.events.filter(e => { - if (e.type === AuthoringKind.ChangeLink) { - if (sameLink(e.after, before)) { - previousBefore = e.before; - return false; - } - } - return true; - }); - const event: AuthoringEvent = { + const newState = clone(state); + const previous = state.links.get(before); + newState.links.set(before, { type: AuthoringKind.ChangeLink, - before: previousBefore, + before: previous ? previous.before : undefined, after: after, - }; - return AuthoringState.set(state, {events: [...events, event]}); + deleted: false, + }); + return newState; } - export function deleteElement(state: AuthoringState, model: ElementModel) { - const events = state.events.filter(e => { - if (e.type === AuthoringKind.ChangeElement) { - if (e.after.id === model.id) { - return false; - } - } else if (e.type === AuthoringKind.ChangeLink) { - if (isLinkConnectedToElement(e.after, model.id)) { - return false; - } - } else if (e.type === AuthoringKind.DeleteLink) { - if (isLinkConnectedToElement(e.model, model.id)) { - return false; - } + export function deleteElement(state: AuthoringState, model: ElementModel): AuthoringState { + const newState = clone(state); + newState.elements.delete(model.id); + state.links.forEach(e => { + if (isLinkConnectedToElement(e.after, model.id)) { + newState.links.delete(e.after); } - return true; }); - if (!isNewElement(state, model.id)) { - events.push({type: AuthoringKind.DeleteElement, model}); + newState.elements.set(model.id, { + type: AuthoringKind.ChangeElement, + before: model, + after: model, + deleted: true, + }); } - return AuthoringState.set(state, {events}); + return newState; } - export function deleteLink(state: AuthoringState, target: LinkModel) { - const events = state.events.filter(e => { - if (e.type === AuthoringKind.ChangeLink) { - if (sameLink(e.after, target)) { - return false; - } - } else if (e.type === AuthoringKind.DeleteLink) { - if (sameLink(e.model, target)) { - return false; - } - } - return true; - }); + export function deleteLink(state: AuthoringState, target: LinkModel): AuthoringState { + const newState = clone(state); + newState.links.delete(target); if (!isNewLink(state, target)) { - events.push({ - type: AuthoringKind.DeleteLink, - model: target, + newState.links.set(target, { + type: AuthoringKind.ChangeLink, + before: target, + after: target, + deleted: true, }); } - return AuthoringState.set(state, {events}); + return newState; } export function deleteNewLinksConnectedToElements( state: AuthoringState, elementIris: Set ): AuthoringState { - const events = state.events.filter(event => { - if (event.type !== AuthoringKind.ChangeLink || event.before) { return true; } - - const linkModel = event.after; - - return !elementIris.has(linkModel.sourceId) && - !elementIris.has(linkModel.targetId); - }); - return AuthoringState.set(state, {events}); - } - - function makeIndex(events: ReadonlyArray): AuthoringIndex { - const elements = new Map(); - const links = new HashMap(hashLink, sameLink); - for (const e of events) { - if (e.type === AuthoringKind.ChangeElement) { - elements.set(e.after.id, e); - } else if (e.type === AuthoringKind.DeleteElement) { - elements.set(e.model.id, e); - } else if (e.type === AuthoringKind.ChangeLink) { - links.set(e.after, e); - } else if (e.type === AuthoringKind.DeleteLink) { - links.set(e.model, e); + const newState = clone(state); + state.links.forEach(e => { + if (!e.before) { + const target = e.after; + if (elementIris.has(target.sourceId) || elementIris.has(target.targetId)) { + newState.links.delete(target); + } } - } - return {elements, links}; + }); + return newState; } export function isNewElement(state: AuthoringState, target: ElementIri): boolean { - const event = state.index.elements.get(target); + const event = state.elements.get(target); return event && event.type === AuthoringKind.ChangeElement && !event.before; } export function isDeletedElement(state: AuthoringState, target: ElementIri): boolean { - const event = state.index.elements.get(target); - return event && event.type === AuthoringKind.DeleteElement; + const event = state.elements.get(target); + return event && event.deleted; + } + + export function isElementWithModifiedIri(state: AuthoringState, target: ElementIri): boolean { + const event = state.elements.get(target); + return event && event.type === AuthoringKind.ChangeElement && + event.before && Boolean(event.newIri); } export function isNewLink(state: AuthoringState, linkModel: LinkModel): boolean { - const event = state.index.links.get(linkModel); - return event && event.type === AuthoringKind.ChangeLink && !event.before; + const event = state.links.get(linkModel); + return event && !event.before; } export function isDeletedLink(state: AuthoringState, linkModel: LinkModel): boolean { - const event = state.index.links.get(linkModel); - return event && event.type === AuthoringKind.DeleteLink || + const event = state.links.get(linkModel); + return event && event.deleted || isDeletedElement(state, linkModel.sourceId) || isDeletedElement(state, linkModel.targetId); } + + export function isUncertainLink(state: AuthoringState, linkModel: LinkModel): boolean { + return !isDeletedLink(state, linkModel) && ( + isElementWithModifiedIri(state, linkModel.sourceId) || + isElementWithModifiedIri(state, linkModel.targetId) + ); + } } export interface TemporaryState { diff --git a/src/ontodia/editor/editLayer.tsx b/src/ontodia/editor/editLayer.tsx index 5b29adca..82f5952f 100644 --- a/src/ontodia/editor/editLayer.tsx +++ b/src/ontodia/editor/editLayer.tsx @@ -266,10 +266,10 @@ export class EditLayer extends React.Component { const labelText = classId === PLACEHOLDER_ELEMENT_TYPE ? 'New Entity' : `New ${typeName}`; const types = [classId]; const entityIri = await metadataApi.generateNewElementIri(types, Cancellation.NEVER_SIGNAL); - const elementModel = { + const elementModel: ElementModel = { id: entityIri, types, - label: {values: [{text: labelText, lang: ''}]}, + label: {values: [{value: labelText, language: ''}]}, properties: {}, }; return editor.createNewEntity({elementModel, temporary: true}); diff --git a/src/ontodia/editor/editorController.tsx b/src/ontodia/editor/editorController.tsx index 96f2b3e8..a827e436 100644 --- a/src/ontodia/editor/editorController.tsx +++ b/src/ontodia/editor/editorController.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import { MetadataApi } from '../data/metadataApi'; import { ValidationApi } from '../data/validationApi'; -import { ElementModel, LinkModel, ElementIri, sameLink } from '../data/model'; +import { ElementModel, LinkModel, ElementIri, sameLink, sameElement } from '../data/model'; import { setElementExpanded, setElementData, setLinkData, changeLinkTypeVisibility } from '../diagram/commands'; import { Element, Link, LinkVertex, FatLinkType } from '../diagram/elements'; @@ -29,7 +29,7 @@ import { Spinner, SpinnerProps } from '../viewUtils/spinner'; import { AsyncModel, requestElementData, restoreLinksBetweenElements } from './asyncModel'; import { - AuthoringState, AuthoringKind, AuthoringEvent, TemporaryState, + AuthoringState, AuthoringKind, AuthoringEvent, TemporaryState, ElementChange, } from './authoringState'; import { EditLayer, EditLayerMode } from './editLayer'; import { ValidationState, changedElementsToValidate, validateElements } from './validation'; @@ -243,8 +243,10 @@ export class EditorController { for (const item of items) { if (item instanceof Element) { - const event = this.authoringState.index.elements.get(item.iri); - this.discardChange(event); + const event = this.authoringState.elements.get(item.iri); + if (event) { + this.discardChange(event); + } this.model.removeElement(item.id); deletedElementIris.add(item.iri); } else if (item instanceof Link) { @@ -370,12 +372,6 @@ export class EditorController { target={selected} onEdit={() => this.showEditLinkForm(selected)} onDelete={() => this.deleteLink(selected.data)} - onRevert={() => { - const deletion = this.authoringState.index.links.get(selected.data); - if (deletion && deletion.type === AuthoringKind.DeleteLink) { - this.discardChange(deletion); - } - }} onSourceMove={(point: { x: number; y: number }) => this.startEditing({target: selected, mode: EditLayerMode.moveLinkSource, point}) } @@ -413,9 +409,24 @@ export class EditorController { this.hideDialog(); this.changeEntityData(target.data.id, newData); }; + const isIriModified = AuthoringState.isElementWithModifiedIri(this.authoringState, target.data.id); + let modelToEdit; + if (isIriModified) { + const relatedEvent = this.authoringState.elements.get(target.data.id); + modelToEdit = { + ...target.data, + id: (relatedEvent as ElementChange).newIri, + }; + } else { + modelToEdit = target.data; + } const onCancel = () => this.hideDialog(); const content = propertyEditor ? propertyEditor({elementData: target.data, onSubmit, onCancel}) : ( - + ); this.showDialog({target, dialogType, content, onClose: onCancel}); } @@ -720,10 +731,13 @@ export class EditorController { } const oldData = elements[0].data; const batch = this.model.history.startBatch('Edit entity'); - this.model.history.execute(setElementData(this.model, targetIri, newData)); - this.setAuthoringState( - AuthoringState.changeElement(this._authoringState, oldData, newData) - ); + + const newState = AuthoringState.changeElement(this._authoringState, oldData, newData); + // get created authoring event by either old or new IRI (in case of new entities) + const event = newState.elements.get(targetIri) || newState.elements.get(newData.id); + this.model.history.execute(setElementData(this.model, targetIri, event.after)); + this.setAuthoringState(newState); + batch.store(); } @@ -737,7 +751,7 @@ export class EditorController { const batch = this.model.history.startBatch('Delete entity'); const model = elements[0].data; - const event = state.index.elements.get(elementIri); + const event = state.elements.get(elementIri); // remove new connected links const linksToRemove = new Set(); for (const element of elements) { @@ -830,8 +844,7 @@ export class EditorController { deleteLink(model: LinkModel) { const state = this.authoringState; - const event = state.index.links.get(model); - if (event && event.type === AuthoringKind.DeleteLink) { + if (AuthoringState.isDeletedLink(state, model)) { return; } const batch = this.model.history.startBatch('Delete link'); @@ -910,7 +923,9 @@ export class EditorController { const batch = this.model.history.startBatch('Discard change'); if (event.type === AuthoringKind.ChangeElement) { - if (event.before) { + if (event.deleted) { + /* nothing */ + } else if (event.before) { this.model.history.execute( setElementData(this.model, event.after.id, event.before) ); @@ -920,7 +935,9 @@ export class EditorController { .forEach(el => this.model.removeElement(el.id)); } } else if (event.type === AuthoringKind.ChangeLink) { - if (event.before) { + if (event.deleted) { + /* nothing */ + } else if (event.before) { this.model.history.execute( setLinkData(this.model, event.after, event.before) ); diff --git a/src/ontodia/editor/elementDecorator.tsx b/src/ontodia/editor/elementDecorator.tsx index 9b178b73..5ce8a776 100644 --- a/src/ontodia/editor/elementDecorator.tsx +++ b/src/ontodia/editor/elementDecorator.tsx @@ -9,7 +9,7 @@ import { DiagramView } from '../diagram/view'; import { Vector } from '../diagram/geometry'; import { EditorController } from './editorController'; -import { AuthoringKind, ElementChange, ElementDeletion } from './authoringState'; +import { AuthoringKind, ElementChange } from './authoringState'; import { ElementValidation, LinkValidation } from './validation'; const CLASS_NAME = `ontodia-authoring-state`; @@ -22,7 +22,7 @@ export interface ElementDecoratorProps { } interface State { - state?: ElementChange | ElementDeletion; + state?: ElementChange; validation?: ElementValidation; isTemporary?: boolean; } @@ -35,7 +35,7 @@ export class ElementDecorator extends React.Component - this.setState({state: editor.authoringState.index.elements.get(model.iri)}) + this.setState({state: editor.authoringState.elements.get(model.iri)}) ); this.listener.listen(editor.events, 'changeValidationState', () => this.setState({validation: editor.validationState.elements.get(model.iri)}) @@ -55,6 +55,15 @@ export class ElementDecorator extends React.Component this.setState({isTemporary: editor.temporaryState.elements.has(model.iri)}) ); + this.listener.listen(model.events, 'changeData', event => { + if (event.previous.id !== model.iri) { + this.setState({ + isTemporary: editor.temporaryState.elements.has(model.iri), + validation: editor.validationState.elements.get(model.iri), + state: editor.authoringState.elements.get(model.iri), + }); + } + }); } componentWillUnmount() { @@ -83,7 +92,7 @@ export class ElementDecorator extends React.Component ]; } - if (state && state.type === AuthoringKind.DeleteElement) { + if (state && state.deleted) { const right = width; const bottom = height; return ( @@ -136,15 +145,15 @@ export class ElementDecorator extends React.Component { @@ -102,22 +101,22 @@ export class LinkStateWidget extends React.Component { return editor.model.links.map(link => { let renderedState: JSX.Element | null = null; - const state = editor.authoringState.index.links.get(link.data); + const state = editor.authoringState.links.get(link.data); if (state) { const onCancel = () => editor.discardChange(state); let statusText: string; let title: string; - if (state.type === AuthoringKind.ChangeLink && !state.before) { + if (state.deleted) { + statusText = 'Delete'; + title = 'Revert deletion of the link'; + } else if (!state.before) { statusText = 'New'; title = 'Revert creation of the link'; - } else if (state.type === AuthoringKind.ChangeLink && state.before) { + } else { statusText = 'Change'; title = 'Revert all changes in properties of the link'; - } else if (state.type === AuthoringKind.DeleteLink) { - statusText = 'Delete'; - title = 'Revert deletion of the link'; } if (statusText && title) { @@ -164,15 +163,18 @@ export class LinkStateWidget extends React.Component { strokeDasharray={'8 8'} /> ); } - const state = editor.authoringState.index.links.get(link.data); + const event = editor.authoringState.links.get(link.data); const isDeletedLink = AuthoringState.isDeletedLink(editor.authoringState, link.data); - if (state || isDeletedLink) { + const isUncertainLink = AuthoringState.isUncertainLink(editor.authoringState, link.data); + if (event || isDeletedLink || isUncertainLink) { const path = this.calculateLinkPath(link); let color: string; if (isDeletedLink) { color = 'red'; - } else if (state && state.type === AuthoringKind.ChangeLink) { - color = state.before ? 'blue' : 'green'; + } else if (isUncertainLink) { + color = 'blue'; + } else if (event && event.type === AuthoringKind.ChangeLink) { + color = event.before ? 'blue' : 'green'; } return ( diff --git a/src/ontodia/editor/validation.ts b/src/ontodia/editor/validation.ts index 8c179c94..691b0e57 100644 --- a/src/ontodia/editor/validation.ts +++ b/src/ontodia/editor/validation.ts @@ -64,26 +64,26 @@ export function changedElementsToValidate( const currentAuthoring = editor.authoringState; const links = new HashMap(hashLink, sameLink); - previousAuthoring.index.links.forEach((e, model) => links.set(model, true)); - currentAuthoring.index.links.forEach((e, model) => links.set(model, true)); + previousAuthoring.links.forEach((e, model) => links.set(model, true)); + currentAuthoring.links.forEach((e, model) => links.set(model, true)); const toValidate = new Set(); links.forEach((value, linkModel) => { - const current = currentAuthoring.index.links.get(linkModel); - const previous = previousAuthoring.index.links.get(linkModel); + const current = currentAuthoring.links.get(linkModel); + const previous = previousAuthoring.links.get(linkModel); if (current !== previous) { toValidate.add(linkModel.sourceId); } }); for (const element of editor.model.elements) { - const current = currentAuthoring.index.elements.get(element.iri); - const previous = previousAuthoring.index.elements.get(element.iri); + const current = currentAuthoring.elements.get(element.iri); + const previous = previousAuthoring.elements.get(element.iri); if (current !== previous) { toValidate.add(element.iri); // when we remove element incoming link are removed as well so we should update their sources - if ((current || previous).type === AuthoringKind.DeleteElement) { + if ((current || previous).deleted) { for (const link of element.links) { if (link.data.sourceId !== element.iri) { toValidate.add(link.data.sourceId); diff --git a/src/ontodia/forms/editEntityForm.tsx b/src/ontodia/forms/editEntityForm.tsx index 1b1fe42a..2ed01731 100644 --- a/src/ontodia/forms/editEntityForm.tsx +++ b/src/ontodia/forms/editEntityForm.tsx @@ -1,7 +1,11 @@ import * as React from 'react'; import { DiagramView } from '../diagram/view'; -import { ElementModel, PropertyTypeIri, Property, isIriProperty, isLiteralProperty } from '../data/model'; +import { + ElementModel, LocalizedString, PropertyTypeIri, + Property, isIriProperty, isLiteralProperty, + ElementIri, +} from '../data/model'; const CLASS_NAME = 'ontodia-edit-form'; @@ -38,7 +42,7 @@ export class EditEntityForm extends React.Component { if (isIriProperty(property)) { values = property.values.map(({value}) => value); } else if (isLiteralProperty(property)) { - values = property.values.map(({text}) => text); + values = property.values.map(({value}) => value); } return (
@@ -78,10 +82,37 @@ export class EditEntityForm extends React.Component { ); } + private onChangeIri = (e: React.FormEvent) => { + const target = (e.target as HTMLInputElement); + const iri = target.value as ElementIri; + this.setState(prevState => { + return { + elementModel: { + ...prevState.elementModel, + id: iri, + } + }; + }); + } + + private renderIri() { + const {elementModel} = this.state; + return ( + + ); + } + private onChangeLabel = (e: React.FormEvent) => { const target = (e.target as HTMLInputElement); - const labels = target.value.length > 0 ? [{text: target.value, lang: ''}] : []; + const labels: LocalizedString[] = target.value.length > 0 ? [{value: target.value, language: ''}] : []; this.setState({elementModel: { ...this.state.elementModel, @@ -92,7 +123,7 @@ export class EditEntityForm extends React.Component { private renderLabel() { const {view} = this.props; const label = view.selectLabel(this.state.elementModel.label.values); - const text = label ? label.text : ''; + const text = label ? label.value : ''; return (