From 29c41f28a904f24272c023c4ae5873f811c1b688 Mon Sep 17 00:00:00 2001 From: Alexey Morozov Date: Tue, 10 Jun 2025 01:58:33 +0300 Subject: [PATCH] Fix graph authoring issues: * Fix marking existing relation as new or changed after moving its source or target to its original source or target; * Fix marking new relation as changed after editing its properties; * Fix creating a new entity from a class tree in React strict mode (due to always cancelled operation); * Close currently open dialog with a target when importing a diagram; * Export vocabulary namespaces under `$namespace` property; --- CHANGELOG.md | 1 + src/data/rdf/rdfDataProvider.ts | 16 ++++------------ src/data/rdf/vocabulary.ts | 24 ++++++++++++++++++++---- src/editor/authoringState.ts | 17 ++++++++++++----- src/editor/editorController.tsx | 6 ++++++ src/editor/overlayController.tsx | 5 +++++ src/widgets/classTree/classTree.tsx | 3 +++ styles/widgets/_classTree.scss | 2 +- 8 files changed, 52 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b01c8898..38858464 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p - Display elliptical authoring state overlays for elliptically-shaped entity elements. - Use provided `duration` in `CanvasApi.animateGraph()` for element transitions without the need to override the styles. - Trigger `keydown`, `keyup`, `scroll` and `contextMenu` canvas events only from a non-widget layer. +- Fix marking existing relation as new or changed after moving its source or target to its original source or target. #### ⏱ Performance - Optimize diagram loading time by avoiding unnecessary updates and separating a measuring element sizes step from applying the sizes to the rendering state. diff --git a/src/data/rdf/rdfDataProvider.ts b/src/data/rdf/rdfDataProvider.ts index 80bd63a1..5dbf6861 100644 --- a/src/data/rdf/rdfDataProvider.ts +++ b/src/data/rdf/rdfDataProvider.ts @@ -12,7 +12,7 @@ import { makeCaseInsensitiveFilter } from '../utils'; import { MemoryDataset, IndexQuadBy, indexedDataset } from './memoryDataset'; import * as Rdf from './rdfModel'; -import { rdf, rdfs, schema } from './vocabulary'; +import { owl, rdf, rdfs, schema } from './vocabulary'; /** * Options for {@link RdfDataProvider}. @@ -70,14 +70,6 @@ export interface RdfDataProviderOptions { const BLANK_PREFIX = 'urn:reactodia:blank:rdf:'; -const OWL_CLASS = 'http://www.w3.org/2002/07/owl#Class'; -const OWL_OBJECT_PROPERTY = 'http://www.w3.org/2002/07/owl#ObjectProperty'; - -const RDF_PROPERTY = `${rdf.namespace}#Property` as const; - -const RDFS_CLASS = `${rdfs.namespace}#Class` as const; -const RDFS_SUB_CLASS_OF = `${rdfs.namespace}#subClassOf`; - /** * Provides graph data from in-memory [RDF/JS-compatible](https://rdf.js.org/data-model-spec/) * graph dataset. @@ -113,11 +105,11 @@ export class RdfDataProvider implements DataProvider { ? null : this.factory.namedNode(options.labelPredicate ?? rdfs.label); this.imagePredicate = options.imagePredicate === null ? null : this.factory.namedNode(options.imagePredicate ?? schema.thumbnailUrl); - this.elementTypeBaseTypes = (options.elementTypeBaseTypes ?? [OWL_CLASS, RDFS_CLASS]) + this.elementTypeBaseTypes = (options.elementTypeBaseTypes ?? [owl.Class, rdfs.Class]) .map(iri => this.factory.namedNode(iri)); this.elementSubtypePredicate = options.elementSubtypePredicate === null - ? null : this.factory.namedNode(options.elementSubtypePredicate ?? RDFS_SUB_CLASS_OF); - this.linkTypeBaseTypes = (options.linkTypeBaseTypes ?? [OWL_OBJECT_PROPERTY, RDF_PROPERTY]) + ? null : this.factory.namedNode(options.elementSubtypePredicate ?? rdfs.subClassOf); + this.linkTypeBaseTypes = (options.linkTypeBaseTypes ?? [owl.ObjectProperty, rdf.Property]) .map(iri => this.factory.namedNode(iri)); } diff --git a/src/data/rdf/vocabulary.ts b/src/data/rdf/vocabulary.ts index 3c09f202..9fe8a9d8 100644 --- a/src/data/rdf/vocabulary.ts +++ b/src/data/rdf/vocabulary.ts @@ -1,3 +1,16 @@ +const NAMESPACE_OWL = 'http://www.w3.org/2002/07/owl#'; +/** + * Vocabulary for common terms from `owl: ` namespace. + * + * @category Constants + */ +export const owl = { + $namespace: NAMESPACE_OWL, + Class: `${NAMESPACE_OWL}Class`, + DatatypeProperty: `${NAMESPACE_OWL}DatatypeProperty`, + ObjectProperty: `${NAMESPACE_OWL}ObjectProperty`, +} as const; + const NAMESPACE_RDF = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#'; /** * Vocabulary for common terms from `rdf: ` namespace. @@ -5,7 +18,8 @@ const NAMESPACE_RDF = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#'; * @category Constants */ export const rdf = { - namespace: NAMESPACE_RDF, + $namespace: NAMESPACE_RDF, + Property: `${NAMESPACE_RDF}Property`, langString: `${NAMESPACE_RDF}langString`, type: `${NAMESPACE_RDF}type`, JSON: `${NAMESPACE_RDF}JSON`, @@ -18,8 +32,10 @@ const NAMESPACE_RDFS = 'http://www.w3.org/2000/01/rdf-schema#'; * @category Constants */ export const rdfs = { - namespace: NAMESPACE_RDFS, + $namespace: NAMESPACE_RDFS, + Class: `${NAMESPACE_RDFS}Class`, label: `${NAMESPACE_RDFS}label`, + subClassOf: `${NAMESPACE_RDFS}subClassOf`, } as const; const NAMESPACE_SCHEMA = 'http://schema.org/'; @@ -29,7 +45,7 @@ const NAMESPACE_SCHEMA = 'http://schema.org/'; * @category Constants */ export const schema = { - namespace: NAMESPACE_SCHEMA, + $namespace: NAMESPACE_SCHEMA, thumbnailUrl: `${NAMESPACE_SCHEMA}thumbnailUrl`, } as const; @@ -40,6 +56,6 @@ const NAMESPACE_XSD = 'http://www.w3.org/2001/XMLSchema#'; * @category Constants */ export const xsd = { - namespace: NAMESPACE_XSD, + $namespace: NAMESPACE_XSD, string: `${NAMESPACE_XSD}string`, } as const; diff --git a/src/editor/authoringState.ts b/src/editor/authoringState.ts index 4b0528c2..9291ef9e 100644 --- a/src/editor/authoringState.ts +++ b/src/editor/authoringState.ts @@ -315,11 +315,18 @@ export namespace AuthoringState { } const newState = clone(state); const previous = state.links.get(before); - newState.links.set(before, { - type: 'relationChange', - before: (previous && previous.type === 'relationChange') ? previous.before : before, - data: after, - }); + if (previous?.type === 'relationAdd') { + newState.links.set(before, { + type: 'relationAdd', + data: after, + }); + } else { + newState.links.set(before, { + type: 'relationChange', + before: (previous && previous.type === 'relationChange') ? previous.before : before, + data: after, + }); + } return newState; } diff --git a/src/editor/editorController.tsx b/src/editor/editorController.tsx index 25ad92c0..700f47cf 100644 --- a/src/editor/editorController.tsx +++ b/src/editor/editorController.tsx @@ -478,6 +478,9 @@ export class EditorController { }): RelationLink { const {model} = this; const {link, newSource} = params; + if (link.data.sourceId === newSource.data.id) { + return link; + } const batch = model.history.startBatch( TranslatedText.text('editor_controller.relation_move_source.command') ); @@ -500,6 +503,9 @@ export class EditorController { }): RelationLink { const {model} = this; const {link, newTarget} = params; + if (link.data.targetId === newTarget.data.id) { + return link; + } const batch = model.history.startBatch( TranslatedText.text('editor_controller.relation_move_target.command') ); diff --git a/src/editor/overlayController.tsx b/src/editor/overlayController.tsx index acfadcc4..7c506551 100644 --- a/src/editor/overlayController.tsx +++ b/src/editor/overlayController.tsx @@ -97,6 +97,11 @@ export class OverlayController { this.hideDialog(); } }); + this.listener.listen(this.model.events, 'discardGraph', () => { + if (this.openedDialog && this.openedDialog.target) { + this.hideDialog(); + } + }); view.setCanvasWidget('selectionHandler', { element: , diff --git a/src/widgets/classTree/classTree.tsx b/src/widgets/classTree/classTree.tsx index 182cd9da..c4b5682a 100644 --- a/src/widgets/classTree/classTree.tsx +++ b/src/widgets/classTree/classTree.tsx @@ -408,7 +408,10 @@ class ClassTreeInner extends React.Component { const {workspace: {model, view, editor, getCommandBus}} = this.props; const batch = model.history.startBatch(); + this.createElementCancellation.abort(); + this.createElementCancellation = new AbortController(); const signal = this.createElementCancellation.signal; + const elementModel = await mapAbortedToNull( editor.metadataProvider!.createEntity(elementType, {signal}), signal diff --git a/styles/widgets/_classTree.scss b/styles/widgets/_classTree.scss index 7d572835..6bc9a596 100644 --- a/styles/widgets/_classTree.scss +++ b/styles/widgets/_classTree.scss @@ -146,7 +146,7 @@ &__create-button { margin-left: 5px; - padding: 5px; + padding: 5px 4px 5px 5px; cursor: move; &::before {