From d6232de86ddf13c38d57fd284545fb87201cd1b2 Mon Sep 17 00:00:00 2001 From: Alexey Morozov Date: Sat, 14 Mar 2026 17:56:03 +0300 Subject: [PATCH] Support `order`, `uniqueLang` and `defaultValue` in entity/relation property authoring: * Allow to order properties with `MetadataPropertyShape.order`; * Allow to specify default value with `MetadataValueShape.defaultValue`; * Provide `source` and `target` entities to `MetadataProvider.getRelationShape()`. * Display language selector in `FormInputText` only when shape `datatype` is `rdf:langString` or `uniqueLang` is set to either `true` or `false`. --- CHANGELOG.md | 9 ++++++++- examples/resources/exampleMetadata.ts | 2 +- src/data/metadataProvider.ts | 20 ++++++++++++++++++-- src/forms/editRelationForm.tsx | 7 ++++++- src/forms/input/formInputGroup.tsx | 13 ++++++++++++- src/forms/input/formInputList.tsx | 8 ++++---- src/forms/input/formInputText.tsx | 2 +- 7 files changed, 50 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ac714b2f..c173a8c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ All notable changes to the Reactodia will be documented in this document. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). ## [Unreleased] +#### 🚀 New Features +- Allow to open "Edit entity" dialog from `VisualAuthoring` for an entity not present on the canvas (e.g. to edit related or well-known entities). +- Extends metadata support for editing an entity or relation properties: + * Allow to order properties with `MetadataPropertyShape.order`; + * Allow to specify default value with `MetadataValueShape.defaultValue`; + * Provide `source` and `target` entities to `MetadataProvider.getRelationShape()`. + #### 🐛 Fixed - Fix stale rendering (i.e. missing links after moving an endpoint until an element move) due to the lost scheduled layer updates. - Fix unable to scroll inside canvas components and templates when `requireCtrl` in `zoomOptions` is set `false`. @@ -17,7 +24,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p - Remove superfluous "Type" fields from the default "Edit entity" dialog. - Provide `translation` and current `language` to `ValidationProvider.validate()` method. - Allow to specify related property IRI when returning a validation item for a relation from `ValidationProvider`. -- Allow to open "Edit entity" dialog from `VisualAuthoring` for an entity not present on the canvas (e.g. to edit related or well-known entities). +- Display language selector in `FormInputText` only when shape `datatype` is `rdf:langString` or `uniqueLang` is set to either `true` or `false` (the selector will be hidden by default when `datatype` is `xsd:string`). ## [0.32.0] - 2026-03-10 #### 🐛 Fixed diff --git a/examples/resources/exampleMetadata.ts b/examples/resources/exampleMetadata.ts index d56d3e36..d4e76c74 100644 --- a/examples/resources/exampleMetadata.ts +++ b/examples/resources/exampleMetadata.ts @@ -158,7 +158,7 @@ export class ExampleMetadataProvider extends Reactodia.BaseMetadataProvider { properties, }; }, - getRelationShape: async (linkType, {signal}) => { + getRelationShape: async (linkType, source, target, {signal}) => { await Reactodia.delay(SIMULATED_DELAY, {signal: signal}); const properties = new Map(); if (this.editableRelations.has(linkType)) { diff --git a/src/data/metadataProvider.ts b/src/data/metadataProvider.ts index 817a7e0e..45e99cdf 100644 --- a/src/data/metadataProvider.ts +++ b/src/data/metadataProvider.ts @@ -58,6 +58,8 @@ export interface MetadataProvider { getRelationShape( linkType: LinkTypeIri, + source: ElementModel, + target: ElementModel, options: { readonly signal?: AbortSignal } ): Promise; @@ -128,13 +130,25 @@ export interface MetadataPropertyShape { * @default Infinity */ readonly maxCount?: number; + /** + * Relative order for the property compared the other for the display purposes. + * + * If `undefined` or have the same `order` value, the properties will be ordered + * based on their labels. + */ + readonly order?: number; } export type MetadataValueShape = - | { readonly termType: 'NamedNode' } + | { + readonly termType: 'NamedNode'; + readonly defaultValue?: Rdf.NamedNode; + } | { readonly termType: 'Literal'; readonly datatype?: Rdf.NamedNode; + readonly uniqueLang?: boolean; + readonly defaultValue?: Rdf.Literal; }; /** @@ -241,10 +255,12 @@ export class BaseMetadataProvider implements MetadataProvider { async getRelationShape( linkType: LinkTypeIri, + source: ElementModel, + target: ElementModel, options: { readonly signal?: AbortSignal; } ): Promise { if (this.methods.getRelationShape) { - return this.methods.getRelationShape(linkType, options); + return this.methods.getRelationShape(linkType, source, target, options); } return { properties: this.emptyProperties, diff --git a/src/forms/editRelationForm.tsx b/src/forms/editRelationForm.tsx index dc62a755..041379ba 100644 --- a/src/forms/editRelationForm.tsx +++ b/src/forms/editRelationForm.tsx @@ -324,7 +324,12 @@ function useRelationMetadata( mapAbortedToNull( Promise.all([ metadataProvider.canModifyRelation(link, linkSource, linkTarget, {signal}), - metadataProvider.getRelationShape(link.linkTypeId, {signal}), + metadataProvider.getRelationShape( + link.linkTypeId, + linkSource, + linkTarget, + {signal} + ), ]), signal ).then( diff --git a/src/forms/input/formInputGroup.tsx b/src/forms/input/formInputGroup.tsx index 31121255..85b8aa4b 100644 --- a/src/forms/input/formInputGroup.tsx +++ b/src/forms/input/formInputGroup.tsx @@ -72,7 +72,7 @@ export function FormInputGroup(props: FormInputGroupProps) { } } - labelledProperties.sort((a, b) => a.label.localeCompare(b.label)); + labelledProperties.sort(compareProperties); return (
; } +function compareProperties(a: LabelledProperty, b: LabelledProperty): number { + if (!(a.shape.order === undefined && b.shape.order === undefined)) { + const aOrder = a.shape.order ?? Infinity; + const bOrder = b.shape.order ?? Infinity; + if (aOrder !== bOrder) { + return aOrder - bOrder; + } + } + return a.label.localeCompare(b.label); +} + function Property(props: { iri: PropertyTypeIri; label: string; diff --git a/src/forms/input/formInputList.tsx b/src/forms/input/formInputList.tsx index c5dd5f54..a8d86495 100644 --- a/src/forms/input/formInputList.tsx +++ b/src/forms/input/formInputList.tsx @@ -91,7 +91,7 @@ function FormInputListInner(props: FormInputListProps) { )} title={t.text('visual_authoring.property.add_value.title')} onClick={() => updateValues(previous => { - return [...previous, makeEmptyTerm(shape.valueShape, factory)]; + return [...previous, makeDefaultTerm(shape.valueShape, factory)]; })} />
@@ -117,16 +117,16 @@ export const FormInputList = React.memo( ) ); -function makeEmptyTerm( +function makeDefaultTerm( valueShape: MetadataValueShape, factory: Rdf.DataFactory ): Rdf.NamedNode | Rdf.Literal { switch (valueShape.termType) { case 'NamedNode': { - return factory.namedNode(''); + return valueShape.defaultValue ?? factory.namedNode(''); } default: { - return factory.literal('', valueShape.datatype); + return valueShape.defaultValue ?? factory.literal('', valueShape.datatype); } } } diff --git a/src/forms/input/formInputText.tsx b/src/forms/input/formInputText.tsx index a3c7d220..6a06c40e 100644 --- a/src/forms/input/formInputText.tsx +++ b/src/forms/input/formInputText.tsx @@ -51,7 +51,7 @@ export function FormInputText(props: FormInputTextProps) { const hasLanguageSelector = valueShape.termType === 'Literal' && ( !valueShape.datatype || valueShape.datatype.value === rdf.langString || - valueShape.datatype.value === xsd.string + valueShape.uniqueLang !== undefined ); return (