From c26cdb5c435c6ad6d72caa0518491f6892b2a9b3 Mon Sep 17 00:00:00 2001 From: Alexey Morozov Date: Sun, 29 Mar 2026 22:15:58 +0300 Subject: [PATCH] Add `placeCreatedEntity` to `ClassTree` / `SearchSectionElementTypes`, expose `fixedElements` in `performLayout()` --- CHANGELOG.md | 2 ++ src/widgets/classTree/classTree.tsx | 20 +++++++++-- .../unifiedSearch/builtinSearchSections.tsx | 18 +++++++++- src/workspace/defaultWorkspace.tsx | 34 +++++++++---------- src/workspace/workspace.tsx | 5 +-- src/workspace/workspaceContext.ts | 5 +++ 6 files changed, 62 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dec96753..da5f9808 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p - Fix `SelectionAction*` components not updating on selection change when provided as children to `Halo`. #### 💅 Polish +- Allow to control the placement of newly created entity elements from `ClassTree` and `SearchSectionElementTypes` via `placeCreatedEntity` prop. +- Expose `fixedElements` option for `performLayout()` to be able to constrain elements not to move (if supported by the layout function). - Improve ARIA-attributes and other accessibility interaction: * Allow to resize and toggle `WorkspaceLayout*` with a keyboard; * Change `WorkspaceLayout*` to be `
` elements with `aria-label` (i.e. regions); diff --git a/src/widgets/classTree/classTree.tsx b/src/widgets/classTree/classTree.tsx index e1020e98..465e0886 100644 --- a/src/widgets/classTree/classTree.tsx +++ b/src/widgets/classTree/classTree.tsx @@ -16,7 +16,8 @@ import { Element } from '../../diagram/elements'; import { Vector, SizeProvider } from '../../diagram/geometry'; import { HtmlSpinner } from '../../diagram/spinner'; -import { DataDiagramModel } from '../../editor/dataDiagramModel'; +import type { DataDiagramModel } from '../../editor/dataDiagramModel'; +import type { EntityElement } from '../../editor/dataElements'; import { NoSearchResults } from '../utility/noSearchResults'; import { ProgressBar, ProgressState } from '../utility/progressBar'; @@ -65,6 +66,13 @@ export interface ClassTreeProps { * @default true */ draggableItems?: boolean; + /** + * Handler to place newly created entity from a tree. + */ + placeCreatedEntity?: ( + element: EntityElement, + dropEvent: CanvasDropEvent | undefined + ) => Promise; } /** @@ -428,7 +436,11 @@ class ClassTreeInner extends React.Component { elementType: ElementTypeIri, dropEvent?: CanvasDropEvent ): Promise { - const {workspace: {model, view, editor, getCommandBus}, translation: t} = this.props; + const { + placeCreatedEntity, + workspace: {model, view, editor, getCommandBus}, + translation: t, + } = this.props; const batch = model.history.startBatch(); this.createElementCancellation.abort(); @@ -469,6 +481,10 @@ class ClassTreeInner extends React.Component { moveElementCenterToPosition(element, targetPosition, canvas.renderingState); } + if (placeCreatedEntity) { + await placeCreatedEntity(element, dropEvent); + } + batch.store(); model.setSelection([element]); getCommandBus(VisualAuthoringTopic) diff --git a/src/widgets/unifiedSearch/builtinSearchSections.tsx b/src/widgets/unifiedSearch/builtinSearchSections.tsx index a69eb51f..e38de8b5 100644 --- a/src/widgets/unifiedSearch/builtinSearchSections.tsx +++ b/src/widgets/unifiedSearch/builtinSearchSections.tsx @@ -2,6 +2,9 @@ import * as React from 'react'; import { EventObserver } from '../../coreUtils/events'; +import type { CanvasDropEvent } from '../../diagram/canvasApi'; +import type { EntityElement } from '../../editor/dataElements'; + import { ClassTree } from '../classTree'; import { InstancesSearch, SearchCriteria } from '../instancesSearch'; import { LinkTypesToolbox } from '../linksToolbox'; @@ -37,8 +40,20 @@ export function SearchSectionElementTypes(props: { * @default true */ draggableItems?: boolean; + /** + * Handler to place newly created entity from an element type tree. + */ + placeCreatedEntity?: ( + element: EntityElement, + dropEvent: CanvasDropEvent | undefined + ) => Promise; }) { - const {searchTimeout = 200, minSearchTermLength = 2, draggableItems} = props; + const { + searchTimeout = 200, + minSearchTermLength = 2, + draggableItems, + placeCreatedEntity, + } = props; const {searchStore, shouldRender} = useUnifiedSearchSection({ searchTimeout, allowSubmit: term => term.length >= minSearchTermLength, @@ -48,6 +63,7 @@ export function SearchSectionElementTypes(props: { ) : null; } diff --git a/src/workspace/defaultWorkspace.tsx b/src/workspace/defaultWorkspace.tsx index 51fbfcd1..ef6f3a0d 100644 --- a/src/workspace/defaultWorkspace.tsx +++ b/src/workspace/defaultWorkspace.tsx @@ -147,23 +147,23 @@ export interface DefaultWorkspaceProps extends BaseDefaultWorkspaceProps { * { * sections: [ * { - * key: 'elementTypes', - * label: t.text('default_workspace.search_section_entity_types.label'), - * title: t.text('default_workspace.search_section_entity_types.title'), - * component: , - * }, - * { - * key: 'entities', - * label: t.text('default_workspace.search_section_entities.label'), - * title: t.text('default_workspace.search_section_entities.title'), - * component: , - * }, - * { - * key: 'linkTypes', - * label: t.text('default_workspace.search_section_link_types.label'), - * title: t.text('default_workspace.search_section_link_types.title'), - * component: , - * } + * key: 'elementTypes', + * label: t.text('default_workspace.search_section_entity_types.label'), + * title: t.text('default_workspace.search_section_entity_types.title'), + * component: , + * }, + * { + * key: 'entities', + * label: t.text('default_workspace.search_section_entities.label'), + * title: t.text('default_workspace.search_section_entities.title'), + * component: , + * }, + * { + * key: 'linkTypes', + * label: t.text('default_workspace.search_section_link_types.label'), + * title: t.text('default_workspace.search_section_link_types.title'), + * component: , + * }, * ] * } * ``` diff --git a/src/workspace/workspace.tsx b/src/workspace/workspace.tsx index 1f48b970..2c61ec40 100644 --- a/src/workspace/workspace.tsx +++ b/src/workspace/workspace.tsx @@ -389,8 +389,8 @@ export class Workspace extends React.Component { private onPerformLayout: WorkspaceContext['performLayout'] = async params => { const { - canvas: targetCanvas, layoutFunction, selectedElements, animate, signal, - zoomToFit = true, + canvas: targetCanvas, layoutFunction, selectedElements, fixedElements, + animate, signal, zoomToFit = true, } = params; const {model, view, overlay, disposeSignal} = this.workspaceContext; const t = this.translation; @@ -412,6 +412,7 @@ export class Workspace extends React.Component { layoutFunction: layoutFunction ?? view.defaultLayout, model, selectedElements, + fixedElements, sizeProvider: canvas.renderingState, typeProvider: this.layoutTypeProvider, signal: signal ?? disposeSignal, diff --git a/src/workspace/workspaceContext.ts b/src/workspace/workspaceContext.ts index 010ab97d..68ee5ff5 100644 --- a/src/workspace/workspaceContext.ts +++ b/src/workspace/workspaceContext.ts @@ -135,6 +135,11 @@ export interface WorkspacePerformLayoutParams { * Restrict the layout application to the subset of graph elements. */ selectedElements?: ReadonlySet; + /** + * Set of elements which should not be moved by layout algorithm + * (if supported). + */ + fixedElements?: ReadonlySet; /** * Whether moving elements to final layout positions should be animated. *