This is an automated email from the ASF dual-hosted git repository.
tiagobento pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/incubator-kie-tools.git
The following commit(s) were added to refs/heads/main by this push:
new b108e14b3dd kie-issues#831: Make the new DMN Editor actually generate
an SVG representing the currently open DRD (#2124)
b108e14b3dd is described below
commit b108e14b3dd3dfe9352b0e792f735ecd7b2ec0a7
Author: Tiago Bento <[email protected]>
AuthorDate: Fri Jan 19 09:55:01 2024 -0500
kie-issues#831: Make the new DMN Editor actually generate an SVG
representing the currently open DRD (#2124)
---
.../dmn-editor-envelope/src/DmnEditorFactory.tsx | 23 +-
packages/dmn-editor-envelope/src/DmnEditorRoot.tsx | 8 +
packages/dmn-editor/package.json | 1 +
packages/dmn-editor/src/DmnEditor.css | 6 +-
packages/dmn-editor/src/DmnEditor.tsx | 74 +-
packages/dmn-editor/src/diagram/Diagram.tsx | 1588 ++++++++++----------
.../dmn-editor/src/diagram/edges/EdgeMarkers.tsx | 102 +-
packages/dmn-editor/src/diagram/maths/DmnMaths.ts | 3 -
.../dmn-editor/src/diagram/nodes/DefaultSizes.ts | 2 +-
.../src/diagram/nodes/EditableNodeLabel.tsx | 11 +-
packages/dmn-editor/src/diagram/nodes/NodeStyle.ts | 194 ++-
packages/dmn-editor/src/diagram/nodes/NodeSvgs.tsx | 34 +-
packages/dmn-editor/src/diagram/nodes/Nodes.tsx | 54 +-
packages/dmn-editor/src/store/useDiagramData.tsx | 4 +
packages/dmn-editor/src/svg/DmnDiagramSvg.tsx | 290 ++++
pnpm-lock.yaml | 49 +
16 files changed, 1472 insertions(+), 971 deletions(-)
diff --git a/packages/dmn-editor-envelope/src/DmnEditorFactory.tsx
b/packages/dmn-editor-envelope/src/DmnEditorFactory.tsx
index bc35756f9a9..b5012e4cb2f 100644
--- a/packages/dmn-editor-envelope/src/DmnEditorFactory.tsx
+++ b/packages/dmn-editor-envelope/src/DmnEditorFactory.tsx
@@ -55,28 +55,7 @@ export class DmnEditorInterface implements Editor {
// Not in-editor
public getPreview(): Promise<string | undefined> {
- return Promise.resolve(`
-<svg
- xmlns="http://www.w3.org/2000/svg"
- xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1"
- width="540" height="540" viewBox="0 0 540 540" xml:space="preserve">
- <g transform="matrix(1 0 0 1 270 270)"
id="ee1530d3-d469-49de-b8ad-62ffb6e5db7a"></g>
- <g transform="matrix(1 0 0 1 270 270)"
id="b6eca5e2-94e1-4e3f-a04e-16bc0ada9ea4">
- <rect style="stroke: none; stroke-width: 1; stroke-dasharray: none;
stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: miter;
stroke-miterlimit: 4; fill: rgb(255,255,255); fill-rule: nonzero; opacity: 1;
visibility: hidden;" vector-effect="non-scaling-stroke" x="-540" y="-540"
rx="0" ry="0" width="1080" height="1080" />
- </g>
- <g transform="matrix(0.68 0 0 0.68 270 192.07)" style=""
id="12bd02ec-b291-4d62-acdc-3fdd30cc84d7" >
- <text xml:space="preserve" font-family="Raleway" font-size="105"
font-style="normal" font-weight="900" style="stroke: none; stroke-width: 1;
stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0;
stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(0,0,0); fill-rule:
nonzero; opacity: 1; white-space: pre;" >
- <tspan x="-187.11" y="-26.34" >Not yet</tspan>
- <tspan x="-321.65" y="92.31" >implemented</tspan>
- </text>
- </g>
- <g transform="matrix(1 0 0 1 200 354.97)" style=""
id="b2ea8c5b-9fc6-43c0-9e3f-5837adab8b51" >
- <text xml:space="preserve" font-family="Alegreya" font-size="38"
font-style="normal" font-weight="700" style="stroke: none; stroke-width: 1;
stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0;
stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(0,0,0); fill-rule:
nonzero; opacity: 1; white-space: pre;" >
- <tspan x="-190" y="-11.04" style="white-space: pre; ">Use the legacy DMN
Editor to </tspan>
- <tspan x="-170" y="38.68" >generate SVGs temporarily.</tspan>
- </text>
- </g>
-</svg>`);
+ return this.self.getDiagramSvg();
}
public async validate(): Promise<Notification[]> {
diff --git a/packages/dmn-editor-envelope/src/DmnEditorRoot.tsx
b/packages/dmn-editor-envelope/src/DmnEditorRoot.tsx
index 367febf5be6..958e86cb072 100644
--- a/packages/dmn-editor-envelope/src/DmnEditorRoot.tsx
+++ b/packages/dmn-editor-envelope/src/DmnEditorRoot.tsx
@@ -75,9 +75,12 @@ export type DmnEditorRootState = {
export class DmnEditorRoot extends React.Component<DmnEditorRootProps,
DmnEditorRootState> {
private readonly externalModelsManagerDoneBootstraping =
imperativePromiseHandle<void>();
+ private readonly dmnEditorRef: React.RefObject<DmnEditor.DmnEditorRef>;
+
constructor(props: DmnEditorRootProps) {
super(props);
props.exposing(this);
+ this.dmnEditorRef = React.createRef();
this.state = {
externalModelsByNamespace: {},
marshaller: undefined,
@@ -99,6 +102,10 @@ export class DmnEditorRoot extends
React.Component<DmnEditorRootProps, DmnEditor
this.setState((prev) => ({ ...prev, pointer: Math.min(prev.stack.length -
1, prev.pointer + 1) }));
}
+ public async getDiagramSvg(): Promise<string | undefined> {
+ return this.dmnEditorRef.current?.getDiagramSvg();
+ }
+
public async getContent(): Promise<string> {
if (!this.state.marshaller || !this.model) {
throw new Error(
@@ -270,6 +277,7 @@ export class DmnEditorRoot extends
React.Component<DmnEditorRootProps, DmnEditor
{this.model && (
<>
<DmnEditor.DmnEditor
+ ref={this.dmnEditorRef}
originalVersion={this.state.marshaller?.originalVersion}
model={this.model}
externalModelsByNamespace={this.state.externalModelsByNamespace}
diff --git a/packages/dmn-editor/package.json b/packages/dmn-editor/package.json
index db8f700bcdc..8a25ed3216e 100644
--- a/packages/dmn-editor/package.json
+++ b/packages/dmn-editor/package.json
@@ -43,6 +43,7 @@
"@patternfly/react-core": "^4.276.6",
"@patternfly/react-icons": "^4.93.6",
"@patternfly/react-styles": "^4.92.6",
+ "@visx/text": "^3.3.0",
"d3-drag": "^3.0.0",
"d3-selection": "^3.0.0",
"immer": "^10.0.3",
diff --git a/packages/dmn-editor/src/DmnEditor.css
b/packages/dmn-editor/src/DmnEditor.css
index 58f840323ae..861f8f78c2b 100644
--- a/packages/dmn-editor/src/DmnEditor.css
+++ b/packages/dmn-editor/src/DmnEditor.css
@@ -886,7 +886,7 @@ th {
.react-flow__node > .kie-dmn-editor--node-shape.drop-target-invalid {
border-color: red !important;
}
-.react-flow__node > .kie-dmn-editor--node-shape.drop-target-invalid > g * {
+.react-flow__node > .kie-dmn-editor--node-shape.drop-target-invalid > * {
stroke: rgba(255, 0, 0, 0.2) !important;
fill: rgba(164, 0, 0, 0.1) !important;
}
@@ -898,12 +898,12 @@ th {
border-color: #006ba4 !important;
filter: drop-shadow(2px 2px 2px #006ba477);
}
-.react-flow__node > .kie-dmn-editor--node-shape.drop-target > g * {
+.react-flow__node > .kie-dmn-editor--node-shape.drop-target > * {
stroke: #006ba4 !important;
fill: rgba(0, 107, 164, 0.1) !important;
}
-.react-flow__node.selected:not(.react-flow__node-node_unknown) >
.kie-dmn-editor--node-shape > g * {
+.react-flow__node.selected:not(.react-flow__node-node_unknown) >
.kie-dmn-editor--node-shape > * {
stroke: #006ba4 !important;
}
diff --git a/packages/dmn-editor/src/DmnEditor.tsx
b/packages/dmn-editor/src/DmnEditor.tsx
index 520bc1fdeef..522838bb0f6 100644
--- a/packages/dmn-editor/src/DmnEditor.tsx
+++ b/packages/dmn-editor/src/DmnEditor.tsx
@@ -1,7 +1,28 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
import "@patternfly/react-core/dist/styles/base.css";
import "reactflow/dist/style.css";
import * as React from "react";
+import * as ReactDOM from "react-dom";
+import * as RF from "reactflow";
import { useCallback, useEffect, useImperativeHandle, useRef, useState,
useMemo } from "react";
import { Drawer, DrawerContent, DrawerContentBody } from
"@patternfly/react-core/dist/js/components/Drawer";
import { Tab, TabTitleIcon, TabTitleText, Tabs } from
"@patternfly/react-core/dist/js/components/Tabs";
@@ -10,7 +31,7 @@ import { InfrastructureIcon } from
"@patternfly/react-icons/dist/js/icons/infras
import { PficonTemplateIcon } from
"@patternfly/react-icons/dist/js/icons/pficon-template-icon";
import { BoxedExpression } from "./boxedExpressions/BoxedExpression";
import { DataTypes } from "./dataTypes/DataTypes";
-import { Diagram } from "./diagram/Diagram";
+import { Diagram, DiagramRef } from "./diagram/Diagram";
import { DmnVersionLabel } from "./diagram/DmnVersionLabel";
import { IncludedModels } from "./includedModels/IncludedModels";
import { DiagramPropertiesPanel } from
"./propertiesPanel/DiagramPropertiesPanel";
@@ -38,10 +59,15 @@ import { original } from "immer";
import "@kie-tools/dmn-marshaller/dist/kie-extensions"; // This is here
because of the KIE Extension for DMN.
import "./DmnEditor.css"; // Leave it for last, as this overrides some of the
PF and RF styles.
+import { DmnDiagramSvg } from "./svg/DmnDiagramSvg";
+
const ON_MODEL_CHANGE_DEBOUNCE_TIME_IN_MS = 500;
+const SVG_PADDING = 20;
+
export type DmnEditorRef = {
reset: (mode: DmnLatestModel) => void;
+ getDiagramSvg: () => Promise<string | undefined>;
};
export type EvaluationResults = Record<string, any>;
@@ -140,16 +166,53 @@ export const DmnEditorInternal = ({
const { boxedExpressionEditor, dmn, navigation, dispatch, diagram } =
useDmnEditorStore((s) => s);
const dmnEditorStoreApi = useDmnEditorStoreApi();
- const { isDiagramEditingInProgress } = useDmnEditorDerivedStore();
+ const { isDiagramEditingInProgress, importsByNamespace } =
useDmnEditorDerivedStore();
const { dmnModelBeforeEditingRef, dmnEditorRootElementRef } = useDmnEditor();
+ // Refs
+
+ const diagramRef = useRef<DiagramRef>(null);
+ const diagramContainerRef = useRef<HTMLDivElement>(null);
+ const beeContainerRef = useRef<HTMLDivElement>(null);
+
// Allow imperativelly controlling the Editor.
useImperativeHandle(
forwardRef,
() => ({
reset: (model) => dispatch.dmn.reset(model),
+ getDiagramSvg: async () => {
+ const nodes = diagramRef.current?.getReactFlowInstance()?.getNodes();
+ const edges = diagramRef.current?.getReactFlowInstance()?.getEdges();
+ if (!nodes || !edges) {
+ return undefined;
+ }
+
+ const bounds = RF.getRectOfNodes(nodes);
+
+ const svg = document.createElementNS("http://www.w3.org/2000/svg",
"svg");
+ svg.setAttribute("width", bounds.width + SVG_PADDING * 2 + "");
+ svg.setAttribute("height", bounds.height + SVG_PADDING * 2 + "");
+
+ // We're still on React 17.
+ // eslint-disable-next-line react/no-deprecated
+ ReactDOM.render(
+ // Indepdent of where the nodes are located, they'll always be
rendered at the top-left corner of the SVG
+ <g transform={`translate(${-bounds.x + SVG_PADDING} ${-bounds.y +
SVG_PADDING})`}>
+ <DmnDiagramSvg
+ nodes={nodes}
+ edges={edges}
+ snapGrid={diagram.snapGrid}
+ importsByNamespace={importsByNamespace}
+ thisDmn={dmnEditorStoreApi.getState().dmn}
+ />
+ </g>,
+ svg
+ );
+
+ return new XMLSerializer().serializeToString(svg);
+ },
}),
- [dispatch.dmn]
+ [diagram.snapGrid, dispatch.dmn, dmnEditorStoreApi, importsByNamespace]
);
// Make sure the DMN Editor reacts to props changing.
@@ -206,9 +269,6 @@ export const DmnEditorInternal = ({
[dmnEditorStoreApi]
);
- const diagramContainerRef = useRef<HTMLDivElement>(null);
- const beeContainerRef = useRef<HTMLDivElement>(null);
-
const tabTitle = useMemo(() => {
return {
editor: (
@@ -265,7 +325,7 @@ export const DmnEditorInternal = ({
<DrawerContentBody>
<div className={"kie-dmn-editor--diagram-container"}
ref={diagramContainerRef}>
{originalVersion && <DmnVersionLabel
version={originalVersion} />}
- <Diagram container={diagramContainerRef} />
+ <Diagram ref={diagramRef}
container={diagramContainerRef} />
</div>
</DrawerContentBody>
</DrawerContent>
diff --git a/packages/dmn-editor/src/diagram/Diagram.tsx
b/packages/dmn-editor/src/diagram/Diagram.tsx
index eb1c5ebd87b..13757d1cf4f 100644
--- a/packages/dmn-editor/src/diagram/Diagram.tsx
+++ b/packages/dmn-editor/src/diagram/Diagram.tsx
@@ -147,422 +147,471 @@ const edgeTypes: Record<EdgeType, any> = {
[EDGE_TYPES.association]: AssociationEdge,
};
-export function Diagram({ container }: { container:
React.RefObject<HTMLElement> }) {
- // Contexts
-
- const dmnEditorStoreApi = useDmnEditorStoreApi();
- const diagram = useDmnEditorStore((s) => s.diagram);
- const thisDmn = useDmnEditorStore((s) => s.dmn);
-
- const { dmnModelBeforeEditingRef } = useDmnEditor();
-
- const {
- dmnShapesByHref,
- nodesById,
- selectedNodesById,
- selectedEdgesById,
- edgesById,
- nodes,
- edges,
- isDropTargetNodeValidForSelection,
- isDiagramEditingInProgress,
- selectedNodeTypes,
- externalDmnsByNamespace,
- drgElementsWithoutVisualRepresentationOnCurrentDrd,
- } = useDmnEditorDerivedStore();
-
- // State
-
- const [reactFlowInstance, setReactFlowInstance] = useState<
- RF.ReactFlowInstance<DmnDiagramNodeData, DmnDiagramEdgeData> | undefined
- >(undefined);
-
- // Refs
-
- const nodeIdBeingDraggedRef = useRef<string | null>(null);
+export type DiagramRef = {
+ getReactFlowInstance: () => RF.ReactFlowInstance | undefined;
+};
- // Memos
+export const Diagram = React.forwardRef<DiagramRef, { container:
React.RefObject<HTMLElement> }>(
+ ({ container }, ref) => {
+ // Contexts
- const rfSnapGrid = useMemo<[number, number]>(
- () => (diagram.snapGrid.isEnabled ? [diagram.snapGrid.x,
diagram.snapGrid.y] : [1, 1]),
- [diagram.snapGrid.isEnabled, diagram.snapGrid.x, diagram.snapGrid.y]
- );
+ const dmnEditorStoreApi = useDmnEditorStoreApi();
+ const diagram = useDmnEditorStore((s) => s.diagram);
+ const thisDmn = useDmnEditorStore((s) => s.dmn);
- // Callbacks
+ const { dmnModelBeforeEditingRef } = useDmnEditor();
- const onConnect = useCallback<RF.OnConnect>(
- (connection) => {
- console.debug("DMN DIAGRAM: `onConnect`: ", connection);
+ const {
+ dmnShapesByHref,
+ nodesById,
+ selectedNodesById,
+ selectedEdgesById,
+ edgesById,
+ nodes,
+ edges,
+ isDropTargetNodeValidForSelection,
+ isDiagramEditingInProgress,
+ selectedNodeTypes,
+ externalDmnsByNamespace,
+ drgElementsWithoutVisualRepresentationOnCurrentDrd,
+ } = useDmnEditorDerivedStore();
- const sourceNode = nodesById.get(connection.source!);
- const targetNode = nodesById.get(connection.target!);
- if (!sourceNode || !targetNode) {
- throw new Error("Cannot create connection without target and source
nodes!");
- }
+ // State
- const sourceBounds = sourceNode.data.shape["dc:Bounds"];
- const targetBounds = targetNode.data.shape["dc:Bounds"];
- if (!sourceBounds || !targetBounds) {
- throw new Error("Cannot create connection without target bounds!");
- }
+ const [reactFlowInstance, setReactFlowInstance] = useState<
+ RF.ReactFlowInstance<DmnDiagramNodeData, DmnDiagramEdgeData> | undefined
+ >(undefined);
- // --------- This is where we draw the line between the diagram and the
model.
+ // Refs
- dmnEditorStoreApi.setState((state) => {
- addEdge({
- definitions: state.dmn.model.definitions,
- drdIndex: state.diagram.drdIndex,
- edge: {
- type: connection.sourceHandle as EdgeType,
- targetHandle: connection.targetHandle as PositionalNodeHandleId,
- sourceHandle: PositionalNodeHandleId.Center,
- },
- sourceNode: {
- type: sourceNode.type as NodeType,
- data: sourceNode.data,
- href: sourceNode.id,
- bounds: sourceBounds,
- shapeId: sourceNode.data.shape["@_id"],
- },
- targetNode: {
- type: targetNode.type as NodeType,
- href: targetNode.id,
- data: targetNode.data,
- bounds: targetBounds,
- index: targetNode.data.index,
- shapeId: targetNode.data.shape["@_id"],
- },
- keepWaypoints: false,
- });
- });
- },
- [dmnEditorStoreApi, nodesById]
- );
+ React.useImperativeHandle(
+ ref,
+ () => ({
+ getReactFlowInstance: () => {
+ return reactFlowInstance;
+ },
+ }),
+ [reactFlowInstance]
+ );
- const getFirstNodeFittingBounds = useCallback(
- (nodeIdToIgnore: string, bounds: DC__Bounds, minSizes: (snapGrid:
SnapGrid) => DC__Dimension, snapGrid: SnapGrid) =>
- reactFlowInstance
- ?.getNodes()
- .reverse() // Respect the nodes z-index.
- .find(
- (node) =>
- node.id !== nodeIdToIgnore && // don't ever use the node being
dragged
- getContainmentRelationship({
- bounds: bounds!,
- container: node.data.shape["dc:Bounds"]!,
- snapGrid,
- containerMinSizes: MIN_NODE_SIZES[node.type as NodeType],
- boundsMinSizes: minSizes,
- }).isInside
- ),
- [reactFlowInstance]
- );
+ const nodeIdBeingDraggedRef = useRef<string | null>(null);
- const onDragOver = useCallback((e: React.DragEvent) => {
- if (
- !e.dataTransfer.types.find(
- (t) =>
- t === MIME_TYPE_FOR_DMN_EDITOR_NEW_NODE_FROM_PALETTE ||
- t === MIME_TYPE_FOR_DMN_EDITOR_EXTERNAL_NODES_FROM_INCLUDED_MODELS ||
- t === MIME_TYPE_FOR_DMN_EDITOR_DRG_NODE
- )
- ) {
- return;
- }
+ // Memos
- e.preventDefault();
- e.dataTransfer.dropEffect = "move";
- }, []);
+ const rfSnapGrid = useMemo<[number, number]>(
+ () => (diagram.snapGrid.isEnabled ? [diagram.snapGrid.x,
diagram.snapGrid.y] : [1, 1]),
+ [diagram.snapGrid.isEnabled, diagram.snapGrid.x, diagram.snapGrid.y]
+ );
- const onDrop = useCallback(
- (e: React.DragEvent) => {
- e.preventDefault();
+ // Callbacks
- if (!container.current || !reactFlowInstance) {
- return;
- }
+ const onConnect = useCallback<RF.OnConnect>(
+ (connection) => {
+ console.debug("DMN DIAGRAM: `onConnect`: ", connection);
- // we need to remove the wrapper bounds, in order to get the correct
position
- const dropPoint = reactFlowInstance.screenToFlowPosition({
- x: e.clientX,
- y: e.clientY,
- });
+ const sourceNode = nodesById.get(connection.source!);
+ const targetNode = nodesById.get(connection.target!);
+ if (!sourceNode || !targetNode) {
+ throw new Error("Cannot create connection without target and source
nodes!");
+ }
- if
(e.dataTransfer.getData(MIME_TYPE_FOR_DMN_EDITOR_NEW_NODE_FROM_PALETTE)) {
- const typeOfNewNodeFromPalette = e.dataTransfer.getData(
- MIME_TYPE_FOR_DMN_EDITOR_NEW_NODE_FROM_PALETTE
- ) as NodeType;
- e.stopPropagation();
+ const sourceBounds = sourceNode.data.shape["dc:Bounds"];
+ const targetBounds = targetNode.data.shape["dc:Bounds"];
+ if (!sourceBounds || !targetBounds) {
+ throw new Error("Cannot create connection without target bounds!");
+ }
// --------- This is where we draw the line between the diagram and
the model.
dmnEditorStoreApi.setState((state) => {
- const { id, href: newNodeId } = addStandaloneNode({
+ addEdge({
definitions: state.dmn.model.definitions,
drdIndex: state.diagram.drdIndex,
- newNode: {
- type: typeOfNewNodeFromPalette,
- bounds: {
- "@_x": dropPoint.x,
- "@_y": dropPoint.y,
- "@_width":
DEFAULT_NODE_SIZES[typeOfNewNodeFromPalette](state.diagram.snapGrid)["@_width"],
- "@_height":
DEFAULT_NODE_SIZES[typeOfNewNodeFromPalette](state.diagram.snapGrid)["@_height"],
- },
+ edge: {
+ type: connection.sourceHandle as EdgeType,
+ targetHandle: connection.targetHandle as PositionalNodeHandleId,
+ sourceHandle: PositionalNodeHandleId.Center,
},
+ sourceNode: {
+ type: sourceNode.type as NodeType,
+ data: sourceNode.data,
+ href: sourceNode.id,
+ bounds: sourceBounds,
+ shapeId: sourceNode.data.shape["@_id"],
+ },
+ targetNode: {
+ type: targetNode.type as NodeType,
+ href: targetNode.id,
+ data: targetNode.data,
+ bounds: targetBounds,
+ index: targetNode.data.index,
+ shapeId: targetNode.data.shape["@_id"],
+ },
+ keepWaypoints: false,
});
- state.diagram._selectedNodes = [newNodeId];
- state.focus.consumableId = id;
});
- } else if
(e.dataTransfer.getData(MIME_TYPE_FOR_DMN_EDITOR_EXTERNAL_NODES_FROM_INCLUDED_MODELS))
{
- e.stopPropagation();
- const externalNode = JSON.parse(
-
e.dataTransfer.getData(MIME_TYPE_FOR_DMN_EDITOR_EXTERNAL_NODES_FROM_INCLUDED_MODELS)
- ) as ExternalNode;
+ },
+ [dmnEditorStoreApi, nodesById]
+ );
- // --------- This is where we draw the line between the diagram and
the model.
+ const getFirstNodeFittingBounds = useCallback(
+ (
+ nodeIdToIgnore: string,
+ bounds: DC__Bounds,
+ minSizes: (snapGrid: SnapGrid) => DC__Dimension,
+ snapGrid: SnapGrid
+ ) =>
+ reactFlowInstance
+ ?.getNodes()
+ .reverse() // Respect the nodes z-index.
+ .find(
+ (node) =>
+ node.id !== nodeIdToIgnore && // don't ever use the node being
dragged
+ getContainmentRelationship({
+ bounds: bounds!,
+ container: node.data.shape["dc:Bounds"]!,
+ snapGrid,
+ containerMinSizes: MIN_NODE_SIZES[node.type as NodeType],
+ boundsMinSizes: minSizes,
+ }).isInside
+ ),
+ [reactFlowInstance]
+ );
+
+ const onDragOver = useCallback((e: React.DragEvent) => {
+ if (
+ !e.dataTransfer.types.find(
+ (t) =>
+ t === MIME_TYPE_FOR_DMN_EDITOR_NEW_NODE_FROM_PALETTE ||
+ t === MIME_TYPE_FOR_DMN_EDITOR_EXTERNAL_NODES_FROM_INCLUDED_MODELS
||
+ t === MIME_TYPE_FOR_DMN_EDITOR_DRG_NODE
+ )
+ ) {
+ return;
+ }
- const externalDrgElement = (
-
externalDmnsByNamespace.get(externalNode.externalDrgElementNamespace)?.model.definitions.drgElement
?? []
- ).find((s) => s["@_id"] === externalNode.externalDrgElementId);
- if (!externalDrgElement) {
- throw new Error(`Can't find DRG element with id
'${externalNode.externalDrgElementId}'.`);
+ e.preventDefault();
+ e.dataTransfer.dropEffect = "move";
+ }, []);
+
+ const onDrop = useCallback(
+ (e: React.DragEvent) => {
+ e.preventDefault();
+
+ if (!container.current || !reactFlowInstance) {
+ return;
}
- const externalNodeType = getNodeTypeFromDmnObject(externalDrgElement)!;
+ // we need to remove the wrapper bounds, in order to get the correct
position
+ const dropPoint = reactFlowInstance.screenToFlowPosition({
+ x: e.clientX,
+ y: e.clientY,
+ });
- dmnEditorStoreApi.setState((state) => {
- const defaultExternalNodeDimensions =
DEFAULT_NODE_SIZES[externalNodeType](state.diagram.snapGrid);
+ if
(e.dataTransfer.getData(MIME_TYPE_FOR_DMN_EDITOR_NEW_NODE_FROM_PALETTE)) {
+ const typeOfNewNodeFromPalette = e.dataTransfer.getData(
+ MIME_TYPE_FOR_DMN_EDITOR_NEW_NODE_FROM_PALETTE
+ ) as NodeType;
+ e.stopPropagation();
- const namespaceName = getXmlNamespaceDeclarationName({
- model: state.dmn.model.definitions,
- namespace: externalNode.externalDrgElementNamespace,
+ // --------- This is where we draw the line between the diagram and
the model.
+
+ dmnEditorStoreApi.setState((state) => {
+ const { id, href: newNodeId } = addStandaloneNode({
+ definitions: state.dmn.model.definitions,
+ drdIndex: state.diagram.drdIndex,
+ newNode: {
+ type: typeOfNewNodeFromPalette,
+ bounds: {
+ "@_x": dropPoint.x,
+ "@_y": dropPoint.y,
+ "@_width":
DEFAULT_NODE_SIZES[typeOfNewNodeFromPalette](state.diagram.snapGrid)["@_width"],
+ "@_height":
DEFAULT_NODE_SIZES[typeOfNewNodeFromPalette](state.diagram.snapGrid)["@_height"],
+ },
+ },
+ });
+ state.diagram._selectedNodes = [newNodeId];
+ state.focus.consumableId = id;
});
+ } else if
(e.dataTransfer.getData(MIME_TYPE_FOR_DMN_EDITOR_EXTERNAL_NODES_FROM_INCLUDED_MODELS))
{
+ e.stopPropagation();
+ const externalNode = JSON.parse(
+
e.dataTransfer.getData(MIME_TYPE_FOR_DMN_EDITOR_EXTERNAL_NODES_FROM_INCLUDED_MODELS)
+ ) as ExternalNode;
+
+ // --------- This is where we draw the line between the diagram and
the model.
- if (!namespaceName) {
- throw new Error(`Can't find namespace name for
'${externalNode.externalDrgElementNamespace}'.`);
+ const externalDrgElement = (
+
externalDmnsByNamespace.get(externalNode.externalDrgElementNamespace)?.model.definitions.drgElement
?? []
+ ).find((s) => s["@_id"] === externalNode.externalDrgElementId);
+ if (!externalDrgElement) {
+ throw new Error(`Can't find DRG element with id
'${externalNode.externalDrgElementId}'.`);
}
- addShape({
- definitions: state.dmn.model.definitions,
- drdIndex: state.diagram.drdIndex,
- nodeType: externalNodeType,
- shape: {
- "@_dmnElementRef": buildXmlQName({
- type: "xml-qname",
- prefix: namespaceName,
- localPart: externalDrgElement["@_id"]!,
- }),
- "@_isCollapsed": true,
- "dc:Bounds": {
- "@_x": dropPoint.x,
- "@_y": dropPoint.y,
- "@_width": defaultExternalNodeDimensions["@_width"],
- "@_height": defaultExternalNodeDimensions["@_height"],
+ const externalNodeType =
getNodeTypeFromDmnObject(externalDrgElement)!;
+
+ dmnEditorStoreApi.setState((state) => {
+ const defaultExternalNodeDimensions =
DEFAULT_NODE_SIZES[externalNodeType](state.diagram.snapGrid);
+
+ const namespaceName = getXmlNamespaceDeclarationName({
+ model: state.dmn.model.definitions,
+ namespace: externalNode.externalDrgElementNamespace,
+ });
+
+ if (!namespaceName) {
+ throw new Error(`Can't find namespace name for
'${externalNode.externalDrgElementNamespace}'.`);
+ }
+
+ addShape({
+ definitions: state.dmn.model.definitions,
+ drdIndex: state.diagram.drdIndex,
+ nodeType: externalNodeType,
+ shape: {
+ "@_dmnElementRef": buildXmlQName({
+ type: "xml-qname",
+ prefix: namespaceName,
+ localPart: externalDrgElement["@_id"]!,
+ }),
+ "@_isCollapsed": true,
+ "dc:Bounds": {
+ "@_x": dropPoint.x,
+ "@_y": dropPoint.y,
+ "@_width": defaultExternalNodeDimensions["@_width"],
+ "@_height": defaultExternalNodeDimensions["@_height"],
+ },
},
- },
+ });
+ state.diagram._selectedNodes = [
+ buildXmlHref({
+ namespace: externalNode.externalDrgElementNamespace,
+ id: externalNode.externalDrgElementId,
+ }),
+ ];
});
- state.diagram._selectedNodes = [
- buildXmlHref({
- namespace: externalNode.externalDrgElementNamespace,
- id: externalNode.externalDrgElementId,
- }),
- ];
- });
- console.debug(`DMN DIAGRAM: Adding external node`,
JSON.stringify(externalNode));
- } else if (e.dataTransfer.getData(MIME_TYPE_FOR_DMN_EDITOR_DRG_NODE)) {
- const drgElement =
JSON.parse(e.dataTransfer.getData(MIME_TYPE_FOR_DMN_EDITOR_DRG_NODE)) as
Unpacked<
- DMN15__tDefinitions["drgElement"]
- >;
+ console.debug(`DMN DIAGRAM: Adding external node`,
JSON.stringify(externalNode));
+ } else if (e.dataTransfer.getData(MIME_TYPE_FOR_DMN_EDITOR_DRG_NODE)) {
+ const drgElement =
JSON.parse(e.dataTransfer.getData(MIME_TYPE_FOR_DMN_EDITOR_DRG_NODE)) as
Unpacked<
+ DMN15__tDefinitions["drgElement"]
+ >;
- const nodeType = getNodeTypeFromDmnObject(drgElement)!;
+ const nodeType = getNodeTypeFromDmnObject(drgElement)!;
- dmnEditorStoreApi.setState((state) => {
- const defaultNodeDimensions =
DEFAULT_NODE_SIZES[nodeType](state.diagram.snapGrid);
- addShape({
- definitions: state.dmn.model.definitions,
- drdIndex: state.diagram.drdIndex,
- nodeType,
- shape: {
- "@_dmnElementRef": buildXmlQName({ type: "xml-qname", localPart:
drgElement["@_id"]! }),
- "@_isCollapsed": false,
- "dc:Bounds": {
- "@_x": dropPoint.x,
- "@_y": dropPoint.y,
- "@_width": defaultNodeDimensions["@_width"],
- "@_height": defaultNodeDimensions["@_height"],
+ dmnEditorStoreApi.setState((state) => {
+ const defaultNodeDimensions =
DEFAULT_NODE_SIZES[nodeType](state.diagram.snapGrid);
+ addShape({
+ definitions: state.dmn.model.definitions,
+ drdIndex: state.diagram.drdIndex,
+ nodeType,
+ shape: {
+ "@_dmnElementRef": buildXmlQName({ type: "xml-qname",
localPart: drgElement["@_id"]! }),
+ "@_isCollapsed": false,
+ "dc:Bounds": {
+ "@_x": dropPoint.x,
+ "@_y": dropPoint.y,
+ "@_width": defaultNodeDimensions["@_width"],
+ "@_height": defaultNodeDimensions["@_height"],
+ },
},
- },
+ });
});
- });
- console.debug(`DMN DIAGRAM: Adding DRG node`,
JSON.stringify(drgElement));
+ console.debug(`DMN DIAGRAM: Adding DRG node`,
JSON.stringify(drgElement));
+ }
+ },
+ [container, reactFlowInstance, dmnEditorStoreApi,
externalDmnsByNamespace]
+ );
+
+ useEffect(() => {
+ const edgeUpdaterSource = document.querySelectorAll(
+ ".react-flow__edgeupdater-source, .react-flow__edgeupdater-target"
+ );
+ if (diagram.ongoingConnection) {
+ edgeUpdaterSource.forEach((e) => e.classList.add("hidden"));
+ } else {
+ edgeUpdaterSource.forEach((e) => e.classList.remove("hidden"));
}
- },
- [container, reactFlowInstance, dmnEditorStoreApi, externalDmnsByNamespace]
- );
+ }, [diagram.ongoingConnection]);
- useEffect(() => {
- const edgeUpdaterSource = document.querySelectorAll(
- ".react-flow__edgeupdater-source, .react-flow__edgeupdater-target"
+ const onConnectStart = useCallback<RF.OnConnectStart>(
+ (e, newConnection) => {
+ console.debug("DMN DIAGRAM: `onConnectStart`");
+ dmnEditorStoreApi.setState((state) => {
+ state.diagram.ongoingConnection = newConnection;
+ });
+ },
+ [dmnEditorStoreApi]
);
- if (diagram.ongoingConnection) {
- edgeUpdaterSource.forEach((e) => e.classList.add("hidden"));
- } else {
- edgeUpdaterSource.forEach((e) => e.classList.remove("hidden"));
- }
- }, [diagram.ongoingConnection]);
-
- const onConnectStart = useCallback<RF.OnConnectStart>(
- (e, newConnection) => {
- console.debug("DMN DIAGRAM: `onConnectStart`");
- dmnEditorStoreApi.setState((state) => {
- state.diagram.ongoingConnection = newConnection;
- });
- },
- [dmnEditorStoreApi]
- );
- const onConnectEnd = useCallback(
- (e: MouseEvent) => {
- console.debug("DMN DIAGRAM: `onConnectEnd`");
- dmnEditorStoreApi.setState((state) => {
- state.diagram.ongoingConnection = undefined;
- });
+ const onConnectEnd = useCallback(
+ (e: MouseEvent) => {
+ console.debug("DMN DIAGRAM: `onConnectEnd`");
+ dmnEditorStoreApi.setState((state) => {
+ state.diagram.ongoingConnection = undefined;
+ });
- const targetIsPane = (e.target as Element |
null)?.classList?.contains("react-flow__pane");
- if (!targetIsPane || !container.current || !diagram.ongoingConnection ||
!reactFlowInstance) {
- return;
- }
+ const targetIsPane = (e.target as Element |
null)?.classList?.contains("react-flow__pane");
+ if (!targetIsPane || !container.current || !diagram.ongoingConnection
|| !reactFlowInstance) {
+ return;
+ }
- const dropPoint = reactFlowInstance.screenToFlowPosition({
- x: e.clientX,
- y: e.clientY,
- });
+ const dropPoint = reactFlowInstance.screenToFlowPosition({
+ x: e.clientX,
+ y: e.clientY,
+ });
- // only try to create node if source handle is compatible
- if (!Object.values(NODE_TYPES).find((n) => n ===
diagram.ongoingConnection!.handleId)) {
- return;
- }
+ // only try to create node if source handle is compatible
+ if (!Object.values(NODE_TYPES).find((n) => n ===
diagram.ongoingConnection!.handleId)) {
+ return;
+ }
- if (!diagram.ongoingConnection.nodeId) {
- return;
- }
+ if (!diagram.ongoingConnection.nodeId) {
+ return;
+ }
- const sourceNode = nodesById.get(diagram.ongoingConnection.nodeId);
- if (!sourceNode) {
- return;
- }
+ const sourceNode = nodesById.get(diagram.ongoingConnection.nodeId);
+ if (!sourceNode) {
+ return;
+ }
- const sourceNodeBounds =
dmnShapesByHref.get(sourceNode.id)?.["dc:Bounds"];
- if (!sourceNodeBounds) {
- return;
- }
+ const sourceNodeBounds =
dmnShapesByHref.get(sourceNode.id)?.["dc:Bounds"];
+ if (!sourceNodeBounds) {
+ return;
+ }
- const newNodeType = diagram.ongoingConnection.handleId as NodeType;
- const sourceNodeType = sourceNode.type as NodeType;
+ const newNodeType = diagram.ongoingConnection.handleId as NodeType;
+ const sourceNodeType = sourceNode.type as NodeType;
- const edge = getDefaultEdgeTypeBetween(sourceNodeType as NodeType,
newNodeType);
- if (!edge) {
- throw new Error(`DMN DIAGRAM: Invalid structure: ${sourceNodeType}
--(any)--> ${newNodeType}`);
- }
+ const edge = getDefaultEdgeTypeBetween(sourceNodeType as NodeType,
newNodeType);
+ if (!edge) {
+ throw new Error(`DMN DIAGRAM: Invalid structure: ${sourceNodeType}
--(any)--> ${newNodeType}`);
+ }
- // --------- This is where we draw the line between the diagram and the
model.
+ // --------- This is where we draw the line between the diagram and
the model.
- dmnEditorStoreApi.setState((state) => {
- const { id, href: newDmnObejctHref } = addConnectedNode({
- definitions: state.dmn.model.definitions,
- drdIndex: state.diagram.drdIndex,
- edge,
- sourceNode: {
- href: sourceNode.id,
- type: sourceNodeType as NodeType,
- bounds: sourceNodeBounds,
- shapeId: sourceNode.data.shape["@_id"],
- },
- newNode: {
- type: newNodeType,
- bounds: {
- "@_x": dropPoint.x,
- "@_y": dropPoint.y,
- "@_width":
DEFAULT_NODE_SIZES[newNodeType](state.diagram.snapGrid)["@_width"],
- "@_height":
DEFAULT_NODE_SIZES[newNodeType](state.diagram.snapGrid)["@_height"],
+ dmnEditorStoreApi.setState((state) => {
+ const { id, href: newDmnObejctHref } = addConnectedNode({
+ definitions: state.dmn.model.definitions,
+ drdIndex: state.diagram.drdIndex,
+ edge,
+ sourceNode: {
+ href: sourceNode.id,
+ type: sourceNodeType as NodeType,
+ bounds: sourceNodeBounds,
+ shapeId: sourceNode.data.shape["@_id"],
},
- },
- });
+ newNode: {
+ type: newNodeType,
+ bounds: {
+ "@_x": dropPoint.x,
+ "@_y": dropPoint.y,
+ "@_width":
DEFAULT_NODE_SIZES[newNodeType](state.diagram.snapGrid)["@_width"],
+ "@_height":
DEFAULT_NODE_SIZES[newNodeType](state.diagram.snapGrid)["@_height"],
+ },
+ },
+ });
- state.diagram._selectedNodes = [newDmnObejctHref];
- state.focus.consumableId = id;
- });
- },
- [dmnEditorStoreApi, container, diagram.ongoingConnection,
reactFlowInstance, nodesById, dmnShapesByHref]
- );
+ state.diagram._selectedNodes = [newDmnObejctHref];
+ state.focus.consumableId = id;
+ });
+ },
+ [dmnEditorStoreApi, container, diagram.ongoingConnection,
reactFlowInstance, nodesById, dmnShapesByHref]
+ );
- const isValidConnection = useCallback<RF.IsValidConnection>(
- (edgeOrConnection) => {
- const state = dmnEditorStoreApi.getState();
- const edgeId = state.diagram.edgeIdBeingUpdated;
- const edgeType = edgeId ? (reactFlowInstance?.getEdge(edgeId)?.type as
EdgeType) : undefined;
+ const isValidConnection = useCallback<RF.IsValidConnection>(
+ (edgeOrConnection) => {
+ const state = dmnEditorStoreApi.getState();
+ const edgeId = state.diagram.edgeIdBeingUpdated;
+ const edgeType = edgeId ? (reactFlowInstance?.getEdge(edgeId)?.type as
EdgeType) : undefined;
- const ongoingConnectionHierarchy = buildHierarchy({
- nodeId: state.diagram.ongoingConnection?.nodeId,
- edges: reactFlowInstance?.getEdges() ?? [],
- });
+ const ongoingConnectionHierarchy = buildHierarchy({
+ nodeId: state.diagram.ongoingConnection?.nodeId,
+ edges: reactFlowInstance?.getEdges() ?? [],
+ });
- return (
- // Reflexive edges are not allowed for DMN
- edgeOrConnection.source !== edgeOrConnection.target &&
- // Matches DMNs structure.
- checkIsValidConnection(nodesById, edgeOrConnection, edgeType) &&
- // Does not form cycles.
- !!edgeOrConnection.target &&
- !ongoingConnectionHierarchy.dependencies.has(edgeOrConnection.target)
&&
- !!edgeOrConnection.source &&
- !ongoingConnectionHierarchy.dependents.has(edgeOrConnection.source)
- );
- },
- [dmnEditorStoreApi, reactFlowInstance, nodesById]
- );
+ return (
+ // Reflexive edges are not allowed for DMN
+ edgeOrConnection.source !== edgeOrConnection.target &&
+ // Matches DMNs structure.
+ checkIsValidConnection(nodesById, edgeOrConnection, edgeType) &&
+ // Does not form cycles.
+ !!edgeOrConnection.target &&
+
!ongoingConnectionHierarchy.dependencies.has(edgeOrConnection.target) &&
+ !!edgeOrConnection.source &&
+ !ongoingConnectionHierarchy.dependents.has(edgeOrConnection.source)
+ );
+ },
+ [dmnEditorStoreApi, reactFlowInstance, nodesById]
+ );
- const onNodesChange = useCallback<RF.OnNodesChange>(
- (changes) => {
- if (!reactFlowInstance) {
- return;
- }
+ const onNodesChange = useCallback<RF.OnNodesChange>(
+ (changes) => {
+ if (!reactFlowInstance) {
+ return;
+ }
- dmnEditorStoreApi.setState((state) => {
- const controlWaypointsByEdge = new Map<number, Set<number>>();
-
- for (const change of changes) {
- switch (change.type) {
- case "add":
- console.debug(`DMN DIAGRAM: 'onNodesChange' --> add
'${change.item.id}'`);
- state.dispatch.diagram.setNodeStatus(state, change.item.id, {
selected: true });
- break;
- case "dimensions":
- console.debug(`DMN DIAGRAM: 'onNodesChange' --> dimensions
'${change.id}'`);
- state.dispatch.diagram.setNodeStatus(state, change.id, {
resizing: change.resizing });
- if (change.dimensions) {
- const node = nodesById.get(change.id)!;
- // We only need to resize the node if its snapped dimensions
change, as snapping is non-destructive.
- const snappedShape = snapShapeDimensions(
- state.diagram.snapGrid,
- node.data.shape,
- MIN_NODE_SIZES[node.type as NodeType](state.diagram.snapGrid)
- );
- if (
- snappedShape.width !== change.dimensions.width ||
- snappedShape.height !== change.dimensions.height
- ) {
- resizeNode({
+ dmnEditorStoreApi.setState((state) => {
+ const controlWaypointsByEdge = new Map<number, Set<number>>();
+
+ for (const change of changes) {
+ switch (change.type) {
+ case "add":
+ console.debug(`DMN DIAGRAM: 'onNodesChange' --> add
'${change.item.id}'`);
+ state.dispatch.diagram.setNodeStatus(state, change.item.id, {
selected: true });
+ break;
+ case "dimensions":
+ console.debug(`DMN DIAGRAM: 'onNodesChange' --> dimensions
'${change.id}'`);
+ state.dispatch.diagram.setNodeStatus(state, change.id, {
resizing: change.resizing });
+ if (change.dimensions) {
+ const node = nodesById.get(change.id)!;
+ // We only need to resize the node if its snapped dimensions
change, as snapping is non-destructive.
+ const snappedShape = snapShapeDimensions(
+ state.diagram.snapGrid,
+ node.data.shape,
+ MIN_NODE_SIZES[node.type as
NodeType](state.diagram.snapGrid)
+ );
+ if (
+ snappedShape.width !== change.dimensions.width ||
+ snappedShape.height !== change.dimensions.height
+ ) {
+ resizeNode({
+ definitions: state.dmn.model.definitions,
+ drdIndex: state.diagram.drdIndex,
+ dmnShapesByHref,
+ snapGrid: state.diagram.snapGrid,
+ change: {
+ isExternal: !!node.data.dmnObjectQName.prefix,
+ nodeType: node.type as NodeType,
+ index: node.data.index,
+ shapeIndex: node.data.shape.index,
+ sourceEdgeIndexes: edges.flatMap((e) =>
+ e.source === change.id && e.data?.dmnEdge ?
[e.data.dmnEdge.index] : []
+ ),
+ targetEdgeIndexes: edges.flatMap((e) =>
+ e.target === change.id && e.data?.dmnEdge ?
[e.data.dmnEdge.index] : []
+ ),
+ dimension: {
+ "@_width": change.dimensions?.width ?? 0,
+ "@_height": change.dimensions?.height ?? 0,
+ },
+ },
+ });
+ }
+ }
+ break;
+ case "position":
+ console.debug(`DMN DIAGRAM: 'onNodesChange' --> position
'${change.id}'`);
+ state.dispatch.diagram.setNodeStatus(state, change.id, {
dragging: change.dragging });
+ if (change.positionAbsolute) {
+ const node = nodesById.get(change.id)!;
+ const { delta } = repositionNode({
definitions: state.dmn.model.definitions,
drdIndex: state.diagram.drdIndex,
- dmnShapesByHref,
- snapGrid: state.diagram.snapGrid,
+ controlWaypointsByEdge,
change: {
- isExternal: !!node.data.dmnObjectQName.prefix,
+ type: "absolute",
nodeType: node.type as NodeType,
- index: node.data.index,
+ selectedEdges: [...selectedEdgesById.keys()],
shapeIndex: node.data.shape.index,
sourceEdgeIndexes: edges.flatMap((e) =>
e.source === change.id && e.data?.dmnEdge ?
[e.data.dmnEdge.index] : []
@@ -570,492 +619,467 @@ export function Diagram({ container }: { container:
React.RefObject<HTMLElement>
targetEdgeIndexes: edges.flatMap((e) =>
e.target === change.id && e.data?.dmnEdge ?
[e.data.dmnEdge.index] : []
),
- dimension: {
- "@_width": change.dimensions?.width ?? 0,
- "@_height": change.dimensions?.height ?? 0,
- },
+ position: change.positionAbsolute,
},
});
+
+ // FIXME: This should be inside `repositionNode` I guess?
+
+ // Update nested
+ // External Decision Services will have encapsulated and
output decisions, but they aren't depicted in the graph.
+ if (node.type === NODE_TYPES.decisionService &&
!node.data.dmnObjectQName.prefix) {
+ const decisionService = node.data.dmnObject as
DMN15__tDecisionService;
+ const nested = [
+ ...(decisionService.outputDecision ?? []),
+ ...(decisionService.encapsulatedDecision ?? []),
+ ];
+
+ for (let i = 0; i < nested.length; i++) {
+ const nestedNode = nodesById.get(nested[i]["@_href"])!;
+ const snappedNestedNodeShapeWithAppliedDelta =
snapShapePosition(
+ state.diagram.snapGrid,
+ offsetShapePosition(nestedNode.data.shape, delta)
+ );
+ repositionNode({
+ definitions: state.dmn.model.definitions,
+ drdIndex: state.diagram.drdIndex,
+ controlWaypointsByEdge,
+ change: {
+ type: "absolute",
+ nodeType: nestedNode.type as NodeType,
+ selectedEdges: edges.map((e) => e.id),
+ shapeIndex: nestedNode.data.shape.index,
+ sourceEdgeIndexes: edges.flatMap((e) =>
+ e.source === nestedNode.id && e.data?.dmnEdge ?
[e.data.dmnEdge.index] : []
+ ),
+ targetEdgeIndexes: edges.flatMap((e) =>
+ e.target === nestedNode.id && e.data?.dmnEdge ?
[e.data.dmnEdge.index] : []
+ ),
+ position: snappedNestedNodeShapeWithAppliedDelta,
+ },
+ });
+ }
+ }
}
- }
- break;
- case "position":
- console.debug(`DMN DIAGRAM: 'onNodesChange' --> position
'${change.id}'`);
- state.dispatch.diagram.setNodeStatus(state, change.id, {
dragging: change.dragging });
- if (change.positionAbsolute) {
+ break;
+ case "remove":
+ console.debug(`DMN DIAGRAM: 'onNodesChange' --> remove
'${change.id}'`);
const node = nodesById.get(change.id)!;
- const { delta } = repositionNode({
+ deleteNode({
definitions: state.dmn.model.definitions,
drdIndex: state.diagram.drdIndex,
- controlWaypointsByEdge,
- change: {
- type: "absolute",
- nodeType: node.type as NodeType,
- selectedEdges: [...selectedEdgesById.keys()],
- shapeIndex: node.data.shape.index,
- sourceEdgeIndexes: edges.flatMap((e) =>
- e.source === change.id && e.data?.dmnEdge ?
[e.data.dmnEdge.index] : []
- ),
- targetEdgeIndexes: edges.flatMap((e) =>
- e.target === change.id && e.data?.dmnEdge ?
[e.data.dmnEdge.index] : []
- ),
- position: change.positionAbsolute,
- },
+ dmnObjectQName: node.data.dmnObjectQName,
+ dmnObjectId: node.data.dmnObject?.["@_id"],
+ nodeNature: nodeNatures[node.type as NodeType],
});
-
- // FIXME: This should be inside `repositionNode` I guess?
-
- // Update nested
- // External Decision Services will have encapsulated and
output decisions, but they aren't depicted in the graph.
- if (node.type === NODE_TYPES.decisionService &&
!node.data.dmnObjectQName.prefix) {
- const decisionService = node.data.dmnObject as
DMN15__tDecisionService;
- const nested = [
- ...(decisionService.outputDecision ?? []),
- ...(decisionService.encapsulatedDecision ?? []),
- ];
-
- for (let i = 0; i < nested.length; i++) {
- const nestedNode = nodesById.get(nested[i]["@_href"])!;
- const snappedNestedNodeShapeWithAppliedDelta =
snapShapePosition(
- state.diagram.snapGrid,
- offsetShapePosition(nestedNode.data.shape, delta)
- );
- repositionNode({
- definitions: state.dmn.model.definitions,
- drdIndex: state.diagram.drdIndex,
- controlWaypointsByEdge,
- change: {
- type: "absolute",
- nodeType: nestedNode.type as NodeType,
- selectedEdges: edges.map((e) => e.id),
- shapeIndex: nestedNode.data.shape.index,
- sourceEdgeIndexes: edges.flatMap((e) =>
- e.source === nestedNode.id && e.data?.dmnEdge ?
[e.data.dmnEdge.index] : []
- ),
- targetEdgeIndexes: edges.flatMap((e) =>
- e.target === nestedNode.id && e.data?.dmnEdge ?
[e.data.dmnEdge.index] : []
- ),
- position: snappedNestedNodeShapeWithAppliedDelta,
- },
- });
- }
- }
- }
- break;
- case "remove":
- console.debug(`DMN DIAGRAM: 'onNodesChange' --> remove
'${change.id}'`);
- const node = nodesById.get(change.id)!;
- deleteNode({
- definitions: state.dmn.model.definitions,
- drdIndex: state.diagram.drdIndex,
- dmnObjectQName: node.data.dmnObjectQName,
- dmnObjectId: node.data.dmnObject?.["@_id"],
- nodeNature: nodeNatures[node.type as NodeType],
- });
- state.dispatch.diagram.setNodeStatus(state, node.id, {
- selected: false,
- dragging: false,
- resizing: false,
- });
- break;
- case "reset":
- state.dispatch.diagram.setNodeStatus(state, change.item.id, {
- selected: false,
- dragging: false,
- resizing: false,
- });
- break;
- case "select":
- state.dispatch.diagram.setNodeStatus(state, change.id, {
selected: change.selected });
- break;
+ state.dispatch.diagram.setNodeStatus(state, node.id, {
+ selected: false,
+ dragging: false,
+ resizing: false,
+ });
+ break;
+ case "reset":
+ state.dispatch.diagram.setNodeStatus(state, change.item.id, {
+ selected: false,
+ dragging: false,
+ resizing: false,
+ });
+ break;
+ case "select":
+ state.dispatch.diagram.setNodeStatus(state, change.id, {
selected: change.selected });
+ break;
+ }
}
- }
- });
- },
- [reactFlowInstance, dmnEditorStoreApi, nodesById, dmnShapesByHref, edges,
selectedEdgesById]
- );
-
- const resetToBeforeEditingBegan = useCallback(() => {
- dmnEditorStoreApi.setState((state) => {
- state.dmn.model = dmnModelBeforeEditingRef.current;
- state.diagram.draggingNodes = [];
- state.diagram.draggingWaypoints = [];
- state.diagram.resizingNodes = [];
- state.diagram.dropTargetNode = undefined;
- state.diagram.edgeIdBeingUpdated = undefined;
- });
- }, [dmnEditorStoreApi, dmnModelBeforeEditingRef]);
+ });
+ },
+ [reactFlowInstance, dmnEditorStoreApi, nodesById, dmnShapesByHref,
edges, selectedEdgesById]
+ );
- const onNodeDrag = useCallback<RF.NodeDragHandler>(
- (e, node: RF.Node<DmnDiagramNodeData>) => {
- nodeIdBeingDraggedRef.current = node.id;
+ const resetToBeforeEditingBegan = useCallback(() => {
dmnEditorStoreApi.setState((state) => {
- state.diagram.dropTargetNode = getFirstNodeFittingBounds(
- node.id,
- {
- // We can't use node.data.dmnObject because it hasn't been updated
at this point yet.
- "@_x": node.positionAbsolute?.x ?? 0,
- "@_y": node.positionAbsolute?.y ?? 0,
- "@_width": node.width ?? 0,
- "@_height": node.height ?? 0,
- },
- MIN_NODE_SIZES[node.type as NodeType],
- state.diagram.snapGrid
- );
+ state.dmn.model = dmnModelBeforeEditingRef.current;
+ state.diagram.draggingNodes = [];
+ state.diagram.draggingWaypoints = [];
+ state.diagram.resizingNodes = [];
+ state.diagram.dropTargetNode = undefined;
+ state.diagram.edgeIdBeingUpdated = undefined;
});
- },
- [dmnEditorStoreApi, getFirstNodeFittingBounds]
- );
+ }, [dmnEditorStoreApi, dmnModelBeforeEditingRef]);
- const onNodeDragStart = useCallback<RF.NodeDragHandler>(
- (e, node: RF.Node<DmnDiagramNodeData>, nodes) => {
- dmnModelBeforeEditingRef.current = thisDmn.model;
- onNodeDrag(e, node, nodes);
- },
- [thisDmn.model, dmnModelBeforeEditingRef, onNodeDrag]
- );
+ const onNodeDrag = useCallback<RF.NodeDragHandler>(
+ (e, node: RF.Node<DmnDiagramNodeData>) => {
+ nodeIdBeingDraggedRef.current = node.id;
+ dmnEditorStoreApi.setState((state) => {
+ state.diagram.dropTargetNode = getFirstNodeFittingBounds(
+ node.id,
+ {
+ // We can't use node.data.dmnObject because it hasn't been
updated at this point yet.
+ "@_x": node.positionAbsolute?.x ?? 0,
+ "@_y": node.positionAbsolute?.y ?? 0,
+ "@_width": node.width ?? 0,
+ "@_height": node.height ?? 0,
+ },
+ MIN_NODE_SIZES[node.type as NodeType],
+ state.diagram.snapGrid
+ );
+ });
+ },
+ [dmnEditorStoreApi, getFirstNodeFittingBounds]
+ );
- const onNodeDragStop = useCallback<RF.NodeDragHandler>(
- (e, node: RF.Node<DmnDiagramNodeData>) => {
- console.debug("DMN DIAGRAM: `onNodeDragStop`");
- const nodeBeingDragged = nodesById.get(nodeIdBeingDraggedRef.current!);
- nodeIdBeingDraggedRef.current = null;
- if (!nodeBeingDragged) {
- return;
- }
+ const onNodeDragStart = useCallback<RF.NodeDragHandler>(
+ (e, node: RF.Node<DmnDiagramNodeData>, nodes) => {
+ dmnModelBeforeEditingRef.current = thisDmn.model;
+ onNodeDrag(e, node, nodes);
+ },
+ [thisDmn.model, dmnModelBeforeEditingRef, onNodeDrag]
+ );
- // Validate
- const dropTargetNode =
dmnEditorStoreApi.getState().diagram.dropTargetNode;
- if (dropTargetNode && containment.has(dropTargetNode.type as NodeType)
&& !isDropTargetNodeValidForSelection) {
- console.debug(
- `DMN DIAGRAM: Invalid containment:
'${[...selectedNodeTypes].join("', '")}' inside '${
- dropTargetNode.type
- }'. Ignoring nodes dropped.`
- );
- resetToBeforeEditingBegan();
- return;
- }
+ const onNodeDragStop = useCallback<RF.NodeDragHandler>(
+ (e, node: RF.Node<DmnDiagramNodeData>) => {
+ console.debug("DMN DIAGRAM: `onNodeDragStop`");
+ const nodeBeingDragged = nodesById.get(nodeIdBeingDraggedRef.current!);
+ nodeIdBeingDraggedRef.current = null;
+ if (!nodeBeingDragged) {
+ return;
+ }
+
+ // Validate
+ const dropTargetNode =
dmnEditorStoreApi.getState().diagram.dropTargetNode;
+ if (dropTargetNode && containment.has(dropTargetNode.type as NodeType)
&& !isDropTargetNodeValidForSelection) {
+ console.debug(
+ `DMN DIAGRAM: Invalid containment:
'${[...selectedNodeTypes].join("', '")}' inside '${
+ dropTargetNode.type
+ }'. Ignoring nodes dropped.`
+ );
+ resetToBeforeEditingBegan();
+ return;
+ }
- const selectedNodes = [...selectedNodesById.values()];
+ const selectedNodes = [...selectedNodesById.values()];
- try {
- dmnEditorStoreApi.setState((state) => {
- state.diagram.dropTargetNode = undefined;
+ try {
+ dmnEditorStoreApi.setState((state) => {
+ state.diagram.dropTargetNode = undefined;
- if (!node.dragging) {
- return;
- }
+ if (!node.dragging) {
+ return;
+ }
- // Un-parent
- if (nodeBeingDragged.data.parentRfNode) {
- const p = nodesById.get(nodeBeingDragged.data.parentRfNode.id);
- if (p?.type === NODE_TYPES.decisionService &&
nodeBeingDragged.type === NODE_TYPES.decision) {
+ // Un-parent
+ if (nodeBeingDragged.data.parentRfNode) {
+ const p = nodesById.get(nodeBeingDragged.data.parentRfNode.id);
+ if (p?.type === NODE_TYPES.decisionService &&
nodeBeingDragged.type === NODE_TYPES.decision) {
+ for (let i = 0; i < selectedNodes.length; i++) {
+ deleteDecisionFromDecisionService({
+ definitions: state.dmn.model.definitions,
+ decisionId: selectedNodes[i].data.dmnObject!["@_id"]!, //
We can assume that all selected nodes are Decisions because the contaiment was
validated above.
+ decisionServiceId: p.data.dmnObject!["@_id"]!,
+ });
+ }
+ } else {
+ console.debug(
+ `DMN DIAGRAM: Ignoring '${nodeBeingDragged.type}' with
parent '${dropTargetNode?.type}' dropping somewhere..`
+ );
+ }
+ }
+
+ // Parent
+ if (dropTargetNode?.type === NODE_TYPES.decisionService) {
for (let i = 0; i < selectedNodes.length; i++) {
- deleteDecisionFromDecisionService({
+ addDecisionToDecisionService({
definitions: state.dmn.model.definitions,
+ drdIndex: state.diagram.drdIndex,
decisionId: selectedNodes[i].data.dmnObject!["@_id"]!, // We
can assume that all selected nodes are Decisions because the contaiment was
validated above.
- decisionServiceId: p.data.dmnObject!["@_id"]!,
+ decisionServiceId:
nodesById.get(dropTargetNode.id)!.data.dmnObject!["@_id"]!,
+ snapGrid: state.diagram.snapGrid,
});
}
} else {
console.debug(
- `DMN DIAGRAM: Ignoring '${nodeBeingDragged.type}' with parent
'${dropTargetNode?.type}' dropping somewhere..`
+ `DMN DIAGRAM: Ignoring '${nodeBeingDragged.type}' dropped on
top of '${dropTargetNode?.type}'`
);
}
- }
+ });
+ } catch (e) {
+ console.error(e);
+ resetToBeforeEditingBegan();
+ }
+ },
+ [
+ dmnEditorStoreApi,
+ isDropTargetNodeValidForSelection,
+ nodesById,
+ resetToBeforeEditingBegan,
+ selectedNodeTypes,
+ selectedNodesById,
+ ]
+ );
- // Parent
- if (dropTargetNode?.type === NODE_TYPES.decisionService) {
- for (let i = 0; i < selectedNodes.length; i++) {
- addDecisionToDecisionService({
- definitions: state.dmn.model.definitions,
- drdIndex: state.diagram.drdIndex,
- decisionId: selectedNodes[i].data.dmnObject!["@_id"]!, // We
can assume that all selected nodes are Decisions because the contaiment was
validated above.
- decisionServiceId:
nodesById.get(dropTargetNode.id)!.data.dmnObject!["@_id"]!,
- snapGrid: state.diagram.snapGrid,
- });
+ const onEdgesChange = useCallback<RF.OnEdgesChange>(
+ (changes) => {
+ dmnEditorStoreApi.setState((state) => {
+ for (const change of changes) {
+ switch (change.type) {
+ case "select":
+ console.debug(`DMN DIAGRAM: 'onEdgesChange' --> select
'${change.id}'`);
+ state.dispatch.diagram.setEdgeStatus(state, change.id, {
selected: change.selected });
+ break;
+ case "remove":
+ console.debug(`DMN DIAGRAM: 'onEdgesChange' --> remove
'${change.id}'`);
+ const edge = edgesById.get(change.id);
+ if (edge?.data) {
+ deleteEdge({
+ definitions: state.dmn.model.definitions,
+ drdIndex: state.diagram.drdIndex,
+ edge: { id: change.id, dmnObject: edge.data.dmnObject },
+ });
+ state.dispatch.diagram.setEdgeStatus(state, change.id, {
selected: false, draggingWaypoint: false });
+ }
+ break;
+ case "add":
+ case "reset":
+ console.debug(`DMN DIAGRAM: 'onEdgesChange' --> add/reset
'${change.item.id}'. Ignoring`);
}
- } else {
- console.debug(
- `DMN DIAGRAM: Ignoring '${nodeBeingDragged.type}' dropped on top
of '${dropTargetNode?.type}'`
- );
}
});
- } catch (e) {
- console.error(e);
- resetToBeforeEditingBegan();
- }
- },
- [
- dmnEditorStoreApi,
- isDropTargetNodeValidForSelection,
- nodesById,
- resetToBeforeEditingBegan,
- selectedNodeTypes,
- selectedNodesById,
- ]
- );
-
- const onEdgesChange = useCallback<RF.OnEdgesChange>(
- (changes) => {
- dmnEditorStoreApi.setState((state) => {
- for (const change of changes) {
- switch (change.type) {
- case "select":
- console.debug(`DMN DIAGRAM: 'onEdgesChange' --> select
'${change.id}'`);
- state.dispatch.diagram.setEdgeStatus(state, change.id, {
selected: change.selected });
- break;
- case "remove":
- console.debug(`DMN DIAGRAM: 'onEdgesChange' --> remove
'${change.id}'`);
- const edge = edgesById.get(change.id);
- if (edge?.data) {
- deleteEdge({
- definitions: state.dmn.model.definitions,
- drdIndex: state.diagram.drdIndex,
- edge: { id: change.id, dmnObject: edge.data.dmnObject },
- });
- state.dispatch.diagram.setEdgeStatus(state, change.id, {
selected: false, draggingWaypoint: false });
- }
- break;
- case "add":
- case "reset":
- console.debug(`DMN DIAGRAM: 'onEdgesChange' --> add/reset
'${change.item.id}'. Ignoring`);
- }
- }
- });
- },
- [dmnEditorStoreApi, edgesById]
- );
+ },
+ [dmnEditorStoreApi, edgesById]
+ );
- const onEdgeUpdate = useCallback<RF.OnEdgeUpdateFunc<DmnDiagramEdgeData>>(
- (oldEdge, newConnection) => {
- console.debug("DMN DIAGRAM: `onEdgeUpdate`", oldEdge, newConnection);
+ const onEdgeUpdate = useCallback<RF.OnEdgeUpdateFunc<DmnDiagramEdgeData>>(
+ (oldEdge, newConnection) => {
+ console.debug("DMN DIAGRAM: `onEdgeUpdate`", oldEdge, newConnection);
- const sourceNode = nodesById.get(newConnection.source!);
- const targetNode = nodesById.get(newConnection.target!);
- if (!sourceNode || !targetNode) {
- throw new Error("Cannot create connection without target and source
nodes!");
- }
+ const sourceNode = nodesById.get(newConnection.source!);
+ const targetNode = nodesById.get(newConnection.target!);
+ if (!sourceNode || !targetNode) {
+ throw new Error("Cannot create connection without target and source
nodes!");
+ }
- const sourceBounds = sourceNode.data.shape["dc:Bounds"];
- const targetBounds = targetNode.data.shape["dc:Bounds"];
- if (!sourceBounds || !targetBounds) {
- throw new Error("Cannot create connection without target bounds!");
- }
+ const sourceBounds = sourceNode.data.shape["dc:Bounds"];
+ const targetBounds = targetNode.data.shape["dc:Bounds"];
+ if (!sourceBounds || !targetBounds) {
+ throw new Error("Cannot create connection without target bounds!");
+ }
- // --------- This is where we draw the line between the diagram and the
model.
+ // --------- This is where we draw the line between the diagram and
the model.
- const lastWaypoint = oldEdge.data?.dmnEdge
- ?
oldEdge.data!.dmnEdge!["di:waypoint"]![oldEdge.data!.dmnEdge!["di:waypoint"]!.length
- 1]!
- : getBoundsCenterPoint(targetBounds);
- const firstWaypoint = oldEdge.data?.dmnEdge
- ? oldEdge.data!.dmnEdge!["di:waypoint"]![0]!
- : getBoundsCenterPoint(sourceBounds);
+ const lastWaypoint = oldEdge.data?.dmnEdge
+ ?
oldEdge.data!.dmnEdge!["di:waypoint"]![oldEdge.data!.dmnEdge!["di:waypoint"]!.length
- 1]!
+ : getBoundsCenterPoint(targetBounds);
+ const firstWaypoint = oldEdge.data?.dmnEdge
+ ? oldEdge.data!.dmnEdge!["di:waypoint"]![0]!
+ : getBoundsCenterPoint(sourceBounds);
- dmnEditorStoreApi.setState((state) => {
- const { newDmnEdge } = addEdge({
- definitions: state.dmn.model.definitions,
- drdIndex: state.diagram.drdIndex,
- edge: {
- type: oldEdge.type as EdgeType,
- targetHandle: ((newConnection.targetHandle as
PositionalNodeHandleId) ??
- getHandlePosition({ shapeBounds: targetBounds, waypoint:
lastWaypoint })
- .handlePosition) as PositionalNodeHandleId,
- sourceHandle: ((newConnection.sourceHandle as
PositionalNodeHandleId) ??
- getHandlePosition({ shapeBounds: sourceBounds, waypoint:
firstWaypoint })
- .handlePosition) as PositionalNodeHandleId,
- },
- sourceNode: {
- type: sourceNode.type as NodeType,
- href: sourceNode.id,
- data: sourceNode.data,
- bounds: sourceBounds,
- shapeId: sourceNode.data.shape["@_id"],
- },
- targetNode: {
- type: targetNode.type as NodeType,
- href: targetNode.id,
- data: targetNode.data,
- bounds: targetBounds,
- index: targetNode.data.index,
- shapeId: targetNode.data.shape["@_id"],
- },
- keepWaypoints: true,
- });
-
- // The DMN Edge changed nodes, so we need to delete the old one, but
keep the waypoints!
- if (newDmnEdge["@_dmnElementRef"] !== oldEdge.id) {
- const { dmnEdge: deletedDmnEdge } = deleteEdge({
+ dmnEditorStoreApi.setState((state) => {
+ const { newDmnEdge } = addEdge({
definitions: state.dmn.model.definitions,
drdIndex: state.diagram.drdIndex,
- edge: { id: oldEdge.id, dmnObject: oldEdge.data!.dmnObject },
+ edge: {
+ type: oldEdge.type as EdgeType,
+ targetHandle: ((newConnection.targetHandle as
PositionalNodeHandleId) ??
+ getHandlePosition({ shapeBounds: targetBounds, waypoint:
lastWaypoint })
+ .handlePosition) as PositionalNodeHandleId,
+ sourceHandle: ((newConnection.sourceHandle as
PositionalNodeHandleId) ??
+ getHandlePosition({ shapeBounds: sourceBounds, waypoint:
firstWaypoint })
+ .handlePosition) as PositionalNodeHandleId,
+ },
+ sourceNode: {
+ type: sourceNode.type as NodeType,
+ href: sourceNode.id,
+ data: sourceNode.data,
+ bounds: sourceBounds,
+ shapeId: sourceNode.data.shape["@_id"],
+ },
+ targetNode: {
+ type: targetNode.type as NodeType,
+ href: targetNode.id,
+ data: targetNode.data,
+ bounds: targetBounds,
+ index: targetNode.data.index,
+ shapeId: targetNode.data.shape["@_id"],
+ },
+ keepWaypoints: true,
});
- const deletedWaypoints = deletedDmnEdge?.["di:waypoint"];
+ // The DMN Edge changed nodes, so we need to delete the old one, but
keep the waypoints!
+ if (newDmnEdge["@_dmnElementRef"] !== oldEdge.id) {
+ const { dmnEdge: deletedDmnEdge } = deleteEdge({
+ definitions: state.dmn.model.definitions,
+ drdIndex: state.diagram.drdIndex,
+ edge: { id: oldEdge.id, dmnObject: oldEdge.data!.dmnObject },
+ });
- if (oldEdge.source !== newConnection.source && deletedWaypoints) {
- newDmnEdge["di:waypoint"] = [newDmnEdge["di:waypoint"]![0],
...deletedWaypoints.slice(1)];
- }
+ const deletedWaypoints = deletedDmnEdge?.["di:waypoint"];
- if (oldEdge.target !== newConnection.target && deletedWaypoints) {
- newDmnEdge["di:waypoint"] = [
- ...deletedWaypoints.slice(0, deletedWaypoints.length - 1),
- newDmnEdge["di:waypoint"]![newDmnEdge["di:waypoint"]!.length -
1],
- ];
- }
- }
+ if (oldEdge.source !== newConnection.source && deletedWaypoints) {
+ newDmnEdge["di:waypoint"] = [newDmnEdge["di:waypoint"]![0],
...deletedWaypoints.slice(1)];
+ }
- // Keep the updated edge selected
- state.diagram._selectedEdges = [newDmnEdge["@_dmnElementRef"]!];
+ if (oldEdge.target !== newConnection.target && deletedWaypoints) {
+ newDmnEdge["di:waypoint"] = [
+ ...deletedWaypoints.slice(0, deletedWaypoints.length - 1),
+ newDmnEdge["di:waypoint"]![newDmnEdge["di:waypoint"]!.length -
1],
+ ];
+ }
+ }
- // Finish edge update atomically.
- state.diagram.ongoingConnection = undefined;
- state.diagram.edgeIdBeingUpdated = undefined;
- });
- },
- [dmnEditorStoreApi, nodesById]
- );
+ // Keep the updated edge selected
+ state.diagram._selectedEdges = [newDmnEdge["@_dmnElementRef"]!];
- const onEdgeUpdateStart = useCallback(
- (e: React.MouseEvent | React.TouchEvent, edge: RF.Edge, handleType:
RF.HandleType) => {
- console.debug("DMN DIAGRAM: `onEdgeUpdateStart`");
- dmnEditorStoreApi.setState((state) => {
- state.diagram.edgeIdBeingUpdated = edge.id;
- });
- },
- [dmnEditorStoreApi]
- );
+ // Finish edge update atomically.
+ state.diagram.ongoingConnection = undefined;
+ state.diagram.edgeIdBeingUpdated = undefined;
+ });
+ },
+ [dmnEditorStoreApi, nodesById]
+ );
- const onEdgeUpdateEnd = useCallback(
- (e: MouseEvent | TouchEvent, edge: RF.Edge, handleType: RF.HandleType) => {
- console.debug("DMN DIAGRAM: `onEdgeUpdateEnd`");
+ const onEdgeUpdateStart = useCallback(
+ (e: React.MouseEvent | React.TouchEvent, edge: RF.Edge, handleType:
RF.HandleType) => {
+ console.debug("DMN DIAGRAM: `onEdgeUpdateStart`");
+ dmnEditorStoreApi.setState((state) => {
+ state.diagram.edgeIdBeingUpdated = edge.id;
+ });
+ },
+ [dmnEditorStoreApi]
+ );
- // Needed for when the edge update operation doesn't change anything.
- dmnEditorStoreApi.setState((state) => {
- state.diagram.ongoingConnection = undefined;
- state.diagram.edgeIdBeingUpdated = undefined;
- });
- },
- [dmnEditorStoreApi]
- );
+ const onEdgeUpdateEnd = useCallback(
+ (e: MouseEvent | TouchEvent, edge: RF.Edge, handleType: RF.HandleType)
=> {
+ console.debug("DMN DIAGRAM: `onEdgeUpdateEnd`");
- // Override Reactflow's behavior by intercepting the keydown event using its
`capture` variant.
- const handleRfKeyDownCapture = useCallback(
- (e: React.KeyboardEvent) => {
- if (e.key === "Escape") {
- if (isDiagramEditingInProgress && dmnModelBeforeEditingRef.current) {
- console.debug(
- "DMN DIAGRAM: Intercepting Escape pressed and preventing
propagation. Reverting DMN model to what it was before editing began."
- );
+ // Needed for when the edge update operation doesn't change anything.
+ dmnEditorStoreApi.setState((state) => {
+ state.diagram.ongoingConnection = undefined;
+ state.diagram.edgeIdBeingUpdated = undefined;
+ });
+ },
+ [dmnEditorStoreApi]
+ );
- e.stopPropagation();
- e.preventDefault();
+ // Override Reactflow's behavior by intercepting the keydown event using
its `capture` variant.
+ const handleRfKeyDownCapture = useCallback(
+ (e: React.KeyboardEvent) => {
+ if (e.key === "Escape") {
+ if (isDiagramEditingInProgress && dmnModelBeforeEditingRef.current) {
+ console.debug(
+ "DMN DIAGRAM: Intercepting Escape pressed and preventing
propagation. Reverting DMN model to what it was before editing began."
+ );
- resetToBeforeEditingBegan();
- } else if (!diagram.ongoingConnection) {
- dmnEditorStoreApi.setState((state) => {
- if (selectedNodesById.size > 0 || selectedEdgesById.size > 0) {
- console.debug("DMN DIAGRAM: Esc pressed. Desselecting
everything.");
- state.diagram._selectedNodes = [];
- state.diagram._selectedEdges = [];
- e.preventDefault();
- } else if (selectedNodesById.size <= 0 && selectedEdgesById.size
<= 0) {
- console.debug("DMN DIAGRAM: Esc pressed. Closing all open
panels.");
- state.diagram.propertiesPanel.isOpen = false;
- state.diagram.overlaysPanel.isOpen = false;
- state.diagram.openNodesPanel = DiagramNodesPanel.NONE;
- e.preventDefault();
- } else {
- // Let the
- }
- });
- } else {
- // Let the KeyboardShortcuts handle it.
+ e.stopPropagation();
+ e.preventDefault();
+
+ resetToBeforeEditingBegan();
+ } else if (!diagram.ongoingConnection) {
+ dmnEditorStoreApi.setState((state) => {
+ if (selectedNodesById.size > 0 || selectedEdgesById.size > 0) {
+ console.debug("DMN DIAGRAM: Esc pressed. Desselecting
everything.");
+ state.diagram._selectedNodes = [];
+ state.diagram._selectedEdges = [];
+ e.preventDefault();
+ } else if (selectedNodesById.size <= 0 && selectedEdgesById.size
<= 0) {
+ console.debug("DMN DIAGRAM: Esc pressed. Closing all open
panels.");
+ state.diagram.propertiesPanel.isOpen = false;
+ state.diagram.overlaysPanel.isOpen = false;
+ state.diagram.openNodesPanel = DiagramNodesPanel.NONE;
+ e.preventDefault();
+ } else {
+ // Let the
+ }
+ });
+ } else {
+ // Let the KeyboardShortcuts handle it.
+ }
}
- }
- },
- [
- diagram.ongoingConnection,
- dmnEditorStoreApi,
- dmnModelBeforeEditingRef,
- isDiagramEditingInProgress,
- resetToBeforeEditingBegan,
- selectedEdgesById.size,
- selectedNodesById.size,
- ]
- );
-
- const [showEmptyState, setShowEmptyState] = useState(true);
-
- const isEmptyStateShowing =
- showEmptyState && nodes.length === 0 &&
drgElementsWithoutVisualRepresentationOnCurrentDrd.length === 0;
+ },
+ [
+ diagram.ongoingConnection,
+ dmnEditorStoreApi,
+ dmnModelBeforeEditingRef,
+ isDiagramEditingInProgress,
+ resetToBeforeEditingBegan,
+ selectedEdgesById.size,
+ selectedNodesById.size,
+ ]
+ );
- return (
- <>
- {isEmptyStateShowing && <DmnDiagramEmptyState
setShowEmptyState={setShowEmptyState} />}
- <DiagramContainerContextProvider container={container}>
- <EdgeMarkers />
- <RF.ReactFlow
- connectionMode={RF.ConnectionMode.Loose} // Allow target handles to
be used as source. This is very important for allowing the positional handles
to be updated for the base of an edge.
- onKeyDownCapture={handleRfKeyDownCapture} // Override Reactflow's
keyboard listeners.
- nodes={nodes}
- edges={edges}
- onNodesChange={onNodesChange}
- onEdgesChange={onEdgesChange}
- onEdgeUpdateStart={onEdgeUpdateStart}
- onEdgeUpdateEnd={onEdgeUpdateEnd}
- onEdgeUpdate={onEdgeUpdate}
- onlyRenderVisibleElements={true}
- zoomOnDoubleClick={false}
- elementsSelectable={true}
- panOnScroll={true}
- zoomOnScroll={false}
- preventScrolling={true}
- selectionOnDrag={true}
- panOnDrag={PAN_ON_DRAG}
- panActivationKeyCode={"Alt"}
- selectionMode={RF.SelectionMode.Full} // For selections happening
inside Groups/DecisionServices it's better to leave it as "Full"
- isValidConnection={isValidConnection}
- connectionLineComponent={ConnectionLine}
- onConnect={onConnect}
- onConnectStart={onConnectStart}
- onConnectEnd={onConnectEnd}
- // (begin)
- // 'Starting to drag' and 'dragging' should have the same behavior.
Otherwise,
- // clicking a node and letting it go, without moving, won't work
properly, and
- // Decisions will be removed from Decision Services.
- onNodeDragStart={onNodeDragStart}
- onNodeDrag={onNodeDrag}
- // (end)
- onNodeDragStop={onNodeDragStop}
- nodeTypes={nodeTypes}
- edgeTypes={edgeTypes}
- snapToGrid={true}
- snapGrid={rfSnapGrid}
- defaultViewport={DEFAULT_VIEWPORT}
- fitView={false}
- fitViewOptions={FIT_VIEW_OPTIONS}
- attributionPosition={"bottom-right"}
- onInit={setReactFlowInstance}
- // (begin)
- // Used to make the Palette work by dropping nodes on the Reactflow
Canvas
- onDrop={onDrop}
- onDragOver={onDragOver}
- // (end)
- >
- <SelectionStatus />
- <Palette pulse={isEmptyStateShowing} />
- <TopRightCornerPanels />
- <PanWhenAltPressed />
- <KeyboardShortcuts />
- {!isFirefox && <RF.Background />}
- <RF.Controls fitViewOptions={FIT_VIEW_OPTIONS}
position={"bottom-right"} />
- <SetConnectionToReactFlowStore />
- </RF.ReactFlow>
- </DiagramContainerContextProvider>
- </>
- );
-}
+ const [showEmptyState, setShowEmptyState] = useState(true);
+
+ const isEmptyStateShowing =
+ showEmptyState && nodes.length === 0 &&
drgElementsWithoutVisualRepresentationOnCurrentDrd.length === 0;
+
+ return (
+ <>
+ {isEmptyStateShowing && <DmnDiagramEmptyState
setShowEmptyState={setShowEmptyState} />}
+ <DiagramContainerContextProvider container={container}>
+ <svg style={{ position: "absolute", top: 0, left: 0 }}>
+ <EdgeMarkers />
+ </svg>
+
+ <RF.ReactFlow
+ connectionMode={RF.ConnectionMode.Loose} // Allow target handles
to be used as source. This is very important for allowing the positional
handles to be updated for the base of an edge.
+ onKeyDownCapture={handleRfKeyDownCapture} // Override Reactflow's
keyboard listeners.
+ nodes={nodes}
+ edges={edges}
+ onNodesChange={onNodesChange}
+ onEdgesChange={onEdgesChange}
+ onEdgeUpdateStart={onEdgeUpdateStart}
+ onEdgeUpdateEnd={onEdgeUpdateEnd}
+ onEdgeUpdate={onEdgeUpdate}
+ onlyRenderVisibleElements={true}
+ zoomOnDoubleClick={false}
+ elementsSelectable={true}
+ panOnScroll={true}
+ zoomOnScroll={false}
+ preventScrolling={true}
+ selectionOnDrag={true}
+ panOnDrag={PAN_ON_DRAG}
+ panActivationKeyCode={"Alt"}
+ selectionMode={RF.SelectionMode.Full} // For selections happening
inside Groups/DecisionServices it's better to leave it as "Full"
+ isValidConnection={isValidConnection}
+ connectionLineComponent={ConnectionLine}
+ onConnect={onConnect}
+ onConnectStart={onConnectStart}
+ onConnectEnd={onConnectEnd}
+ // (begin)
+ // 'Starting to drag' and 'dragging' should have the same
behavior. Otherwise,
+ // clicking a node and letting it go, without moving, won't work
properly, and
+ // Decisions will be removed from Decision Services.
+ onNodeDragStart={onNodeDragStart}
+ onNodeDrag={onNodeDrag}
+ // (end)
+ onNodeDragStop={onNodeDragStop}
+ nodeTypes={nodeTypes}
+ edgeTypes={edgeTypes}
+ snapToGrid={true}
+ snapGrid={rfSnapGrid}
+ defaultViewport={DEFAULT_VIEWPORT}
+ fitView={false}
+ fitViewOptions={FIT_VIEW_OPTIONS}
+ attributionPosition={"bottom-right"}
+ onInit={setReactFlowInstance}
+ // (begin)
+ // Used to make the Palette work by dropping nodes on the
Reactflow Canvas
+ onDrop={onDrop}
+ onDragOver={onDragOver}
+ // (end)
+ >
+ <SelectionStatus />
+ <Palette pulse={isEmptyStateShowing} />
+ <TopRightCornerPanels />
+ <PanWhenAltPressed />
+ <KeyboardShortcuts />
+ {!isFirefox && <RF.Background />}
+ <RF.Controls fitViewOptions={FIT_VIEW_OPTIONS}
position={"bottom-right"} />
+ <SetConnectionToReactFlowStore />
+ </RF.ReactFlow>
+ </DiagramContainerContextProvider>
+ </>
+ );
+ }
+);
function DmnDiagramEmptyState({
setShowEmptyState,
diff --git a/packages/dmn-editor/src/diagram/edges/EdgeMarkers.tsx
b/packages/dmn-editor/src/diagram/edges/EdgeMarkers.tsx
index 3baec6d9e09..f36ffde8165 100644
--- a/packages/dmn-editor/src/diagram/edges/EdgeMarkers.tsx
+++ b/packages/dmn-editor/src/diagram/edges/EdgeMarkers.tsx
@@ -21,57 +21,55 @@ import * as React from "react";
export function EdgeMarkers() {
return (
- <svg style={{ position: "absolute", top: 0, left: 0 }}>
- <defs>
- <marker
- id="closed-circle-at-center"
- viewBox="0 0 10 10"
- refX={5}
- refY={5}
- markerUnits="userSpaceOnUse"
- markerWidth="8"
- markerHeight="8"
- orient="auto-start-reverse"
- >
- <circle cx="5" cy="5" r="5" fill="context-fill"
stroke="context-stroke" />
- </marker>
- <marker
- id="closed-circle-at-border"
- viewBox="0 0 10 10"
- refX={10}
- refY={5}
- markerUnits="userSpaceOnUse"
- markerWidth="8"
- markerHeight="8"
- orient="auto-start-reverse"
- >
- <circle cx="5" cy="5" r="5" fill="context-fill"
stroke="context-stroke" />
- </marker>
- <marker
- id="closed-arrow"
- viewBox="0 0 10 10"
- refX={10}
- refY={5}
- markerUnits="userSpaceOnUse"
- markerWidth="8"
- markerHeight="8"
- orient="auto-start-reverse"
- >
- <path d="M 0 0 L 10 5 L 0 10 z" fill="context-fill"
stroke="context-stroke" />
- </marker>
- <marker
- id="open-arrow"
- viewBox="0 0 10 10"
- refX={10}
- refY={5}
- markerUnits="userSpaceOnUse"
- markerWidth="8"
- markerHeight="8"
- orient="auto-start-reverse"
- >
- <path d="M 0,0 L 10,5 M 10,5 L 0,10" stroke="black" />
- </marker>
- </defs>
- </svg>
+ <defs>
+ <marker
+ id="closed-circle-at-center"
+ viewBox="0 0 10 10"
+ refX={5}
+ refY={5}
+ markerUnits="userSpaceOnUse"
+ markerWidth="8"
+ markerHeight="8"
+ orient="auto-start-reverse"
+ >
+ <circle cx="5" cy="5" r="5" fill="context-fill"
stroke="context-stroke" />
+ </marker>
+ <marker
+ id="closed-circle-at-border"
+ viewBox="0 0 10 10"
+ refX={10}
+ refY={5}
+ markerUnits="userSpaceOnUse"
+ markerWidth="8"
+ markerHeight="8"
+ orient="auto-start-reverse"
+ >
+ <circle cx="5" cy="5" r="5" fill="context-fill"
stroke="context-stroke" />
+ </marker>
+ <marker
+ id="closed-arrow"
+ viewBox="0 0 10 10"
+ refX={10}
+ refY={5}
+ markerUnits="userSpaceOnUse"
+ markerWidth="8"
+ markerHeight="8"
+ orient="auto-start-reverse"
+ >
+ <path d="M 0 0 L 10 5 L 0 10 z" fill="context-fill"
stroke="context-stroke" />
+ </marker>
+ <marker
+ id="open-arrow"
+ viewBox="0 0 10 10"
+ refX={10}
+ refY={5}
+ markerUnits="userSpaceOnUse"
+ markerWidth="8"
+ markerHeight="8"
+ orient="auto-start-reverse"
+ >
+ <path d="M 0,0 L 10,5 M 10,5 L 0,10" stroke="black" />
+ </marker>
+ </defs>
);
}
diff --git a/packages/dmn-editor/src/diagram/maths/DmnMaths.ts
b/packages/dmn-editor/src/diagram/maths/DmnMaths.ts
index a070f818944..5e965c0a7db 100644
--- a/packages/dmn-editor/src/diagram/maths/DmnMaths.ts
+++ b/packages/dmn-editor/src/diagram/maths/DmnMaths.ts
@@ -302,7 +302,6 @@ export function getNodeTypeFromDmnObject(dmnObject:
NodeDmnObjects) {
}
const type = switchExpression(dmnObject.__$$element, {
- // Normal nodes
inputData: NODE_TYPES.inputData,
decision: NODE_TYPES.decision,
businessKnowledgeModel: NODE_TYPES.bkm,
@@ -310,8 +309,6 @@ export function getNodeTypeFromDmnObject(dmnObject:
NodeDmnObjects) {
decisionService: NODE_TYPES.decisionService,
group: NODE_TYPES.group,
textAnnotation: NODE_TYPES.textAnnotation,
- // No nodes associated with
- association: undefined,
default: undefined,
});
diff --git a/packages/dmn-editor/src/diagram/nodes/DefaultSizes.ts
b/packages/dmn-editor/src/diagram/nodes/DefaultSizes.ts
index 88fd8ee0a5b..c4c3a1b45c2 100644
--- a/packages/dmn-editor/src/diagram/nodes/DefaultSizes.ts
+++ b/packages/dmn-editor/src/diagram/nodes/DefaultSizes.ts
@@ -65,7 +65,7 @@ export const MIN_NODE_SIZES: Record<NodeType, (snapGrid:
SnapGrid) => DC__Dimens
};
},
[NODE_TYPES.textAnnotation]: (snapGrid) => {
- const snappedMinSize = MIN_SIZE_FOR_NODES(snapGrid, 200, 200);
+ const snappedMinSize = MIN_SIZE_FOR_NODES(snapGrid, 200, 60);
return {
"@_width": snappedMinSize.width,
"@_height": snappedMinSize.height,
diff --git a/packages/dmn-editor/src/diagram/nodes/EditableNodeLabel.tsx
b/packages/dmn-editor/src/diagram/nodes/EditableNodeLabel.tsx
index a5dd9c7fc90..ffd5a9d4663 100644
--- a/packages/dmn-editor/src/diagram/nodes/EditableNodeLabel.tsx
+++ b/packages/dmn-editor/src/diagram/nodes/EditableNodeLabel.tsx
@@ -33,6 +33,7 @@ import { generateUuid } from
"@kie-tools/boxed-expression-component/dist/api";
import "./EditableNodeLabel.css";
import { useFocusableElement } from "../../focus/useFocusableElement";
import { flushSync } from "react-dom";
+import { NodeLabelPosition } from "./NodeSvgs";
export type OnEditableNodeLabelChange = (value: string | undefined) => void;
@@ -50,7 +51,7 @@ export function EditableNodeLabel({
shouldCommitOnBlur,
skipValidation,
allUniqueNames,
- fontStyle,
+ fontCssProperties: fontStyle,
}: {
id?: string;
shouldCommitOnBlur?: boolean;
@@ -58,14 +59,14 @@ export function EditableNodeLabel({
truncate?: boolean;
namedElement?: DMN15__tNamedElement;
namedElementQName?: XmlQName;
- position?: "center-center" | "top-center" | "center-left" | "top-left";
+ position: NodeLabelPosition;
isEditing: boolean;
value: string | undefined;
setEditing: React.Dispatch<React.SetStateAction<boolean>>;
onChange: OnEditableNodeLabelChange;
skipValidation?: boolean;
allUniqueNames: UniqueNameIndex;
- fontStyle?: React.CSSProperties;
+ fontCssProperties?: React.CSSProperties;
}) {
const thisDmn = useDmnEditorStore((s) => s.dmn);
const { importsByNamespace } = useDmnEditorDerivedStore();
@@ -211,10 +212,8 @@ export function EditableNodeLabel({
)
);
- const positionClass = position ?? "center-center";
-
return (
- <div className={`kie-dmn-editor--editable-node-name-input ${positionClass}
${grow ? "grow" : ""}`}>
+ <div className={`kie-dmn-editor--editable-node-name-input ${position}
${grow ? "grow" : ""}`}>
{(isEditing && (
<input
spellCheck={"false"} // Let's not confuse FEEL name validation with
the browser's grammar check.
diff --git a/packages/dmn-editor/src/diagram/nodes/NodeStyle.ts
b/packages/dmn-editor/src/diagram/nodes/NodeStyle.ts
index 8df30f6e224..9374cc576e8 100644
--- a/packages/dmn-editor/src/diagram/nodes/NodeStyle.ts
+++ b/packages/dmn-editor/src/diagram/nodes/NodeStyle.ts
@@ -20,9 +20,11 @@
import React, { useMemo } from "react";
import { DMNDI15__DMNStyle } from
"@kie-tools/dmn-marshaller/dist/schemas/dmn-1_5/ts-gen/types";
import { NodeType } from "../connections/graphStructure";
+import { NODE_TYPES } from "./NodeTypes";
+import { NodeLabelPosition } from "./NodeSvgs";
export interface NodeStyle {
- fontStyle: React.CSSProperties;
+ fontCssProperties: React.CSSProperties;
shapeStyle: ShapeStyle;
}
@@ -63,57 +65,43 @@ export function useNodeStyle(args: {
nodeType?: NodeType;
isEnabled?: boolean;
}): NodeStyle {
- const fillColor = useMemo(() => {
- const blue = args.dmnStyle?.["dmndi:FillColor"]?.["@_blue"];
- const green = args.dmnStyle?.["dmndi:FillColor"]?.["@_green"];
- const red = args.dmnStyle?.["dmndi:FillColor"]?.["@_red"];
-
- const opacity =
- args.nodeType === "node_decisionService" ||
- args.nodeType === "node_group" ||
- args.nodeType === "node_textAnnotation"
- ? 0.1
- : DEFAULT_NODE_OPACITY;
- if (!args.isEnabled || blue === undefined || green === undefined || red
=== undefined) {
- return `rgba(${DEFAULT_NODE_RED_FILL}, ${DEFAULT_NODE_GREEN_FILL},
${DEFAULT_NODE_BLUE_FILL}, ${opacity})`;
- }
-
- return `rgba(${red}, ${green}, ${blue}, ${opacity})`;
- }, [args.dmnStyle, args.nodeType, args.isEnabled]);
- const strokeColor = useMemo(() => {
- const blue = args.dmnStyle?.["dmndi:StrokeColor"]?.["@_blue"];
- const green = args.dmnStyle?.["dmndi:StrokeColor"]?.["@_green"];
- const red = args.dmnStyle?.["dmndi:StrokeColor"]?.["@_red"];
-
- if (!args.isEnabled || blue === undefined || green === undefined || red
=== undefined) {
- return DEFAULT_NODE_STROKE_COLOR;
- }
- return `rgba(${red}, ${green}, ${blue}, 1)`;
- }, [args.dmnStyle, args.isEnabled]);
-
- const fontProperties = useMemo(() => {
- const blue = args.dmnStyle?.["dmndi:FontColor"]?.["@_blue"];
- const green = args.dmnStyle?.["dmndi:FontColor"]?.["@_green"];
- const red = args.dmnStyle?.["dmndi:FontColor"]?.["@_red"];
-
- const fontColor =
- !args.isEnabled || blue === undefined || green === undefined || red ===
undefined
- ? DEFAULT_FONT_COLOR
- : `rgba(${red}, ${green}, ${blue}, 1)`;
-
- return {
- bold: args.isEnabled ? args.dmnStyle?.["@_fontBold"] ?? false : false,
- italic: args.isEnabled ? args.dmnStyle?.["@_fontItalic"] ?? false :
false,
- underline: args.isEnabled ? args.dmnStyle?.["@_fontUnderline"] ?? false
: false,
- strikeThrough: args.isEnabled ? args.dmnStyle?.["@_fontStrikeThrough"]
?? false : false,
- family: args.isEnabled ? args.dmnStyle?.["@_fontFamily"] : undefined,
- size: args.isEnabled ? args.dmnStyle?.["@_fontSize"] : undefined,
- color: fontColor,
- };
- }, [args.dmnStyle, args.isEnabled]);
+ const fillColor = useMemo(
+ () => getNodeShapeFillColor({ dmnStyle: args.dmnStyle, nodeType:
args.nodeType, isEnabled: args.isEnabled }),
+ [args.dmnStyle, args.isEnabled, args.nodeType]
+ );
+
+ const strokeColor = useMemo(
+ () => getNodeShapeStrokeColor({ dmnStyle: args.dmnStyle, isEnabled:
args.isEnabled }),
+ [args.dmnStyle, args.isEnabled]
+ );
+
+ const dmnFontStyle = useMemo(
+ () => getDmnFontStyle({ dmnStyle: args.dmnStyle, isEnabled: args.isEnabled
}),
+ [args.dmnStyle, args.isEnabled]
+ );
+
+ return useMemo(
+ () =>
+ getNodeStyle({
+ fillColor,
+ strokeColor,
+ dmnFontStyle,
+ }),
+ [fillColor, dmnFontStyle, strokeColor]
+ );
+}
+export function getNodeStyle({
+ fillColor,
+ strokeColor,
+ dmnFontStyle,
+}: {
+ fillColor: string;
+ strokeColor: string;
+ dmnFontStyle: DmnFontStyle;
+}): NodeStyle {
return {
- fontStyle: getFonteStyle(fontProperties),
+ fontCssProperties: getFontCssProperties(dmnFontStyle),
shapeStyle: {
fillColor,
strokeColor,
@@ -122,21 +110,111 @@ export function useNodeStyle(args: {
};
}
-export function getFonteStyle(fontProperties?: DmnFontStyle):
React.CSSProperties {
+export function getNodeShapeFillColor(args: {
+ dmnStyle?: DMNDI15__DMNStyle | undefined;
+ nodeType?: NodeType | undefined;
+ isEnabled?: boolean | undefined;
+}) {
+ const blue = args.dmnStyle?.["dmndi:FillColor"]?.["@_blue"];
+ const green = args.dmnStyle?.["dmndi:FillColor"]?.["@_green"];
+ const red = args.dmnStyle?.["dmndi:FillColor"]?.["@_red"];
+
+ const opacity =
+ args.nodeType === NODE_TYPES.decisionService ||
+ args.nodeType === NODE_TYPES.group ||
+ args.nodeType === NODE_TYPES.textAnnotation
+ ? 0.1
+ : DEFAULT_NODE_OPACITY;
+
+ if (!args.isEnabled || blue === undefined || green === undefined || red ===
undefined) {
+ return `rgba(${DEFAULT_NODE_RED_FILL}, ${DEFAULT_NODE_GREEN_FILL},
${DEFAULT_NODE_BLUE_FILL}, ${opacity})`;
+ }
+
+ return `rgba(${red}, ${green}, ${blue}, ${opacity})`;
+}
+
+export function getNodeShapeStrokeColor(args: {
+ dmnStyle?: DMNDI15__DMNStyle | undefined;
+ isEnabled?: boolean | undefined;
+}) {
+ const blue = args.dmnStyle?.["dmndi:StrokeColor"]?.["@_blue"];
+ const green = args.dmnStyle?.["dmndi:StrokeColor"]?.["@_green"];
+ const red = args.dmnStyle?.["dmndi:StrokeColor"]?.["@_red"];
+
+ if (!args.isEnabled || blue === undefined || green === undefined || red ===
undefined) {
+ return DEFAULT_NODE_STROKE_COLOR;
+ }
+ return `rgba(${red}, ${green}, ${blue}, 1)`;
+}
+
+export function getDmnFontStyle(args: {
+ dmnStyle?: DMNDI15__DMNStyle | undefined;
+ isEnabled?: boolean | undefined;
+}): DmnFontStyle {
+ const blue = args.dmnStyle?.["dmndi:FontColor"]?.["@_blue"];
+ const green = args.dmnStyle?.["dmndi:FontColor"]?.["@_green"];
+ const red = args.dmnStyle?.["dmndi:FontColor"]?.["@_red"];
+
+ const fontColor =
+ !args.isEnabled || blue === undefined || green === undefined || red ===
undefined
+ ? DEFAULT_FONT_COLOR
+ : `rgba(${red}, ${green}, ${blue}, 1)`;
+
+ return {
+ bold: args.isEnabled ? args.dmnStyle?.["@_fontBold"] ?? false : false,
+ italic: args.isEnabled ? args.dmnStyle?.["@_fontItalic"] ?? false : false,
+ underline: args.isEnabled ? args.dmnStyle?.["@_fontUnderline"] ?? false :
false,
+ strikeThrough: args.isEnabled ? args.dmnStyle?.["@_fontStrikeThrough"] ??
false : false,
+ family: args.isEnabled ? args.dmnStyle?.["@_fontFamily"] : undefined,
+ size: args.isEnabled ? args.dmnStyle?.["@_fontSize"] : undefined,
+ color: fontColor,
+ };
+}
+
+export function getFontCssProperties(dmnFontStyle?: DmnFontStyle):
React.CSSProperties {
let textDecoration = "";
- if (fontProperties?.underline) {
+ if (dmnFontStyle?.underline) {
textDecoration += "underline ";
}
- if (fontProperties?.strikeThrough) {
+ if (dmnFontStyle?.strikeThrough) {
textDecoration += "line-through";
}
+ // Using default values here ensures that the editable Diagram rendered by
ReactFlow and the SVG generated are the closest possible.
return {
- fontWeight: fontProperties?.bold ? "bold" : "",
- fontStyle: fontProperties?.italic ? "italic" : "",
- fontFamily: fontProperties?.family,
+ fontWeight: dmnFontStyle?.bold ? "bold" : "",
+ fontStyle: dmnFontStyle?.italic ? "italic" : "",
+ fontFamily: dmnFontStyle?.family ?? "arial",
textDecoration,
- fontSize: fontProperties?.size,
- color: fontProperties?.color,
+ fontSize: dmnFontStyle?.size ?? "16px",
+ color: dmnFontStyle?.color ?? "black",
+ lineHeight: "1.5em", // This needs to be em `em` otherwise `@visx/text`
breaks when generating the SVG.
};
}
+
+export function getNodeLabelPosition(nodeType: NodeType): NodeLabelPosition {
+ switch (nodeType) {
+ case NODE_TYPES.inputData:
+ return "center-center";
+ case NODE_TYPES.decision:
+ return "center-center";
+ case NODE_TYPES.bkm:
+ return "center-center";
+ case NODE_TYPES.decisionService:
+ return "top-center";
+ case NODE_TYPES.knowledgeSource:
+ return "center-left";
+ case NODE_TYPES.textAnnotation:
+ return "top-left";
+ case NODE_TYPES.group:
+ return "top-left";
+ case NODE_TYPES.unknown:
+ return "center-center";
+ default:
+ assertUnreachable(nodeType);
+ }
+}
+
+export function assertUnreachable(_x: never): never {
+ throw new Error("Didn't expect to get here: " + _x);
+}
diff --git a/packages/dmn-editor/src/diagram/nodes/NodeSvgs.tsx
b/packages/dmn-editor/src/diagram/nodes/NodeSvgs.tsx
index e4fdbfa9934..03db3ededc7 100644
--- a/packages/dmn-editor/src/diagram/nodes/NodeSvgs.tsx
+++ b/packages/dmn-editor/src/diagram/nodes/NodeSvgs.tsx
@@ -22,6 +22,8 @@ import * as RF from "reactflow";
import { DEFAULT_INTRACTION_WIDTH } from "../maths/DmnMaths";
import { DEFAULT_NODE_FILL, DEFAULT_NODE_STROKE_COLOR,
DEFAULT_NODE_STROKE_WIDTH } from "./NodeStyle";
+export type NodeLabelPosition = "center-center" | "top-center" | "center-left"
| "top-left";
+
export type NodeSvgProps = RF.Dimensions &
RF.XYPosition & {
fillColor?: string;
@@ -81,7 +83,7 @@ export function InputDataNodeSvg(__props: NodeSvgProps) {
})();
return (
- <g>
+ <>
<rect
{...props}
x={x}
@@ -95,7 +97,7 @@ export function InputDataNodeSvg(__props: NodeSvgProps) {
rx={rx}
ry={ry}
/>
- </g>
+ </>
);
}
@@ -103,7 +105,7 @@ export function DecisionNodeSvg(__props: NodeSvgProps) {
const { strokeWidth, x, y, width, height, fillColor, strokeColor, props } =
normalize(__props);
return (
- <g>
+ <>
<rect
x={x}
y={y}
@@ -115,7 +117,7 @@ export function DecisionNodeSvg(__props: NodeSvgProps) {
strokeLinejoin={"round"}
{...props}
/>
- </g>
+ </>
);
}
@@ -123,7 +125,7 @@ export function BkmNodeSvg(__props: NodeSvgProps) {
const { strokeWidth, x, y, width, height, fillColor, strokeColor, props } =
normalize(__props);
const bevel = 25;
return (
- <g>
+ <>
<polygon
{...props}
points={`${bevel},0 0,${bevel} 0,${height} ${width - bevel},${height}
${width},${height - bevel}, ${width},0`}
@@ -133,7 +135,7 @@ export function BkmNodeSvg(__props: NodeSvgProps) {
strokeLinejoin={"round"}
transform={`translate(${x},${y})`}
/>
- </g>
+ </>
);
}
@@ -145,7 +147,7 @@ export function KnowledgeSourceNodeSvg(__props:
NodeSvgProps) {
const straightLines = `M${width},${height} L${width},0 L0,0 L0,${height}`;
const bottomWave = `Q${width / 4},${height + amplitude} ${width /
2},${height} T${width},${height}`;
return (
- <g>
+ <>
<path
{...props}
d={`${straightLines} ${bottomWave} Z`}
@@ -155,7 +157,7 @@ export function KnowledgeSourceNodeSvg(__props:
NodeSvgProps) {
strokeLinejoin={"round"}
transform={`translate(${x},${y})`}
/>
- </g>
+ </>
);
}
@@ -197,7 +199,7 @@ export const DecisionServiceNodeSvg = React.forwardRef<
} = _interactionRectProps;
return (
- <g>
+ <>
{!isCollapsed && (
<>
<path
@@ -267,7 +269,7 @@ export const DecisionServiceNodeSvg = React.forwardRef<
</text>
</>
)}
- </g>
+ </>
);
});
@@ -275,7 +277,7 @@ export function TextAnnotationNodeSvg(__props: NodeSvgProps
& { showPlaceholder?
const { strokeWidth, x, y, width, height, fillColor, strokeColor, props:
_props } = normalize(__props);
const { showPlaceholder, ...props } = _props;
return (
- <g>
+ <>
<rect
x={x}
y={y}
@@ -302,7 +304,7 @@ export function TextAnnotationNodeSvg(__props: NodeSvgProps
& { showPlaceholder?
Text
</text>
)}
- </g>
+ </>
);
}
@@ -322,7 +324,7 @@ export const GroupNodeSvg =
React.forwardRef<SVGRectElement, NodeSvgProps & { st
const strokeDasharray = props.strokeDasharray ?? "14,10,3,10";
return (
- <g>
+ <>
<rect
{...props}
x={x}
@@ -351,7 +353,7 @@ export const GroupNodeSvg =
React.forwardRef<SVGRectElement, NodeSvgProps & { st
ry={"30"}
className={containerNodeInteractionRectCssClassName}
/>
- </g>
+ </>
);
}
);
@@ -360,7 +362,7 @@ export const UnknownNodeSvg = (_props: NodeSvgProps & {
strokeDasharray?: string
const { strokeWidth, x, y, width, height, props } = normalize(_props);
const strokeDasharray = props.strokeDasharray ?? "2,4";
return (
- <g>
+ <>
<rect
{...props}
x={x}
@@ -373,6 +375,6 @@ export const UnknownNodeSvg = (_props: NodeSvgProps & {
strokeDasharray?: string
strokeWidth={strokeWidth}
strokeDasharray={strokeDasharray}
/>
- </g>
+ </>
);
};
diff --git a/packages/dmn-editor/src/diagram/nodes/Nodes.tsx
b/packages/dmn-editor/src/diagram/nodes/Nodes.tsx
index 5727dcd5a3e..068a395eaed 100644
--- a/packages/dmn-editor/src/diagram/nodes/Nodes.tsx
+++ b/packages/dmn-editor/src/diagram/nodes/Nodes.tsx
@@ -67,9 +67,18 @@ import { drag } from "d3-drag";
import { updateDecisionServiceDividerLine } from
"../../mutations/updateDecisionServiceDividerLine";
import { addTopLevelItemDefinition } from
"../../mutations/addTopLevelItemDefinition";
import { DmnBuiltInDataType } from
"@kie-tools/boxed-expression-component/dist/api";
-import { useNodeStyle } from "./NodeStyle";
+import { getNodeLabelPosition, useNodeStyle } from "./NodeStyle";
-export type NodeDmnObjects = Unpacked<DMN15__tDefinitions["drgElement"] |
DMN15__tDefinitions["artifact"]> | null;
+export type ElementFilter<E extends { __$$element: string }, Filter extends
string> = E extends any
+ ? E["__$$element"] extends Filter
+ ? E
+ : never
+ : never;
+
+export type NodeDmnObjects =
+ | null
+ | Unpacked<DMN15__tDefinitions["drgElement"]>
+ | ElementFilter<Unpacked<DMN15__tDefinitions["artifact"]>, "textAnnotation"
| "group">;
export type DmnDiagramNodeData<T extends NodeDmnObjects = NodeDmnObjects> = {
dmnObjectNamespace: string | undefined;
@@ -136,7 +145,7 @@ export const InputDataNode = React.memo(
const onCreateDataType = useDataTypeCreationCallbackForNodes(index,
inputData["@_name"]);
- const { fontStyle, shapeStyle } = useNodeStyle({
+ const { fontCssProperties, shapeStyle } = useNodeStyle({
dmnStyle: shape["di:Style"],
nodeType: type as NodeType,
isEnabled: diagram.overlays.enableStyles,
@@ -176,11 +185,12 @@ export const InputDataNode = React.memo(
namedElementQName={dmnObjectQName}
isEditing={isEditingLabel}
setEditing={setEditingLabel}
+ position={getNodeLabelPosition(type as NodeType)}
value={inputData["@_label"] ?? inputData["@_name"]}
onChange={setName}
allUniqueNames={allFeelVariableUniqueNames}
shouldCommitOnBlur={true}
- fontStyle={fontStyle}
+ fontCssProperties={fontCssProperties}
/>
{isHovered && (
<NodeResizerHandle
@@ -254,7 +264,7 @@ export const DecisionNode = React.memo(
const onCreateDataType = useDataTypeCreationCallbackForNodes(index,
decision["@_name"]);
- const { fontStyle, shapeStyle } = useNodeStyle({
+ const { fontCssProperties, shapeStyle } = useNodeStyle({
dmnStyle: shape["di:Style"],
nodeType: type as NodeType,
isEnabled: diagram.overlays.enableStyles,
@@ -297,11 +307,12 @@ export const DecisionNode = React.memo(
namedElementQName={dmnObjectQName}
isEditing={isEditingLabel}
setEditing={setEditingLabel}
+ position={getNodeLabelPosition(type as NodeType)}
value={decision["@_label"] ?? decision["@_name"]}
onChange={setName}
allUniqueNames={allFeelVariableUniqueNames}
shouldCommitOnBlur={true}
- fontStyle={fontStyle}
+ fontCssProperties={fontCssProperties}
/>
{isHovered && (
<NodeResizerHandle
@@ -375,7 +386,7 @@ export const BkmNode = React.memo(
const onCreateDataType = useDataTypeCreationCallbackForNodes(index,
bkm["@_name"]);
- const { fontStyle, shapeStyle } = useNodeStyle({
+ const { fontCssProperties, shapeStyle } = useNodeStyle({
dmnStyle: shape["di:Style"],
nodeType: type as NodeType,
isEnabled: diagram.overlays.enableStyles,
@@ -418,11 +429,12 @@ export const BkmNode = React.memo(
namedElementQName={dmnObjectQName}
isEditing={isEditingLabel}
setEditing={setEditingLabel}
+ position={getNodeLabelPosition(type as NodeType)}
value={bkm["@_label"] ?? bkm["@_name"]}
onChange={setName}
allUniqueNames={allFeelVariableUniqueNames}
shouldCommitOnBlur={true}
- fontStyle={fontStyle}
+ fontCssProperties={fontCssProperties}
/>
{isHovered && (
<NodeResizerHandle
@@ -483,7 +495,7 @@ export const KnowledgeSourceNode = React.memo(
const { allFeelVariableUniqueNames } = useDmnEditorDerivedStore();
- const { fontStyle, shapeStyle } = useNodeStyle({
+ const { fontCssProperties, shapeStyle } = useNodeStyle({
dmnStyle: shape["di:Style"],
nodeType: type as NodeType,
isEnabled: diagram.overlays.enableStyles,
@@ -522,7 +534,7 @@ export const KnowledgeSourceNode = React.memo(
<EditableNodeLabel
namedElement={knowledgeSource}
namedElementQName={dmnObjectQName}
- position={"center-left"}
+ position={getNodeLabelPosition(type as NodeType)}
isEditing={isEditingLabel}
setEditing={setEditingLabel}
value={knowledgeSource["@_label"] ?? knowledgeSource["@_name"]}
@@ -530,7 +542,7 @@ export const KnowledgeSourceNode = React.memo(
skipValidation={true}
allUniqueNames={allFeelVariableUniqueNames}
shouldCommitOnBlur={true}
- fontStyle={fontStyle}
+ fontCssProperties={fontCssProperties}
/>
{isHovered && (
<NodeResizerHandle
@@ -583,7 +595,7 @@ export const TextAnnotationNode = React.memo(
const { allFeelVariableUniqueNames } = useDmnEditorDerivedStore();
- const { fontStyle, shapeStyle } = useNodeStyle({
+ const { fontCssProperties, shapeStyle } = useNodeStyle({
dmnStyle: shape["di:Style"],
nodeType: type as NodeType,
isEnabled: diagram.overlays.enableStyles,
@@ -623,7 +635,7 @@ export const TextAnnotationNode = React.memo(
id={textAnnotation["@_id"]}
namedElement={undefined}
namedElementQName={undefined}
- position={"top-left"}
+ position={getNodeLabelPosition(type as NodeType)}
isEditing={isEditingLabel}
setEditing={setEditingLabel}
value={textAnnotation["@_label"] ?? textAnnotation.text?.__$$text}
@@ -631,7 +643,7 @@ export const TextAnnotationNode = React.memo(
skipValidation={true}
allUniqueNames={allFeelVariableUniqueNames}
shouldCommitOnBlur={true}
- fontStyle={fontStyle}
+ fontCssProperties={fontCssProperties}
/>
{isHovered && (
<NodeResizerHandle
@@ -768,7 +780,7 @@ export const DecisionServiceNode = React.memo(
shape.index,
]);
- const { fontStyle, shapeStyle } = useNodeStyle({
+ const { fontCssProperties, shapeStyle } = useNodeStyle({
dmnStyle: shape["di:Style"],
nodeType: type as NodeType,
isEnabled: diagram.overlays.enableStyles,
@@ -813,14 +825,14 @@ export const DecisionServiceNode = React.memo(
<EditableNodeLabel
namedElement={decisionService}
namedElementQName={dmnObjectQName}
- position={"top-center"}
+ position={getNodeLabelPosition(type as NodeType)}
isEditing={isEditingLabel}
setEditing={setEditingLabel}
value={decisionService["@_label"] ?? decisionService["@_name"]}
onChange={setName}
allUniqueNames={allFeelVariableUniqueNames}
shouldCommitOnBlur={true}
- fontStyle={fontStyle}
+ fontCssProperties={fontCssProperties}
/>
{selected && !dragging && !isCollapsed && (
<NodeResizerHandle
@@ -906,7 +918,7 @@ export const GroupNode = React.memo(
const { allFeelVariableUniqueNames } = useDmnEditorDerivedStore();
- const { fontStyle, shapeStyle } = useNodeStyle({
+ const { fontCssProperties, shapeStyle } = useNodeStyle({
dmnStyle: shape["di:Style"],
nodeType: type as NodeType,
isEnabled: diagram.overlays.enableStyles,
@@ -943,7 +955,7 @@ export const GroupNode = React.memo(
id={group["@_id"]}
namedElement={undefined}
namedElementQName={undefined}
- position={"top-left"}
+ position={getNodeLabelPosition(type as NodeType)}
isEditing={isEditingLabel}
setEditing={setEditingLabel}
value={group["@_label"] ?? group["@_name"]}
@@ -951,7 +963,7 @@ export const GroupNode = React.memo(
skipValidation={true}
allUniqueNames={allFeelVariableUniqueNames}
shouldCommitOnBlur={true}
- fontStyle={fontStyle}
+ fontCssProperties={fontCssProperties}
/>
{selected && !dragging && (
<NodeResizerHandle
@@ -1001,7 +1013,7 @@ export const UnknownNode = React.memo(
<EditableNodeLabel
namedElement={undefined}
namedElementQName={undefined}
- position={"center-center"}
+ position={getNodeLabelPosition(type as NodeType)}
isEditing={false}
setEditing={() => {}}
value={`? `}
diff --git a/packages/dmn-editor/src/store/useDiagramData.tsx
b/packages/dmn-editor/src/store/useDiagramData.tsx
index 9789f1c4f59..3ca1beaf800 100644
--- a/packages/dmn-editor/src/store/useDiagramData.tsx
+++ b/packages/dmn-editor/src/store/useDiagramData.tsx
@@ -326,6 +326,10 @@ export function useDiagramData(externalDmnsByNamespace:
ExternalDmnsIndex) {
return newNode ? [newNode] : [];
}),
...(thisDmn.model.definitions.artifact ?? []).flatMap((dmnObject, index)
=> {
+ if (dmnObject.__$$element === "association") {
+ return [];
+ }
+
const newNode = ackNode({ type: "xml-qname", localPart:
dmnObject["@_id"]! }, dmnObject, index);
return newNode ? [newNode] : [];
}),
diff --git a/packages/dmn-editor/src/svg/DmnDiagramSvg.tsx
b/packages/dmn-editor/src/svg/DmnDiagramSvg.tsx
new file mode 100644
index 00000000000..7dc91246484
--- /dev/null
+++ b/packages/dmn-editor/src/svg/DmnDiagramSvg.tsx
@@ -0,0 +1,290 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import * as React from "react";
+import * as RF from "reactflow";
+import {
+ AssociationPath,
+ AuthorityRequirementPath,
+ DmnDiagramEdgeData,
+ InformationRequirementPath,
+ KnowledgeRequirementPath,
+} from "../diagram/edges/Edges";
+import { DmnDiagramNodeData } from "../diagram/nodes/Nodes";
+import { SnapGrid, State } from "../store/Store";
+import { EdgeMarkers } from "../diagram/edges/EdgeMarkers";
+import { EDGE_TYPES } from "../diagram/edges/EdgeTypes";
+import { getSnappedMultiPointAnchoredEdgePath } from
"../diagram/edges/getSnappedMultiPointAnchoredEdgePath";
+import {
+ InputDataNodeSvg,
+ DecisionNodeSvg,
+ BkmNodeSvg,
+ KnowledgeSourceNodeSvg,
+ DecisionServiceNodeSvg,
+ GroupNodeSvg,
+ TextAnnotationNodeSvg,
+ UnknownNodeSvg,
+ NodeLabelPosition,
+} from "../diagram/nodes/NodeSvgs";
+import { NODE_TYPES } from "../diagram/nodes/NodeTypes";
+import { useMemo } from "react";
+import {
+ assertUnreachable,
+ getDmnFontStyle,
+ getNodeLabelPosition,
+ getNodeShapeFillColor,
+ getNodeShapeStrokeColor,
+ getNodeStyle,
+} from "../diagram/nodes/NodeStyle";
+import { NodeType } from "../diagram/connections/graphStructure";
+import { buildFeelQNameFromXmlQName } from "../feel/buildFeelQName";
+import { DerivedStore } from "../store/DerivedStore";
+import { Text } from "@visx/text";
+
+export function DmnDiagramSvg({
+ nodes,
+ edges,
+ snapGrid,
+ thisDmn,
+ importsByNamespace,
+}: {
+ nodes: RF.Node<DmnDiagramNodeData>[];
+ edges: RF.Edge<DmnDiagramEdgeData>[];
+ snapGrid: SnapGrid;
+ thisDmn: State["dmn"];
+ importsByNamespace: DerivedStore["importsByNamespace"];
+}) {
+ const { nodesSvg, nodesById } = useMemo(() => {
+ const nodesById = new Map<string, RF.Node<DmnDiagramNodeData>>();
+
+ const nodesSvg = nodes.map((node) => {
+ const { fontCssProperties: fontStyle, shapeStyle } = getNodeStyle({
+ fillColor: getNodeShapeFillColor({
+ dmnStyle: node.data.shape["di:Style"],
+ nodeType: node.type as NodeType,
+ isEnabled: true,
+ }),
+ strokeColor: getNodeShapeStrokeColor({ dmnStyle:
node.data.shape["di:Style"], isEnabled: true }),
+ dmnFontStyle: getDmnFontStyle({ dmnStyle: node.data.shape["di:Style"],
isEnabled: true }),
+ });
+
+ nodesById.set(node.id, node);
+
+ const { height, width, ...style } = node.style!;
+
+ const label =
+ node.data?.dmnObject?.__$$element === "group"
+ ? node.data.dmnObject?.["@_label"] ??
node.data?.dmnObject?.["@_name"] ?? "<Empty>"
+ : node.data?.dmnObject?.__$$element === "textAnnotation"
+ ? node.data.dmnObject?.["@_label"] ??
node.data?.dmnObject?.text?.__$$text ?? "<Empty>"
+ : buildFeelQNameFromXmlQName({
+ namedElement: node.data!.dmnObject!,
+ importsByNamespace,
+ model: thisDmn.model.definitions,
+ namedElementQName: node.data!.dmnObjectQName,
+ relativeToNamespace: thisDmn.model.definitions["@_namespace"],
+ }).full;
+
+ return (
+ <>
+ <g data-kie-dmn-node-id={node.id}>
+ {node.type === NODE_TYPES.inputData && (
+ <InputDataNodeSvg
+ width={node.width!}
+ height={node.height!}
+ x={node.positionAbsolute!.x}
+ y={node.positionAbsolute!.y}
+ {...style}
+ {...shapeStyle}
+ />
+ )}
+ {node.type === NODE_TYPES.decision && (
+ <DecisionNodeSvg
+ width={node.width!}
+ height={node.height!}
+ x={node.positionAbsolute!.x}
+ y={node.positionAbsolute!.y}
+ {...style}
+ {...shapeStyle}
+ />
+ )}
+ {node.type === NODE_TYPES.bkm && (
+ <BkmNodeSvg
+ width={node.width!}
+ height={node.height!}
+ x={node.positionAbsolute!.x}
+ y={node.positionAbsolute!.y}
+ {...style}
+ {...shapeStyle}
+ />
+ )}
+ {node.type === NODE_TYPES.knowledgeSource && (
+ <KnowledgeSourceNodeSvg
+ width={node.width!}
+ height={node.height!}
+ x={node.positionAbsolute!.x}
+ y={node.positionAbsolute!.y}
+ {...style}
+ {...shapeStyle}
+ />
+ )}
+ {node.type === NODE_TYPES.decisionService && (
+ <DecisionServiceNodeSvg
+ width={node.width!}
+ height={node.height!}
+ x={node.positionAbsolute!.x}
+ y={node.positionAbsolute!.y}
+ showSectionLabels={false}
+ isReadonly={true}
+ {...style}
+ {...shapeStyle}
+ />
+ )}
+ {node.type === NODE_TYPES.group && (
+ <GroupNodeSvg
+ width={node.width!}
+ height={node.height!}
+ x={node.positionAbsolute!.x}
+ y={node.positionAbsolute!.y}
+ {...style}
+ {...(shapeStyle as any)}
+ />
+ )}
+ {node.type === NODE_TYPES.textAnnotation && (
+ <TextAnnotationNodeSvg
+ width={node.width!}
+ height={node.height!}
+ x={node.positionAbsolute!.x}
+ y={node.positionAbsolute!.y}
+ {...style}
+ {...shapeStyle}
+ />
+ )}
+ {node.type === NODE_TYPES.unknown && (
+ <UnknownNodeSvg
+ width={node.width!}
+ height={node.height!}
+ x={node.positionAbsolute!.x}
+ y={node.positionAbsolute!.y}
+ {...style}
+ {...(shapeStyle as any)}
+ />
+ )}
+ <>
+ {label.split("\n").map((labelLine, i) => (
+ <Text
+ key={i}
+ lineHeight={fontStyle.lineHeight}
+ style={{ ...fontStyle, fill: fontStyle.color }}
+ dy={`calc(1.5em * ${i})`}
+ {...getNodeLabelSvgTextAlignmentProps(node,
getNodeLabelPosition(node.type as NodeType))}
+ >
+ {labelLine}
+ </Text>
+ ))}
+ </>
+ </g>
+ </>
+ );
+ });
+
+ return { nodesSvg, nodesById };
+ }, [importsByNamespace, nodes, thisDmn.model.definitions]);
+
+ return (
+ <>
+ <EdgeMarkers />
+ {edges.map((e) => {
+ const { path } = getSnappedMultiPointAnchoredEdgePath({
+ snapGrid,
+ dmnEdge: e.data?.dmnEdge,
+ dmnShapeSource: e.data?.dmnShapeSource,
+ dmnShapeTarget: e.data?.dmnShapeTarget,
+ sourceNode: nodesById?.get(e.source),
+ targetNode: nodesById?.get(e.target),
+ });
+ return (
+ <>
+ {e.type === EDGE_TYPES.informationRequirement &&
<InformationRequirementPath d={path} />}
+ {e.type === EDGE_TYPES.knowledgeRequirement &&
<KnowledgeRequirementPath d={path} />}
+ {e.type === EDGE_TYPES.authorityRequirement && (
+ <AuthorityRequirementPath d={path}
centerToConnectionPoint={true} />
+ )}
+ {e.type === EDGE_TYPES.association && <AssociationPath d={path} />}
+ </>
+ );
+ })}
+ {nodesSvg}
+ </>
+ );
+}
+
+const SVG_NODE_LABEL_TEXT_PADDING_ALL = 10;
+const SVG_NODE_LABEL_TEXT_ADDITIONAL_PADDING_TOP_LEFT = 8;
+
+export function getNodeLabelSvgTextAlignmentProps(n:
RF.Node<DmnDiagramNodeData>, labelPosition: NodeLabelPosition) {
+ switch (labelPosition) {
+ case "center-center":
+ const ccTx = n.position.x! + n.width! / 2;
+ const ccTy = n.position.y! + n.height! / 2;
+ const ccWidth = n.width! - 2 * SVG_NODE_LABEL_TEXT_PADDING_ALL;
+ return {
+ verticalAnchor: "middle",
+ textAnchor: "middle",
+ transform: `translate(${ccTx},${ccTy})`,
+ width: ccWidth,
+ } as const;
+
+ case "top-center":
+ const tcTx = n.position.x! + n.width! / 2;
+ const tcTy = n.position.y! + SVG_NODE_LABEL_TEXT_PADDING_ALL;
+ const tcWidth = n.width! - 2 * SVG_NODE_LABEL_TEXT_PADDING_ALL;
+ return {
+ verticalAnchor: "start",
+ textAnchor: "middle",
+ transform: `translate(${tcTx},${tcTy})`,
+ width: tcWidth,
+ } as const;
+
+ case "center-left":
+ const clTx = n.position.x! + SVG_NODE_LABEL_TEXT_PADDING_ALL;
+ const clTy = n.position.y! + n.height! / 2;
+ const clWidth = n.width! - 2 * SVG_NODE_LABEL_TEXT_PADDING_ALL;
+ return {
+ verticalAnchor: "middle",
+ textAnchor: "start",
+ transform: `translate(${clTx},${clTy})`,
+ width: clWidth,
+ } as const;
+
+ case "top-left":
+ const tlTx = n.position.x! + SVG_NODE_LABEL_TEXT_PADDING_ALL +
SVG_NODE_LABEL_TEXT_ADDITIONAL_PADDING_TOP_LEFT;
+ const tlTy = n.position.y! + SVG_NODE_LABEL_TEXT_PADDING_ALL +
SVG_NODE_LABEL_TEXT_ADDITIONAL_PADDING_TOP_LEFT;
+ const tlWidth =
+ n.width! - 2 * SVG_NODE_LABEL_TEXT_PADDING_ALL - 2 *
SVG_NODE_LABEL_TEXT_ADDITIONAL_PADDING_TOP_LEFT;
+ return {
+ verticalAnchor: "start",
+ textAnchor: "start",
+ transform: `translate(${tlTx},${tlTy})`,
+ width: tlWidth,
+ } as const;
+ default:
+ assertUnreachable(labelPosition);
+ }
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 536e627202e..6f2694a9d90 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -3300,6 +3300,9 @@ importers:
"@patternfly/react-styles":
specifier: ^4.92.6
version: 4.92.6
+ "@visx/text":
+ specifier: ^3.3.0
+ version: 3.3.0([email protected])
d3-drag:
specifier: ^3.0.0
version: 3.0.0
@@ -23185,6 +23188,11 @@ packages:
resolution:
{ integrity:
sha512-DvmZHoHTFJ8zhVYwCLWbQ7uAbYQEk52Ev2/ZiQ7Y7gQGeV9pjBqjnQpECMHfKS1rCYAhMI7LHVxwyZLZinJgdw==
}
+ /@types/[email protected]:
+ resolution:
+ { integrity:
sha512-OvlIYQK9tNneDlS0VN54LLd5uiPCBOp7gS5Z0f1mjoJYBrtStzgmJBxONW3U6OZqdtNzZPmn9BS/7WI7BFFcFQ==
}
+ dev: false
+
/@types/[email protected]:
resolution:
{ integrity:
sha512-r7/zWe+f9x+zjXqGxf821qz++ld8tp6Z4jUS6qmPZUXH6tfh4riXOhAqb12tWGWAevCFtMt1goLWkQMqIJKpsA==
}
@@ -23769,6 +23777,21 @@ packages:
{ integrity:
sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==
}
dev: true
+ /@visx/[email protected]([email protected]):
+ resolution:
+ { integrity:
sha512-fOimcsf0GtQE9whM5MdA/xIkHMaV29z7qNqNXysUDE8znSMKsN+ott7kSg2ljAEE89CQo3WKHkPNettoVsa84w==
}
+ peerDependencies:
+ react: ^16.3.0-0 || ^17.0.0-0 || ^18.0.0-0
+ dependencies:
+ "@types/lodash": 4.14.202
+ "@types/react": 17.0.21
+ classnames: 2.3.2
+ lodash: 4.17.21
+ prop-types: 15.8.1
+ react: 17.0.2
+ reduce-css-calc: 1.3.0
+ dev: false
+
/@vscode/[email protected]:
resolution:
{ integrity:
sha512-M31xGH0RgqNU6CZ4/9g39oUMJ99nLzfjA+4UbtIQ6TcXQ6+2qkjOOxedmPBDDCg26/3Al5ubjY80hIoaMwKYSw==
}
@@ -25889,6 +25912,11 @@ packages:
babel-preset-current-node-syntax: 1.0.1(@babel/[email protected])
dev: true
+ /[email protected]:
+ resolution:
+ { integrity:
sha512-STw03mQKnGUYtoNjmowo4F2cRmIIxYEGiMsjjwla/u5P1lxadj/05WkNaFjNiKTgJkj8KiXbgAiRTmcQRwQNtg==
}
+ dev: false
+
/[email protected]:
resolution:
{ integrity:
sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
}
@@ -35566,6 +35594,11 @@ packages:
react: 17.0.2
dev: true
+ /[email protected]:
+ resolution:
+ { integrity:
sha512-4vRUvPyxdO8cWULGTh9dZWL2tZK6LDBvj+OGHBER7poH9Qdt7kXEoj20wiz4lQUbUXQZFjPbe5mVDo9nutizCw==
}
+ dev: false
+
/[email protected]:
resolution:
{ integrity:
sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==
}
@@ -39840,6 +39873,22 @@ packages:
strip-indent: 3.0.0
dev: true
+ /[email protected]:
+ resolution:
+ { integrity:
sha512-0dVfwYVOlf/LBA2ec4OwQ6p3X9mYxn/wOl2xTcLwjnPYrkgEfPx3VI4eGCH3rQLlPISG5v9I9bkZosKsNRTRKA==
}
+ dependencies:
+ balanced-match: 0.4.2
+ math-expression-evaluator: 1.4.0
+ reduce-function-call: 1.0.3
+ dev: false
+
+ /[email protected]:
+ resolution:
+ { integrity:
sha512-Hl/tuV2VDgWgCSEeWMLwxLZqX7OK59eU1guxXsRKTAyeYimivsKdtcV4fu3r710tpG5GmDKDhQ0HSZLExnNmyQ==
}
+ dependencies:
+ balanced-match: 1.0.2
+ dev: false
+
/[email protected]:
resolution:
{ integrity:
sha512-uI2dQN43zqLWCt6B/BMGRMY6db7TTY4qeHHfGeKb3EOhmOKjU3KdWvNLJyqaHRksv/ErdNH7cFZWg9jXtewy4g==
}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]