diff --git a/CHANGELOG.md b/CHANGELOG.md index 42bd07de..00836411 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,6 +42,8 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p - Add `changeTransform` event to `CanvasApi.events` which triggers on `CanvasApi.metrics.getTransform()` changes, i.e. when coordinate mapping changes due to scale or canvas size is re-adjusted. - Add `DiagramModel.cellsVersion` property which updates on every element or link addition/removal/reordering to be able to subscribe to `changeCells` event with `useSyncStore()` hook. - Deprecate `canvasWidgets` prop on `DefaultWorkspace` and `ClassicWorkspace` in favor of passing widgets directly as children. +- Mark placeholder entity data with `PlaceholderDataProperty` property key to distinguish not-loaded-yet elements with `EntityElement.isPlaceholderData()`: + * Add `DataDiagramModel.requestData()` as a convenient method to load all placeholder entities at once. - Move expanded element state from distinct property on `Element` to be stored in `Element.elementState` with `TemplateProperties.Expanded` property: * All existing properties, methods and commands works as before but use element template state as storage for expanded state; * `changeExpanded` event is removed from element events, use `changeElementState` event instead; diff --git a/examples/basic.tsx b/examples/basic.tsx index 520d7df7..c246bedb 100644 --- a/examples/basic.tsx +++ b/examples/basic.tsx @@ -29,9 +29,9 @@ function BasicExample() { await model.createNewDiagram({dataProvider, signal}); const elementTypeId = 'http://www.w3.org/2002/07/owl#Class'; for (const {element} of await dataProvider.lookup({elementTypeId})) { - model.createElement(element); + model.createElement(element.id); } - await model.requestLinks(); + await model.requestData(); // Layout elements on canvas await performLayout({signal}); diff --git a/examples/graphAuthoring.tsx b/examples/graphAuthoring.tsx index 7725e5aa..aa1ba294 100644 --- a/examples/graphAuthoring.tsx +++ b/examples/graphAuthoring.tsx @@ -36,18 +36,18 @@ function GraphAuthoringExample() { }); if (!diagram) { - const elements = [ - model.createElement('http://www.w3.org/ns/org#Organization'), - model.createElement('http://www.w3.org/ns/org#FormalOrganization'), - model.createElement('http://www.w3.org/ns/org#hasMember'), - model.createElement('http://www.w3.org/ns/org#hasSubOrganization'), - model.createElement('http://www.w3.org/ns/org#subOrganizationOf'), - model.createElement('http://www.w3.org/ns/org#unitOf'), + const entities = [ + 'http://www.w3.org/ns/org#Organization', + 'http://www.w3.org/ns/org#FormalOrganization', + 'http://www.w3.org/ns/org#hasMember', + 'http://www.w3.org/ns/org#hasSubOrganization', + 'http://www.w3.org/ns/org#subOrganizationOf', + 'http://www.w3.org/ns/org#unitOf', ]; - await Promise.all([ - model.requestElementData(elements.map(el => el.iri)), - model.requestLinks(), - ]); + for (const entity of entities) { + model.createElement(entity); + } + await model.requestData(); await performLayout({signal}); } }, []); diff --git a/examples/stressTest.tsx b/examples/stressTest.tsx index 8a6ad012..9701e02c 100644 --- a/examples/stressTest.tsx +++ b/examples/stressTest.tsx @@ -47,10 +47,7 @@ function StressTestExample() { })); } batch.store(); - await Promise.all([ - model.requestElementData(nodes), - model.requestLinks(), - ]); + await model.requestData(); model.history.reset(); const canvas = view.findAnyCanvas(); diff --git a/src/data/schema.ts b/src/data/schema.ts index f80641d9..83e40168 100644 --- a/src/data/schema.ts +++ b/src/data/schema.ts @@ -1,4 +1,4 @@ -import type { ElementTypeIri, LinkTypeIri } from './model'; +import type { ElementTypeIri, LinkTypeIri, PropertyTypeIri } from './model'; /** * [JSON-LD](https://json-ld.org/) context IRI (`@context` value) for the @@ -7,6 +7,13 @@ import type { ElementTypeIri, LinkTypeIri } from './model'; * @category Constants */ export const DiagramContextV1 = 'https://ontodia.org/context/v1.json'; +/** + * Property type to mark placeholder (i.e. not loaded yet) entity data. + * + * @category Constants + * @see {@link EntityElement.placeholderData} + */ +export const PlaceholderDataProperty: PropertyTypeIri = 'urn:reactodia:isPlaceholder'; /** * Type for a newly created temporary entity in graph authoring mode. * diff --git a/src/editor/dataDiagramModel.ts b/src/editor/dataDiagramModel.ts index 677475c5..b0a015a6 100644 --- a/src/editor/dataDiagramModel.ts +++ b/src/editor/dataDiagramModel.ts @@ -578,11 +578,36 @@ export class DataDiagramModel extends DiagramModel implements DataGraphStructure } } + /** + * Requests to load all {@link EntityElement entities} with + * {@link EntityElement.isPlaceholderData placeholder data} and all + * {@link RelationLink relations} connected to them. + * + * @see {@link DataDiagramModel.requestElementData} + * @see {@link DataDiagramModel.requestLinks} + */ + async requestData(): Promise { + const targets = new Set(); + for (const element of this.elements) { + for (const entity of iterateEntitiesOf(element)) { + if (EntityElement.isPlaceholderData(entity)) { + targets.add(entity.id); + } + } + } + + const elements = Array.from(targets); + await Promise.all([ + this.fetcher.fetchElementData(targets), + this.requestLinks({addedElements: elements}), + ]); + } + /** * Requests to fetch the data for the specified elements from a data provider. */ requestElementData(elementIris: ReadonlyArray): Promise { - return this.fetcher.fetchElementData(elementIris); + return this.fetcher.fetchElementData(new Set(elementIris)); } /** @@ -618,8 +643,8 @@ export class DataDiagramModel extends DiagramModel implements DataGraphStructure /** * Creates or gets an existing entity element on the diagram. * - * If element is specified as an IRI only, then the placeholder data will - * be used. + * If element is specified as an IRI only, then the + * {@link EntityElement.placeholderData placeholder data} will be used. * * If multiple entity elements with the same IRI is on the diagram, * the first one in the order will be returned. diff --git a/src/editor/dataElements.ts b/src/editor/dataElements.ts index f739611b..ff304788 100644 --- a/src/editor/dataElements.ts +++ b/src/editor/dataElements.ts @@ -9,6 +9,7 @@ import { PropertyTypeIri, PropertyTypeModel, equalLinks, hashLink, } from '../data/model'; +import { PlaceholderDataProperty } from '../data/schema'; import { Element, ElementEvents, ElementProps, ElementTemplateState, @@ -64,15 +65,35 @@ export class EntityElement extends Element { * * This data can be used to display an entity in the UI * until the actual data is loaded from a data provider. + * + * @see {@link PlaceholderDataProperty} */ static placeholderData(iri: ElementIri): ElementModel { return { id: iri, types: [], - properties: {}, + properties: { + [PlaceholderDataProperty]: [], + }, }; } + /** + * Returns `true` if the `data` is an empty placeholder (not yet loaded) data, + * otherwise `false`. + * + * The entity data is considered to be a placeholder data if `data.properties` + * contains `PlaceholderDataProperty` key with a empty or non-empty values. + * + * @see {@link PlaceholderDataProperty} + */ + static isPlaceholderData(data: ElementModel): boolean { + return ( + Object.prototype.hasOwnProperty.call(data.properties, PlaceholderDataProperty) && + data.properties[PlaceholderDataProperty] !== undefined + ); + } + protected get entitySource(): EventSource { return this.source as EventSource; } diff --git a/src/editor/dataFetcher.ts b/src/editor/dataFetcher.ts index 735f18e5..c2431335 100644 --- a/src/editor/dataFetcher.ts +++ b/src/editor/dataFetcher.ts @@ -6,6 +6,7 @@ import { ElementIri, ElementTypeIri, LinkTypeIri, PropertyTypeIri, } from '../data/model'; import { DataProvider } from '../data/dataProvider'; +import { PlaceholderDataProperty } from '../data/schema'; import { Graph } from '../diagram/graph'; @@ -264,39 +265,44 @@ export class DataFetcher { return reasons; } - fetchElementData(elementIris: ReadonlyArray): Promise { - if (elementIris.length === 0) { + fetchElementData(targets: ReadonlySet): Promise { + if (targets.size === 0) { return Promise.resolve(); } const operation: FetchOperationElement = { type: 'element', - targets: new Set(elementIris), + targets, }; const task = this.dataProvider - .elements({elementIds: [...elementIris], signal: this.signal}) - .then(this.onElementInfoLoaded); + .elements({elementIds: Array.from(targets), signal: this.signal}) + .then(result => this.onElementInfoLoaded(targets, result)); this.addOperation(operation, task); return task; } - private onElementInfoLoaded = (elements: Map) => { + private onElementInfoLoaded( + targets: ReadonlySet, + elements: Map + ): void { for (const element of this.graph.getElements()) { if (element instanceof EntityElement) { const loadedModel = elements.get(element.iri); if (loadedModel) { element.setData(loadedModel); + } else if (targets.has(element.iri)) { + element.setData(unsetPlaceholder(element.data)); } } else if (element instanceof EntityGroup) { let hasLoadedModel = false; for (const item of element.items) { - if (elements.has(item.data.id)) { + if (targets.has(item.data.id)) { hasLoadedModel = true; } } if (hasLoadedModel) { const loadedItems = element.items.map((item): EntityGroupItem => { - const loadedData = elements.get(item.data.id); - return loadedData ? {...item, data: loadedData} : item; + const nextData = elements.get(item.data.id) ?? unsetPlaceholder(item.data); + return nextData === item.data ? item : {...item, data: nextData}; }); element.setItems(loadedItems); } @@ -360,3 +366,11 @@ export class DataFetcher { } }; } + +function unsetPlaceholder(data: ElementModel): ElementModel { + if (EntityElement.isPlaceholderData(data)) { + const {[PlaceholderDataProperty]: _, ...restProperties} = data.properties; + return {...data, properties: restProperties}; + } + return data; +} diff --git a/src/workspace.ts b/src/workspace.ts index 422288a7..e0ff19ec 100644 --- a/src/workspace.ts +++ b/src/workspace.ts @@ -33,7 +33,7 @@ export { type ValidatedLink, type ValidationSeverity, } from './data/validationProvider'; export { - DiagramContextV1, PlaceholderEntityType, PlaceholderRelationType, + DiagramContextV1, PlaceholderDataProperty, PlaceholderEntityType, PlaceholderRelationType, TemplateProperties, type PinnedProperties, type AnnotationContent, type AnnotationTextStyle, type ColorVariant, setTemplateProperty, } from './data/schema';