From d96ee86ec0b2417e03a97fa681e62a65f4127977 Mon Sep 17 00:00:00 2001 From: Alexey Morozov Date: Sat, 1 Nov 2025 18:38:53 +0300 Subject: [PATCH] Follow-up for annotation links: avoid link type creation, allow to be renamed: * Avoid eager link type creation for relation links (only create and fetch them after being rendered and subscribed to); * Exclude annotation links from UI to control link type visibility, exclude visibility for non-relation types from diagram export; * Add `DefaultRenameLinkProvider` and use it by default to allow to change annotation link labels; * Fix missing link path highlight in `HaloLink` for annotation links; * Refactor link templates: - Add `StandardLinkTemplate` with `StandardRelation` as default (fallback) template for the links; - Deprecate aliases `DefaultLinkTemplate` and `DefaultLink` to standard templates; - Add `LinkMarkerArrowhead` and `BasicLink` components as base bulding blocks for simple links rendering; - Change CSS class for `StandardRelation` from `reactodia-default-link` to `reactodia-standard-link`. --- CHANGELOG.md | 4 + examples/classicWorkspace.tsx | 5 +- examples/graphAuthoring.tsx | 5 +- examples/styleCustomization.tsx | 2 +- src/diagram/linkLayer.tsx | 118 ++++++++---------- src/editor/dataDiagramModel.ts | 26 ++-- src/forms/renameLinkForm.tsx | 32 +++-- src/legacy-styles.tsx | 32 ++--- src/templates/basicLink.tsx | 61 +++++++++ src/templates/noteAnnotation.tsx | 68 ++++++++-- ...tLinkTemplate.tsx => standardRelation.tsx} | 85 ++++++++----- src/widgets/haloLink.tsx | 49 ++++---- src/widgets/linksToolbox.tsx | 8 +- src/workspace.ts | 11 +- src/workspace/workspace.tsx | 35 ++++-- styles/main.scss | 4 +- styles/templates/_noteAnnotation.scss | 10 +- .../{_standard.scss => _standardEntity.scss} | 0 ...efaultLink.scss => _standardRelation.scss} | 2 +- 19 files changed, 365 insertions(+), 192 deletions(-) create mode 100644 src/templates/basicLink.tsx rename src/templates/{defaultLinkTemplate.tsx => standardRelation.tsx} (79%) rename styles/templates/{_standard.scss => _standardEntity.scss} (100%) rename styles/templates/{_defaultLink.scss => _standardRelation.scss} (96%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 03ed3d16..e31f86ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p * `DataDiagramModel.importLayout()` will accept known cell types via `elementCellTypes` and `linkCellTypes` to import. - Add `AnnotationElement` and `AnnotationLink` diagram cell types representing diagram-only elements and links which exports and imports with the diagram but does not exists in the data graph: * Rendered by default with new built-in templates `NoteTemplate` and `NoteLinkTemplate` which use `NoteAnnotation`, `NoteEntity` and `NoteLink` template components; + * Add `DefaultRenameLinkProvider` and use it by default to allow to change annotation link labels; * Support annotation elements in `SelectionActionEstablishLink` and new `SelectionActionAnnotate` components; * Support annotation links in `LinkActionDelete`, `LinkActionMoveEndpoint` components. - Support user-resizable element templates with `ElementSize` template state property: @@ -30,6 +31,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p * Subscribe to canvas `resize` event to track viewport size; * Subscribe to `changeCells` event from `DiagramModel` to track graph content changes. - Add `TemplateProps.onlySelected` flag to use in the element templates to track if the element is the only one selected without performance penalty. +- Avoid eager link type creation for relation links, only create and fetch them on first render. #### 💅 Polish - Make dialogs fill the available viewport when the viewport width is small: @@ -45,6 +47,8 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p * `changeExpanded` event is removed from element events, use `changeElementState` event instead; * When exporting the diagram the expanded state is serialized only with `elementState` while using `isExpanded` property when importing the diagram for backward compatibility. - Introduce `ElementTemplate.supports` property for templates to tell its capabilities such as ability to expand/collapse or resized by user. +- Deprecate `DefaultLinkTemplate` and `DefaultLink` and alias them to `StandardLinkTemplate` and `StandardRelation`: + * Change CSS class for default link template from `reactodia-default-link` to `reactodia-standard-link`. - Move "expand/collapse on double click" global element behavior to `StandardEntity` and `ClassicEntity` implementation only. - Add `setTemplateProperty()` utility function to easily set or unset template state property. diff --git a/examples/classicWorkspace.tsx b/examples/classicWorkspace.tsx index 684b945c..99ad28b3 100644 --- a/examples/classicWorkspace.tsx +++ b/examples/classicWorkspace.tsx @@ -77,7 +77,10 @@ function ClassicWorkspaceExample() { class RenameSubclassOfProvider extends Reactodia.RenameLinkToLinkStateProvider { override canRename(link: Reactodia.Link): boolean { - return link.typeId === 'http://www.w3.org/2000/01/rdf-schema#subClassOf'; + return ( + link instanceof Reactodia.AnnotationLink || + link.typeId === 'http://www.w3.org/2000/01/rdf-schema#subClassOf' + ); } } diff --git a/examples/graphAuthoring.tsx b/examples/graphAuthoring.tsx index 6234748b..778be6b3 100644 --- a/examples/graphAuthoring.tsx +++ b/examples/graphAuthoring.tsx @@ -73,7 +73,10 @@ function GraphAuthoringExample() { class RenameSubclassOfProvider extends Reactodia.RenameLinkToLinkStateProvider { override canRename(link: Reactodia.Link): boolean { - return link.typeId === 'http://www.w3.org/2000/01/rdf-schema#subClassOf'; + return ( + link instanceof Reactodia.AnnotationLink || + link.typeId === 'http://www.w3.org/2000/01/rdf-schema#subClassOf' + ); } } diff --git a/examples/styleCustomization.tsx b/examples/styleCustomization.tsx index 590a6d16..932b9cf7 100644 --- a/examples/styleCustomization.tsx +++ b/examples/styleCustomization.tsx @@ -165,7 +165,7 @@ const DoubleArrowLinkTemplate: Reactodia.LinkTemplate = { height: 12, }, renderLink: props => ( - { - private readonly listener = new EventObserver(); - private readonly delayedUpdate = new Debouncer(); - - render() { - const {model, renderingState} = this.props; +}) { + const {model, renderingState} = props; - const sourceMarkers = new Set(); - const targetMarkers = new Set(); + const [cellsVersion, setCellsVersion] = React.useState(model.cellsVersion); - for (const link of model.links) { - const template = renderingState.getLinkTemplate(link); - const defaultTemplate = renderingState.shared.defaultLinkResolver(link); - - const markerSource = template.markerSource ?? defaultTemplate.markerSource; - if (markerSource) { - sourceMarkers.add(markerSource); - } - - const markerTarget = template.markerTarget ?? defaultTemplate.markerTarget; - if (markerTarget) { - targetMarkers.add(markerTarget); - } - } - - return ( - - {Array.from(sourceMarkers, marker => { - const index = renderingState.ensureLinkMarkerIndex(marker); - return ( - - ); - })} - {Array.from(targetMarkers, marker => { - const index = renderingState.ensureLinkMarkerIndex(marker); - return ( - - ); - })} - - ); - } + React.useEffect(() => { + const listener = new EventObserver(); + const delayedUpdate = new Debouncer(); - componentDidMount() { - const {renderingState} = this.props; - this.listener.listen(renderingState.events, 'syncUpdate', ({layer}) => { + listener.listen(renderingState.events, 'syncUpdate', ({layer}) => { if (layer !== RenderingLayer.Link) { return; } - this.delayedUpdate.runSynchronously(); + delayedUpdate.runSynchronously(); }); - this.listener.listen(renderingState.events, 'changeLinkTemplates', () => { - this.delayedUpdate.call(() => this.forceUpdate()); + listener.listen(renderingState.events, 'changeLinkTemplates', () => { + delayedUpdate.call(() => setCellsVersion(model.cellsVersion)); }); - } + }, []); - shouldComponentUpdate() { - return false; - } + const sourceMarkers = new Set(); + const targetMarkers = new Set(); - componentWillUnmount() { - this.listener.stopListening(); - this.delayedUpdate.dispose(); + for (const link of model.links) { + const template = renderingState.getLinkTemplate(link); + const defaultTemplate = renderingState.shared.defaultLinkResolver(link); + + const markerSource = template.markerSource ?? defaultTemplate.markerSource; + if (markerSource) { + sourceMarkers.add(markerSource); + } + + const markerTarget = template.markerTarget ?? defaultTemplate.markerTarget; + if (markerTarget) { + targetMarkers.add(markerTarget); + } } + + return ( + + {Array.from(sourceMarkers, marker => { + const index = renderingState.ensureLinkMarkerIndex(marker); + return ( + + ); + })} + {Array.from(targetMarkers, marker => { + const index = renderingState.ensureLinkMarkerIndex(marker); + return ( + + ); + })} + + ); } +export const LinkMarkers = React.memo(LinkMarkersInner, () => true); + interface LinkMarkerProps { markerIndex: number; isStartMarker: boolean; diff --git a/src/editor/dataDiagramModel.ts b/src/editor/dataDiagramModel.ts index fcabd602..677475c5 100644 --- a/src/editor/dataDiagramModel.ts +++ b/src/editor/dataDiagramModel.ts @@ -475,7 +475,11 @@ export class DataDiagramModel extends DiagramModel implements DataGraphStructure * @see {@link importLayout} */ exportLayout(): SerializedDiagram { - const knownLinkTypes = new Set(this.graph.getLinks().map(link => link.typeId)); + const knownLinkTypes = new Set( + this.graph.getLinks() + .filter(link => link instanceof RelationLink || link instanceof RelationGroup) + .map(link => link.typeId) + ); const linkTypeVisibility = new Map(); for (const linkTypeIri of knownLinkTypes) { linkTypeVisibility.set(linkTypeIri, this.getLinkVisibility(linkTypeIri)); @@ -543,10 +547,10 @@ export class DataDiagramModel extends DiagramModel implements DataGraphStructure this.setLinkVisibility(linkTypeIri, visibility); } - const usedLinkTypes = new Set(); for (const link of links) { - const linkType = this.createLinkType(link.typeId); - usedLinkTypes.add(linkType.id); + if (link instanceof RelationLink || link instanceof RelationGroup) { + this.createLinkType(link.typeId); + } } for (const element of elements) { @@ -562,7 +566,9 @@ export class DataDiagramModel extends DiagramModel implements DataGraphStructure private hideUnusedLinkTypes(knownLinkTypes: ReadonlyArray): void { const usedTypes = new Set(); for (const link of this.graph.getLinks()) { - usedTypes.add(link.typeId); + if (link instanceof RelationLink || link instanceof RelationGroup) { + usedTypes.add(link.typeId); + } } for (const linkType of knownLinkTypes) { @@ -641,22 +647,12 @@ export class DataDiagramModel extends DiagramModel implements DataGraphStructure return element; } - override addLink(link: Link): void { - // TODO: postpone creating link type until first render - // the same way as with element types - this.createLinkType(link.typeId); - super.addLink(link); - } - private onLinkInfoLoaded(links: LinkModel[]) { let allowToCreate: boolean; const cancel = () => { allowToCreate = false; }; const batch = this.history.startBatch(); for (const linkModel of links) { - // TODO: postpone creating link type until first render - // the same way as with element types - this.createLinkType(linkModel.linkTypeId); allowToCreate = true; this.extendedSource.trigger('createLoadedLink', {source: this, model: linkModel, cancel}); if (allowToCreate) { diff --git a/src/forms/renameLinkForm.tsx b/src/forms/renameLinkForm.tsx index 6d9db06b..c2b70dd0 100644 --- a/src/forms/renameLinkForm.tsx +++ b/src/forms/renameLinkForm.tsx @@ -3,7 +3,7 @@ import * as React from 'react'; import { useEventStore, useSyncStore } from '../coreUtils/hooks'; import { Link } from '../diagram/elements'; - +import { RelationLink, RelationGroup } from '../editor/dataElements'; import { useWorkspace } from '../workspace/workspaceContext'; const FORM_CLASS = 'reactodia-form'; @@ -16,17 +16,9 @@ export interface RenameLinkFormProps { export function RenameLinkForm(props: RenameLinkFormProps) { const {link, onFinish} = props; - const {model, view: {renameLinkProvider}, translation: t} = useWorkspace(); - - const linkType = model.getLinkType(link.typeId); - const linkTypeChangeStore = useEventStore(linkType?.events, 'changeData'); - const linkTypeLabel = useSyncStore(linkTypeChangeStore, () => linkType?.data?.label); - - const defaultLabel = React.useMemo( - () => t.formatLabel(linkTypeLabel, link.typeId, model.language), - [link, linkTypeLabel, model.language] - ); + const {view: {renameLinkProvider}, translation: t} = useWorkspace(); + const defaultLabel = useDefaultLinkLabel(link); const [customLabel, setCustomLabel] = React.useState( renameLinkProvider?.getLabel(link) ?? defaultLabel ); @@ -66,3 +58,21 @@ export function RenameLinkForm(props: RenameLinkFormProps) { ); } + +function useDefaultLinkLabel(link: Link): string { + const {model, translation: t} = useWorkspace(); + const linkType = link instanceof RelationLink || link instanceof RelationGroup + ? model.getLinkType(link.typeId) : undefined; + const linkTypeChangeStore = useEventStore(linkType?.events, 'changeData'); + const linkTypeLabel = useSyncStore(linkTypeChangeStore, () => linkType?.data?.label); + return React.useMemo( + () => { + if (link instanceof RelationLink || link instanceof RelationGroup) { + return t.formatLabel(linkTypeLabel, link.typeId, model.language); + } else { + return ''; + } + }, + [link, linkTypeLabel, model.language] + ); +} diff --git a/src/legacy-styles.tsx b/src/legacy-styles.tsx index 898ce47d..2074cbf4 100644 --- a/src/legacy-styles.tsx +++ b/src/legacy-styles.tsx @@ -68,9 +68,9 @@ export const SemanticTypeStyles: TypeStyleResolver = types => { */ export function makeLinkStyleShowIri(Reactodia: typeof import('./workspace')): LinkTemplate { return { - ...Reactodia.DefaultLinkTemplate, + ...Reactodia.StandardLinkTemplate, renderLink: props => ( - ( - ( - ( - ( - ; + /** + * Additional children for the component. + */ + children?: React.ReactNode; +} + +/** + * Basic link template which displays a {@link link path} with editable geometry (vertices) + * without any labels. + * + * @category Components + */ +export function BasicLink(props: BasicLinkProps) { + const { + link, className, path, pathProps, markerSource, markerTarget, children, + } = props; + const stroke = pathProps?.stroke; + return ( + + + {link.vertices.length === 0 ? null : ( + + )} + {children} + + ); +} diff --git a/src/templates/noteAnnotation.tsx b/src/templates/noteAnnotation.tsx index b258661c..ba4382c2 100644 --- a/src/templates/noteAnnotation.tsx +++ b/src/templates/noteAnnotation.tsx @@ -12,11 +12,12 @@ import { import { useCanvas } from '../diagram/canvasApi'; import { setElementState } from '../diagram/commands'; import type { - ElementTemplate, LinkTemplate, LinkTemplateProps, TemplateProps, + ElementTemplate, LinkTemplate, TemplateProps, } from '../diagram/customization'; import { ElementDecoration } from '../diagram/elementLayer'; import { Rect, Size, boundsOf } from '../diagram/geometry'; import { Command } from '../diagram/history'; +import { LinkLabel, type LinkLabelProps } from '../diagram/linkLayer'; import { AnnotationElement, AnnotationLink } from '../editor/annotationCells'; import { EntityElement } from '../editor/dataElements'; @@ -26,7 +27,7 @@ import { useAuthoredEntity } from '../widgets/visualAuthoring/authoredEntity'; import { useWorkspace } from '../workspace/workspaceContext'; -import { DefaultLink } from './defaultLinkTemplate'; +import { BasicLink, type BasicLinkProps } from './basicLink'; /** * Element template to display an {@link AnnotationElement} on a canvas. @@ -375,15 +376,64 @@ export const NoteLinkTemplate: LinkTemplate = { const LINK_CLASS = 'reactodia-note-link'; -export function NoteLink(props: LinkTemplateProps) { +/** + * Props for {@link NoteLink} component. + * + * @see {@link NoteLink} + */ +export interface NoteLinkProps extends BasicLinkProps { + /** + * Props for the primary link label. + * + * By default the label is not shown unless + * {@link LinkLabelProps.children} is specified or + * the link is renamed with {@link RenameLinkProvider}. + * + * @see {@link LinkLabelProps.primary} + */ + primaryLabelProps?: NoteLinkLabelStyle; +} + +/** + * Additional style props for the link labels in {@link StandardRelation}. + * + * @see {@link StandardRelationProps} + */ +type NoteLinkLabelStyle = + Omit & + Partial>; + +/** + * Displays a link for an {@link AnnotationLink} on the canvas. + * + * {@link RenameLinkProvider} can be used to display a custom label which will + * override a default one from {@link NoteLinkProps.primaryLabelProps}. + * + * @category Components + * @see {@link NoteTemplate} + */ +export function NoteLink(props: NoteLinkProps) { + const {link, getPathPosition, route, primaryLabelProps} = props; + const {canvas} = useCanvas(); + const {renameLinkProvider} = canvas.renderingState.shared; + const label = renameLinkProvider?.getLabel(link); + const labelContent = label ?? primaryLabelProps?.children + ?? (renameLinkProvider?.canRename(link) ? '' : undefined); return ( - + }}> + {labelContent === undefined ? null : ( + + {labelContent} + + )} + ); } diff --git a/src/templates/defaultLinkTemplate.tsx b/src/templates/standardRelation.tsx similarity index 79% rename from src/templates/defaultLinkTemplate.tsx rename to src/templates/standardRelation.tsx index e2384b14..4ec448de 100644 --- a/src/templates/defaultLinkTemplate.tsx +++ b/src/templates/standardRelation.tsx @@ -3,11 +3,11 @@ import * as React from 'react'; import { useKeyedSyncStore } from '../coreUtils/keyedObserver'; -import { PropertyTypeIri } from '../data/model'; +import type { PropertyTypeIri } from '../data/model'; import { TemplateProperties } from '../data/schema'; -import { LinkTemplate, LinkTemplateProps } from '../diagram/customization'; -import { LinkLabel, LinkLabelProps } from '../diagram/linkLayer'; +import type { LinkTemplate, LinkTemplateProps } from '../diagram/customization'; +import { LinkLabel, type LinkLabelProps } from '../diagram/linkLayer'; import { LinkPath, LinkVertices } from '../diagram/linkLayer'; import { RelationGroup, RelationLink } from '../editor/dataElements'; @@ -16,34 +16,36 @@ import { WithFetchStatus } from '../editor/withFetchStatus'; import { useWorkspace } from '../workspace/workspaceContext'; +import { LinkMarkerArrowhead } from './basicLink'; + /** - * Default link template. + * Default link template to display a {@link RelationLink} or + * {@link RelationGroup} on the canvas. * - * Uses {@link DefaultLink} to display the link itself. + * Uses {@link StandardRelation} to display the link itself. * * @category Constants - * @see {@link DefaultLink} + * @see {@link StandardRelation} */ -export const DefaultLinkTemplate: LinkTemplate = { - markerTarget: { - d: 'M0,0 L0,8 L9,4 z', - width: 9, - height: 8, - }, - renderLink: props => , +export const StandardLinkTemplate: LinkTemplate = { + markerTarget: LinkMarkerArrowhead, + renderLink: props => , }; -const CLASS_NAME = 'reactodia-default-link'; - -const PROPERTY_CLASS = `${CLASS_NAME}__property`; -const LABEL_CLASS = `${CLASS_NAME}__label`; +/** + * An alias for {@link StandardLinkTemplate}. + * + * @category Constants + * @deprecated Use {@link StandardLinkTemplate} directly instead. + */ +export const DefaultLinkTemplate = StandardLinkTemplate; /** - * Props for {@link DefaultLink} component. + * Props for {@link StandardRelation} component. * - * @see {@link DefaultLink} + * @see {@link StandardRelation} */ -export interface DefaultLinkProps extends LinkTemplateProps { +export interface StandardRelationProps extends LinkTemplateProps { /** * Additional CSS class for the component. */ @@ -57,12 +59,12 @@ export interface DefaultLinkProps extends LinkTemplateProps { * * @see {@link LinkLabelProps.primary} */ - primaryLabelProps?: CustomizedLinkLabelProps; + primaryLabelProps?: StandardRelationLabelStyle; /** * Additional props for each label displaying a property from * a relation link data. */ - propertyLabelProps?: CustomizedLinkLabelProps; + propertyLabelProps?: StandardRelationLabelStyle; /** * Starting row shift when displaying relation link data properties. * @@ -77,18 +79,26 @@ export interface DefaultLinkProps extends LinkTemplateProps { * {@link propertyLabelStartLine} to avoid overlapping prepended and data labels. */ prependLabels?: React.ReactNode; + /** + * Additional children for the component. + */ + children?: React.ReactNode; } /** - * Additional style props for the link labels in {@link DefaultLink}. + * Additional style props for the link labels in {@link StandardRelation}. * - * @see {@link DefaultLinkProps} + * @see {@link StandardRelationProps} */ -type CustomizedLinkLabelProps = Omit< +type StandardRelationLabelStyle = Omit< LinkLabelProps, - 'primary' | 'link' | 'position' | 'line' | 'content' + 'primary' | 'link' | 'position' | 'line' | 'children' >; +const CLASS_NAME = 'reactodia-standard-link'; +const PROPERTY_CLASS = `${CLASS_NAME}__property`; +const LABEL_CLASS = `${CLASS_NAME}__label`; + /** * Default link template component. * @@ -103,11 +113,12 @@ type CustomizedLinkLabelProps = Omit< * * @category Components */ -export function DefaultLink(props: DefaultLinkProps) { +export function StandardRelation(props: StandardRelationProps) { const { link, className, path, pathProps, markerSource, markerTarget, getPathPosition, route, primaryLabelProps, prependLabels = null, + children, } = props; const {model, view: {renameLinkProvider}, translation: t} = useWorkspace(); @@ -194,11 +205,29 @@ export function DefaultLink(props: DefaultLinkProps) { {link.vertices.length === 0 ? null : ( )} + {children} ); } -function LinkProperties(props: DefaultLinkProps) { +/** + * An alias for {@link StandardRelationProps}. + * + * @deprecated Use {@link StandardRelationProps} directly instead. + */ +export interface DefaultLinkProps extends StandardRelationProps {} + +/** + * An alias for {@link StandardLink}. + * + * @category Components + * @deprecated Use {@link StandardLink} directly instead. + */ +export function DefaultLink(props: StandardRelationProps) { + return ; +} + +function LinkProperties(props: StandardRelationProps) { const { link, getPathPosition, route, propertyLabelProps, propertyLabelStartLine = 1, diff --git a/src/widgets/haloLink.tsx b/src/widgets/haloLink.tsx index 16dbf7d1..a461e594 100644 --- a/src/widgets/haloLink.tsx +++ b/src/widgets/haloLink.tsx @@ -305,25 +305,24 @@ function LinkHighlight(props: LinkHighlightProps) { () => canvas.renderingState.getLinkLabelBounds(link) ); - if (!labelBounds) { - return null; + let labelHighlightStyle: React.CSSProperties | undefined; + if (labelBounds) { + const {x: x0, y: y0} = canvas.metrics.paperToScrollablePaneCoords( + labelBounds.x, + labelBounds.y + ); + const {x: x1, y: y1} = canvas.metrics.paperToScrollablePaneCoords( + labelBounds.x + labelBounds.width, + labelBounds.y + labelBounds.height + ); + labelHighlightStyle = { + left: x0 - margin, + top: y0 - margin, + width: x1 - x0 + margin * 2, + height: y1 - y0 + margin * 2, + }; } - const {x: x0, y: y0} = canvas.metrics.paperToScrollablePaneCoords( - labelBounds.x, - labelBounds.y - ); - const {x: x1, y: y1} = canvas.metrics.paperToScrollablePaneCoords( - labelBounds.x + labelBounds.width, - labelBounds.y + labelBounds.height - ); - const labelHighlightStyle: React.CSSProperties = { - left: x0 - margin, - top: y0 - margin, - width: x1 - x0 + margin * 2, - height: y1 - y0 + margin * 2, - }; - return <> - -
-
-
- + {labelHighlightStyle && ( + +
+
+
+ + )} ; } diff --git a/src/widgets/linksToolbox.tsx b/src/widgets/linksToolbox.tsx index afd4d110..62783f95 100644 --- a/src/widgets/linksToolbox.tsx +++ b/src/widgets/linksToolbox.tsx @@ -9,7 +9,7 @@ import type { ElementIri, ElementModel, LinkTypeIri } from '../data/model'; import { changeLinkTypeVisibility } from '../diagram/commands'; import { Element, LinkTypeVisibility } from '../diagram/elements'; -import { LinkType, iterateEntitiesOf } from '../editor/dataElements'; +import { LinkType, RelationGroup, RelationLink, iterateEntitiesOf } from '../editor/dataElements'; import { WithFetchStatus } from '../editor/withFetchStatus'; import { InstancesSearchTopic } from '../workspace/commandBusTopic'; @@ -356,11 +356,9 @@ function applyFilter(state: State, term: string, props: LinkTypesToolboxInnerPro const allLinkTypeIris = new Set(); for (const link of model.links) { - if (link.typeId.startsWith('urn:reactodia:')) { - // Skip built-in link types - continue; + if (link instanceof RelationLink || link instanceof RelationGroup) { + allLinkTypeIris.add(link.typeId); } - allLinkTypeIris.add(link.typeId); } const termFilter = makeCaseInsensitiveFilter(term); diff --git a/src/workspace.ts b/src/workspace.ts index cde6d985..509f605e 100644 --- a/src/workspace.ts +++ b/src/workspace.ts @@ -158,15 +158,17 @@ export { SerializedLink, SerializableLinkCell, LinkFromJsonOptions, } from './editor/serializedDiagram'; +export { BasicLink, type BasicLinkProps, LinkMarkerArrowhead } from './templates/basicLink'; export { ClassicTemplate, ClassicEntity, ClassicEntityProps } from './templates/classicTemplate'; -export { - DefaultLinkTemplate, DefaultLink, DefaultLinkProps, -} from './templates/defaultLinkTemplate'; export { GroupPaginator, GroupPaginatorProps } from './templates/groupPaginator'; export { NoteTemplate, NoteAnnotation, NoteEntity, NoteLinkTemplate, NoteLink, } from './templates/noteAnnotation'; export { RoundTemplate, RoundEntity, RoundEntityProps } from './templates/roundTemplate'; +export { + StandardLinkTemplate, StandardRelation, type StandardRelationProps, + DefaultLinkTemplate, DefaultLink, type DefaultLinkProps, +} from './templates/standardRelation'; export { StandardTemplate, StandardEntity, StandardEntityProps, StandardEntityGroup, StandardEntityGroupProps, @@ -261,7 +263,8 @@ export { } from './workspace/classicWorkspace'; export { DefaultWorkspace, DefaultWorkspaceProps } from './workspace/defaultWorkspace'; export { - Workspace, WorkspaceProps, LoadedWorkspace, LoadedWorkspaceParams, useLoadedWorkspace, + Workspace, WorkspaceProps, DefaultRenameLinkProvider, + LoadedWorkspace, LoadedWorkspaceParams, useLoadedWorkspace, } from './workspace/workspace'; export { WorkspaceContext, WorkspaceEventKey, WorkspacePerformLayoutParams, diff --git a/src/workspace/workspace.tsx b/src/workspace/workspace.tsx index 6acb3760..df7758cb 100644 --- a/src/workspace/workspace.tsx +++ b/src/workspace/workspace.tsx @@ -13,7 +13,7 @@ import { ValidationProvider } from '../data/validationProvider'; import { RestoreGeometry, restoreViewport } from '../diagram/commands'; import { TypeStyleResolver, RenameLinkProvider } from '../diagram/customization'; -import { Element } from '../diagram/elements'; +import { Element, Link } from '../diagram/elements'; import { CommandHistory, InMemoryHistory } from '../diagram/history'; import { CalculatedLayout, LayoutFunction, LayoutTypeProvider, calculateLayout, applyLayout, @@ -21,7 +21,7 @@ import { import { DefaultTranslation, DefaultTranslationBundle, TranslationProvider, } from '../diagram/locale'; -import { SharedCanvasState } from '../diagram/sharedCanvasState'; +import { RenameLinkToLinkStateProvider, SharedCanvasState } from '../diagram/sharedCanvasState'; import { AnnotationElement, AnnotationLink } from '../editor/annotationCells'; import { DataDiagramModel } from '../editor/dataDiagramModel'; @@ -33,7 +33,7 @@ import { import { OverlayController } from '../editor/overlayController'; import { NoteTemplate, NoteLinkTemplate } from '../templates/noteAnnotation'; -import { DefaultLinkTemplate } from '../templates/defaultLinkTemplate'; +import { StandardLinkTemplate } from '../templates/standardRelation'; import { StandardTemplate } from '../templates/standardTemplate'; import type { CommandBusTopic } from './commandBusTopic'; @@ -50,7 +50,7 @@ export interface WorkspaceProps { /** * Overrides default command history implementation. * - * By default, it uses {@link InMemoryHistory} instance. + * By default, {@link InMemoryHistory} instance is used. */ history?: CommandHistory; /** @@ -77,8 +77,12 @@ export interface WorkspaceProps { validationProvider?: ValidationProvider; /** * Provides a strategy to rename diagram links (change labels). + * + * By default, {@link DefaultRenameLinkProvider} instance is used. + * + * If specified as `null`, the default provider would not be used. */ - renameLinkProvider?: RenameLinkProvider; + renameLinkProvider?: RenameLinkProvider | null; /** * Overrides how a single label gets selected from multiple of them based on target language. */ @@ -198,10 +202,12 @@ export class Workspace extends React.Component { if (link instanceof AnnotationLink) { return NoteLinkTemplate; } - return DefaultLinkTemplate; + return StandardLinkTemplate; }, defaultLayout, - renameLinkProvider, + renameLinkProvider: renameLinkProvider === null ? undefined : ( + renameLinkProvider ?? new DefaultRenameLinkProvider() + ), }); const editor = new EditorController({ @@ -586,3 +592,18 @@ export function useLoadedWorkspace( return stateRef.current.loadedWorkspace; } + +/** + * Default {@link RenameLinkProvider} implementation for the {@link Workspace workspace}. + * + * Unless overriden, it allows to rename {@link AnnotationLink} graph links + * and stores the changed label in the {@link Link.linkState link template state}. + * + * @see {@link WorkspaceProps.renameLinkProvider} + * @see {@link RenameLinkToLinkStateProvider} + */ +export class DefaultRenameLinkProvider extends RenameLinkToLinkStateProvider { + override canRename(link: Link): boolean { + return link instanceof AnnotationLink; + } +} diff --git a/styles/main.scss b/styles/main.scss index c11cdf62..bd796790 100644 --- a/styles/main.scss +++ b/styles/main.scss @@ -53,8 +53,8 @@ @forward "workspace/workspace"; @forward "templates/classic"; -@forward "templates/defaultLink"; @forward "templates/groupPaginator"; @forward "templates/noteAnnotation"; @forward "templates/round"; -@forward "templates/standard"; +@forward "templates/standardEntity"; +@forward "templates/standardRelation"; diff --git a/styles/templates/_noteAnnotation.scss b/styles/templates/_noteAnnotation.scss index e37093be..4ba19d3c 100644 --- a/styles/templates/_noteAnnotation.scss +++ b/styles/templates/_noteAnnotation.scss @@ -106,12 +106,16 @@ } .reactodia-note-link { - &__path { - --reactodia-link-stroke-color: #{theme.$color-gray-500}; + /* Use :not([attr]) selector to avoid overriding the attribute due to SVG style priority. */ + &__path:not([stroke]) { + stroke: theme.$color-gray-500; + } + &__path:not([stroke-width]) { stroke-width: 2; } &__label { - display: none; + color: theme.$color-emphasis-600; + font-weight: bold; } } diff --git a/styles/templates/_standard.scss b/styles/templates/_standardEntity.scss similarity index 100% rename from styles/templates/_standard.scss rename to styles/templates/_standardEntity.scss diff --git a/styles/templates/_defaultLink.scss b/styles/templates/_standardRelation.scss similarity index 96% rename from styles/templates/_defaultLink.scss rename to styles/templates/_standardRelation.scss index 20ba5ae2..034ea125 100644 --- a/styles/templates/_defaultLink.scss +++ b/styles/templates/_standardRelation.scss @@ -1,6 +1,6 @@ @use "../theme/theme"; -.reactodia-default-link { +.reactodia-standard-link { &--group .reactodia-link-path { stroke-width: 2px; }