You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@kie.apache.org by ti...@apache.org on 2024/02/02 17:16:08 UTC
(incubator-kie-tools) branch main updated: kie-issues#451: Implement autolayout on the new React-based DMN Editor for DMN files without Diagram information (part 1) (#2130)
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 b0d1a40320b kie-issues#451: Implement autolayout on the new React-based DMN Editor for DMN files without Diagram information (part 1) (#2130)
b0d1a40320b is described below
commit b0d1a40320bd24bd44020ae2f542dba80f1c2447
Author: Tiago Bento <15...@users.noreply.github.com>
AuthorDate: Fri Feb 2 12:16:04 2024 -0500
kie-issues#451: Implement autolayout on the new React-based DMN Editor for DMN files without Diagram information (part 1) (#2130)
---
packages/dmn-editor/package.json | 2 +
packages/dmn-editor/src/DmnEditor.css | 51 +-
packages/dmn-editor/src/DmnEditor.tsx | 99 ++--
.../dmn-editor/src/autolayout/AutolayoutButton.tsx | 557 ++++++++++++++++++
.../src/boxedExpressions/BoxedExpression.tsx | 115 ++--
packages/dmn-editor/src/dataTypes/DataTypeName.tsx | 25 +-
.../dmn-editor/src/dataTypes/DataTypePanel.tsx | 14 +-
packages/dmn-editor/src/dataTypes/DataTypes.tsx | 19 +-
.../src/dataTypes/ItemComponentsTable.tsx | 12 +-
packages/dmn-editor/src/dataTypes/TypeRefLabel.tsx | 5 +-
.../dmn-editor/src/dataTypes/TypeRefSelector.tsx | 44 +-
.../dmn-editor/src/dataTypes/useResolvedTypeRef.ts | 24 +-
packages/dmn-editor/src/diagram/Diagram.tsx | 623 +++++++++++++--------
.../dmn-editor/src/diagram/DrdSelectorPanel.tsx | 15 +-
packages/dmn-editor/src/diagram/DrgNodesPanel.tsx | 18 +-
packages/dmn-editor/src/diagram/Palette.tsx | 11 +-
.../src/diagram/connections/ConnectionLine.tsx | 30 +-
.../drd/drdId.ts} | 30 +-
packages/dmn-editor/src/diagram/edges/Edges.tsx | 65 ++-
.../dmn-editor/src/diagram/edges/Waypoints.tsx | 8 +-
.../edges/getSnappedMultiPointAnchoredEdgePath.tsx | 38 +-
.../useAlwaysVisibleEdgeUpdatersAtNodeBorders.ts | 37 +-
.../src/diagram/edges/useKieEdgePath.tsx | 46 +-
.../diagram/edges/usePotentialWaypointControls.ts | 16 +-
packages/dmn-editor/src/diagram/graph/graph.ts | 41 +-
packages/dmn-editor/src/diagram/maths/DmnMaths.ts | 17 +-
packages/dmn-editor/src/diagram/maths/Maths.ts | 31 +-
.../memoization/memoization.ts} | 35 +-
.../src/diagram/nodes/DataTypeNodePanel.tsx | 15 +-
.../src/diagram/nodes/EditExpressionNodePanel.tsx | 5 +-
.../src/diagram/nodes/EditableNodeLabel.tsx | 80 +--
.../dmn-editor/src/diagram/nodes/InfoNodePanel.tsx | 2 +-
packages/dmn-editor/src/diagram/nodes/NodeSvgs.tsx | 6 +-
packages/dmn-editor/src/diagram/nodes/Nodes.tsx | 559 ++++++++++--------
packages/dmn-editor/src/draggable/Draggable.tsx | 3 -
.../src/externalNodes/DmnObjectListItem.tsx | 9 +-
.../src/externalNodes/ExternalNodesPanel.tsx | 12 +-
.../dmn-editor/src/feel/InlineFeelNameInput.tsx | 15 +-
.../dmn-editor/src/focus/useFocusableElement.ts | 9 +-
.../DmnEditorDependenciesContext.tsx | 17 +-
.../src/includedModels/IncludedModels.tsx | 48 +-
.../dmn-editor/src/mutations/addConnectedNode.ts | 4 +-
packages/dmn-editor/src/mutations/addEdge.ts | 12 +-
packages/dmn-editor/src/mutations/deleteEdge.ts | 68 ++-
packages/dmn-editor/src/mutations/deleteNode.ts | 163 +++++-
.../dmn-editor/src/overlaysPanel/OverlaysPanel.tsx | 17 +-
.../src/propertiesPanel/BeePropertiesPanel.tsx | 18 +-
.../src/propertiesPanel/BkmProperties.tsx | 7 +-
.../src/propertiesPanel/DecisionProperties.tsx | 8 +-
.../propertiesPanel/DecisionServiceProperties.tsx | 13 +-
.../src/propertiesPanel/DiagramPropertiesPanel.tsx | 10 +-
.../DocumentationLinksFormGroup.tsx | 6 +-
.../dmn-editor/src/propertiesPanel/FontOptions.tsx | 14 +-
.../propertiesPanel/GlobalDiagramProperties.tsx | 4 +-
.../src/propertiesPanel/GroupProperties.tsx | 2 +-
.../src/propertiesPanel/InputDataProperties.tsx | 8 +-
.../propertiesPanel/KnowledgeSourceProperties.tsx | 8 +-
.../src/propertiesPanel/MultipleNodeProperties.tsx | 2 +-
.../src/propertiesPanel/ShapeOptions.tsx | 15 +-
.../src/propertiesPanel/SingleNodeProperties.tsx | 11 +-
.../propertiesPanel/TextAnnotationProperties.tsx | 2 +-
.../src/propertiesPanel/UnknownProperties.tsx | 10 +-
.../dmn-editor/src/store/ComputedStateCache.ts | 76 +++
packages/dmn-editor/src/store/DerivedStore.tsx | 311 ----------
packages/dmn-editor/src/store/Store.ts | 345 +++++++-----
packages/dmn-editor/src/store/StoreContext.ts | 47 ++
.../computed/computeAllFeelVariableUniqueNames.ts} | 39 +-
.../src/store/computed/computeDataTypes.ts | 131 +++++
.../src/store/computed/computeDiagramData.ts | 432 ++++++++++++++
.../store/computed/computeExternalModelsByType.ts | 50 ++
.../computed/computeImportsByNamespace.ts} | 35 +-
.../src/store/computed/computeIndexes.ts | 68 +++
.../computeIsDropTargetNodeValidForSelection.ts} | 34 +-
.../computed/initial.ts} | 60 +-
packages/dmn-editor/src/store/useDiagramData.tsx | 474 ----------------
packages/dmn-editor/src/svg/DmnDiagramSvg.tsx | 222 ++++----
packages/dmn-editor/tsconfig.json | 6 +-
.../src/parser/VariablesRepository.ts | 2 +-
pnpm-lock.yaml | 11 +
79 files changed, 3337 insertions(+), 2216 deletions(-)
diff --git a/packages/dmn-editor/package.json b/packages/dmn-editor/package.json
index 8a25ed3216e..8e05a5dfe65 100644
--- a/packages/dmn-editor/package.json
+++ b/packages/dmn-editor/package.json
@@ -46,6 +46,8 @@
"@visx/text": "^3.3.0",
"d3-drag": "^3.0.0",
"d3-selection": "^3.0.0",
+ "elkjs": "^0.9.1",
+ "fast-deep-equal": "^3.1.3",
"immer": "^10.0.3",
"moment": "^2.29.4",
"react": "^17.0.2",
diff --git a/packages/dmn-editor/src/DmnEditor.css b/packages/dmn-editor/src/DmnEditor.css
index 861f8f78c2b..444d2b7217c 100644
--- a/packages/dmn-editor/src/DmnEditor.css
+++ b/packages/dmn-editor/src/DmnEditor.css
@@ -318,17 +318,48 @@
}
/* (end) version popover */
-/* (begin) overlays panel toggle */
-.kie-dmn-editor--overlay-panel-popover .pf-c-popover__body {
- padding: 0;
+/* (begin) autolayout panel toggle */
+.kie-dmn-editor--autolayout-panel-toggle {
+ width: 42px;
+ background: #fff;
+ border-radius: 40px;
+ border: 1px solid black;
}
-.kie-dmn-editor--overlays-panel-toggle {
+.kie-dmn-editor--autolayout-panel-toggle:not(:last-child) {
margin-right: 16px;
+}
+
+.kie-dmn-editor--autolayout-panel-toggle:hover {
+ box-shadow: -4px 4px 2px 0px rgb(127 127 127);
+}
+.kie-dmn-editor--autolayout-panel-toggle-button {
+ margin: 0;
+ width: 40px;
+ height: 40px;
+ max-width: 40px;
+ border-radius: 40px;
+ border: 0;
+ background: white;
+}
+.kie-dmn-editor--autolayout-panel-toggle-button:hover {
+ filter: brightness(95%);
+}
+.kie-dmn-editor--autolayout-panel-toggle-button:active {
+ filter: brightness(90%);
+}
+/* (end) autolayout panel toggle */
+
+/* (begin) overlays panel toggle */
+.kie-dmn-editor--overlays-panel-toggle {
width: 42px;
background: #fff;
border-radius: 40px;
border: 1px solid black;
}
+.kie-dmn-editor--overlays-panel-toggle:not(:last-child) {
+ margin-right: 16px;
+}
+
.kie-dmn-editor--overlays-panel-toggle:hover {
box-shadow: -4px 4px 2px 0px rgb(127 127 127);
}
@@ -395,7 +426,8 @@
left: 58px;
overflow: auto;
box-shadow: 0px 5px 10px 0px lightgray;
- background: white;
+ background: rgb(255, 255, 255, 90%);
+ backdrop-filter: blur(4px);
width: 400px;
}
.kie-dmn-editor--external-nodes-panel-toggle {
@@ -700,14 +732,13 @@ circle.kie-dmn-editor--diagram-edge-waypoint:hover {
}
.kie-dmn-editor--external-nodes-list-item {
border-radius: 999px;
- background: white;
}
.kie-dmn-editor--external-nodes-list-item:hover {
- filter: brightness(95%);
+ background-color: rgba(0, 0, 0, 0.1);
cursor: grab;
}
.kie-dmn-editor--external-nodes-list-item:active {
- filter: brightness(90%);
+ background-color: rgba(0, 0, 0, 0.15);
cursor: grabbing;
}
/* (end) external nodes */
@@ -903,7 +934,9 @@ th {
fill: rgba(0, 107, 164, 0.1) !important;
}
-.react-flow__node.selected:not(.react-flow__node-node_unknown) > .kie-dmn-editor--node-shape > * {
+.react-flow__node.selected:not(.react-flow__node-node_unknown):not(.react-flow__node-node_textAnnotation)
+ > .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 522838bb0f6..d1eb175c043 100644
--- a/packages/dmn-editor/src/DmnEditor.tsx
+++ b/packages/dmn-editor/src/DmnEditor.tsx
@@ -20,47 +20,40 @@
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 { AllDmnMarshallers, DmnLatestModel } from "@kie-tools/dmn-marshaller";
+import { PMML } from "@kie-tools/pmml-editor-marshaller";
import { Drawer, DrawerContent, DrawerContentBody } from "@patternfly/react-core/dist/js/components/Drawer";
+import { Label } from "@patternfly/react-core/dist/js/components/Label";
import { Tab, TabTitleIcon, TabTitleText, Tabs } from "@patternfly/react-core/dist/js/components/Tabs";
import { FileIcon } from "@patternfly/react-icons/dist/js/icons/file-icon";
import { InfrastructureIcon } from "@patternfly/react-icons/dist/js/icons/infrastructure-icon";
import { PficonTemplateIcon } from "@patternfly/react-icons/dist/js/icons/pficon-template-icon";
+import { original } from "immer";
+import * as React from "react";
+import { useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from "react";
+import * as ReactDOM from "react-dom";
+import { ErrorBoundary, ErrorBoundaryPropsWithFallback } from "react-error-boundary";
+import * as RF from "reactflow";
+import { DmnEditorContextProvider, useDmnEditor } from "./DmnEditorContext";
+import { DmnEditorErrorFallback } from "./DmnEditorErrorFallback";
import { BoxedExpression } from "./boxedExpressions/BoxedExpression";
import { DataTypes } from "./dataTypes/DataTypes";
import { Diagram, DiagramRef } from "./diagram/Diagram";
import { DmnVersionLabel } from "./diagram/DmnVersionLabel";
+import { DmnEditorExternalModelsContextProvider } from "./includedModels/DmnEditorDependenciesContext";
import { IncludedModels } from "./includedModels/IncludedModels";
+import { BeePropertiesPanel } from "./propertiesPanel/BeePropertiesPanel";
import { DiagramPropertiesPanel } from "./propertiesPanel/DiagramPropertiesPanel";
-import {
- DmnEditorStoreApiContext,
- DmnEditorTab,
- StoreApiType,
- createDmnEditorStore,
- defaultStaticState,
- useDmnEditorStore,
- useDmnEditorStoreApi,
-} from "./store/Store";
+import { ComputedStateCache } from "./store/ComputedStateCache";
+import { Computed, DmnEditorTab, createDmnEditorStore, defaultStaticState } from "./store/Store";
+import { DmnEditorStoreApiContext, StoreApiType, useDmnEditorStore, useDmnEditorStoreApi } from "./store/StoreContext";
+import { DmnDiagramSvg } from "./svg/DmnDiagramSvg";
import { useEffectAfterFirstRender } from "./useEffectAfterFirstRender";
-import { Label } from "@patternfly/react-core/dist/js/components/Label";
-import { BeePropertiesPanel } from "./propertiesPanel/BeePropertiesPanel";
-import { DmnEditorDerivedStoreContextProvider, useDmnEditorDerivedStore } from "./store/DerivedStore";
-import { DmnEditorContextProvider, useDmnEditor } from "./DmnEditorContext";
-import { DmnEditorExternalModelsContextProvider } from "./includedModels/DmnEditorDependenciesContext";
-import { ErrorBoundary, ErrorBoundaryPropsWithFallback } from "react-error-boundary";
-import { DmnEditorErrorFallback } from "./DmnEditorErrorFallback";
-import { DmnLatestModel, AllDmnMarshallers } from "@kie-tools/dmn-marshaller";
-import { PMML } from "@kie-tools/pmml-editor-marshaller";
-import { original } from "immer";
+import { INITIAL_COMPUTED_CACHE } from "./store/computed/initial";
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;
@@ -163,10 +156,15 @@ export const DmnEditorInternal = ({
onModelChange,
forwardRef,
}: DmnEditorProps & { forwardRef?: React.Ref<DmnEditorRef> }) => {
- const { boxedExpressionEditor, dmn, navigation, dispatch, diagram } = useDmnEditorStore((s) => s);
+ const boxedExpressionEditorActiveDrgElementId = useDmnEditorStore((s) => s.boxedExpressionEditor.activeDrgElementId);
+ const isBeePropertiesPanelOpen = useDmnEditorStore((s) => s.boxedExpressionEditor.propertiesPanel.isOpen);
+ const isDiagramPropertiesPanelOpen = useDmnEditorStore((s) => s.diagram.propertiesPanel.isOpen);
+ const navigationTab = useDmnEditorStore((s) => s.navigation.tab);
+ const dmn = useDmnEditorStore((s) => s.dmn);
+ const isDiagramEditingInProgress = useDmnEditorStore((s) => s.computed(s).isDiagramEditingInProgress());
const dmnEditorStoreApi = useDmnEditorStoreApi();
- const { isDiagramEditingInProgress, importsByNamespace } = useDmnEditorDerivedStore();
+
const { dmnModelBeforeEditingRef, dmnEditorRootElementRef } = useDmnEditor();
// Refs
@@ -179,7 +177,10 @@ export const DmnEditorInternal = ({
useImperativeHandle(
forwardRef,
() => ({
- reset: (model) => dispatch.dmn.reset(model),
+ reset: (model) => {
+ const state = dmnEditorStoreApi.getState();
+ return state.dispatch(state).dmn.reset(model);
+ },
getDiagramSvg: async () => {
const nodes = diagramRef.current?.getReactFlowInstance()?.getNodes();
const edges = diagramRef.current?.getReactFlowInstance()?.getEdges();
@@ -187,12 +188,14 @@ export const DmnEditorInternal = ({
return undefined;
}
- const bounds = RF.getRectOfNodes(nodes);
+ const bounds = RF.getNodesBounds(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 + "");
+ const state = dmnEditorStoreApi.getState();
+
// We're still on React 17.
// eslint-disable-next-line react/no-deprecated
ReactDOM.render(
@@ -201,9 +204,9 @@ export const DmnEditorInternal = ({
<DmnDiagramSvg
nodes={nodes}
edges={edges}
- snapGrid={diagram.snapGrid}
- importsByNamespace={importsByNamespace}
- thisDmn={dmnEditorStoreApi.getState().dmn}
+ snapGrid={state.diagram.snapGrid}
+ importsByNamespace={state.computed(state).importsByNamespace()}
+ thisDmn={state.dmn}
/>
</g>,
svg
@@ -212,7 +215,7 @@ export const DmnEditorInternal = ({
return new XMLSerializer().serializeToString(svg);
},
}),
- [diagram.snapGrid, dispatch.dmn, dmnEditorStoreApi, importsByNamespace]
+ [dmnEditorStoreApi]
);
// Make sure the DMN Editor reacts to props changing.
@@ -225,7 +228,7 @@ export const DmnEditorInternal = ({
state.dmn.model = model;
dmnModelBeforeEditingRef.current = model;
});
- }, [dmnEditorStoreApi, dispatch.dmn, model]);
+ }, [dmnEditorStoreApi, model]);
useStateAsItWasBeforeConditionBecameTrue(
dmn.model,
@@ -311,16 +314,16 @@ export const DmnEditorInternal = ({
<div ref={dmnEditorRootElementRef} className={"kie-dmn-editor--root"}>
<Tabs
isFilled={true}
- activeKey={navigation.tab}
+ activeKey={navigationTab}
onSelect={onTabChanged}
role={"region"}
className={"kie-dmn-editor--tabs"}
>
<Tab eventKey={DmnEditorTab.EDITOR} title={tabTitle.editor}>
- {navigation.tab === DmnEditorTab.EDITOR && (
+ {navigationTab === DmnEditorTab.EDITOR && (
<>
- {!boxedExpressionEditor.activeDrgElementId && (
- <Drawer isExpanded={diagram.propertiesPanel.isOpen} isInline={true} position={"right"}>
+ {!boxedExpressionEditorActiveDrgElementId && (
+ <Drawer isExpanded={isDiagramPropertiesPanelOpen} isInline={true} position={"right"}>
<DrawerContent panelContent={diagramPropertiesPanel}>
<DrawerContentBody>
<div className={"kie-dmn-editor--diagram-container"} ref={diagramContainerRef}>
@@ -331,8 +334,8 @@ export const DmnEditorInternal = ({
</DrawerContent>
</Drawer>
)}
- {boxedExpressionEditor.activeDrgElementId && (
- <Drawer isExpanded={boxedExpressionEditor.propertiesPanel.isOpen} isInline={true} position={"right"}>
+ {boxedExpressionEditorActiveDrgElementId && (
+ <Drawer isExpanded={isBeePropertiesPanelOpen} isInline={true} position={"right"}>
<DrawerContent panelContent={beePropertiesPanel}>
<DrawerContentBody>
<div className={"kie-dmn-editor--bee-container"} ref={beeContainerRef}>
@@ -347,11 +350,11 @@ export const DmnEditorInternal = ({
</Tab>
<Tab eventKey={DmnEditorTab.DATA_TYPES} title={tabTitle.dataTypes}>
- {navigation.tab === DmnEditorTab.DATA_TYPES && <DataTypes />}
+ {navigationTab === DmnEditorTab.DATA_TYPES && <DataTypes />}
</Tab>
<Tab eventKey={DmnEditorTab.INCLUDED_MODELS} title={tabTitle.includedModels}>
- {navigation.tab === DmnEditorTab.INCLUDED_MODELS && <IncludedModels />}
+ {navigationTab === DmnEditorTab.INCLUDED_MODELS && <IncludedModels />}
</Tab>
</Tabs>
</div>
@@ -359,7 +362,13 @@ export const DmnEditorInternal = ({
};
export const DmnEditor = React.forwardRef((props: DmnEditorProps, ref: React.Ref<DmnEditorRef>) => {
- const storeRef = React.useRef<StoreApiType>(createDmnEditorStore(props.model));
+ const store = useMemo(
+ () => createDmnEditorStore(props.model, new ComputedStateCache<Computed>(INITIAL_COMPUTED_CACHE)),
+ // Purposefully empty. This memoizes the initial value of the store
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ []
+ );
+ const storeRef = React.useRef<StoreApiType>(store);
const resetState: ErrorBoundaryPropsWithFallback["onReset"] = useCallback(({ args }) => {
storeRef.current?.setState((state) => {
@@ -373,9 +382,7 @@ export const DmnEditor = React.forwardRef((props: DmnEditorProps, ref: React.Ref
<ErrorBoundary FallbackComponent={DmnEditorErrorFallback} onReset={resetState}>
<DmnEditorExternalModelsContextProvider {...props}>
<DmnEditorStoreApiContext.Provider value={storeRef.current}>
- <DmnEditorDerivedStoreContextProvider>
- <DmnEditorInternal forwardRef={ref} {...props} />
- </DmnEditorDerivedStoreContextProvider>
+ <DmnEditorInternal forwardRef={ref} {...props} />
</DmnEditorStoreApiContext.Provider>
</DmnEditorExternalModelsContextProvider>
</ErrorBoundary>
diff --git a/packages/dmn-editor/src/autolayout/AutolayoutButton.tsx b/packages/dmn-editor/src/autolayout/AutolayoutButton.tsx
new file mode 100644
index 00000000000..f207394ed89
--- /dev/null
+++ b/packages/dmn-editor/src/autolayout/AutolayoutButton.tsx
@@ -0,0 +1,557 @@
+/*
+ * 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 { generateUuid } from "@kie-tools/boxed-expression-component/dist/api";
+import { DC__Bounds } from "@kie-tools/dmn-marshaller/dist/schemas/dmn-1_5/ts-gen/types";
+import OptimizeIcon from "@patternfly/react-icons/dist/js/icons/optimize-icon";
+import ELK, * as Elk from "elkjs/lib/elk.bundled.js";
+import * as React from "react";
+import { PositionalNodeHandleId } from "../diagram/connections/PositionalNodeHandles";
+import { EdgeType, NodeType } from "../diagram/connections/graphStructure";
+import { getAdjMatrix, traverse } from "../diagram/graph/graph";
+import { getContainmentRelationship } from "../diagram/maths/DmnMaths";
+import { DEFAULT_NODE_SIZES, MIN_NODE_SIZES } from "../diagram/nodes/DefaultSizes";
+import { NODE_TYPES } from "../diagram/nodes/NodeTypes";
+import { useExternalModels } from "../includedModels/DmnEditorDependenciesContext";
+import { addEdge } from "../mutations/addEdge";
+import { repositionNode } from "../mutations/repositionNode";
+import { resizeNode } from "../mutations/resizeNode";
+import { updateDecisionServiceDividerLine } from "../mutations/updateDecisionServiceDividerLine";
+import { useDmnEditorStoreApi } from "../store/StoreContext";
+
+const elk = new ELK();
+
+export const ELK_OPTIONS = {
+ "elk.algorithm": "layered",
+ "elk.direction": "UP",
+ // By making width a lot bigger than height, we make sure disjoint graph components are placed horizontally, never vertically
+ "elk.aspectRatio": "9999999999",
+ // spacing
+ "elk.spacing.nodeNode": "60",
+ "elk.spacing.componentComponent": "200",
+ "layered.spacing.edgeEdgeBetweenLayers": "0",
+ "layered.spacing.edgeNodeBetweenLayers": "0",
+ "layered.spacing.nodeNodeBetweenLayers": "100",
+ // edges
+ "elk.edgeRouting": "ORTHOGONAL",
+ "elk.layered.mergeEdges": "true", // we need this to make sure space is consistent between layers.
+ "elk.layered.mergeHierarchyEdges": "true",
+ // positioning
+ "elk.partitioning.activate": "true",
+ "elk.nodePlacement.favorStraightEdges": "true",
+ "elk.nodePlacement.bk.fixedAlignment": "LEFTDOWN",
+ "elk.nodePlacement.bk.edgeStraightening": "IMPROVE_STRAIGHTNESS",
+ //
+ "layering.strategy": "LONGEST_PATH_SOURCE",
+};
+
+const PARENT_NODE_ELK_OPTIONS = {
+ "elk.padding": "[left=60, top=60, right=80, bottom=60]",
+ "elk.spacing.componentComponent": "60",
+};
+
+export interface AutolayoutParentNode {
+ decisionServiceSection: "output" | "encapsulated" | "n/a";
+ elkNode: Elk.ElkNode;
+ contained: Set<string>;
+ dependents: Set<string>;
+ dependencies: Set<string>;
+ contains: (otherNode: { id: string; bounds: DC__Bounds | undefined }) => {
+ isInside: boolean;
+ decisionServiceSection: AutolayoutParentNode["decisionServiceSection"];
+ };
+ hasDependencyTo: (otherNode: { id: string }) => boolean;
+ isDependencyOf: (otherNode: { id: string }) => boolean;
+}
+
+const FAKE_MARKER = "__$FAKE$__";
+
+export function AutolayoutButton() {
+ const dmnEditorStoreApi = useDmnEditorStoreApi();
+ const { externalModelsByNamespace } = useExternalModels();
+
+ const onApply = React.useCallback(async () => {
+ const parentNodesById = new Map<string, AutolayoutParentNode>();
+ const nodeParentsById = new Map<string, Set<string>>();
+
+ /**
+ Used to tell ELK that dependencies of nodes' children should be considered the node's dependency too.
+ This allows us to not rely on INCLUDE_STRATEGY hierarchy handling on ELK, keeping disjoint graph components separate, rendering side-by-side.
+ */
+ const fakeEdgesForElk = new Set<Elk.ElkExtendedEdge>();
+
+ const state = dmnEditorStoreApi.getState();
+
+ const snapGrid = state.diagram.snapGrid;
+ const nodesById = state.computed(state).getDiagramData(externalModelsByNamespace).nodesById;
+ const edgesById = state.computed(state).getDiagramData(externalModelsByNamespace).edgesById;
+ const nodes = state.computed(state).getDiagramData(externalModelsByNamespace).nodes;
+ const edges = state.computed(state).getDiagramData(externalModelsByNamespace).edges;
+ const drgEdges = state.computed(state).getDiagramData(externalModelsByNamespace).drgEdges;
+
+ const adjMatrix = getAdjMatrix(drgEdges);
+
+ // 1. First we populate the `parentNodesById` map so that we know exactly what parent nodes we're dealing with. Decision Service nodes have two fake nodes to represent Output and Encapsulated sections.
+ for (const node of nodes) {
+ const dependencies = new Set<string>();
+ const dependents = new Set<string>();
+
+ if (node.data?.dmnObject?.__$$element === "decisionService") {
+ const outputs = new Set([...(node.data.dmnObject.outputDecision ?? []).map((s) => s["@_href"])]);
+ const encapsulated = new Set([...(node.data.dmnObject.encapsulatedDecision ?? []).map((s) => s["@_href"])]);
+
+ const idOfFakeNodeForOutputSection = `${node.id}${FAKE_MARKER}dsOutput`;
+ const idOfFakeNodeForEncapsulatedSection = `${node.id}${FAKE_MARKER}dsEncapsulated`;
+
+ const dsSize = MIN_NODE_SIZES[NODE_TYPES.decisionService](snapGrid);
+ parentNodesById.set(node.id, {
+ elkNode: {
+ id: node.id,
+ width: dsSize["@_width"],
+ height: dsSize["@_height"],
+ children: [
+ {
+ id: idOfFakeNodeForOutputSection,
+ width: dsSize["@_width"],
+ height: dsSize["@_height"] / 2,
+ children: [],
+ layoutOptions: {
+ ...ELK_OPTIONS,
+ ...PARENT_NODE_ELK_OPTIONS,
+ },
+ },
+ {
+ id: idOfFakeNodeForEncapsulatedSection,
+ width: dsSize["@_width"],
+ height: dsSize["@_height"] / 2,
+ children: [],
+ layoutOptions: {
+ ...ELK_OPTIONS,
+ ...PARENT_NODE_ELK_OPTIONS,
+ },
+ },
+ ],
+ layoutOptions: {
+ "elk.algorithm": "layered",
+ "elk.direction": "UP",
+ "elk.aspectRatio": "9999999999",
+ "elk.partitioning.activate": "true",
+ "elk.spacing.nodeNode": "0",
+ "elk.spacing.componentComponent": "0",
+ "layered.spacing.edgeEdgeBetweenLayers": "0",
+ "layered.spacing.edgeNodeBetweenLayers": "0",
+ "layered.spacing.nodeNodeBetweenLayers": "0",
+ "elk.padding": "[left=0, top=0, right=0, bottom=0]",
+ },
+ },
+ decisionServiceSection: "output",
+ dependencies,
+ dependents,
+ contained: outputs,
+ contains: ({ id }) => ({
+ isInside: outputs.has(id) || encapsulated.has(id),
+ decisionServiceSection: outputs.has(id) ? "output" : encapsulated.has(id) ? "encapsulated" : "n/a",
+ }),
+ isDependencyOf: ({ id }) => dependents.has(id),
+ hasDependencyTo: ({ id }) => dependencies.has(id),
+ });
+
+ fakeEdgesForElk.add({
+ id: `${node.id}${FAKE_MARKER}fakeOutputEncapsulatedEdge`,
+ sources: [idOfFakeNodeForEncapsulatedSection],
+ targets: [idOfFakeNodeForOutputSection],
+ });
+ } else if (node.data?.dmnObject?.__$$element === "group") {
+ const groupSize = DEFAULT_NODE_SIZES[NODE_TYPES.group](snapGrid);
+ const groupBounds = node.data.shape["dc:Bounds"];
+ parentNodesById.set(node.id, {
+ decisionServiceSection: "n/a",
+ elkNode: {
+ id: node.id,
+ width: groupBounds?.["@_width"] ?? groupSize["@_width"],
+ height: groupBounds?.["@_height"] ?? groupSize["@_height"],
+ children: [],
+ layoutOptions: {
+ ...ELK_OPTIONS,
+ ...PARENT_NODE_ELK_OPTIONS,
+ },
+ },
+ dependencies,
+ dependents,
+ contained: new Set(),
+ contains: ({ id, bounds }) => ({
+ isInside: getContainmentRelationship({
+ bounds: bounds!,
+ container: groupBounds!,
+ snapGrid,
+ containerMinSizes: MIN_NODE_SIZES[NODE_TYPES.group],
+ boundsMinSizes: MIN_NODE_SIZES[nodesById.get(id)?.type as NodeType],
+ }).isInside,
+ decisionServiceSection: "n/a",
+ }),
+ isDependencyOf: ({ id }) => dependents.has(id),
+ hasDependencyTo: ({ id }) => dependencies.has(id),
+ });
+ }
+ }
+
+ // 2. Then we map all the nodes to elkNodes, including the parents. We mutate parents on the fly when iterating over the nodes list.
+ const elkNodes = nodes.flatMap((node) => {
+ const parent = parentNodesById.get(node.id);
+ if (parent) {
+ return [];
+ }
+
+ const defaultSize = DEFAULT_NODE_SIZES[node.type as NodeType](snapGrid);
+ const elkNode: Elk.ElkNode = {
+ id: node.id,
+ width: node.data.shape["dc:Bounds"]?.["@_width"] ?? defaultSize["@_width"],
+ height: node.data.shape["dc:Bounds"]?.["@_height"] ?? defaultSize["@_height"],
+ children: [],
+ layoutOptions: {
+ "partitioning.partition":
+ // Since textAnnotations and knowledgeSources are not related to the logic, we leave them at the bottom.
+ (node.type as NodeType) === NODE_TYPES.textAnnotation ||
+ (node.type as NodeType) === NODE_TYPES.knowledgeSource
+ ? "0"
+ : "1",
+ },
+ };
+
+ // FIXME: Tiago --> Improve performance here as part of https://github.com/apache/incubator-kie-issues/issues/451.
+ const parents = [...parentNodesById.values()].filter(
+ (p) => p.contains({ id: elkNode.id, bounds: node.data.shape["dc:Bounds"] }).isInside
+ );
+ if (parents.length > 0) {
+ const decisionServiceSection = parents[0].contains({
+ id: elkNode.id,
+ bounds: node.data.shape["dc:Bounds"],
+ }).decisionServiceSection;
+
+ // The only relationship that ELK will know about is the first matching container for this node.
+ if (decisionServiceSection === "n/a") {
+ parents[0].elkNode.children?.push(elkNode);
+ } else if (decisionServiceSection === "output") {
+ parents[0].elkNode.children?.[0].children?.push(elkNode);
+ } else if (decisionServiceSection === "encapsulated") {
+ parents[0].elkNode.children?.[1].children?.push(elkNode);
+ } else {
+ throw new Error(`Unknown decisionServiceSection ${decisionServiceSection}`);
+ }
+
+ for (const p of parents) {
+ p.contained?.add(elkNode.id); // We need to keep track of nodes that are contained by multiple groups, but ELK will only know about one of those containment relationships.
+ nodeParentsById.set(node.id, new Set([...(nodeParentsById.get(node.id) ?? []), p.elkNode.id]));
+ }
+ return [];
+ }
+
+ return [elkNode];
+ });
+
+ // 3. After we have all containment relationships defined, we can proceed to resolving the hierarchical relationships.
+ for (const [_, parentNode] of parentNodesById) {
+ traverse(adjMatrix, parentNode.contained, [...parentNode.contained], "down", (n) => {
+ parentNode.dependencies.add(n);
+ });
+ traverse(adjMatrix, parentNode.contained, [...parentNode.contained], "up", (n) => {
+ parentNode.dependents.add(n);
+ });
+
+ const p = nodesById.get(parentNode.elkNode.id);
+ if (p?.type === NODE_TYPES.group && parentNode.elkNode.children?.length === 0) {
+ continue; // Ignore empty group nodes.
+ } else {
+ elkNodes.push(parentNode.elkNode);
+ }
+ }
+
+ // 4. After we have all containment and hierarchical relationships defined, we can add the fake edges so that ELK creates the structure correctly.
+ for (const node of nodes) {
+ const parentNodes = [...parentNodesById.values()];
+
+ const dependents = parentNodes.filter((p) => p.hasDependencyTo({ id: node.id }));
+ for (const dependent of dependents) {
+ // Not all nodes are present in all DRD
+ if (nodesById.has(node.id) && nodesById.has(dependent.elkNode.id)) {
+ fakeEdgesForElk.add({
+ id: `${generateUuid()}${FAKE_MARKER}__fake`,
+ sources: [node.id],
+ targets: [dependent.elkNode.id],
+ });
+ }
+
+ for (const p of nodeParentsById.get(node.id) ?? []) {
+ // Not all nodes are present in all DRD
+ if (nodesById.has(p) && nodesById.has(dependent.elkNode.id)) {
+ fakeEdgesForElk.add({
+ id: `${generateUuid()}${FAKE_MARKER}__fake`,
+ sources: [p],
+ targets: [dependent.elkNode.id],
+ });
+ }
+ }
+ }
+
+ const dependencies = parentNodes.filter((p) => p.isDependencyOf({ id: node.id }));
+ for (const dependency of dependencies) {
+ // Not all nodes are present in all DRD
+ if (nodesById.has(node.id) && nodesById.has(dependency.elkNode.id)) {
+ fakeEdgesForElk.add({
+ id: `${generateUuid()}${FAKE_MARKER}__fake`,
+ sources: [dependency.elkNode.id],
+ targets: [node.id],
+ });
+ }
+
+ for (const p of nodeParentsById.get(node.id) ?? []) {
+ // Not all nodes are present in all DRD
+ if (nodesById.has(p) && nodesById.has(dependency.elkNode.id)) {
+ fakeEdgesForElk.add({
+ id: `${generateUuid()}${FAKE_MARKER}__fake`,
+ sources: [dependency.elkNode.id],
+ targets: [p],
+ });
+ }
+ }
+ }
+ }
+
+ // 5. Concatenate real and fake edges to pass to ELK.
+ const elkEdges = [
+ ...fakeEdgesForElk,
+ ...[...edgesById.values()].flatMap((e) => {
+ // Not all nodes are present in all DRD
+ if (nodesById.has(e.source) && nodesById.has(e.target)) {
+ return {
+ id: e.id,
+ sources: [e.source],
+ targets: [e.target],
+ };
+ } else {
+ return [];
+ }
+ }),
+ ];
+
+ // 6. Run ELK.
+ const autolayouted = await runElk(elkNodes, elkEdges, ELK_OPTIONS);
+
+ // 7. Update all nodes positions skipping empty groups, which will be positioned manually after all nodes are done being repositioned.
+ dmnEditorStoreApi.setState((s) => {
+ const autolayoutedElkNodesById = new Map<string, Elk.ElkNode>();
+
+ for (const topLevelElkNode of autolayouted.nodes ?? []) {
+ visitNodeAndNested(topLevelElkNode, { x: 100, y: 100 }, (elkNode, positionOffset) => {
+ if (elkNode.id.includes(FAKE_MARKER)) {
+ return;
+ }
+
+ autolayoutedElkNodesById.set(elkNode.id, elkNode);
+
+ const nodeId = elkNode.id;
+ const node = s.computed(s).getDiagramData(externalModelsByNamespace).nodesById.get(nodeId)!;
+
+ repositionNode({
+ definitions: s.dmn.model.definitions,
+ drdIndex: s.diagram.drdIndex,
+ controlWaypointsByEdge: new Map(),
+ change: {
+ nodeType: node.type as NodeType,
+ type: "absolute",
+ position: {
+ x: elkNode.x! + positionOffset.x,
+ y: elkNode.y! + positionOffset.y,
+ },
+ selectedEdges: [...edgesById.keys()],
+ shapeIndex: node.data?.shape.index,
+ sourceEdgeIndexes: edges.flatMap((e) =>
+ e.source === nodeId && e.data?.dmnEdge ? [e.data.dmnEdge.index] : []
+ ),
+ targetEdgeIndexes: edges.flatMap((e) =>
+ e.target === nodeId && e.data?.dmnEdge ? [e.data.dmnEdge.index] : []
+ ),
+ },
+ });
+ });
+ }
+
+ // 8. Resize all nodes using the sizes calculated by ELK.
+ for (const topLevelElkNode of autolayouted.nodes ?? []) {
+ visitNodeAndNested(topLevelElkNode, { x: 0, y: 0 }, (elkNode) => {
+ if (elkNode.id.includes(FAKE_MARKER)) {
+ return;
+ }
+
+ const nodeId = elkNode.id;
+ const node = s.computed(s).getDiagramData(externalModelsByNamespace).nodesById.get(nodeId)!;
+
+ resizeNode({
+ definitions: s.dmn.model.definitions,
+ drdIndex: s.diagram.drdIndex,
+ dmnShapesByHref: s.computed(s).indexes().dmnShapesByHref,
+ snapGrid,
+ change: {
+ index: node.data.index,
+ isExternal: !!node.data.dmnObjectQName.prefix,
+ nodeType: node.type as NodeType,
+ dimension: {
+ "@_width": elkNode.width!,
+ "@_height": elkNode.height!,
+ },
+ shapeIndex: node.data?.shape.index,
+ sourceEdgeIndexes: edges.flatMap((e) =>
+ e.source === nodeId && e.data?.dmnEdge ? [e.data.dmnEdge.index] : []
+ ),
+ targetEdgeIndexes: edges.flatMap((e) =>
+ e.target === nodeId && e.data?.dmnEdge ? [e.data.dmnEdge.index] : []
+ ),
+ },
+ });
+ });
+ }
+
+ // 9. Updating Decision Service divider lines after all nodes are repositioned and resized.
+ for (const [parentNodeId] of parentNodesById) {
+ const parentNode = s.computed(s).getDiagramData(externalModelsByNamespace).nodesById.get(parentNodeId);
+ if (parentNode?.type !== NODE_TYPES.decisionService) {
+ continue;
+ }
+
+ const elkNode = autolayoutedElkNodesById.get(parentNodeId);
+ if (!elkNode) {
+ throw new Error(`Couldn't find Decision Service with id ${parentNode.id} at the autolayouted nodes map`);
+ }
+
+ /**
+ * The second children of a Decision Service elkNode is a node representing the Encapsulated section.
+ * It's Y position will be exactly where the divider line should be.
+ */
+ const dividerLinerLocalYPosition = elkNode.children?.[1]?.y;
+ if (!dividerLinerLocalYPosition) {
+ throw new Error(
+ `Couldn't find second child (which represents the Encapuslated Decision section) of Decision Service with id ${parentNode.id} at the autolayouted nodes map`
+ );
+ }
+
+ updateDecisionServiceDividerLine({
+ definitions: s.dmn.model.definitions,
+ drdIndex: s.diagram.drdIndex,
+ dmnShapesByHref: s.computed(s).indexes().dmnShapesByHref,
+ drgElementIndex: parentNode.data.index,
+ shapeIndex: parentNode.data.shape.index,
+ snapGrid,
+ localYPosition: dividerLinerLocalYPosition,
+ });
+ }
+
+ // 10. Update the edges. Edges always go from top to bottom, removing waypoints.
+ for (const elkEdge of autolayouted.edges ?? []) {
+ if (elkEdge.id.includes(FAKE_MARKER)) {
+ continue;
+ }
+
+ const edge = s.computed(s).getDiagramData(externalModelsByNamespace).edgesById.get(elkEdge.id)!;
+
+ const sourceNode = s.computed(s).getDiagramData(externalModelsByNamespace).nodesById.get(elkEdge.sources[0])!;
+ const targetNode = s.computed(s).getDiagramData(externalModelsByNamespace).nodesById.get(elkEdge.targets[0])!;
+
+ // If the target is an external node, we don't have to create the edge.
+ if (targetNode.data.dmnObjectQName.prefix) {
+ continue;
+ }
+
+ addEdge({
+ definitions: s.dmn.model.definitions,
+ drdIndex: s.diagram.drdIndex,
+ edge: {
+ autoPositionedEdgeMarker: undefined,
+ type: edge.type as EdgeType,
+ targetHandle: PositionalNodeHandleId.Bottom,
+ sourceHandle: PositionalNodeHandleId.Top,
+ },
+ sourceNode: {
+ type: sourceNode.type as NodeType,
+ href: sourceNode.id,
+ data: sourceNode.data,
+ bounds: sourceNode.data.shape["dc:Bounds"]!,
+ shapeId: sourceNode.data.shape["@_id"],
+ },
+ targetNode: {
+ type: targetNode.type as NodeType,
+ href: targetNode.id,
+ data: targetNode.data,
+ bounds: targetNode.data.shape["dc:Bounds"]!,
+ index: targetNode.data.index,
+ shapeId: targetNode.data.shape["@_id"],
+ },
+ keepWaypoints: false,
+ });
+ }
+ });
+ }, [dmnEditorStoreApi, externalModelsByNamespace]);
+
+ return (
+ <button className={"kie-dmn-editor--autolayout-panel-toggle-button"} onClick={onApply}>
+ <OptimizeIcon />
+ </button>
+ );
+}
+
+//
+
+export async function runElk(
+ nodes: Elk.ElkNode[],
+ edges: { id: string; sources: string[]; targets: string[] }[],
+ options: Elk.LayoutOptions = {}
+): Promise<{ isHorizontal: boolean; nodes: Elk.ElkNode[] | undefined; edges: Elk.ElkExtendedEdge[] | undefined }> {
+ const isHorizontal = options?.["elk.direction"] === "RIGHT";
+
+ const graph: Elk.ElkNode = {
+ id: "root",
+ layoutOptions: options,
+ children: nodes,
+ edges,
+ };
+
+ const layoutedGraph = await elk.layout(graph);
+ return {
+ isHorizontal,
+ nodes: layoutedGraph.children,
+ edges: layoutedGraph.edges as any[],
+ };
+}
+
+function visitNodeAndNested(
+ elkNode: Elk.ElkNode,
+ positionOffset: { x: number; y: number },
+ visitor: (elkNode: Elk.ElkNode, positionOffset: { x: number; y: number }) => void
+) {
+ visitor(elkNode, positionOffset);
+ for (const nestedNode of elkNode.children ?? []) {
+ visitNodeAndNested(
+ nestedNode,
+ {
+ x: elkNode.x! + positionOffset.x,
+ y: elkNode.y! + positionOffset.y,
+ },
+ visitor
+ );
+ }
+}
diff --git a/packages/dmn-editor/src/boxedExpressions/BoxedExpression.tsx b/packages/dmn-editor/src/boxedExpressions/BoxedExpression.tsx
index 8ca343f7368..b8972cf0ff1 100644
--- a/packages/dmn-editor/src/boxedExpressions/BoxedExpression.tsx
+++ b/packages/dmn-editor/src/boxedExpressions/BoxedExpression.tsx
@@ -17,7 +17,6 @@
* under the License.
*/
-import * as RF from "reactflow";
import {
BeeGwtService,
DmnBuiltInDataType,
@@ -28,38 +27,15 @@ import {
generateUuid,
} from "@kie-tools/boxed-expression-component/dist/api";
import { BoxedExpressionEditor } from "@kie-tools/boxed-expression-component/dist/expressions";
-import { Label } from "@patternfly/react-core/dist/js/components/Label";
-import * as React from "react";
-import { useCallback, useEffect, useMemo, useState } from "react";
-import { updateExpression } from "../mutations/updateExpression";
-import { DmnEditorTab, useDmnEditorStore, useDmnEditorStoreApi } from "../store/Store";
-import { dmnToBee, getUndefinedExpressionDefinition } from "./dmnToBee";
-import { getDefaultExpressionDefinitionByLogicType } from "./getDefaultExpressionDefinitionByLogicType";
+import { FeelVariables } from "@kie-tools/dmn-feel-antlr4-parser";
+import { DmnLatestModel } from "@kie-tools/dmn-marshaller";
import {
DMN15__tBusinessKnowledgeModel,
DMN15__tDecision,
DMN15__tItemDefinition,
} from "@kie-tools/dmn-marshaller/dist/schemas/dmn-1_5/ts-gen/types";
-import {
- EmptyState,
- EmptyStateBody,
- EmptyStateIcon,
- EmptyStatePrimary,
-} from "@patternfly/react-core/dist/js/components/EmptyState";
-import { Button } from "@patternfly/react-core/dist/js/components/Button";
-import { Title } from "@patternfly/react-core/dist/js/components/Title";
-import { ErrorCircleOIcon } from "@patternfly/react-icons/dist/js/icons/error-circle-o-icon";
-import { InfoIcon } from "@patternfly/react-icons/dist/js/icons/info-icon";
-import { builtInFeelTypes } from "../dataTypes/BuiltInFeelTypes";
-import { useDmnEditorDerivedStore } from "../store/DerivedStore";
-import { isStruct } from "../dataTypes/DataTypeSpec";
-import { DmnDiagramNodeData } from "../diagram/nodes/Nodes";
-import { DataTypeIndex } from "../dataTypes/DataTypes";
-import { Flex, FlexItem } from "@patternfly/react-core/dist/js/layouts/Flex";
-import { getNodeTypeFromDmnObject } from "../diagram/maths/DmnMaths";
-import { NodeIcon } from "../icons/Icons";
-import { Text, TextContent, TextVariants } from "@patternfly/react-core/dist/js/components/Text";
import { PMMLDocumentData } from "@kie-tools/pmml-editor-marshaller/dist/api";
+import { PMMLFieldData } from "@kie-tools/pmml-editor-marshaller/dist/api/PMMLFieldData";
import { PMMLModelData } from "@kie-tools/pmml-editor-marshaller/dist/api/PMMLModelData";
import {
AnomalyDetectionModel,
@@ -84,18 +60,71 @@ import {
TimeSeriesModel,
TreeModel,
} from "@kie-tools/pmml-editor-marshaller/dist/marshaller/model/pmml4_4";
-import { PMMLFieldData } from "@kie-tools/pmml-editor-marshaller/dist/api/PMMLFieldData";
-import { getDefaultColumnWidth } from "./getDefaultColumnWidth";
-import { FeelVariables } from "@kie-tools/dmn-feel-antlr4-parser";
-import { DmnLatestModel } from "@kie-tools/dmn-marshaller";
+import { Button } from "@patternfly/react-core/dist/js/components/Button";
+import {
+ EmptyState,
+ EmptyStateBody,
+ EmptyStateIcon,
+ EmptyStatePrimary,
+} from "@patternfly/react-core/dist/js/components/EmptyState";
+import { Label } from "@patternfly/react-core/dist/js/components/Label";
+import { Text, TextContent, TextVariants } from "@patternfly/react-core/dist/js/components/Text";
+import { Title } from "@patternfly/react-core/dist/js/components/Title";
+import { Flex, FlexItem } from "@patternfly/react-core/dist/js/layouts/Flex";
import { ArrowRightIcon } from "@patternfly/react-icons/dist/js/icons/arrow-right-icon";
+import { ErrorCircleOIcon } from "@patternfly/react-icons/dist/js/icons/error-circle-o-icon";
+import { InfoIcon } from "@patternfly/react-icons/dist/js/icons/info-icon";
+import * as React from "react";
+import { useCallback, useMemo } from "react";
+import * as RF from "reactflow";
+import { builtInFeelTypes } from "../dataTypes/BuiltInFeelTypes";
+import { isStruct } from "../dataTypes/DataTypeSpec";
+import { DataTypeIndex } from "../dataTypes/DataTypes";
+import { getNodeTypeFromDmnObject } from "../diagram/maths/DmnMaths";
+import { DmnDiagramNodeData } from "../diagram/nodes/Nodes";
+import { NodeIcon } from "../icons/Icons";
+import { useExternalModels } from "../includedModels/DmnEditorDependenciesContext";
+import { updateExpression } from "../mutations/updateExpression";
+import { DmnEditorTab } from "../store/Store";
+import { useDmnEditorStore, useDmnEditorStoreApi } from "../store/StoreContext";
+import { dmnToBee, getUndefinedExpressionDefinition } from "./dmnToBee";
+import { getDefaultColumnWidth } from "./getDefaultColumnWidth";
+import { getDefaultExpressionDefinitionByLogicType } from "./getDefaultExpressionDefinitionByLogicType";
export function BoxedExpression({ container }: { container: React.RefObject<HTMLElement> }) {
- const thisDmn = useDmnEditorStore((s) => s.dmn);
- const diagram = useDmnEditorStore((s) => s.diagram);
- const dispatch = useDmnEditorStore((s) => s.dispatch);
- const boxedExpressionEditor = useDmnEditorStore((s) => s.boxedExpressionEditor);
- const { externalDmnsByNamespace } = useDmnEditorDerivedStore();
+ const { externalModelsByNamespace } = useExternalModels();
+
+ //
+
+ const thisDmn = useDmnEditorStore((s) => {
+ return s.dmn;
+ });
+ const diagram = useDmnEditorStore((s) => {
+ return s.diagram;
+ });
+ const boxedExpressionEditor = useDmnEditorStore((s) => {
+ return s.boxedExpressionEditor;
+ });
+ const externalDmnsByNamespace = useDmnEditorStore((s) => {
+ return s.computed(s).getExternalModelTypesByNamespace(externalModelsByNamespace).dmns;
+ });
+ const dataTypesTree = useDmnEditorStore((s) => {
+ return s.computed(s).getDataTypes(externalModelsByNamespace).dataTypesTree;
+ });
+ const allTopLevelDataTypesByFeelName = useDmnEditorStore((s) => {
+ return s.computed(s).getDataTypes(externalModelsByNamespace).allTopLevelDataTypesByFeelName;
+ });
+ const nodesById = useDmnEditorStore((s) => {
+ return s.computed(s).getDiagramData(externalModelsByNamespace).nodesById;
+ });
+ const importsByNamespace = useDmnEditorStore((s) => {
+ return s.computed(s).importsByNamespace();
+ });
+ const externalPmmlsByNamespace = useDmnEditorStore((s) => {
+ return s.computed(s).getExternalModelTypesByNamespace(externalModelsByNamespace).pmmls;
+ });
+
+ //
const dmnEditorStoreApi = useDmnEditorStoreApi();
const feelVariables = useMemo(() => {
@@ -150,13 +179,6 @@ export function BoxedExpression({ container }: { container: React.RefObject<HTML
};
}, [boxedExpressionEditor.activeDrgElementId, thisDmn.model.definitions.drgElement, widthsById]);
- const [lastValidExpression, setLastValidExpression] = useState<typeof expression>(undefined);
- useEffect(() => {
- if (expression) {
- setLastValidExpression(expression);
- }
- }, [expression, setLastValidExpression]);
-
const setExpression: React.Dispatch<React.SetStateAction<ExpressionDefinition>> = useCallback(
(expressionAction) => {
dmnEditorStoreApi.setState((state) => {
@@ -182,9 +204,6 @@ export function BoxedExpression({ container }: { container: React.RefObject<HTML
////
- const { dataTypesTree, allTopLevelDataTypesByFeelName, nodesById, importsByNamespace, externalPmmlsByNamespace } =
- useDmnEditorDerivedStore();
-
const dataTypes = useMemo<DmnDataType[]>(() => {
const customDataTypes = dataTypesTree.map((d) => ({
isCustom: true,
@@ -270,7 +289,7 @@ export function BoxedExpression({ container }: { container: React.RefObject<HTML
className={"kie-dmn-editor--boxed-expression-back"}
onClick={() => {
dmnEditorStoreApi.setState((state) => {
- dispatch.boxedExpressionEditor.close(state);
+ state.dispatch(state).boxedExpressionEditor.close();
});
}}
>
@@ -330,7 +349,7 @@ export function BoxedExpression({ container }: { container: React.RefObject<HTML
variant="link"
onClick={() => {
dmnEditorStoreApi.setState((state) => {
- dispatch.boxedExpressionEditor.close(state);
+ state.dispatch(state).boxedExpressionEditor.close();
});
}}
>
diff --git a/packages/dmn-editor/src/dataTypes/DataTypeName.tsx b/packages/dmn-editor/src/dataTypes/DataTypeName.tsx
index 0d27eff02f6..f7e7c4219bd 100644
--- a/packages/dmn-editor/src/dataTypes/DataTypeName.tsx
+++ b/packages/dmn-editor/src/dataTypes/DataTypeName.tsx
@@ -23,12 +23,13 @@ import { DMN15__tItemDefinition } from "@kie-tools/dmn-marshaller/dist/schemas/d
import { Flex } from "@patternfly/react-core/dist/js/layouts/Flex";
import { EditableNodeLabel, useEditableNodeLabel } from "../diagram/nodes/EditableNodeLabel";
import { TypeRefLabel } from "./TypeRefLabel";
-import { useDmnEditorStoreApi } from "../store/Store";
+import { useDmnEditorStore, useDmnEditorStoreApi } from "../store/StoreContext";
import { renameItemDefinition } from "../mutations/renameItemDefinition";
-import { useDmnEditorDerivedStore } from "../store/DerivedStore";
import { UniqueNameIndex } from "../Dmn15Spec";
import { buildFeelQNameFromNamespace } from "../feel/buildFeelQName";
import { InlineFeelNameInput, OnInlineFeelNameRenamed } from "../feel/InlineFeelNameInput";
+import { useExternalModels } from "../includedModels/DmnEditorDependenciesContext";
+import { State } from "../store/Store";
export function DataTypeName({
isReadonly,
@@ -37,7 +38,7 @@ export function DataTypeName({
editMode,
relativeToNamespace,
shouldCommitOnBlur,
- allUniqueNames,
+ onGetAllUniqueNames,
enableAutoFocusing,
}: {
isReadonly: boolean;
@@ -46,7 +47,7 @@ export function DataTypeName({
isActive: boolean;
relativeToNamespace: string;
shouldCommitOnBlur?: boolean;
- allUniqueNames: UniqueNameIndex;
+ onGetAllUniqueNames: (s: State) => UniqueNameIndex;
enableAutoFocusing?: boolean;
}) {
const { isEditingLabel, setEditingLabel, triggerEditing, triggerEditingIfEnter } = useEditableNodeLabel(
@@ -54,9 +55,11 @@ export function DataTypeName({
);
const dmnEditorStoreApi = useDmnEditorStoreApi();
- const { allDataTypesById, importsByNamespace } = useDmnEditorDerivedStore();
-
- const dataType = allDataTypesById.get(itemDefinition["@_id"]!);
+ const { externalModelsByNamespace } = useExternalModels();
+ const dataType = useDmnEditorStore((s) =>
+ s.computed(s).getDataTypes(externalModelsByNamespace).allDataTypesById.get(itemDefinition["@_id"]!)
+ );
+ const importsByNamespace = useDmnEditorStore((s) => s.computed(s).importsByNamespace());
const feelQNameToDisplay = buildFeelQNameFromNamespace({
namedElement: itemDefinition,
@@ -76,11 +79,11 @@ export function DataTypeName({
definitions: state.dmn.model.definitions,
newName,
itemDefinitionId: itemDefinition["@_id"]!,
- allDataTypesById,
+ allDataTypesById: state.computed(state).getDataTypes(externalModelsByNamespace).allDataTypesById,
});
});
},
- [allDataTypesById, dmnEditorStoreApi, isReadonly, itemDefinition]
+ [dmnEditorStoreApi, externalModelsByNamespace, isReadonly, itemDefinition]
);
const _shouldCommitOnBlur = shouldCommitOnBlur ?? true; // Defaults to true
@@ -95,7 +98,7 @@ export function DataTypeName({
shouldCommitOnBlur={_shouldCommitOnBlur}
name={feelQNameToDisplay.full}
onRenamed={onRenamed}
- allUniqueNames={allUniqueNames}
+ allUniqueNames={onGetAllUniqueNames}
/>
)}
{editMode === "double-click" && (
@@ -126,7 +129,7 @@ export function DataTypeName({
localPart: itemDefinition["@_name"],
prefix: feelQNameToDisplay.prefix,
}}
- allUniqueNames={allUniqueNames}
+ onGetAllUniqueNames={onGetAllUniqueNames}
/>
{!isEditingLabel && (
<TypeRefLabel
diff --git a/packages/dmn-editor/src/dataTypes/DataTypePanel.tsx b/packages/dmn-editor/src/dataTypes/DataTypePanel.tsx
index 35e5bec7bfb..99d5e5634da 100644
--- a/packages/dmn-editor/src/dataTypes/DataTypePanel.tsx
+++ b/packages/dmn-editor/src/dataTypes/DataTypePanel.tsx
@@ -27,7 +27,7 @@ import { Title } from "@patternfly/react-core/dist/js/components/Title";
import { Flex, FlexItem } from "@patternfly/react-core/dist/js/layouts/Flex";
import * as React from "react";
import { useCallback, useMemo, useState } from "react";
-import { useDmnEditorStore, useDmnEditorStoreApi } from "../store/Store";
+import { useDmnEditorStore, useDmnEditorStoreApi } from "../store/StoreContext";
import { TypeRefSelector } from "./TypeRefSelector";
import {
Dropdown,
@@ -42,7 +42,6 @@ import { getNewItemDefinition, isStruct } from "./DataTypeSpec";
import { TrashIcon } from "@patternfly/react-icons/dist/js/icons/trash-icon";
import { Label } from "@patternfly/react-core/dist/js/components/Label";
import { CopyIcon } from "@patternfly/react-icons/dist/js/icons/copy-icon";
-import { useDmnEditorDerivedStore } from "../store/DerivedStore";
import { UniqueNameIndex } from "../Dmn15Spec";
import { buildFeelQNameFromNamespace } from "../feel/buildFeelQName";
import { buildClipboardFromDataType } from "../clipboard/Clipboard";
@@ -51,6 +50,7 @@ import { original } from "immer";
import { builtInFeelTypeNames } from "./BuiltInFeelTypes";
import { useDmnEditor } from "../DmnEditorContext";
import { useResolvedTypeRef } from "./useResolvedTypeRef";
+import { useExternalModels } from "../includedModels/DmnEditorDependenciesContext";
export function DataTypePanel({
isReadonly,
@@ -162,9 +162,11 @@ export function DataTypePanel({
const [dropdownOpenFor, setDropdownOpenFor] = useState<string | undefined>(undefined);
const [topLevelDropdownOpen, setTopLevelDropdownOpen] = useState<boolean>(false);
-
- const { importsByNamespace, allTopLevelItemDefinitionUniqueNames, allTopLevelDataTypesByFeelName } =
- useDmnEditorDerivedStore();
+ const { externalModelsByNamespace } = useExternalModels();
+ const importsByNamespace = useDmnEditorStore((s) => s.computed(s).importsByNamespace());
+ const allTopLevelItemDefinitionUniqueNames = useDmnEditorStore(
+ (s) => s.computed(s).getDataTypes(externalModelsByNamespace).allTopLevelItemDefinitionUniqueNames
+ );
const allUniqueNames = useMemo(
() =>
@@ -236,7 +238,7 @@ export function DataTypePanel({
isActive={false}
editMode={"hover"}
isReadonly={dataType.namespace !== thisDmnsNamespace}
- allUniqueNames={allUniqueNames}
+ onGetAllUniqueNames={() => allUniqueNames}
/>
</div>
</FlexItem>
diff --git a/packages/dmn-editor/src/dataTypes/DataTypes.tsx b/packages/dmn-editor/src/dataTypes/DataTypes.tsx
index 6573dc0563e..575027f2b30 100644
--- a/packages/dmn-editor/src/dataTypes/DataTypes.tsx
+++ b/packages/dmn-editor/src/dataTypes/DataTypes.tsx
@@ -29,12 +29,12 @@ import {
import { Flex } from "@patternfly/react-core/dist/js/layouts/Flex";
import { InfrastructureIcon } from "@patternfly/react-icons/dist/js/icons/infrastructure-icon";
import { PlusCircleIcon } from "@patternfly/react-icons/dist/js/icons/plus-circle-icon";
-import { State, useDmnEditorStore, useDmnEditorStoreApi } from "../store/Store";
+import { State } from "../store/Store";
+import { useDmnEditorStore, useDmnEditorStoreApi } from "../store/StoreContext";
import { DataTypesEmptyState } from "./DataTypesEmptyState";
import { DataTypePanel } from "./DataTypePanel";
-import { findDataTypeById, getNewItemDefinition, isStruct } from "./DataTypeSpec";
+import { findDataTypeById, isStruct } from "./DataTypeSpec";
import { DataTypeName } from "./DataTypeName";
-import { useDmnEditorDerivedStore } from "../store/DerivedStore";
import { Label } from "@patternfly/react-core/dist/js/components/Label";
import { DMN15_SPEC } from "../Dmn15Spec";
import { invalidInlineFeelNameStyle } from "../feel/InlineFeelNameInput";
@@ -56,6 +56,7 @@ import {
import { getNewDmnIdRandomizer } from "../idRandomizer/dmnIdRandomizer";
import { addTopLevelItemDefinition as _addTopLevelItemDefinition } from "../mutations/addTopLevelItemDefinition";
import { DmnBuiltInDataType } from "@kie-tools/boxed-expression-component/dist/api";
+import { useExternalModels } from "../includedModels/DmnEditorDependenciesContext";
export type DataType = {
itemDefinition: DMN15__tItemDefinition;
@@ -89,8 +90,14 @@ export function DataTypes() {
const { activeItemDefinitionId } = useDmnEditorStore((s) => s.dataTypesEditor);
const [filter, setFilter] = useState("");
-
- const { allDataTypesById, dataTypesTree, allTopLevelItemDefinitionUniqueNames } = useDmnEditorDerivedStore();
+ const { externalModelsByNamespace } = useExternalModels();
+ const allTopLevelItemDefinitionUniqueNames = useDmnEditorStore(
+ (s) => s.computed(s).getDataTypes(externalModelsByNamespace).allTopLevelItemDefinitionUniqueNames
+ );
+ const allDataTypesById = useDmnEditorStore(
+ (s) => s.computed(s).getDataTypes(externalModelsByNamespace).allDataTypesById
+ );
+ const dataTypesTree = useDmnEditorStore((s) => s.computed(s).getDataTypes(externalModelsByNamespace).dataTypesTree);
const activeDataType = useMemo(() => {
return activeItemDefinitionId ? allDataTypesById.get(activeItemDefinitionId) : undefined;
@@ -249,7 +256,7 @@ export function DataTypes() {
isActive={isActive}
editMode={"double-click"}
enableAutoFocusing={false} // Let the auto-focusing mechanism go for the title component
- allUniqueNames={allTopLevelItemDefinitionUniqueNames}
+ onGetAllUniqueNames={() => allTopLevelItemDefinitionUniqueNames}
/>
)) || (
<>
diff --git a/packages/dmn-editor/src/dataTypes/ItemComponentsTable.tsx b/packages/dmn-editor/src/dataTypes/ItemComponentsTable.tsx
index 1fd13b7d0b1..afb24f95c13 100644
--- a/packages/dmn-editor/src/dataTypes/ItemComponentsTable.tsx
+++ b/packages/dmn-editor/src/dataTypes/ItemComponentsTable.tsx
@@ -26,7 +26,7 @@ import { PasteIcon } from "@patternfly/react-icons/dist/js/icons/paste-icon";
import { PlusCircleIcon } from "@patternfly/react-icons/dist/js/icons/plus-circle-icon";
import * as React from "react";
import { useCallback, useMemo } from "react";
-import { useDmnEditorStore, useDmnEditorStoreApi } from "../store/Store";
+import { useDmnEditorStore, useDmnEditorStoreApi } from "../store/StoreContext";
import { TypeRefSelector } from "./TypeRefSelector";
import {
Dropdown,
@@ -59,7 +59,6 @@ import { builtInFeelTypeNames } from "./BuiltInFeelTypes";
import { useDmnEditor } from "../DmnEditorContext";
import { DMN15__tItemDefinition } from "@kie-tools/dmn-marshaller/dist/schemas/dmn-1_5/ts-gen/types";
import { resolveTypeRef } from "./resolveTypeRef";
-import { useDmnEditorDerivedStore } from "../store/DerivedStore";
import { useExternalModels } from "../includedModels/DmnEditorDependenciesContext";
export const BRIGHTNESS_DECREASE_STEP_IN_PERCENTAGE_PER_NESTING_LEVEL = 5;
@@ -91,9 +90,12 @@ export function ItemComponentsTable({
}) {
const dmnEditorStoreApi = useDmnEditorStoreApi();
- const { expandedItemComponentIds } = useDmnEditorStore((s) => s.dataTypesEditor);
- const { allTopLevelDataTypesByFeelName, importsByNamespace } = useDmnEditorDerivedStore();
const { externalModelsByNamespace } = useExternalModels();
+ const expandedItemComponentIds = useDmnEditorStore((s) => s.dataTypesEditor.expandedItemComponentIds);
+ const allTopLevelDataTypesByFeelName = useDmnEditorStore(
+ (s) => s.computed(s).getDataTypes(externalModelsByNamespace).allTopLevelDataTypesByFeelName
+ );
+ const importsByNamespace = useDmnEditorStore((s) => s.computed(s).importsByNamespace());
const thisDmnsNamespace = useDmnEditorStore((s) => s.dmn.model.definitions["@_namespace"]);
@@ -361,7 +363,7 @@ export function ItemComponentsTable({
isActive={false}
itemDefinition={dt.itemDefinition}
isReadonly={dt.namespace !== thisDmnsNamespace}
- allUniqueNames={allUniqueNamesAtLevel}
+ onGetAllUniqueNames={() => allUniqueNamesAtLevel}
/>
</div>
</div>
diff --git a/packages/dmn-editor/src/dataTypes/TypeRefLabel.tsx b/packages/dmn-editor/src/dataTypes/TypeRefLabel.tsx
index 5ce941ff73c..31f048e5d38 100644
--- a/packages/dmn-editor/src/dataTypes/TypeRefLabel.tsx
+++ b/packages/dmn-editor/src/dataTypes/TypeRefLabel.tsx
@@ -21,9 +21,8 @@ import * as React from "react";
import { DmnBuiltInDataType } from "@kie-tools/boxed-expression-component/dist/api";
import { buildFeelQNameFromXmlQName } from "../feel/buildFeelQName";
import { useMemo } from "react";
-import { useDmnEditorDerivedStore } from "../store/DerivedStore";
import { buildXmlQName, parseXmlQName } from "@kie-tools/xml-parser-ts/dist/qNames";
-import { useDmnEditorStore } from "../store/Store";
+import { useDmnEditorStore } from "../store/StoreContext";
import { getXmlNamespaceDeclarationName } from "../xml/xmlNamespaceDeclarations";
import { parseFeelQName } from "../feel/parseFeelQName";
import { builtInFeelTypeNames } from "./BuiltInFeelTypes";
@@ -37,7 +36,7 @@ export function TypeRefLabel({
typeRef: string | undefined;
relativeToNamespace?: string;
}) {
- const { importsByNamespace } = useDmnEditorDerivedStore();
+ const importsByNamespace = useDmnEditorStore((s) => s.computed(s).importsByNamespace());
const thisDmn = useDmnEditorStore((s) => s.dmn);
const feelName = useMemo(() => {
diff --git a/packages/dmn-editor/src/dataTypes/TypeRefSelector.tsx b/packages/dmn-editor/src/dataTypes/TypeRefSelector.tsx
index 13a67bedd69..c6021dc5155 100644
--- a/packages/dmn-editor/src/dataTypes/TypeRefSelector.tsx
+++ b/packages/dmn-editor/src/dataTypes/TypeRefSelector.tsx
@@ -23,14 +23,15 @@ import * as React from "react";
import { useMemo, useRef, useState } from "react";
import { TypeRefLabel } from "./TypeRefLabel";
import { ArrowUpIcon } from "@patternfly/react-icons/dist/js/icons/arrow-up-icon";
-import { DmnEditorTab, useDmnEditorStore, useDmnEditorStoreApi } from "../store/Store";
+import { DmnEditorTab } from "../store/Store";
+import { useDmnEditorStore, useDmnEditorStoreApi } from "../store/StoreContext";
import { Button, ButtonVariant } from "@patternfly/react-core/dist/js/components/Button";
import { Tooltip } from "@patternfly/react-core/dist/js/components/Tooltip";
-import { useDmnEditorDerivedStore } from "../store/DerivedStore";
import { DataType } from "./DataTypes";
import { builtInFeelTypeNames, builtInFeelTypes } from "./BuiltInFeelTypes";
import { Flex } from "@patternfly/react-core/dist/js/layouts/Flex";
import { useInViewSelect } from "../responsiveness/useInViewSelect";
+import { useExternalModels } from "../includedModels/DmnEditorDependenciesContext";
export type OnTypeRefChange = (newDataType: DmnBuiltInDataType) => void;
export type OnCreateDataType = (newDataTypeName: string) => void;
@@ -47,34 +48,35 @@ export function TypeRefSelector(props: {
menuAppendTo?: "parent";
}) {
const [isOpen, setOpen] = useState(false);
-
- const { allTopLevelDataTypesByFeelName } = useDmnEditorDerivedStore();
-
- const selectedDataType = useMemo(() => {
- return props.typeRef ? allTopLevelDataTypesByFeelName.get(props.typeRef) : undefined;
- }, [allTopLevelDataTypesByFeelName, props.typeRef]);
+ const { externalModelsByNamespace } = useExternalModels();
+ const selectedDataType = useDmnEditorStore((s) =>
+ props.typeRef
+ ? s.computed(s).getDataTypes(externalModelsByNamespace).allTopLevelDataTypesByFeelName.get(props.typeRef)
+ : undefined
+ );
const dmnEditorStoreApi = useDmnEditorStoreApi();
- const thisDmnsNamespace = useDmnEditorStore((s) => s.dmn.model.definitions["@_namespace"]);
- const { customDataTypes, externalDataTypes } = useMemo(() => {
+ const { customDataTypes, externalDataTypes } = useDmnEditorStore((state) => {
const customDataTypes: DataType[] = [];
const externalDataTypes: DataType[] = [];
- [...allTopLevelDataTypesByFeelName.values()].forEach((s) => {
- if (s.parentId) {
- return; // Not top-level.
- }
-
- if (s.namespace === thisDmnsNamespace) {
- customDataTypes.push(s);
- } else {
- externalDataTypes.push(s);
+ [...state.computed(state).getDataTypes(externalModelsByNamespace).allTopLevelDataTypesByFeelName.values()].forEach(
+ (s) => {
+ if (s.parentId) {
+ return; // Not top-level.
+ }
+
+ if (s.namespace === state.dmn.model.definitions["@_namespace"]) {
+ customDataTypes.push(s);
+ } else {
+ externalDataTypes.push(s);
+ }
}
- });
+ );
return { customDataTypes, externalDataTypes };
- }, [allTopLevelDataTypesByFeelName, thisDmnsNamespace]);
+ });
const exists = selectedDataType || (props.typeRef && builtInFeelTypeNames.has(props.typeRef));
diff --git a/packages/dmn-editor/src/dataTypes/useResolvedTypeRef.ts b/packages/dmn-editor/src/dataTypes/useResolvedTypeRef.ts
index ddbeda853e5..58e4b2508ae 100644
--- a/packages/dmn-editor/src/dataTypes/useResolvedTypeRef.ts
+++ b/packages/dmn-editor/src/dataTypes/useResolvedTypeRef.ts
@@ -19,22 +19,22 @@
import { DmnBuiltInDataType } from "@kie-tools/boxed-expression-component/dist/api";
import { useExternalModels } from "../includedModels/DmnEditorDependenciesContext";
-import { useDmnEditorDerivedStore } from "../store/DerivedStore";
-import { useDmnEditorStore } from "../store/Store";
+import { useDmnEditorStore } from "../store/StoreContext";
import { resolveTypeRef } from "./resolveTypeRef";
export function useResolvedTypeRef(typeRef: string | undefined, relativeToNamespace: string | undefined) {
- const { allTopLevelDataTypesByFeelName, importsByNamespace } = useDmnEditorDerivedStore();
const { externalModelsByNamespace } = useExternalModels();
- const thisDmnsNamespace = useDmnEditorStore((s) => s.dmn.model.definitions["@_namespace"]);
-
- return resolveTypeRef({
- typeRef: typeRef || DmnBuiltInDataType.Undefined,
- namespace: relativeToNamespace || thisDmnsNamespace,
- allTopLevelDataTypesByFeelName,
- externalModelsByNamespace,
- thisDmnsImportsByNamespace: importsByNamespace,
- relativeToNamespace: thisDmnsNamespace,
+ return useDmnEditorStore((s) => {
+ const thisDmnsNamespace = s.dmn.model.definitions["@_namespace"];
+ return resolveTypeRef({
+ typeRef: typeRef || DmnBuiltInDataType.Undefined,
+ namespace: relativeToNamespace || thisDmnsNamespace,
+ allTopLevelDataTypesByFeelName: s.computed(s).getDataTypes(externalModelsByNamespace)
+ .allTopLevelDataTypesByFeelName,
+ externalModelsByNamespace,
+ thisDmnsImportsByNamespace: s.computed(s).importsByNamespace(),
+ relativeToNamespace: thisDmnsNamespace,
+ });
});
}
diff --git a/packages/dmn-editor/src/diagram/Diagram.tsx b/packages/dmn-editor/src/diagram/Diagram.tsx
index 13757d1cf4f..d20a79b3234 100644
--- a/packages/dmn-editor/src/diagram/Diagram.tsx
+++ b/packages/dmn-editor/src/diagram/Diagram.tsx
@@ -18,10 +18,9 @@
*/
import * as RF from "reactflow";
-
import * as React from "react";
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
-
+import { DmnBuiltInDataType, ExpressionDefinitionLogicType } from "@kie-tools/boxed-expression-component/dist/api";
import {
DC__Bounds,
DC__Dimension,
@@ -40,11 +39,16 @@ import { Label } from "@patternfly/react-core/dist/js/components/Label";
import { Popover } from "@patternfly/react-core/dist/js/components/Popover";
import { Title } from "@patternfly/react-core/dist/js/components/Title";
import { Bullseye } from "@patternfly/react-core/dist/js/layouts/Bullseye";
-import { TableIcon } from "@patternfly/react-icons/dist/js/icons/table-icon";
+import { BlueprintIcon } from "@patternfly/react-icons/dist/js/icons/blueprint-icon";
import { InfoIcon } from "@patternfly/react-icons/dist/js/icons/info-icon";
+import { MousePointerIcon } from "@patternfly/react-icons/dist/js/icons/mouse-pointer-icon";
+import { TableIcon } from "@patternfly/react-icons/dist/js/icons/table-icon";
import { TimesIcon } from "@patternfly/react-icons/dist/js/icons/times-icon";
import { VirtualMachineIcon } from "@patternfly/react-icons/dist/js/icons/virtual-machine-icon";
import { useDmnEditor } from "../DmnEditorContext";
+import { AutolayoutButton } from "../autolayout/AutolayoutButton";
+import { getDefaultColumnWidth } from "../boxedExpressions/getDefaultColumnWidth";
+import { getDefaultExpressionDefinitionByLogicType } from "../boxedExpressions/getDefaultExpressionDefinitionByLogicType";
import {
DMN_EDITOR_DIAGRAM_CLIPBOARD_MIME_TYPE,
DmnEditorDiagramClipboard,
@@ -64,17 +68,20 @@ import { addOrGetDrd } from "../mutations/addOrGetDrd";
import { addShape } from "../mutations/addShape";
import { addStandaloneNode } from "../mutations/addStandaloneNode";
import { deleteDecisionFromDecisionService } from "../mutations/deleteDecisionFromDecisionService";
-import { deleteEdge } from "../mutations/deleteEdge";
-import { deleteNode } from "../mutations/deleteNode";
+import { EdgeDeletionMode, deleteEdge } from "../mutations/deleteEdge";
+import { NodeDeletionMode, canRemoveNodeFromDrdOnly, deleteNode } from "../mutations/deleteNode";
import { repopulateInputDataAndDecisionsOnAllDecisionServices } from "../mutations/repopulateInputDataAndDecisionsOnDecisionService";
import { repositionNode } from "../mutations/repositionNode";
import { resizeNode } from "../mutations/resizeNode";
+import { updateExpression } from "../mutations/updateExpression";
import { OverlaysPanel } from "../overlaysPanel/OverlaysPanel";
-import { useDmnEditorDerivedStore } from "../store/DerivedStore";
-import { DiagramNodesPanel, SnapGrid, StoreApiType, useDmnEditorStore, useDmnEditorStoreApi } from "../store/Store";
+import { DiagramNodesPanel, SnapGrid } from "../store/Store";
+import { useDmnEditorStore, useDmnEditorStoreApi } from "../store/StoreContext";
+import { Unpacked } from "../tsExt/tsExt";
import { buildXmlHref, parseXmlHref } from "../xml/xmlHrefs";
import { getXmlNamespaceDeclarationName } from "../xml/xmlNamespaceDeclarations";
import { DiagramContainerContextProvider } from "./DiagramContainerContext";
+import { MIME_TYPE_FOR_DMN_EDITOR_DRG_NODE } from "./DrgNodesPanel";
import { MIME_TYPE_FOR_DMN_EDITOR_NEW_NODE_FROM_PALETTE, Palette } from "./Palette";
import { offsetShapePosition, snapShapeDimensions, snapShapePosition } from "./SnapGrid";
import { ConnectionLine } from "./connections/ConnectionLine";
@@ -90,10 +97,11 @@ import {
InformationRequirementEdge,
KnowledgeRequirementEdge,
} from "./edges/Edges";
+import { buildHierarchy } from "./graph/graph";
import {
CONTAINER_NODES_DESIRABLE_PADDING,
getBounds,
- getBoundsCenterPoint,
+ getDmnBoundsCenterPoint,
getContainmentRelationship,
getHandlePosition,
getNodeTypeFromDmnObject,
@@ -111,15 +119,7 @@ import {
TextAnnotationNode,
UnknownNode,
} from "./nodes/Nodes";
-import { BlueprintIcon } from "@patternfly/react-icons/dist/js/icons/blueprint-icon";
-import { MousePointerIcon } from "@patternfly/react-icons/dist/js/icons/mouse-pointer-icon";
-import { updateExpression } from "../mutations/updateExpression";
-import { getDefaultExpressionDefinitionByLogicType } from "../boxedExpressions/getDefaultExpressionDefinitionByLogicType";
-import { DmnBuiltInDataType, ExpressionDefinitionLogicType } from "@kie-tools/boxed-expression-component/dist/api";
-import { getDefaultColumnWidth } from "../boxedExpressions/getDefaultColumnWidth";
-import { buildHierarchy } from "./graph/graph";
-import { MIME_TYPE_FOR_DMN_EDITOR_DRG_NODE } from "./DrgNodesPanel";
-import { Unpacked } from "../tsExt/tsExt";
+import { useExternalModels } from "../includedModels/DmnEditorDependenciesContext";
const isFirefox = typeof (window as any).InstallTrigger !== "undefined"; // See https://stackoverflow.com/questions/9847580/how-to-detect-safari-chrome-ie-firefox-and-opera-browsers
@@ -129,6 +129,8 @@ const FIT_VIEW_OPTIONS: RF.FitViewOptions = { maxZoom: 1, minZoom: 0.1, duration
const DEFAULT_VIEWPORT = { x: 100, y: 0, zoom: 1 };
+const DELETE_NODE_KEY_CODES = ["Backspace", "Delete"];
+
const nodeTypes: Record<NodeType, any> = {
[NODE_TYPES.decisionService]: DecisionServiceNode,
[NODE_TYPES.group]: GroupNode,
@@ -156,26 +158,12 @@ export const Diagram = React.forwardRef<DiagramRef, { container: React.RefObject
// Contexts
const dmnEditorStoreApi = useDmnEditorStoreApi();
- const diagram = useDmnEditorStore((s) => s.diagram);
+ const { externalModelsByNamespace } = useExternalModels();
+ const snapGrid = useDmnEditorStore((s) => s.diagram.snapGrid);
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<
@@ -199,38 +187,38 @@ export const Diagram = React.forwardRef<DiagramRef, { container: React.RefObject
// Memos
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]
+ () => (snapGrid.isEnabled ? [snapGrid.x, snapGrid.y] : [1, 1]),
+ [snapGrid.isEnabled, snapGrid.x, snapGrid.y]
);
// Callbacks
const onConnect = useCallback<RF.OnConnect>(
- (connection) => {
- console.debug("DMN DIAGRAM: `onConnect`: ", connection);
-
- 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!");
- }
+ ({ source, target, sourceHandle, targetHandle }) => {
+ console.debug("DMN DIAGRAM: `onConnect`: ", { source, target, sourceHandle, targetHandle });
+ dmnEditorStoreApi.setState((state) => {
+ const sourceNode = state.computed(state).getDiagramData(externalModelsByNamespace).nodesById.get(source!);
+ const targetNode = state.computed(state).getDiagramData(externalModelsByNamespace).nodesById.get(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.
- dmnEditorStoreApi.setState((state) => {
addEdge({
definitions: state.dmn.model.definitions,
drdIndex: state.diagram.drdIndex,
edge: {
- type: connection.sourceHandle as EdgeType,
- targetHandle: connection.targetHandle as PositionalNodeHandleId,
+ type: sourceHandle as EdgeType,
+ targetHandle: targetHandle as PositionalNodeHandleId,
sourceHandle: PositionalNodeHandleId.Center,
+ autoPositionedEdgeMarker: undefined,
},
sourceNode: {
type: sourceNode.type as NodeType,
@@ -251,7 +239,7 @@ export const Diagram = React.forwardRef<DiagramRef, { container: React.RefObject
});
});
},
- [dmnEditorStoreApi, nodesById]
+ [dmnEditorStoreApi, externalModelsByNamespace]
);
const getFirstNodeFittingBounds = useCallback(
@@ -340,17 +328,19 @@ export const Diagram = React.forwardRef<DiagramRef, { container: React.RefObject
) as ExternalNode;
// --------- This is where we draw the line between the diagram and the model.
+ dmnEditorStoreApi.setState((state) => {
+ const externalDrgElement = (
+ state
+ .computed(state)
+ .getExternalModelTypesByNamespace(externalModelsByNamespace)
+ .dmns.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}'.`);
+ }
- 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}'.`);
- }
-
- const externalNodeType = getNodeTypeFromDmnObject(externalDrgElement)!;
+ const externalNodeType = getNodeTypeFromDmnObject(externalDrgElement)!;
- dmnEditorStoreApi.setState((state) => {
const defaultExternalNodeDimensions = DEFAULT_NODE_SIZES[externalNodeType](state.diagram.snapGrid);
const namespaceName = getXmlNamespaceDeclarationName({
@@ -419,19 +409,20 @@ export const Diagram = React.forwardRef<DiagramRef, { container: React.RefObject
console.debug(`DMN DIAGRAM: Adding DRG node`, JSON.stringify(drgElement));
}
},
- [container, reactFlowInstance, dmnEditorStoreApi, externalDmnsByNamespace]
+ [container, reactFlowInstance, dmnEditorStoreApi, externalModelsByNamespace]
);
+ const ongoingConnection = useDmnEditorStore((s) => s.diagram.ongoingConnection);
useEffect(() => {
const edgeUpdaterSource = document.querySelectorAll(
".react-flow__edgeupdater-source, .react-flow__edgeupdater-target"
);
- if (diagram.ongoingConnection) {
+ if (ongoingConnection) {
edgeUpdaterSource.forEach((e) => e.classList.add("hidden"));
} else {
edgeUpdaterSource.forEach((e) => e.classList.remove("hidden"));
}
- }, [diagram.ongoingConnection]);
+ }, [ongoingConnection]);
const onConnectStart = useCallback<RF.OnConnectStart>(
(e, newConnection) => {
@@ -446,50 +437,50 @@ export const Diagram = React.forwardRef<DiagramRef, { container: React.RefObject
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;
- }
+ dmnEditorStoreApi.setState((state) => {
+ const targetIsPane = (e.target as Element | null)?.classList?.contains("react-flow__pane");
+ if (!targetIsPane || !container.current || !state.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 === state.diagram.ongoingConnection!.handleId)) {
+ return;
+ }
- if (!diagram.ongoingConnection.nodeId) {
- return;
- }
+ if (!state.diagram.ongoingConnection.nodeId) {
+ return;
+ }
- const sourceNode = nodesById.get(diagram.ongoingConnection.nodeId);
- if (!sourceNode) {
- return;
- }
+ const sourceNode = state
+ .computed(state)
+ .getDiagramData(externalModelsByNamespace)
+ .nodesById.get(state.diagram.ongoingConnection.nodeId);
+ if (!sourceNode) {
+ return;
+ }
- const sourceNodeBounds = dmnShapesByHref.get(sourceNode.id)?.["dc:Bounds"];
- if (!sourceNodeBounds) {
- return;
- }
+ const sourceNodeBounds = state.computed(state).indexes().dmnShapesByHref.get(sourceNode.id)?.["dc:Bounds"];
+ if (!sourceNodeBounds) {
+ return;
+ }
- const newNodeType = diagram.ongoingConnection.handleId as NodeType;
- const sourceNodeType = sourceNode.type as NodeType;
+ const newNodeType = state.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,
@@ -514,8 +505,13 @@ export const Diagram = React.forwardRef<DiagramRef, { container: React.RefObject
state.diagram._selectedNodes = [newDmnObejctHref];
state.focus.consumableId = id;
});
+
+ // Indepdent of what happens in the state mutation above, we always need to reset the `ongoingConnection` at the end here.
+ dmnEditorStoreApi.setState((state) => {
+ state.diagram.ongoingConnection = undefined;
+ });
},
- [dmnEditorStoreApi, container, diagram.ongoingConnection, reactFlowInstance, nodesById, dmnShapesByHref]
+ [dmnEditorStoreApi, container, reactFlowInstance, externalModelsByNamespace]
);
const isValidConnection = useCallback<RF.IsValidConnection>(
@@ -526,14 +522,18 @@ export const Diagram = React.forwardRef<DiagramRef, { container: React.RefObject
const ongoingConnectionHierarchy = buildHierarchy({
nodeId: state.diagram.ongoingConnection?.nodeId,
- edges: reactFlowInstance?.getEdges() ?? [],
+ edges: state.computed(state).getDiagramData(externalModelsByNamespace).drgEdges,
});
return (
// Reflexive edges are not allowed for DMN
edgeOrConnection.source !== edgeOrConnection.target &&
// Matches DMNs structure.
- checkIsValidConnection(nodesById, edgeOrConnection, edgeType) &&
+ checkIsValidConnection(
+ state.computed(state).getDiagramData(externalModelsByNamespace).nodesById,
+ edgeOrConnection,
+ edgeType
+ ) &&
// Does not form cycles.
!!edgeOrConnection.target &&
!ongoingConnectionHierarchy.dependencies.has(edgeOrConnection.target) &&
@@ -541,7 +541,7 @@ export const Diagram = React.forwardRef<DiagramRef, { container: React.RefObject
!ongoingConnectionHierarchy.dependents.has(edgeOrConnection.source)
);
},
- [dmnEditorStoreApi, reactFlowInstance, nodesById]
+ [dmnEditorStoreApi, externalModelsByNamespace, reactFlowInstance]
);
const onNodesChange = useCallback<RF.OnNodesChange>(
@@ -557,13 +557,16 @@ export const Diagram = React.forwardRef<DiagramRef, { container: React.RefObject
switch (change.type) {
case "add":
console.debug(`DMN DIAGRAM: 'onNodesChange' --> add '${change.item.id}'`);
- state.dispatch.diagram.setNodeStatus(state, change.item.id, { selected: true });
+ state.dispatch(state).diagram.setNodeStatus(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 });
+ state.dispatch(state).diagram.setNodeStatus(change.id, { resizing: change.resizing });
if (change.dimensions) {
- const node = nodesById.get(change.id)!;
+ const node = state
+ .computed(state)
+ .getDiagramData(externalModelsByNamespace)
+ .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,
@@ -577,19 +580,25 @@ export const Diagram = React.forwardRef<DiagramRef, { container: React.RefObject
resizeNode({
definitions: state.dmn.model.definitions,
drdIndex: state.diagram.drdIndex,
- dmnShapesByHref,
+ dmnShapesByHref: state.computed(state).indexes().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] : []
- ),
+ sourceEdgeIndexes: state
+ .computed(state)
+ .getDiagramData(externalModelsByNamespace)
+ .edges.flatMap((e) =>
+ e.source === change.id && e.data?.dmnEdge ? [e.data.dmnEdge.index] : []
+ ),
+ targetEdgeIndexes: state
+ .computed(state)
+ .getDiagramData(externalModelsByNamespace)
+ .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,
@@ -601,9 +610,12 @@ export const Diagram = React.forwardRef<DiagramRef, { container: React.RefObject
break;
case "position":
console.debug(`DMN DIAGRAM: 'onNodesChange' --> position '${change.id}'`);
- state.dispatch.diagram.setNodeStatus(state, change.id, { dragging: change.dragging });
+ state.dispatch(state).diagram.setNodeStatus(change.id, { dragging: change.dragging });
if (change.positionAbsolute) {
- const node = nodesById.get(change.id)!;
+ const node = state
+ .computed(state)
+ .getDiagramData(externalModelsByNamespace)
+ .nodesById.get(change.id)!;
const { delta } = repositionNode({
definitions: state.dmn.model.definitions,
drdIndex: state.diagram.drdIndex,
@@ -611,14 +623,22 @@ export const Diagram = React.forwardRef<DiagramRef, { container: React.RefObject
change: {
type: "absolute",
nodeType: node.type as NodeType,
- selectedEdges: [...selectedEdgesById.keys()],
+ selectedEdges: [
+ ...state.computed(state).getDiagramData(externalModelsByNamespace).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] : []
- ),
+ sourceEdgeIndexes: state
+ .computed(state)
+ .getDiagramData(externalModelsByNamespace)
+ .edges.flatMap((e) =>
+ e.source === change.id && e.data?.dmnEdge ? [e.data.dmnEdge.index] : []
+ ),
+ targetEdgeIndexes: state
+ .computed(state)
+ .getDiagramData(externalModelsByNamespace)
+ .edges.flatMap((e) =>
+ e.target === change.id && e.data?.dmnEdge ? [e.data.dmnEdge.index] : []
+ ),
position: change.positionAbsolute,
},
});
@@ -635,7 +655,10 @@ export const Diagram = React.forwardRef<DiagramRef, { container: React.RefObject
];
for (let i = 0; i < nested.length; i++) {
- const nestedNode = nodesById.get(nested[i]["@_href"])!;
+ const nestedNode = state
+ .computed(state)
+ .getDiagramData(externalModelsByNamespace)
+ .nodesById.get(nested[i]["@_href"])!;
const snappedNestedNodeShapeWithAppliedDelta = snapShapePosition(
state.diagram.snapGrid,
offsetShapePosition(nestedNode.data.shape, delta)
@@ -647,14 +670,23 @@ export const Diagram = React.forwardRef<DiagramRef, { container: React.RefObject
change: {
type: "absolute",
nodeType: nestedNode.type as NodeType,
- selectedEdges: edges.map((e) => e.id),
+ selectedEdges: state
+ .computed(state)
+ .getDiagramData(externalModelsByNamespace)
+ .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] : []
- ),
+ sourceEdgeIndexes: state
+ .computed(state)
+ .getDiagramData(externalModelsByNamespace)
+ .edges.flatMap((e) =>
+ e.source === nestedNode.id && e.data?.dmnEdge ? [e.data.dmnEdge.index] : []
+ ),
+ targetEdgeIndexes: state
+ .computed(state)
+ .getDiagramData(externalModelsByNamespace)
+ .edges.flatMap((e) =>
+ e.target === nestedNode.id && e.data?.dmnEdge ? [e.data.dmnEdge.index] : []
+ ),
position: snappedNestedNodeShapeWithAppliedDelta,
},
});
@@ -664,35 +696,38 @@ export const Diagram = React.forwardRef<DiagramRef, { container: React.RefObject
break;
case "remove":
console.debug(`DMN DIAGRAM: 'onNodesChange' --> remove '${change.id}'`);
- const node = nodesById.get(change.id)!;
+ const node = state.computed(state).getDiagramData(externalModelsByNamespace).nodesById.get(change.id)!;
deleteNode({
+ drgEdges: state.computed(state).getDiagramData(externalModelsByNamespace).drgEdges,
definitions: state.dmn.model.definitions,
drdIndex: state.diagram.drdIndex,
+ dmnObjectNamespace: node.data.dmnObjectNamespace,
dmnObjectQName: node.data.dmnObjectQName,
dmnObjectId: node.data.dmnObject?.["@_id"],
nodeNature: nodeNatures[node.type as NodeType],
+ mode: NodeDeletionMode.FORM_DRG_AND_ALL_DRDS,
});
- state.dispatch.diagram.setNodeStatus(state, node.id, {
+ state.dispatch(state).diagram.setNodeStatus(node.id, {
selected: false,
dragging: false,
resizing: false,
});
break;
case "reset":
- state.dispatch.diagram.setNodeStatus(state, change.item.id, {
+ state.dispatch(state).diagram.setNodeStatus(change.item.id, {
selected: false,
dragging: false,
resizing: false,
});
break;
case "select":
- state.dispatch.diagram.setNodeStatus(state, change.id, { selected: change.selected });
+ state.dispatch(state).diagram.setNodeStatus(change.id, { selected: change.selected });
break;
}
}
});
},
- [reactFlowInstance, dmnEditorStoreApi, nodesById, dmnShapesByHref, edges, selectedEdgesById]
+ [reactFlowInstance, dmnEditorStoreApi, externalModelsByNamespace]
);
const resetToBeforeEditingBegan = useCallback(() => {
@@ -737,29 +772,38 @@ export const Diagram = React.forwardRef<DiagramRef, { container: React.RefObject
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;
- }
+ try {
+ dmnEditorStoreApi.setState((state) => {
+ console.debug("DMN DIAGRAM: `onNodeDragStop`");
+ const nodeBeingDragged = state
+ .computed(state)
+ .getDiagramData(externalModelsByNamespace)
+ .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;
- }
+ // Validate
+ const dropTargetNode = dmnEditorStoreApi.getState().diagram.dropTargetNode;
+ if (
+ dropTargetNode &&
+ containment.has(dropTargetNode.type as NodeType) &&
+ !state.computed(state).isDropTargetNodeValidForSelection
+ ) {
+ console.debug(
+ `DMN DIAGRAM: Invalid containment: '${[
+ ...state.computed(state).getDiagramData(externalModelsByNamespace).selectedNodeTypes,
+ ].join("', '")}' inside '${dropTargetNode.type}'. Ignoring nodes dropped.`
+ );
+ resetToBeforeEditingBegan();
+ return;
+ }
- const selectedNodes = [...selectedNodesById.values()];
+ const selectedNodes = [
+ ...state.computed(state).getDiagramData(externalModelsByNamespace).selectedNodesById.values(),
+ ];
- try {
- dmnEditorStoreApi.setState((state) => {
state.diagram.dropTargetNode = undefined;
if (!node.dragging) {
@@ -768,7 +812,10 @@ export const Diagram = React.forwardRef<DiagramRef, { container: React.RefObject
// Un-parent
if (nodeBeingDragged.data.parentRfNode) {
- const p = nodesById.get(nodeBeingDragged.data.parentRfNode.id);
+ const p = state
+ .computed(state)
+ .getDiagramData(externalModelsByNamespace)
+ .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({
@@ -791,7 +838,10 @@ export const Diagram = React.forwardRef<DiagramRef, { container: React.RefObject
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"]!,
+ decisionServiceId: state
+ .computed(state)
+ .getDiagramData(externalModelsByNamespace)
+ .nodesById.get(dropTargetNode.id)!.data.dmnObject!["@_id"]!,
snapGrid: state.diagram.snapGrid,
});
}
@@ -806,14 +856,7 @@ export const Diagram = React.forwardRef<DiagramRef, { container: React.RefObject
resetToBeforeEditingBegan();
}
},
- [
- dmnEditorStoreApi,
- isDropTargetNodeValidForSelection,
- nodesById,
- resetToBeforeEditingBegan,
- selectedNodeTypes,
- selectedNodesById,
- ]
+ [dmnEditorStoreApi, externalModelsByNamespace, resetToBeforeEditingBegan]
);
const onEdgesChange = useCallback<RF.OnEdgesChange>(
@@ -823,18 +866,19 @@ export const Diagram = React.forwardRef<DiagramRef, { container: React.RefObject
switch (change.type) {
case "select":
console.debug(`DMN DIAGRAM: 'onEdgesChange' --> select '${change.id}'`);
- state.dispatch.diagram.setEdgeStatus(state, change.id, { selected: change.selected });
+ state.dispatch(state).diagram.setEdgeStatus(change.id, { selected: change.selected });
break;
case "remove":
console.debug(`DMN DIAGRAM: 'onEdgesChange' --> remove '${change.id}'`);
- const edge = edgesById.get(change.id);
+ const edge = state.computed(state).getDiagramData(externalModelsByNamespace).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 },
+ mode: EdgeDeletionMode.FORM_DRG_AND_ALL_DRDS,
});
- state.dispatch.diagram.setEdgeStatus(state, change.id, { selected: false, draggingWaypoint: false });
+ state.dispatch(state).diagram.setEdgeStatus(change.id, { selected: false, draggingWaypoint: false });
}
break;
case "add":
@@ -844,39 +888,46 @@ export const Diagram = React.forwardRef<DiagramRef, { container: React.RefObject
}
});
},
- [dmnEditorStoreApi, edgesById]
+ [dmnEditorStoreApi, externalModelsByNamespace]
);
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!");
- }
+ dmnEditorStoreApi.setState((state) => {
+ const sourceNode = state
+ .computed(state)
+ .getDiagramData(externalModelsByNamespace)
+ .nodesById.get(newConnection.source!);
+ const targetNode = state
+ .computed(state)
+ .getDiagramData(externalModelsByNamespace)
+ .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]!
+ : getDmnBoundsCenterPoint(targetBounds);
+ const firstWaypoint = oldEdge.data?.dmnEdge
+ ? oldEdge.data!.dmnEdge!["di:waypoint"]![0]!
+ : getDmnBoundsCenterPoint(sourceBounds);
- dmnEditorStoreApi.setState((state) => {
const { newDmnEdge } = addEdge({
definitions: state.dmn.model.definitions,
drdIndex: state.diagram.drdIndex,
edge: {
+ autoPositionedEdgeMarker: undefined,
type: oldEdge.type as EdgeType,
targetHandle: ((newConnection.targetHandle as PositionalNodeHandleId) ??
getHandlePosition({ shapeBounds: targetBounds, waypoint: lastWaypoint })
@@ -903,15 +954,16 @@ export const Diagram = React.forwardRef<DiagramRef, { container: React.RefObject
keepWaypoints: true,
});
- // The DMN Edge changed nodes, so we need to delete the old one, but keep the waypoints!
+ // The DMN Edge changed nodes, so we need to delete the old one, but keep the waypoints on the same DRD.
if (newDmnEdge["@_dmnElementRef"] !== oldEdge.id) {
- const { dmnEdge: deletedDmnEdge } = deleteEdge({
+ const { deletedDmnEdgeOnCurrentDrd } = deleteEdge({
definitions: state.dmn.model.definitions,
drdIndex: state.diagram.drdIndex,
edge: { id: oldEdge.id, dmnObject: oldEdge.data!.dmnObject },
+ mode: EdgeDeletionMode.FORM_DRG_AND_ALL_DRDS,
});
- const deletedWaypoints = deletedDmnEdge?.["di:waypoint"];
+ const deletedWaypoints = deletedDmnEdgeOnCurrentDrd?.["di:waypoint"];
if (oldEdge.source !== newConnection.source && deletedWaypoints) {
newDmnEdge["di:waypoint"] = [newDmnEdge["di:waypoint"]![0], ...deletedWaypoints.slice(1)];
@@ -933,7 +985,7 @@ export const Diagram = React.forwardRef<DiagramRef, { container: React.RefObject
state.diagram.edgeIdBeingUpdated = undefined;
});
},
- [dmnEditorStoreApi, nodesById]
+ [dmnEditorStoreApi, externalModelsByNamespace]
);
const onEdgeUpdateStart = useCallback(
@@ -962,8 +1014,10 @@ export const Diagram = React.forwardRef<DiagramRef, { container: React.RefObject
// Override Reactflow's behavior by intercepting the keydown event using its `capture` variant.
const handleRfKeyDownCapture = useCallback(
(e: React.KeyboardEvent) => {
+ const s = dmnEditorStoreApi.getState();
+
if (e.key === "Escape") {
- if (isDiagramEditingInProgress && dmnModelBeforeEditingRef.current) {
+ if (s.computed(s).isDiagramEditingInProgress() && dmnModelBeforeEditingRef.current) {
console.debug(
"DMN DIAGRAM: Intercepting Escape pressed and preventing propagation. Reverting DMN model to what it was before editing began."
);
@@ -972,14 +1026,20 @@ export const Diagram = React.forwardRef<DiagramRef, { container: React.RefObject
e.preventDefault();
resetToBeforeEditingBegan();
- } else if (!diagram.ongoingConnection) {
+ } else if (!s.diagram.ongoingConnection) {
dmnEditorStoreApi.setState((state) => {
- if (selectedNodesById.size > 0 || selectedEdgesById.size > 0) {
+ if (
+ state.computed(s).getDiagramData(externalModelsByNamespace).selectedNodesById.size > 0 ||
+ state.computed(s).getDiagramData(externalModelsByNamespace).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) {
+ } else if (
+ state.computed(s).getDiagramData(externalModelsByNamespace).selectedNodesById.size <= 0 &&
+ state.computed(s).getDiagramData(externalModelsByNamespace).selectedEdgesById.size <= 0
+ ) {
console.debug("DMN DIAGRAM: Esc pressed. Closing all open panels.");
state.diagram.propertiesPanel.isOpen = false;
state.diagram.overlaysPanel.isOpen = false;
@@ -994,21 +1054,21 @@ export const Diagram = React.forwardRef<DiagramRef, { container: React.RefObject
}
}
},
- [
- diagram.ongoingConnection,
- dmnEditorStoreApi,
- dmnModelBeforeEditingRef,
- isDiagramEditingInProgress,
- resetToBeforeEditingBegan,
- selectedEdgesById.size,
- selectedNodesById.size,
- ]
+ [dmnEditorStoreApi, dmnModelBeforeEditingRef, externalModelsByNamespace, resetToBeforeEditingBegan]
);
const [showEmptyState, setShowEmptyState] = useState(true);
+ const nodes = useDmnEditorStore((s) => s.computed(s).getDiagramData(externalModelsByNamespace).nodes);
+ const edges = useDmnEditorStore((s) => s.computed(s).getDiagramData(externalModelsByNamespace).edges);
+ const drgElementsWithoutVisualRepresentationOnCurrentDrdLength = useDmnEditorStore(
+ (s) =>
+ s.computed(s).getDiagramData(externalModelsByNamespace).drgElementsWithoutVisualRepresentationOnCurrentDrd
+ .length
+ );
+
const isEmptyStateShowing =
- showEmptyState && nodes.length === 0 && drgElementsWithoutVisualRepresentationOnCurrentDrd.length === 0;
+ showEmptyState && nodes.length === 0 && drgElementsWithoutVisualRepresentationOnCurrentDrdLength === 0;
return (
<>
@@ -1060,6 +1120,7 @@ export const Diagram = React.forwardRef<DiagramRef, { container: React.RefObject
fitViewOptions={FIT_VIEW_OPTIONS}
attributionPosition={"bottom-right"}
onInit={setReactFlowInstance}
+ deleteKeyCode={DELETE_NODE_KEY_CODES}
// (begin)
// Used to make the Palette work by dropping nodes on the Reactflow Canvas
onDrop={onDrop}
@@ -1155,7 +1216,7 @@ function DmnDiagramEmptyState({
}),
});
- state.dispatch.boxedExpressionEditor.open(state, parseXmlHref(decisionNodeHref).id);
+ state.dispatch(state).boxedExpressionEditor.open(parseXmlHref(decisionNodeHref).id);
});
}}
>
@@ -1219,20 +1280,15 @@ function DmnDiagramEmptyState({
}
export function SetConnectionToReactFlowStore(props: {}) {
- const diagram = useDmnEditorStore((s) => s.diagram);
+ const ongoingConnection = useDmnEditorStore((s) => s.diagram.ongoingConnection);
const rfStoreApi = RF.useStoreApi();
useEffect(() => {
rfStoreApi.setState({
- connectionHandleId: diagram.ongoingConnection?.handleId,
- connectionHandleType: diagram.ongoingConnection?.handleType,
- connectionNodeId: diagram.ongoingConnection?.nodeId,
+ connectionHandleId: ongoingConnection?.handleId,
+ connectionHandleType: ongoingConnection?.handleType,
+ connectionNodeId: ongoingConnection?.nodeId,
});
- }, [
- diagram.ongoingConnection?.handleId,
- diagram.ongoingConnection?.handleType,
- diagram.ongoingConnection?.nodeId,
- rfStoreApi,
- ]);
+ }, [ongoingConnection?.handleId, ongoingConnection?.handleType, ongoingConnection?.nodeId, rfStoreApi]);
return <></>;
}
@@ -1270,15 +1326,19 @@ export function TopRightCornerPanels() {
return (
<>
<RF.Panel position={"top-right"} style={{ display: "flex" }}>
+ <aside className={"kie-dmn-editor--autolayout-panel-toggle"}>
+ <AutolayoutButton />
+ </aside>
<aside className={"kie-dmn-editor--overlays-panel-toggle"}>
<Popover
className={"kie-dmn-editor--overlay-panel-popover"}
key={`${diagram.overlaysPanel.isOpen}`}
aria-label="Overlays Panel"
position={"bottom-end"}
+ enableFlip={false}
+ flipBehavior={["bottom-end"]}
hideOnOutsideClick={false}
isVisible={diagram.overlaysPanel.isOpen}
- enableFlip={true}
bodyContent={<OverlaysPanel />}
>
<button className={"kie-dmn-editor--overlays-panel-toggle-button"} onClick={toggleOverlaysPanel}>
@@ -1301,14 +1361,20 @@ export function TopRightCornerPanels() {
export function SelectionStatus() {
const rfStoreApi = RF.useStoreApi();
- const { selectedNodesById, selectedEdgesById } = useDmnEditorDerivedStore();
+ const { externalModelsByNamespace } = useExternalModels();
+ const selectedNodesCount = useDmnEditorStore(
+ (s) => s.computed(s).getDiagramData(externalModelsByNamespace).selectedNodesById.size
+ );
+ const selectedEdgesCount = useDmnEditorStore(
+ (s) => s.computed(s).getDiagramData(externalModelsByNamespace).selectedEdgesById.size
+ );
const dmnEditorStoreApi = useDmnEditorStoreApi();
useEffect(() => {
- if (selectedNodesById.size >= 2) {
+ if (selectedNodesCount >= 2) {
rfStoreApi.setState({ nodesSelectionActive: true });
}
- }, [rfStoreApi, selectedNodesById.size]);
+ }, [rfStoreApi, selectedNodesCount]);
const onClose = useCallback(
(e: React.MouseEvent) => {
@@ -1323,14 +1389,14 @@ export function SelectionStatus() {
return (
<>
- {(selectedNodesById.size + selectedEdgesById.size >= 2 && (
+ {(selectedNodesCount + selectedEdgesCount >= 2 && (
<RF.Panel position={"top-center"}>
<Label style={{ paddingLeft: "24px" }} onClose={onClose}>
- {(selectedEdgesById.size === 0 && `${selectedNodesById.size} nodes selected`) ||
- (selectedNodesById.size === 0 && `${selectedEdgesById.size} edges selected`) ||
- `${selectedNodesById.size} node${selectedNodesById.size === 1 ? "" : "s"}, ${
- selectedEdgesById.size
- } edge${selectedEdgesById.size === 1 ? "" : "s"} selected`}
+ {(selectedEdgesCount === 0 && `${selectedNodesCount} nodes selected`) ||
+ (selectedNodesCount === 0 && `${selectedEdgesCount} edges selected`) ||
+ `${selectedNodesCount} node${selectedNodesCount === 1 ? "" : "s"}, ${selectedEdgesCount} edge${
+ selectedEdgesCount === 1 ? "" : "s"
+ } selected`}
</Label>
</RF.Panel>
)) || <></>}
@@ -1341,10 +1407,11 @@ export function SelectionStatus() {
export function KeyboardShortcuts(props: {}) {
const rfStoreApi = RF.useStoreApi();
const dmnEditorStoreApi = useDmnEditorStoreApi();
- const diagram = useDmnEditorStore((s) => s.diagram);
+ const { externalModelsByNamespace } = useExternalModels();
- const rf = RF.useReactFlow();
+ const rf = RF.useReactFlow<DmnDiagramNodeData, DmnDiagramEdgeData>();
+ // Reset position to origin
const space = RF.useKeyPress(["Space"]);
useEffect(() => {
if (!space) {
@@ -1354,6 +1421,7 @@ export function KeyboardShortcuts(props: {}) {
rf.setViewport(DEFAULT_VIEWPORT, { duration: 200 });
}, [rf, space]);
+ // Focus on node bounds
const b = RF.useKeyPress(["b"]);
useEffect(() => {
if (!b) {
@@ -1381,6 +1449,7 @@ export function KeyboardShortcuts(props: {}) {
);
}, [b, rf]);
+ // Cancel action
const esc = RF.useKeyPress(["Escape"]);
useEffect(() => {
if (!esc) {
@@ -1400,8 +1469,9 @@ export function KeyboardShortcuts(props: {}) {
return rfState;
});
- }, [dmnEditorStoreApi, esc, rfStoreApi]);
+ }, [esc, dmnEditorStoreApi, rfStoreApi]);
+ // Cut
const cut = RF.useKeyPress(["Meta+x"]);
useEffect(() => {
if (!cut) {
@@ -1416,33 +1486,37 @@ export function KeyboardShortcuts(props: {}) {
navigator.clipboard.writeText(JSON.stringify(clipboard)).then(() => {
dmnEditorStoreApi.setState((state) => {
- // Delete edges first
+ // Delete edges
[...copiedEdgesById.values(), ...danglingEdgesById.values()].forEach((edge) => {
deleteEdge({
definitions: state.dmn.model.definitions,
drdIndex: state.diagram.drdIndex,
edge: { id: edge.id, dmnObject: edge.data!.dmnObject },
+ mode: EdgeDeletionMode.FORM_DRG_AND_ALL_DRDS,
});
- state.dispatch.diagram.setEdgeStatus(state, edge.id, {
+ state.dispatch(state).diagram.setEdgeStatus(edge.id, {
selected: false,
draggingWaypoint: false,
});
});
- // Then delete nodes
+ // Delete nodes
rfStoreApi
.getState()
.getNodes()
.forEach((node: RF.Node<DmnDiagramNodeData>) => {
if (copiedNodesById.has(node.id)) {
deleteNode({
+ drgEdges: state.computed(state).getDiagramData(externalModelsByNamespace).drgEdges,
definitions: state.dmn.model.definitions,
drdIndex: state.diagram.drdIndex,
+ dmnObjectNamespace: node.data.dmnObjectNamespace,
dmnObjectQName: node.data.dmnObjectQName,
dmnObjectId: node.data.dmnObject?.["@_id"],
nodeNature: nodeNatures[node.type as NodeType],
+ mode: NodeDeletionMode.FORM_DRG_AND_ALL_DRDS,
});
- state.dispatch.diagram.setNodeStatus(state, node.id, {
+ state.dispatch(state).diagram.setNodeStatus(node.id, {
selected: false,
dragging: false,
resizing: false,
@@ -1451,8 +1525,9 @@ export function KeyboardShortcuts(props: {}) {
});
});
});
- }, [cut, dmnEditorStoreApi, rfStoreApi]);
+ }, [cut, dmnEditorStoreApi, rfStoreApi, externalModelsByNamespace]);
+ // Copy
const copy = RF.useKeyPress(["Meta+c"]);
useEffect(() => {
if (!copy) {
@@ -1465,6 +1540,7 @@ export function KeyboardShortcuts(props: {}) {
navigator.clipboard.writeText(JSON.stringify(clipboard));
}, [copy, dmnEditorStoreApi, rfStoreApi]);
+ // Paste
const paste = RF.useKeyPress(["Meta+v"]);
useEffect(() => {
if (!paste) {
@@ -1536,8 +1612,9 @@ export function KeyboardShortcuts(props: {}) {
}
});
});
- }, [dmnEditorStoreApi, paste]);
+ }, [paste, dmnEditorStoreApi]);
+ // Select/deselect all
const selectAll = RF.useKeyPress(["a", "Meta+a"]);
useEffect(() => {
if (!selectAll) {
@@ -1566,8 +1643,9 @@ export function KeyboardShortcuts(props: {}) {
state.diagram._selectedEdges = allEdgeIds;
}
});
- }, [dmnEditorStoreApi, rfStoreApi, selectAll]);
+ }, [selectAll, dmnEditorStoreApi, rfStoreApi]);
+ // Create group wrapping selection
const g = RF.useKeyPress(["g"]);
useEffect(() => {
if (!g) {
@@ -1596,10 +1674,11 @@ export function KeyboardShortcuts(props: {}) {
},
});
- state.dispatch.diagram.setNodeStatus(state, newNodeId, { selected: true });
+ state.dispatch(state).diagram.setNodeStatus(newNodeId, { selected: true });
});
- }, [dmnEditorStoreApi, g, rf]);
+ }, [g, dmnEditorStoreApi, rf]);
+ // Toggle hierarchy highlights
const h = RF.useKeyPress(["h"]);
useEffect(() => {
if (!h) {
@@ -1609,8 +1688,9 @@ export function KeyboardShortcuts(props: {}) {
dmnEditorStoreApi.setState((state) => {
state.diagram.overlays.enableNodeHierarchyHighlight = !state.diagram.overlays.enableNodeHierarchyHighlight;
});
- }, [dmnEditorStoreApi, h]);
+ }, [h, dmnEditorStoreApi]);
+ // Show Properties panel
const i = RF.useKeyPress(["i"]);
useEffect(() => {
if (!i) {
@@ -1620,7 +1700,70 @@ export function KeyboardShortcuts(props: {}) {
dmnEditorStoreApi.setState((state) => {
state.diagram.propertiesPanel.isOpen = !state.diagram.propertiesPanel.isOpen;
});
- }, [dmnEditorStoreApi, i]);
+ }, [i, dmnEditorStoreApi]);
+
+ // Hide from DRD
+ const x = RF.useKeyPress(["x"]);
+ useEffect(() => {
+ if (!x) {
+ return;
+ }
+
+ const nodesById = rf
+ .getNodes()
+ .reduce((acc, s) => acc.set(s.id, s), new Map<string, RF.Node<DmnDiagramNodeData>>());
+
+ dmnEditorStoreApi.setState((state) => {
+ const selectedNodeIds = new Set(state.diagram._selectedNodes);
+ for (const edge of rf.getEdges()) {
+ if (
+ (selectedNodeIds.has(edge.source) &&
+ canRemoveNodeFromDrdOnly({
+ definitions: state.dmn.model.definitions,
+ drdIndex: state.diagram.drdIndex,
+ dmnObjectNamespace: nodesById.get(edge.source)!.data.dmnObjectNamespace,
+ dmnObjectId: nodesById.get(edge.source)!.data.dmnObject?.["@_id"],
+ })) ||
+ (selectedNodeIds.has(edge.target) &&
+ canRemoveNodeFromDrdOnly({
+ definitions: state.dmn.model.definitions,
+ drdIndex: state.diagram.drdIndex,
+ dmnObjectNamespace: nodesById.get(edge.target)!.data.dmnObjectNamespace,
+ dmnObjectId: nodesById.get(edge.target)!.data.dmnObject?.["@_id"],
+ }))
+ ) {
+ deleteEdge({
+ definitions: state.dmn.model.definitions,
+ drdIndex: state.diagram.drdIndex,
+ edge: { id: edge.id, dmnObject: edge.data!.dmnObject },
+ mode: EdgeDeletionMode.FROM_CURRENT_DRD_ONLY,
+ });
+ state.dispatch(state).diagram.setEdgeStatus(edge.id, { selected: false, draggingWaypoint: false });
+ }
+ }
+
+ for (const node of rf.getNodes().filter((s) => s.selected)) {
+ const { deletedDmnShapeOnCurrentDrd: deletedShape } = deleteNode({
+ drgEdges: [], // Deleting from DRD only.
+ definitions: state.dmn.model.definitions,
+ drdIndex: state.diagram.drdIndex,
+ dmnObjectNamespace: node.data.dmnObjectNamespace,
+ dmnObjectQName: node.data.dmnObjectQName,
+ dmnObjectId: node.data.dmnObject?.["@_id"],
+ nodeNature: nodeNatures[node.type as NodeType],
+ mode: NodeDeletionMode.FROM_CURRENT_DRD_ONLY,
+ });
+
+ if (deletedShape) {
+ state.dispatch(state).diagram.setNodeStatus(node.id, {
+ selected: false,
+ dragging: false,
+ resizing: false,
+ });
+ }
+ }
+ });
+ }, [x, dmnEditorStoreApi, rf]);
return <></>;
}
diff --git a/packages/dmn-editor/src/diagram/DrdSelectorPanel.tsx b/packages/dmn-editor/src/diagram/DrdSelectorPanel.tsx
index 0340973a26e..1aab48d71dc 100644
--- a/packages/dmn-editor/src/diagram/DrdSelectorPanel.tsx
+++ b/packages/dmn-editor/src/diagram/DrdSelectorPanel.tsx
@@ -18,13 +18,16 @@
*/
import * as React from "react";
-import { useDmnEditorStore, useDmnEditorStoreApi } from "../store/Store";
+import { useDmnEditorStore, useDmnEditorStoreApi } from "../store/StoreContext";
import { addOrGetDrd, getDefaultDrdName } from "../mutations/addOrGetDrd";
import { Text, TextContent } from "@patternfly/react-core/dist/js/components/Text";
-import { Flex } from "@patternfly/react-core/dist/js/layouts/Flex";
+import { Flex, FlexItem } from "@patternfly/react-core/dist/js/layouts/Flex";
import { Button, ButtonVariant } from "@patternfly/react-core/dist/js/components/Button";
import { PlusCircleIcon } from "@patternfly/react-icons/dist/js/icons/plus-circle-icon";
import { Divider } from "@patternfly/react-core/dist/js/components/Divider";
+import { ArrowRightIcon } from "@patternfly/react-icons/dist/js/icons/arrow-right-icon";
+import { DiagramNodesPanel } from "../store/Store";
+import { getDrdId } from "./drd/drdId";
export function DrdSelectorPanel() {
const thisDmn = useDmnEditorStore((s) => s.dmn);
@@ -49,7 +52,11 @@ export function DrdSelectorPanel() {
definitions: state.dmn.model.definitions,
drdIndex: newIndex,
});
+
state.diagram.drdIndex = newIndex;
+ state.diagram.drdSelector.isOpen = false;
+ state.diagram.openNodesPanel = DiagramNodesPanel.DRG_NODES;
+ state.focus.consumableId = getDrdId({ drdIndex: newIndex });
});
}}
>
@@ -59,7 +66,7 @@ export function DrdSelectorPanel() {
<Divider style={{ marginBottom: "8px" }} />
<div className={"kie-dmn-editor--drd-list"}>
{thisDmn.model.definitions["dmndi:DMNDI"]?.["dmndi:DMNDiagram"]?.map((drd, i) => (
- <React.Fragment key={drd["@_id"] ?? i}>
+ <React.Fragment key={drd["@_id"]!}>
<button
className={i === diagram.drdIndex ? "active" : undefined}
onClick={() => {
@@ -69,7 +76,7 @@ export function DrdSelectorPanel() {
});
}}
>
- {drd["@_name"] || getDefaultDrdName({ drdIndex: i })}
+ {`${i + 1}. ${drd["@_name"] || getDefaultDrdName({ drdIndex: i })}`}
</button>
<br />
</React.Fragment>
diff --git a/packages/dmn-editor/src/diagram/DrgNodesPanel.tsx b/packages/dmn-editor/src/diagram/DrgNodesPanel.tsx
index 11b3b936795..2957f0eec09 100644
--- a/packages/dmn-editor/src/diagram/DrgNodesPanel.tsx
+++ b/packages/dmn-editor/src/diagram/DrgNodesPanel.tsx
@@ -17,26 +17,26 @@
* under the License.
*/
-import { Divider } from "@patternfly/react-core/dist/js/components/Divider";
+import { DMN15__tDefinitions } from "@kie-tools/dmn-marshaller/dist/schemas/dmn-1_5/ts-gen/types";
+import { Button, ButtonVariant } from "@patternfly/react-core/dist/js/components/Button/Button";
import { SearchInput } from "@patternfly/react-core/dist/js/components/SearchInput";
import { Text, TextContent } from "@patternfly/react-core/dist/js/components/Text";
import { Flex } from "@patternfly/react-core/dist/js/layouts/Flex";
+import { TimesIcon } from "@patternfly/react-icons/dist/js/icons/times-icon";
import * as React from "react";
import { useCallback, useState } from "react";
-import { DiagramNodesPanel, useDmnEditorStore, useDmnEditorStoreApi } from "../store/Store";
-import { buildXmlHref } from "../xml/xmlHrefs";
import { DmnObjectListItem } from "../externalNodes/DmnObjectListItem";
-import { useDmnEditorDerivedStore } from "../store/DerivedStore";
-import { DMN15__tDefinitions } from "@kie-tools/dmn-marshaller/dist/schemas/dmn-1_5/ts-gen/types";
+import { DiagramNodesPanel } from "../store/Store";
+import { useDmnEditorStore, useDmnEditorStoreApi } from "../store/StoreContext";
import { Unpacked } from "../tsExt/tsExt";
-import { Button, ButtonVariant } from "@patternfly/react-core/dist/js/components/Button/Button";
-import { TimesIcon } from "@patternfly/react-icons/dist/js/icons/times-icon";
+import { buildXmlHref } from "../xml/xmlHrefs";
+import { Divider } from "@patternfly/react-core/dist/js/components/Divider";
export const MIME_TYPE_FOR_DMN_EDITOR_DRG_NODE = "kie-dmn-editor--drg-node";
export function DrgNodesPanel() {
const thisDmn = useDmnEditorStore((s) => s.dmn);
- const { dmnShapesByHref } = useDmnEditorDerivedStore();
+ const dmnShapesByHref = useDmnEditorStore((s) => s.computed(s).indexes().dmnShapesByHref);
const dmnEditorStoreApi = useDmnEditorStoreApi();
@@ -98,6 +98,8 @@ export function DrgNodesPanel() {
</Button>
</Flex>
+ <Divider style={{ marginBottom: "12px" }} />
+
<SearchInput
style={{ marginBottom: "12px", height: "36px" }}
onKeyDown={(e) => e.stopPropagation()}
diff --git a/packages/dmn-editor/src/diagram/Palette.tsx b/packages/dmn-editor/src/diagram/Palette.tsx
index 06c716af6d4..cb3640e19c5 100644
--- a/packages/dmn-editor/src/diagram/Palette.tsx
+++ b/packages/dmn-editor/src/diagram/Palette.tsx
@@ -22,7 +22,8 @@ import * as React from "react";
import { useCallback } from "react";
import { NodeType } from "./connections/graphStructure";
import { NODE_TYPES } from "./nodes/NodeTypes";
-import { DiagramNodesPanel, useDmnEditorStore, useDmnEditorStoreApi } from "../store/Store";
+import { DiagramNodesPanel } from "../store/Store";
+import { useDmnEditorStore, useDmnEditorStoreApi } from "../store/StoreContext";
import { addStandaloneNode } from "../mutations/addStandaloneNode";
import { CONTAINER_NODES_DESIRABLE_PADDING, getBounds } from "./maths/DmnMaths";
import { Popover } from "@patternfly/react-core/dist/js/components/Popover";
@@ -45,6 +46,7 @@ import { DrgNodesPanel } from "./DrgNodesPanel";
import { CaretDownIcon } from "@patternfly/react-icons/dist/js/icons/caret-down-icon";
import { useInViewSelect } from "../responsiveness/useInViewSelect";
import { useDmnEditor } from "../DmnEditorContext";
+import { getDrdId } from "./drd/drdId";
export const MIME_TYPE_FOR_DMN_EDITOR_NEW_NODE_FROM_PALETTE = "application/kie-dmn-editor--new-node-from-palette";
@@ -83,7 +85,7 @@ export function Palette({ pulse }: { pulse: boolean }) {
},
});
- state.dispatch.diagram.setNodeStatus(state, newNodeId, { selected: true });
+ state.dispatch(state).diagram.setNodeStatus(newNodeId, { selected: true });
});
}, [diagram.drdIndex, dmnEditorStoreApi, rfStoreApi]);
@@ -101,9 +103,10 @@ export function Palette({ pulse }: { pulse: boolean }) {
<div ref={drdSelectorPopoverRef} style={{ position: "absolute", left: "56px", height: "100%", zIndex: -1 }} />
<InlineFeelNameInput
validate={() => true}
- allUniqueNames={new Map()}
+ allUniqueNames={() => new Map()}
name={drd?.["@_name"] ?? ""}
- id={diagram.drdIndex + ""}
+ prefix={`${diagram.drdIndex + 1}.`}
+ id={getDrdId({ drdIndex: diagram.drdIndex })}
onRenamed={(newName) => {
dmnEditorStoreApi.setState((state) => {
const drd = addOrGetDrd({ definitions: state.dmn.model.definitions, drdIndex: diagram.drdIndex });
diff --git a/packages/dmn-editor/src/diagram/connections/ConnectionLine.tsx b/packages/dmn-editor/src/diagram/connections/ConnectionLine.tsx
index 37a657b148b..ea393275710 100644
--- a/packages/dmn-editor/src/diagram/connections/ConnectionLine.tsx
+++ b/packages/dmn-editor/src/diagram/connections/ConnectionLine.tsx
@@ -30,21 +30,24 @@ import {
import { NODE_TYPES } from "../nodes/NodeTypes";
import { getPositionalHandlePosition } from "../maths/Maths";
import { DecisionNodeSvg, BkmNodeSvg, KnowledgeSourceNodeSvg, TextAnnotationNodeSvg } from "../nodes/NodeSvgs";
-import { getNodeCenterPoint, pointsToPath } from "../maths/DmnMaths";
+import { pointsToPath } from "../maths/DmnMaths";
+import { getBoundsCenterPoint } from "../maths/Maths";
import { NodeType, getDefaultEdgeTypeBetween } from "./graphStructure";
import { switchExpression } from "@kie-tools-core/switch-expression-ts";
import { DEFAULT_NODE_SIZES } from "../nodes/DefaultSizes";
-import { useDmnEditorStore } from "../../store/Store";
+import { useDmnEditorStore } from "../../store/StoreContext";
import { useKieEdgePath } from "../edges/useKieEdgePath";
import { PositionalNodeHandleId } from "./PositionalNodeHandles";
-import { useDmnEditorDerivedStore } from "../../store/DerivedStore";
+import { useExternalModels } from "../../includedModels/DmnEditorDependenciesContext";
export function ConnectionLine({ toX, toY, fromNode, fromHandle }: RF.ConnectionLineComponentProps) {
- const diagram = useDmnEditorStore((s) => s.diagram);
-
- const edgeId = useDmnEditorStore((s) => s.diagram.edgeIdBeingUpdated);
- const { edgesById } = useDmnEditorDerivedStore();
- const edge = edgeId ? edgesById.get(edgeId) : undefined;
+ const snapGrid = useDmnEditorStore((s) => s.diagram.snapGrid);
+ const { externalModelsByNamespace } = useExternalModels();
+ const edge = useDmnEditorStore((s) =>
+ s.diagram.edgeIdBeingUpdated
+ ? s.computed(s).getDiagramData(externalModelsByNamespace).edgesById.get(s.diagram.edgeIdBeingUpdated)
+ : undefined
+ );
const kieEdgePath = useKieEdgePath(edge?.source, edge?.target, edge?.data);
// This works because nodes are configured with:
@@ -57,7 +60,12 @@ export function ConnectionLine({ toX, toY, fromNode, fromHandle }: RF.Connection
(k) => (PositionalNodeHandleId as any)[k] === fromHandle?.id
);
- const { "@_x": fromX, "@_y": fromY } = getNodeCenterPoint(fromNode);
+ const { "@_x": fromX, "@_y": fromY } = getBoundsCenterPoint({
+ x: fromNode?.positionAbsolute?.x,
+ y: fromNode?.positionAbsolute?.y,
+ width: fromNode?.width,
+ height: fromNode?.height,
+ });
const connectionLinePath =
edge && kieEdgePath.points
@@ -81,9 +89,9 @@ export function ConnectionLine({ toX, toY, fromNode, fromHandle }: RF.Connection
// Nodes
else {
const nodeType = handleId as NodeType;
- const { "@_x": toXsnapped, "@_y": toYsnapped } = snapPoint(diagram.snapGrid, { "@_x": toX, "@_y": toY });
+ const { "@_x": toXsnapped, "@_y": toYsnapped } = snapPoint(snapGrid, { "@_x": toX, "@_y": toY });
- const defaultSize = DEFAULT_NODE_SIZES[nodeType](diagram.snapGrid);
+ const defaultSize = DEFAULT_NODE_SIZES[nodeType](snapGrid);
const [toXauto, toYauto] = getPositionalHandlePosition(
{ x: toXsnapped, y: toYsnapped, width: defaultSize["@_width"], height: defaultSize["@_height"] },
{ x: fromX, y: fromY, width: 1, height: 1 }
diff --git a/packages/dmn-editor/src/focus/useFocusableElement.ts b/packages/dmn-editor/src/diagram/drd/drdId.ts
similarity index 52%
copy from packages/dmn-editor/src/focus/useFocusableElement.ts
copy to packages/dmn-editor/src/diagram/drd/drdId.ts
index d4b0823497b..60196289a50 100644
--- a/packages/dmn-editor/src/focus/useFocusableElement.ts
+++ b/packages/dmn-editor/src/diagram/drd/drdId.ts
@@ -17,32 +17,6 @@
* under the License.
*/
-import { useEffect } from "react";
-import { useDmnEditorStore, useDmnEditorStoreApi } from "../store/Store";
-
-export function useFocusableElement(
- ref: React.RefObject<HTMLInputElement>,
- id: string | undefined,
- before?: (cb: () => void) => void
-) {
- const focus = useDmnEditorStore((s) => s.focus);
- const dmnEditorStoreApi = useDmnEditorStoreApi();
-
- useEffect(() => {
- if (!id) {
- return;
- }
-
- const cb = () => {
- ref.current?.select();
-
- dmnEditorStoreApi.setState((state) => {
- state.focus.consumableId = undefined;
- });
- };
-
- if (focus.consumableId === id && ref.current) {
- before?.(cb) ?? cb();
- }
- }, [before, dmnEditorStoreApi, focus.consumableId, id, ref]);
+export function getDrdId({ drdIndex }: { drdIndex: number }) {
+ return `drd__${drdIndex}`;
}
diff --git a/packages/dmn-editor/src/diagram/edges/Edges.tsx b/packages/dmn-editor/src/diagram/edges/Edges.tsx
index 27567ccf980..a210dc55170 100644
--- a/packages/dmn-editor/src/diagram/edges/Edges.tsx
+++ b/packages/dmn-editor/src/diagram/edges/Edges.tsx
@@ -17,31 +17,22 @@
* under the License.
*/
-import {
- DMN15__tDefinitions,
- DMNDI15__DMNEdge,
- DMNDI15__DMNShape,
-} from "@kie-tools/dmn-marshaller/dist/schemas/dmn-1_5/ts-gen/types";
+import { DMNDI15__DMNEdge, DMNDI15__DMNShape } from "@kie-tools/dmn-marshaller/dist/schemas/dmn-1_5/ts-gen/types";
import * as React from "react";
+import { useRef } from "react";
import * as RF from "reactflow";
-import { Unpacked } from "../../tsExt/tsExt";
+import { DrgEdge } from "../graph/graph";
+import { DEFAULT_INTRACTION_WIDTH } from "../maths/DmnMaths";
+import { propsHaveSameValuesDeep } from "../memoization/memoization";
+import { useIsHovered } from "../useIsHovered";
import { PotentialWaypoint, Waypoints } from "./Waypoints";
+import { useAlwaysVisibleEdgeUpdatersAtNodeBorders } from "./useAlwaysVisibleEdgeUpdatersAtNodeBorders";
import { useKieEdgePath } from "./useKieEdgePath";
-import { useIsHovered } from "../useIsHovered";
import { usePotentialWaypointControls } from "./usePotentialWaypointControls";
-import { useAlwaysVisibleEdgeUpdatersAtNodeBorders } from "./useAlwaysVisibleEdgeUpdatersAtNodeBorders";
-import { DEFAULT_INTRACTION_WIDTH } from "../maths/DmnMaths";
export type DmnDiagramEdgeData = {
dmnEdge: (DMNDI15__DMNEdge & { index: number }) | undefined;
- dmnObject: {
- id: string;
- type:
- | Unpacked<DMN15__tDefinitions["artifact"]>["__$$element"]
- | Unpacked<DMN15__tDefinitions["drgElement"]>["__$$element"];
- requirementType: "informationRequirement" | "knowledgeRequirement" | "authorityRequirement" | "association";
- index: number;
- };
+ dmnObject: DrgEdge["dmnObject"];
dmnShapeSource: DMNDI15__DMNShape | undefined;
dmnShapeTarget: DMNDI15__DMNShape | undefined;
};
@@ -136,6 +127,9 @@ const interactionStrokeProps: Partial<React.SVGAttributes<SVGPathElement>> = {
};
export const InformationRequirementEdge = React.memo((props: RF.EdgeProps<DmnDiagramEdgeData>) => {
+ const renderCount = useRef<number>(0);
+ renderCount.current++;
+
const { path, points: waypoints } = useKieEdgePath(props.source, props.target, props.data);
const interactionPathRef = React.useRef<SVGPathElement>(null);
@@ -152,9 +146,7 @@ export const InformationRequirementEdge = React.memo((props: RF.EdgeProps<DmnDia
const isConnecting = !!RF.useStore((s) => s.connectionNodeId);
const className = useEdgeClassName(isConnecting, isDraggingWaypoint);
- const sourceNode = RF.useStore((s) => s.nodeInternals.get(props.source)!);
- const targetNode = RF.useStore((s) => s.nodeInternals.get(props.target)!);
- useAlwaysVisibleEdgeUpdatersAtNodeBorders(interactionPathRef, sourceNode, targetNode, waypoints);
+ useAlwaysVisibleEdgeUpdatersAtNodeBorders(interactionPathRef, props.source, props.target, waypoints);
return (
<>
@@ -168,6 +160,7 @@ export const InformationRequirementEdge = React.memo((props: RF.EdgeProps<DmnDia
onDoubleClick={onDoubleClick}
/>
<InformationRequirementPath d={path} className={`kie-dmn-editor--edge ${className}`} />
+
{props.selected && !isConnecting && props.data?.dmnEdge && (
<Waypoints
edgeId={props.id}
@@ -179,9 +172,12 @@ export const InformationRequirementEdge = React.memo((props: RF.EdgeProps<DmnDia
{isHovered && potentialWaypoint && <PotentialWaypoint point={potentialWaypoint.point} />}
</>
);
-});
+}, propsHaveSameValuesDeep);
export const KnowledgeRequirementEdge = React.memo((props: RF.EdgeProps<DmnDiagramEdgeData>) => {
+ const renderCount = useRef<number>(0);
+ renderCount.current++;
+
const { path, points: waypoints } = useKieEdgePath(props.source, props.target, props.data);
const interactionPathRef = React.useRef<SVGPathElement>(null);
@@ -198,9 +194,7 @@ export const KnowledgeRequirementEdge = React.memo((props: RF.EdgeProps<DmnDiagr
const isConnecting = !!RF.useStore((s) => s.connectionNodeId);
const className = useEdgeClassName(isConnecting, isDraggingWaypoint);
- const sourceNode = RF.useStore((s) => s.nodeInternals.get(props.source)!);
- const targetNode = RF.useStore((s) => s.nodeInternals.get(props.target)!);
- useAlwaysVisibleEdgeUpdatersAtNodeBorders(interactionPathRef, sourceNode, targetNode, waypoints);
+ useAlwaysVisibleEdgeUpdatersAtNodeBorders(interactionPathRef, props.source, props.target, waypoints);
return (
<>
@@ -214,6 +208,7 @@ export const KnowledgeRequirementEdge = React.memo((props: RF.EdgeProps<DmnDiagr
onDoubleClick={onDoubleClick}
/>
<KnowledgeRequirementPath d={path} className={`kie-dmn-editor--edge ${className}`} />
+
{props.selected && !isConnecting && props.data?.dmnEdge && (
<Waypoints
edgeId={props.id}
@@ -225,9 +220,12 @@ export const KnowledgeRequirementEdge = React.memo((props: RF.EdgeProps<DmnDiagr
{isHovered && potentialWaypoint && <PotentialWaypoint point={potentialWaypoint.point} />}
</>
);
-});
+}, propsHaveSameValuesDeep);
export const AuthorityRequirementEdge = React.memo((props: RF.EdgeProps<DmnDiagramEdgeData>) => {
+ const renderCount = useRef<number>(0);
+ renderCount.current++;
+
const { path, points: waypoints } = useKieEdgePath(props.source, props.target, props.data);
const interactionPathRef = React.useRef<SVGPathElement>(null);
@@ -244,9 +242,7 @@ export const AuthorityRequirementEdge = React.memo((props: RF.EdgeProps<DmnDiagr
const isConnecting = !!RF.useStore((s) => s.connectionNodeId);
const className = useEdgeClassName(isConnecting, isDraggingWaypoint);
- const sourceNode = RF.useStore((s) => s.nodeInternals.get(props.source)!);
- const targetNode = RF.useStore((s) => s.nodeInternals.get(props.target)!);
- useAlwaysVisibleEdgeUpdatersAtNodeBorders(interactionPathRef, sourceNode, targetNode, waypoints);
+ useAlwaysVisibleEdgeUpdatersAtNodeBorders(interactionPathRef, props.source, props.target, waypoints);
return (
<>
@@ -265,6 +261,7 @@ export const AuthorityRequirementEdge = React.memo((props: RF.EdgeProps<DmnDiagr
className={`kie-dmn-editor--edge ${className}`}
centerToConnectionPoint={false}
/>
+
{props.selected && !isConnecting && props.data?.dmnEdge && (
<Waypoints
edgeId={props.id}
@@ -276,9 +273,12 @@ export const AuthorityRequirementEdge = React.memo((props: RF.EdgeProps<DmnDiagr
{isHovered && potentialWaypoint && <PotentialWaypoint point={potentialWaypoint.point} />}
</>
);
-});
+}, propsHaveSameValuesDeep);
export const AssociationEdge = React.memo((props: RF.EdgeProps<DmnDiagramEdgeData>) => {
+ const renderCount = useRef<number>(0);
+ renderCount.current++;
+
const { path, points: waypoints } = useKieEdgePath(props.source, props.target, props.data);
const interactionPathRef = React.useRef<SVGPathElement>(null);
@@ -295,9 +295,7 @@ export const AssociationEdge = React.memo((props: RF.EdgeProps<DmnDiagramEdgeDat
const isConnecting = !!RF.useStore((s) => s.connectionNodeId);
const className = useEdgeClassName(isConnecting, isDraggingWaypoint);
- const sourceNode = RF.useStore((s) => s.nodeInternals.get(props.source)!);
- const targetNode = RF.useStore((s) => s.nodeInternals.get(props.target)!);
- useAlwaysVisibleEdgeUpdatersAtNodeBorders(interactionPathRef, sourceNode, targetNode, waypoints);
+ useAlwaysVisibleEdgeUpdatersAtNodeBorders(interactionPathRef, props.source, props.target, waypoints);
return (
<>
@@ -311,6 +309,7 @@ export const AssociationEdge = React.memo((props: RF.EdgeProps<DmnDiagramEdgeDat
onDoubleClick={onDoubleClick}
/>
<AssociationPath d={path} className={`kie-dmn-editor--edge ${className}`} />
+
{props.selected && !isConnecting && props.data?.dmnEdge && (
<Waypoints
edgeId={props.id}
@@ -322,4 +321,4 @@ export const AssociationEdge = React.memo((props: RF.EdgeProps<DmnDiagramEdgeDat
{isHovered && potentialWaypoint && <PotentialWaypoint point={potentialWaypoint.point} />}
</>
);
-});
+}, propsHaveSameValuesDeep);
diff --git a/packages/dmn-editor/src/diagram/edges/Waypoints.tsx b/packages/dmn-editor/src/diagram/edges/Waypoints.tsx
index eba58d37035..ec3f318c001 100644
--- a/packages/dmn-editor/src/diagram/edges/Waypoints.tsx
+++ b/packages/dmn-editor/src/diagram/edges/Waypoints.tsx
@@ -20,7 +20,7 @@
import * as React from "react";
import { DC__Point } from "@kie-tools/dmn-marshaller/dist/schemas/dmn-1_5/ts-gen/types";
import { deleteEdgeWaypoint } from "../../mutations/deleteEdgeWaypoint";
-import { useDmnEditorStore, useDmnEditorStoreApi } from "../../store/Store";
+import { useDmnEditorStore, useDmnEditorStoreApi } from "../../store/StoreContext";
import { drag } from "d3-drag";
import { select } from "d3-selection";
import { useEffect } from "react";
@@ -79,7 +79,7 @@ export function Waypoint({
const selection = select(circleRef.current);
const dragHandler = drag<SVGCircleElement, unknown>()
.on("start", () => {
- setState((state) => dispatch.diagram.setEdgeStatus(state, edgeId, { draggingWaypoint: true }));
+ setState((state) => state.dispatch(state).diagram.setEdgeStatus(edgeId, { draggingWaypoint: true }));
})
.on("drag", (e) => {
setState((state) => {
@@ -94,14 +94,14 @@ export function Waypoint({
})
.on("end", (e) => {
onDragStop(e.sourceEvent);
- setState((state) => dispatch.diagram.setEdgeStatus(state, edgeId, { draggingWaypoint: false }));
+ setState((state) => state.dispatch(state).diagram.setEdgeStatus(edgeId, { draggingWaypoint: false }));
});
selection.call(dragHandler);
return () => {
selection.on(".drag", null);
};
- }, [diagram.drdIndex, diagram.snapGrid, dispatch.diagram, edgeId, edgeIndex, index, onDragStop, setState]);
+ }, [diagram.drdIndex, diagram.snapGrid, edgeId, edgeIndex, index, onDragStop, setState]);
return (
<circle
diff --git a/packages/dmn-editor/src/diagram/edges/getSnappedMultiPointAnchoredEdgePath.tsx b/packages/dmn-editor/src/diagram/edges/getSnappedMultiPointAnchoredEdgePath.tsx
index 1c36bbcb58a..a1540c58adc 100644
--- a/packages/dmn-editor/src/diagram/edges/getSnappedMultiPointAnchoredEdgePath.tsx
+++ b/packages/dmn-editor/src/diagram/edges/getSnappedMultiPointAnchoredEdgePath.tsx
@@ -26,38 +26,34 @@ import * as RF from "reactflow";
import { switchExpression } from "@kie-tools-core/switch-expression-ts";
import { snapPoint } from "../SnapGrid";
import { PositionalNodeHandleId } from "../connections/PositionalNodeHandles";
-import {
- getLineRectangleIntersectionPoint,
- getHandlePosition,
- getNodeCenterPoint,
- pointsToPath,
-} from "../maths/DmnMaths";
-import { getDiscretelyAutoPositionedEdgeParamsForRfNodes } from "../maths/Maths";
+import { getLineRectangleIntersectionPoint, getHandlePosition, pointsToPath } from "../maths/DmnMaths";
+import { getBoundsCenterPoint } from "../maths/Maths";
+import { Bounds, getDiscretelyAutoPositionedEdgeParams } from "../maths/Maths";
import { AutoPositionedEdgeMarker } from "./AutoPositionedEdgeMarker";
import { SnapGrid } from "../../store/Store";
export function getSnappedMultiPointAnchoredEdgePath({
snapGrid,
dmnEdge,
- sourceNode,
- targetNode,
+ sourceNodeBounds,
+ targetNodeBounds,
dmnShapeSource,
dmnShapeTarget,
}: {
snapGrid: SnapGrid;
dmnEdge: DMNDI15__DMNEdge | undefined;
- sourceNode: RF.Node<any, string | undefined> | undefined;
- targetNode: RF.Node<any, string | undefined> | undefined;
+ sourceNodeBounds: Bounds | undefined;
+ targetNodeBounds: Bounds | undefined;
dmnShapeSource: DMNDI15__DMNShape | undefined;
dmnShapeTarget: DMNDI15__DMNShape | undefined;
}) {
- if (!sourceNode || !targetNode) {
+ if (!sourceNodeBounds || !targetNodeBounds) {
return { path: undefined, points: [] };
}
const points: DC__Point[] = new Array(Math.max(2, dmnEdge?.["di:waypoint"]?.length ?? 0));
- const discreteAuto = getDiscretelyAutoPositionedEdgeParamsForRfNodes(sourceNode, targetNode);
+ const discreteAuto = getDiscretelyAutoPositionedEdgeParams(sourceNodeBounds, targetNodeBounds);
if (dmnEdge?.["@_id"]?.endsWith(AutoPositionedEdgeMarker.BOTH)) {
points[0] = { "@_x": discreteAuto.sx, "@_y": discreteAuto.sy };
@@ -83,9 +79,9 @@ export function getSnappedMultiPointAnchoredEdgePath({
const secondWaypoint = points[1] ?? dmnEdge["di:waypoint"][1];
const sourceHandlePoint = getSnappedHandlePosition(
dmnShapeSource!,
- sourceNode,
+ sourceNodeBounds,
firstWaypoint,
- points.length === 2 ? getNodeCenterPoint(targetNode) : snapPoint(snapGrid, secondWaypoint)
+ points.length === 2 ? getBoundsCenterPoint(targetNodeBounds) : snapPoint(snapGrid, secondWaypoint)
);
points[0] ??= sourceHandlePoint;
@@ -93,9 +89,9 @@ export function getSnappedMultiPointAnchoredEdgePath({
const secondToLastWaypoint = points[points.length - 2] ?? dmnEdge["di:waypoint"][dmnEdge["di:waypoint"].length - 2];
const targetHandlePoint = getSnappedHandlePosition(
dmnShapeTarget!,
- targetNode,
+ targetNodeBounds,
lastWaypoint,
- points.length === 2 ? getNodeCenterPoint(sourceNode) : snapPoint(snapGrid, secondToLastWaypoint)
+ points.length === 2 ? getBoundsCenterPoint(sourceNodeBounds) : snapPoint(snapGrid, secondToLastWaypoint)
);
points[points.length - 1] ??= targetHandlePoint;
}
@@ -112,17 +108,17 @@ export function getSnappedMultiPointAnchoredEdgePath({
export function getSnappedHandlePosition(
shape: DMNDI15__DMNShape,
- snappedNode: RF.Node,
+ snappedNode: Bounds,
originalHandleWaypoint: DC__Point,
snappedSecondWaypoint: DC__Point
): DC__Point {
const { handlePosition } = getHandlePosition({ shapeBounds: shape["dc:Bounds"], waypoint: originalHandleWaypoint });
- const centerHandleWaypoint = getNodeCenterPoint(snappedNode);
+ const centerHandleWaypoint = getBoundsCenterPoint(snappedNode);
const nodeRectangle = {
- x: snappedNode.positionAbsolute?.x ?? 0,
- y: snappedNode.positionAbsolute?.y ?? 0,
+ x: snappedNode.x ?? 0,
+ y: snappedNode.y ?? 0,
width: snappedNode.width ?? 0,
height: snappedNode.height ?? 0,
};
diff --git a/packages/dmn-editor/src/diagram/edges/useAlwaysVisibleEdgeUpdatersAtNodeBorders.ts b/packages/dmn-editor/src/diagram/edges/useAlwaysVisibleEdgeUpdatersAtNodeBorders.ts
index 606451f2e7a..883dcf6d75c 100644
--- a/packages/dmn-editor/src/diagram/edges/useAlwaysVisibleEdgeUpdatersAtNodeBorders.ts
+++ b/packages/dmn-editor/src/diagram/edges/useAlwaysVisibleEdgeUpdatersAtNodeBorders.ts
@@ -28,10 +28,20 @@ const HOVERED_RADIUS = 10;
export function useAlwaysVisibleEdgeUpdatersAtNodeBorders(
interactionPathRef: React.RefObject<SVGPathElement>,
- sourceNode: RF.Node,
- targetNode: RF.Node,
+ source: string,
+ target: string,
snappedWaypoints: DC__Point[]
) {
+ const sourceNodeX = RF.useStore((s) => (source ? s.nodeInternals.get(source)?.positionAbsolute?.x : undefined));
+ const sourceNodeY = RF.useStore((s) => (source ? s.nodeInternals.get(source)?.positionAbsolute?.y : undefined));
+ const sourceNodeWidth = RF.useStore((s) => (source ? s.nodeInternals.get(source)?.width : undefined));
+ const sourceNodeHeight = RF.useStore((s) => (source ? s.nodeInternals.get(source)?.height : undefined));
+
+ const targetNodeX = RF.useStore((s) => (target ? s.nodeInternals.get(target)?.positionAbsolute?.x : undefined));
+ const targetNodeY = RF.useStore((s) => (target ? s.nodeInternals.get(target)?.positionAbsolute?.y : undefined));
+ const targetNodeWidth = RF.useStore((s) => (target ? s.nodeInternals.get(target)?.width : undefined));
+ const targetNodeHeight = RF.useStore((s) => (target ? s.nodeInternals.get(target)?.height : undefined));
+
useLayoutEffect(() => {
const edgeSvgGroup = interactionPathRef.current!.parentElement;
@@ -56,19 +66,19 @@ export function useAlwaysVisibleEdgeUpdatersAtNodeBorders(
edgeUpdaterTarget?.removeEventListener("mouseleave", onLeave);
edgeUpdaterTarget?.removeEventListener("mouseenter", onEnter);
};
- }, [interactionPathRef, sourceNode, targetNode, snappedWaypoints]);
+ }, [interactionPathRef]);
useLayoutEffect(() => {
const edgeSvgGroup = interactionPathRef.current!.parentElement;
// Get fake scaled bounds to give the Edge Updaters some distance of the node.
const scaledSourceNode = scaleFromCenter(HOVERED_RADIUS, {
- position: sourceNode.positionAbsolute,
- dimensions: sourceNode,
+ position: { x: sourceNodeX ?? 0, y: sourceNodeY ?? 0 },
+ dimensions: { width: sourceNodeWidth, height: sourceNodeHeight },
});
const scaledTargetNode = scaleFromCenter(HOVERED_RADIUS, {
- position: targetNode.positionAbsolute,
- dimensions: targetNode,
+ position: { x: targetNodeX ?? 0, y: targetNodeY ?? 0 },
+ dimensions: { width: targetNodeWidth, height: targetNodeHeight },
});
// Get the intersection point between the edge and the nodes. The Edge Updater must be visible at all times!
@@ -106,5 +116,16 @@ export function useAlwaysVisibleEdgeUpdatersAtNodeBorders(
edgeUpdaterTarget.setAttribute("cx", "" + targetPoint!["@_x"]);
edgeUpdaterTarget.setAttribute("cy", "" + targetPoint!["@_y"]);
edgeUpdaterTarget.setAttribute("r", `${RADIUS}`);
- }, [interactionPathRef, sourceNode, targetNode, snappedWaypoints]);
+ }, [
+ interactionPathRef,
+ snappedWaypoints,
+ sourceNodeHeight,
+ sourceNodeWidth,
+ sourceNodeX,
+ sourceNodeY,
+ targetNodeHeight,
+ targetNodeWidth,
+ targetNodeX,
+ targetNodeY,
+ ]);
}
diff --git a/packages/dmn-editor/src/diagram/edges/useKieEdgePath.tsx b/packages/dmn-editor/src/diagram/edges/useKieEdgePath.tsx
index 9ee59487744..d9212f9fd31 100644
--- a/packages/dmn-editor/src/diagram/edges/useKieEdgePath.tsx
+++ b/packages/dmn-editor/src/diagram/edges/useKieEdgePath.tsx
@@ -17,24 +17,29 @@
* under the License.
*/
+import { useMemo } from "react";
import * as RF from "reactflow";
-import { useCallback, useMemo } from "react";
-import { getSnappedMultiPointAnchoredEdgePath } from "./getSnappedMultiPointAnchoredEdgePath";
-import { useDmnEditorStore } from "../../store/Store";
+import { useDmnEditorStore } from "../../store/StoreContext";
import { DmnDiagramEdgeData } from "./Edges";
+import { getSnappedMultiPointAnchoredEdgePath } from "./getSnappedMultiPointAnchoredEdgePath";
export function useKieEdgePath(
source: string | undefined,
target: string | undefined,
data: DmnDiagramEdgeData | undefined
) {
- const diagram = useDmnEditorStore((s) => s.diagram);
- const sourceNode = RF.useStore(
- useCallback((store) => (source ? store.nodeInternals.get(source) : undefined), [source])
- );
- const targetNode = RF.useStore(
- useCallback((store) => (target ? store.nodeInternals.get(target) : undefined), [target])
- );
+ const snapGrid = useDmnEditorStore((s) => s.diagram.snapGrid);
+
+ const sourceNodeX = RF.useStore((s) => (source ? s.nodeInternals.get(source)?.positionAbsolute?.x : undefined));
+ const sourceNodeY = RF.useStore((s) => (source ? s.nodeInternals.get(source)?.positionAbsolute?.y : undefined));
+ const sourceNodeWidth = RF.useStore((s) => (source ? s.nodeInternals.get(source)?.width : undefined));
+ const sourceNodeHeight = RF.useStore((s) => (source ? s.nodeInternals.get(source)?.height : undefined));
+
+ const targetNodeX = RF.useStore((s) => (target ? s.nodeInternals.get(target)?.positionAbsolute?.x : undefined));
+ const targetNodeY = RF.useStore((s) => (target ? s.nodeInternals.get(target)?.positionAbsolute?.y : undefined));
+ const targetNodeWidth = RF.useStore((s) => (target ? s.nodeInternals.get(target)?.width : undefined));
+ const targetNodeHeight = RF.useStore((s) => (target ? s.nodeInternals.get(target)?.height : undefined));
+
const dmnEdge = data?.dmnEdge;
const dmnShapeSource = data?.dmnShapeSource;
const dmnShapeTarget = data?.dmnShapeTarget;
@@ -42,13 +47,26 @@ export function useKieEdgePath(
return useMemo(
() =>
getSnappedMultiPointAnchoredEdgePath({
- snapGrid: diagram.snapGrid,
+ snapGrid,
dmnEdge,
- sourceNode,
- targetNode,
+ sourceNodeBounds: { x: sourceNodeX, y: sourceNodeY, width: sourceNodeWidth, height: sourceNodeHeight },
+ targetNodeBounds: { x: targetNodeX, y: targetNodeY, width: targetNodeWidth, height: targetNodeHeight },
dmnShapeSource,
dmnShapeTarget,
}),
- [diagram.snapGrid, dmnEdge, dmnShapeSource, dmnShapeTarget, sourceNode, targetNode]
+ [
+ dmnEdge,
+ dmnShapeSource,
+ dmnShapeTarget,
+ snapGrid,
+ sourceNodeHeight,
+ sourceNodeWidth,
+ sourceNodeX,
+ sourceNodeY,
+ targetNodeHeight,
+ targetNodeWidth,
+ targetNodeX,
+ targetNodeY,
+ ]
);
}
diff --git a/packages/dmn-editor/src/diagram/edges/usePotentialWaypointControls.ts b/packages/dmn-editor/src/diagram/edges/usePotentialWaypointControls.ts
index 6e77aa97007..4106ae35051 100644
--- a/packages/dmn-editor/src/diagram/edges/usePotentialWaypointControls.ts
+++ b/packages/dmn-editor/src/diagram/edges/usePotentialWaypointControls.ts
@@ -20,7 +20,7 @@
import * as RF from "reactflow";
import { useState, useCallback, useMemo } from "react";
import { addEdgeWaypoint } from "../../mutations/addEdgeWaypoint";
-import { useDmnEditorStore, useDmnEditorStoreApi } from "../../store/Store";
+import { useDmnEditorStore, useDmnEditorStoreApi } from "../../store/StoreContext";
import { snapPoint } from "../SnapGrid";
import { DC__Point } from "@kie-tools/dmn-marshaller/dist/schemas/dmn-1_5/ts-gen/types";
import { DmnDiagramNodeData } from "../nodes/Nodes";
@@ -33,7 +33,9 @@ export function usePotentialWaypointControls(
edgeIndex: number | undefined,
interactionPathRef: React.RefObject<SVGPathElement>
) {
- const diagram = useDmnEditorStore((s) => s.diagram);
+ const snapGrid = useDmnEditorStore((s) => s.diagram.snapGrid);
+ const drdIndex = useDmnEditorStore((s) => s.diagram.drdIndex);
+ const isDraggingWaypoint = useDmnEditorStore((s) => !!s.diagram.draggingWaypoints.find((e) => e === edgeId));
const dmnEditorStoreApi = useDmnEditorStoreApi();
const reactFlowInstance = RF.useReactFlow<DmnDiagramNodeData, DmnDiagramEdgeData>();
@@ -65,11 +67,11 @@ export function usePotentialWaypointControls(
return undefined;
}
- return snapPoint(diagram.snapGrid, {
+ return snapPoint(snapGrid, {
"@_x": potentialWaypoint.point.x,
"@_y": potentialWaypoint.point.y,
});
- }, [diagram.snapGrid, potentialWaypoint]);
+ }, [snapGrid, potentialWaypoint]);
const onDoubleClick = useCallback(() => {
if (!potentialWaypoint || !snappedPotentialWaypoint || edgeIndex === undefined) {
@@ -96,14 +98,14 @@ export function usePotentialWaypointControls(
dmnEditorStoreApi.setState((state) => {
addEdgeWaypoint({
definitions: state.dmn.model.definitions,
- drdIndex: diagram.drdIndex,
+ drdIndex,
beforeIndex: i - 1,
edgeIndex,
waypoint: snappedPotentialWaypoint,
});
});
}, [
- diagram.drdIndex,
+ drdIndex,
dmnEditorStoreApi,
edgeIndex,
isExistingWaypoint,
@@ -112,8 +114,6 @@ export function usePotentialWaypointControls(
waypoints,
]);
- const isDraggingWaypoint = !!diagram.draggingWaypoints.find((e) => e === edgeId);
-
const shouldReturnPotentialWaypoint =
isEdgeSelected &&
!isDraggingWaypoint &&
diff --git a/packages/dmn-editor/src/diagram/graph/graph.ts b/packages/dmn-editor/src/diagram/graph/graph.ts
index 0a7976c3d2d..eff1914b59a 100644
--- a/packages/dmn-editor/src/diagram/graph/graph.ts
+++ b/packages/dmn-editor/src/diagram/graph/graph.ts
@@ -17,29 +17,42 @@
* under the License.
*/
-import * as RF from "reactflow";
-import { DmnDiagramEdgeData } from "../edges/Edges";
+import { DMN15__tDefinitions } from "@kie-tools/dmn-marshaller/dist/schemas/dmn-1_5/ts-gen/types";
+import { Unpacked } from "../../tsExt/tsExt";
export type AdjMatrix = Record<
string,
- undefined | Record<string, undefined | { direction: HierarchyDirection; edge: RF.Edge }>
+ undefined | Record<string, undefined | { direction: HierarchyDirection; edge: DrgEdge }>
>;
export type HierarchyDirection = "up" | "down";
-export function getAdjMatrix(edges: RF.Edge<any>[]): AdjMatrix {
+export type DrgEdge = {
+ sourceId: string;
+ targetId: string;
+ id: string;
+ dmnObject: {
+ namespace: string;
+ id: string;
+ type: Unpacked<DMN15__tDefinitions["artifact" | "drgElement"]>["__$$element"];
+ requirementType: "informationRequirement" | "knowledgeRequirement" | "authorityRequirement" | "association";
+ index: number;
+ };
+};
+
+export function getAdjMatrix(edges: DrgEdge[]): AdjMatrix {
const __adjMatrix: AdjMatrix = {};
for (const e of edges) {
- __adjMatrix[e.source] ??= {};
- __adjMatrix[e.target] ??= {};
- __adjMatrix[e.source]![e.target] = { direction: "up", edge: e };
- __adjMatrix[e.target]![e.source] = { direction: "down", edge: e };
+ __adjMatrix[e.sourceId] ??= {};
+ __adjMatrix[e.targetId] ??= {};
+ __adjMatrix[e.sourceId]![e.targetId] = { direction: "up", edge: e };
+ __adjMatrix[e.targetId]![e.sourceId] = { direction: "down", edge: e };
}
return __adjMatrix;
}
export type NodeVisitor = (nodeId: string, traversalDirection: HierarchyDirection) => void;
-export type EdgeVisitor = (edge: RF.Edge<DmnDiagramEdgeData>, traversalDirection: HierarchyDirection) => void;
+export type EdgeVisitor = (edge: DrgEdge, traversalDirection: HierarchyDirection) => void;
export function traverse(
__adjMatrix: AdjMatrix,
@@ -74,7 +87,7 @@ export function traverse(
visited.add(curNodeId);
// Only paint edges if at least one of the endpoints is not selected.
- if (!(originalStartingNodeIds.has(edge.source) && originalStartingNodeIds.has(edge.target))) {
+ if (!(originalStartingNodeIds.has(edge.sourceId) && originalStartingNodeIds.has(edge.targetId))) {
edgeVisitor?.(edge, traversalDirection);
}
@@ -85,13 +98,7 @@ export function traverse(
traverse(__adjMatrix, originalStartingNodeIds, nextNodeIds, traversalDirection, nodeVisitor, edgeVisitor, visited);
}
-export function buildHierarchy({
- nodeId,
- edges,
-}: {
- nodeId: string | undefined | null;
- edges: RF.Edge<DmnDiagramEdgeData>[];
-}) {
+export function buildHierarchy({ nodeId, edges }: { nodeId: string | undefined | null; edges: DrgEdge[] }) {
if (!nodeId) {
return { dependencies: new Set<string>(), dependents: new Set<string>() };
}
diff --git a/packages/dmn-editor/src/diagram/maths/DmnMaths.ts b/packages/dmn-editor/src/diagram/maths/DmnMaths.ts
index 5e965c0a7db..39846e985a2 100644
--- a/packages/dmn-editor/src/diagram/maths/DmnMaths.ts
+++ b/packages/dmn-editor/src/diagram/maths/DmnMaths.ts
@@ -24,15 +24,13 @@ import {
DC__Point,
DMNDI15__DMNShape,
} from "@kie-tools/dmn-marshaller/dist/schemas/dmn-1_5/ts-gen/types";
-import * as RF from "reactflow";
+import { SnapGrid } from "../../store/Store";
+import { snapBoundsDimensions, snapBoundsPosition } from "../SnapGrid";
import { PositionalNodeHandleId } from "../connections/PositionalNodeHandles";
import { AutoPositionedEdgeMarker } from "../edges/AutoPositionedEdgeMarker";
import { NODE_TYPES } from "../nodes/NodeTypes";
import { NodeDmnObjects } from "../nodes/Nodes";
import { getCenter } from "./Maths";
-import { SnapGrid } from "../../store/Store";
-import { snapBoundsDimensions, snapBoundsPosition, snapShapePosition } from "../SnapGrid";
-import { MIN_NODE_SIZES } from "../nodes/DefaultSizes";
export const DEFAULT_INTRACTION_WIDTH = 40;
export const CONTAINER_NODES_DESIRABLE_PADDING = 60;
@@ -41,12 +39,7 @@ export function getDistance(a: DC__Point, b: DC__Point) {
return Math.sqrt(Math.pow(a["@_x"] - b["@_x"], 2) + Math.pow(a["@_y"] - b["@_y"], 2));
}
-export function getNodeCenterPoint(node: RF.Node | undefined): DC__Point {
- const { x, y } = getCenter(node?.positionAbsolute?.x, node?.positionAbsolute?.y, node?.width, node?.height);
- return { "@_x": x, "@_y": y };
-}
-
-export function getBoundsCenterPoint(bounds: DC__Bounds): DC__Point {
+export function getDmnBoundsCenterPoint(bounds: DC__Bounds): DC__Point {
const { x, y } = getCenter(bounds["@_x"], bounds["@_y"], bounds["@_width"], bounds["@_height"]);
return { "@_x": x, "@_y": y };
}
@@ -59,7 +52,7 @@ export function getPointForHandle({
handle: PositionalNodeHandleId;
}): DC__Point {
if (handle === PositionalNodeHandleId.Center) {
- return getBoundsCenterPoint(bounds);
+ return getDmnBoundsCenterPoint(bounds);
} else if (handle === PositionalNodeHandleId.Top) {
return { "@_x": bounds["@_x"] + bounds["@_width"] / 2, "@_y": bounds["@_y"] };
} else if (handle === PositionalNodeHandleId.Right) {
@@ -209,7 +202,7 @@ export function getContainmentRelationship({
const { x: bx, y: by } = snapBoundsPosition(snapGrid, bounds);
const { width: bw, height: bh } = snapBoundsDimensions(snapGrid, bounds, boundsMinSizes(snapGrid));
- const center = getBoundsCenterPoint({
+ const center = getDmnBoundsCenterPoint({
"@_height": bh,
"@_width": bw,
"@_x": bx,
diff --git a/packages/dmn-editor/src/diagram/maths/Maths.ts b/packages/dmn-editor/src/diagram/maths/Maths.ts
index 11120d012fe..10e608608d0 100644
--- a/packages/dmn-editor/src/diagram/maths/Maths.ts
+++ b/packages/dmn-editor/src/diagram/maths/Maths.ts
@@ -17,39 +17,24 @@
* under the License.
*/
+import { DC__Point } from "@kie-tools/dmn-marshaller/dist/schemas/dmn-1_5/ts-gen/types";
import * as RF from "reactflow";
-export type Shape = {
+export type Bounds = {
width: number | undefined | null;
height: number | undefined | null;
x: number | undefined | null;
y: number | undefined | null;
};
-// returns the parameters (sx, sy, tx, ty, sourcePos, targetPos) you need to create an edge
-export function getDiscretelyAutoPositionedEdgeParamsForRfNodes(source: RF.Node, target: RF.Node) {
- const src = {
- x: source.positionAbsolute?.x,
- y: source.positionAbsolute?.y,
- width: source.width,
- height: source.height,
- };
- const tgt = {
- x: target.positionAbsolute?.x,
- y: target.positionAbsolute?.y,
- width: target.width,
- height: target.height,
- };
- return getDiscretelyAutoPositionedEdgeParams(src, tgt);
-}
-
-export function getDiscretelyAutoPositionedEdgeParams(src: Shape, tgt: Shape) {
+export function getDiscretelyAutoPositionedEdgeParams(src: Bounds, tgt: Bounds) {
const [sx, sy, sourcePos] = getPositionalHandlePosition(src, tgt);
const [tx, ty, targetPos] = getPositionalHandlePosition(tgt, src);
return { sx, sy, tx, ty, sourcePos, targetPos };
}
+
// returns the position (top, right, bottom, or right) passed node compared to
-export function getPositionalHandlePosition(a: Shape, b: Shape) {
+export function getPositionalHandlePosition(a: Bounds, b: Bounds) {
const centerA = getCenter(a.x, a.y, a.width, a.height);
const centerB = getCenter(b.x, b.y, b.width, b.height);
@@ -101,7 +86,7 @@ export function scaleFromCenter(
};
}
-function getHandleCoordsByPosition(node: Shape, handlePosition: RF.Position) {
+function getHandleCoordsByPosition(node: Bounds, handlePosition: RF.Position) {
let handleX = 0;
let handleY = 0;
@@ -126,3 +111,7 @@ function getHandleCoordsByPosition(node: Shape, handlePosition: RF.Position) {
return [(node?.x ?? 0) + handleX, (node?.y ?? 0) + handleY];
}
+export function getBoundsCenterPoint(node: Bounds | undefined): DC__Point {
+ const { x, y } = getCenter(node?.x, node?.y, node?.width, node?.height);
+ return { "@_x": x, "@_y": y };
+}
diff --git a/packages/dmn-editor/src/focus/useFocusableElement.ts b/packages/dmn-editor/src/diagram/memoization/memoization.ts
similarity index 52%
copy from packages/dmn-editor/src/focus/useFocusableElement.ts
copy to packages/dmn-editor/src/diagram/memoization/memoization.ts
index d4b0823497b..1b94827254e 100644
--- a/packages/dmn-editor/src/focus/useFocusableElement.ts
+++ b/packages/dmn-editor/src/diagram/memoization/memoization.ts
@@ -17,32 +17,11 @@
* under the License.
*/
-import { useEffect } from "react";
-import { useDmnEditorStore, useDmnEditorStoreApi } from "../store/Store";
+import fastDeepEqual from "fast-deep-equal";
-export function useFocusableElement(
- ref: React.RefObject<HTMLInputElement>,
- id: string | undefined,
- before?: (cb: () => void) => void
-) {
- const focus = useDmnEditorStore((s) => s.focus);
- const dmnEditorStoreApi = useDmnEditorStoreApi();
-
- useEffect(() => {
- if (!id) {
- return;
- }
-
- const cb = () => {
- ref.current?.select();
-
- dmnEditorStoreApi.setState((state) => {
- state.focus.consumableId = undefined;
- });
- };
-
- if (focus.consumableId === id && ref.current) {
- before?.(cb) ?? cb();
- }
- }, [before, dmnEditorStoreApi, focus.consumableId, id, ref]);
-}
+/**
+ * This function is the reason why nodes and edges are memoized. Without it, the prev and next objects are compared as
+ * references, and they'll always be different, resulting in every node and edge re-rendering for every change. By
+ * comparing the values inside the objects, we avoid re-rendering unnecessarily.
+ */
+export const propsHaveSameValuesDeep = fastDeepEqual;
diff --git a/packages/dmn-editor/src/diagram/nodes/DataTypeNodePanel.tsx b/packages/dmn-editor/src/diagram/nodes/DataTypeNodePanel.tsx
index 428fbd43ed8..1ce71e28e05 100644
--- a/packages/dmn-editor/src/diagram/nodes/DataTypeNodePanel.tsx
+++ b/packages/dmn-editor/src/diagram/nodes/DataTypeNodePanel.tsx
@@ -23,7 +23,7 @@ import {
DMNDI15__DMNShape,
} from "@kie-tools/dmn-marshaller/dist/schemas/dmn-1_5/ts-gen/types";
import { DmnBuiltInDataType } from "@kie-tools/boxed-expression-component/dist/api";
-import { useDmnEditorStore } from "../../store/Store";
+import { useDmnEditorStore } from "../../store/StoreContext";
import { OnCreateDataType, OnTypeRefChange, TypeRefSelector } from "../../dataTypes/TypeRefSelector";
import { useDmnEditor } from "../../DmnEditorContext";
import { useResolvedTypeRef } from "../../dataTypes/useResolvedTypeRef";
@@ -38,20 +38,22 @@ export function DataTypeNodePanel(props: {
shape: DMNDI15__DMNShape | undefined;
onChange: OnTypeRefChange;
onCreate?: OnCreateDataType;
- namespace: string | undefined;
+ dmnObjectNamespace: string | undefined;
}) {
- const diagram = useDmnEditorStore((s) => s.diagram);
+ const enableDataTypesToolbarOnNodes = useDmnEditorStore((s) => s.diagram.overlays.enableDataTypesToolbarOnNodes);
const { dmnEditorRootElementRef } = useDmnEditor();
const resolvedTypeRef = useResolvedTypeRef(
props.variable?.["@_typeRef"] ?? DmnBuiltInDataType.Undefined,
- props.namespace
+ props.dmnObjectNamespace
);
+ const isExternalNode = !!props.dmnObjectNamespace;
+
return (
<>
- {props.isVisible && diagram.overlays.enableDataTypesToolbarOnNodes && (
+ {props.isVisible && enableDataTypesToolbarOnNodes && (
<div
className={"kie-dmn-editor--data-type-node-panel"}
// Do not allow any events to go to the node itself...
@@ -61,7 +63,7 @@ export function DataTypeNodePanel(props: {
onDoubleClick={stopPropagation}
onMouseLeave={stopPropagation}
>
- <div>
+ <div style={{ background: isExternalNode ? "rgb(240, 240, 240)" : undefined }}>
<TypeRefSelector
zoom={0.8}
heightRef={dmnEditorRootElementRef}
@@ -69,6 +71,7 @@ export function DataTypeNodePanel(props: {
onChange={props.onChange}
onCreate={props.onCreate}
menuAppendTo={"parent"}
+ isDisabled={isExternalNode}
/>
</div>
</div>
diff --git a/packages/dmn-editor/src/diagram/nodes/EditExpressionNodePanel.tsx b/packages/dmn-editor/src/diagram/nodes/EditExpressionNodePanel.tsx
index 7de7bc3cc69..1dc64147e0f 100644
--- a/packages/dmn-editor/src/diagram/nodes/EditExpressionNodePanel.tsx
+++ b/packages/dmn-editor/src/diagram/nodes/EditExpressionNodePanel.tsx
@@ -19,10 +19,9 @@
import * as React from "react";
import { Label } from "@patternfly/react-core/dist/js/components/Label";
-import { useDmnEditorStore, useDmnEditorStoreApi } from "../../store/Store";
+import { useDmnEditorStoreApi } from "../../store/StoreContext";
export function EditExpressionNodePanel(props: { isVisible: boolean; id: string }) {
- const dispatch = useDmnEditorStore((s) => s.dispatch);
const dmnEditorStoreApi = useDmnEditorStoreApi();
return (
@@ -31,7 +30,7 @@ export function EditExpressionNodePanel(props: { isVisible: boolean; id: string
<Label
onClick={() =>
dmnEditorStoreApi.setState((state) => {
- dispatch.boxedExpressionEditor.open(state, props.id);
+ state.dispatch(state).boxedExpressionEditor.open(props.id);
})
}
className={"kie-dmn-editor--edit-expression-node-panel"}
diff --git a/packages/dmn-editor/src/diagram/nodes/EditableNodeLabel.tsx b/packages/dmn-editor/src/diagram/nodes/EditableNodeLabel.tsx
index ffd5a9d4663..2503ce966f7 100644
--- a/packages/dmn-editor/src/diagram/nodes/EditableNodeLabel.tsx
+++ b/packages/dmn-editor/src/diagram/nodes/EditableNodeLabel.tsx
@@ -21,8 +21,7 @@ import * as React from "react";
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
import { EmptyLabel } from "./Nodes";
import { XmlQName } from "@kie-tools/xml-parser-ts/dist/qNames";
-import { useDmnEditorStore } from "../../store/Store";
-import { useDmnEditorDerivedStore } from "../../store/DerivedStore";
+import { useDmnEditorStore, useDmnEditorStoreApi } from "../../store/StoreContext";
import { UniqueNameIndex } from "../../Dmn15Spec";
import { buildFeelQNameFromXmlQName } from "../../feel/buildFeelQName";
import { DMN15__tNamedElement } from "@kie-tools/dmn-marshaller/dist/schemas/dmn-1_5/ts-gen/types";
@@ -30,10 +29,11 @@ import { Truncate } from "@patternfly/react-core/dist/js/components/Truncate";
import { DMN15_SPEC } from "../../Dmn15Spec";
import { invalidInlineFeelNameStyle } from "../../feel/InlineFeelNameInput";
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";
+import "./EditableNodeLabel.css";
+import { State } from "../../store/Store";
export type OnEditableNodeLabelChange = (value: string | undefined) => void;
@@ -50,8 +50,8 @@ export function EditableNodeLabel({
grow,
shouldCommitOnBlur,
skipValidation,
- allUniqueNames,
- fontCssProperties: fontStyle,
+ onGetAllUniqueNames,
+ fontCssProperties,
}: {
id?: string;
shouldCommitOnBlur?: boolean;
@@ -65,11 +65,28 @@ export function EditableNodeLabel({
setEditing: React.Dispatch<React.SetStateAction<boolean>>;
onChange: OnEditableNodeLabelChange;
skipValidation?: boolean;
- allUniqueNames: UniqueNameIndex;
+ onGetAllUniqueNames: (s: State) => UniqueNameIndex;
fontCssProperties?: React.CSSProperties;
}) {
- const thisDmn = useDmnEditorStore((s) => s.dmn);
- const { importsByNamespace } = useDmnEditorDerivedStore();
+ const displayValue = useDmnEditorStore((s) => {
+ if (!value) {
+ return undefined;
+ }
+
+ if (!namedElement || !namedElementQName) {
+ return value;
+ }
+
+ const feelName = buildFeelQNameFromXmlQName({
+ namedElement,
+ importsByNamespace: s.computed(s).importsByNamespace(),
+ model: s.dmn.model.definitions,
+ namedElementQName,
+ relativeToNamespace: s.dmn.model.definitions["@_namespace"],
+ });
+
+ return feelName.full;
+ });
const isEditing = useMemo(() => {
return !namedElementQName?.prefix && _isEditing; // Can't ever change the names of external nodes
@@ -87,26 +104,6 @@ export function EditableNodeLabel({
[_setEditing, namedElementQName?.prefix]
);
- const displayValue = useMemo(() => {
- if (!value) {
- return undefined;
- }
-
- if (!namedElement || !namedElementQName) {
- return value;
- }
-
- const feelName = buildFeelQNameFromXmlQName({
- namedElement,
- importsByNamespace,
- model: thisDmn.model.definitions,
- namedElementQName,
- relativeToNamespace: thisDmn.model.definitions["@_namespace"],
- });
-
- return feelName.full;
- }, [value, namedElement, namedElementQName, importsByNamespace, thisDmn.model.definitions]);
-
const [internalValue, setInternalValue] = useState(displayValue);
useEffect(() => {
// Give `value` priority over `internalValue`, if it changes externally, we take that as the new `internalValue`.
@@ -130,13 +127,17 @@ export function EditableNodeLabel({
}, 0);
}, []);
- const isValid = useMemo(() => {
+ const isValid = useDmnEditorStore((s) => {
if (skipValidation) {
return true;
}
- return DMN15_SPEC.namedElement.isValidName(namedElement?.["@_id"] ?? generateUuid(), internalValue, allUniqueNames);
- }, [skipValidation, namedElement, internalValue, allUniqueNames]);
+ return DMN15_SPEC.namedElement.isValidName(
+ namedElement?.["@_id"] ?? generateUuid(),
+ internalValue,
+ onGetAllUniqueNames(s)
+ );
+ });
const onBlur = useCallback(() => {
setEditing(false);
@@ -218,7 +219,7 @@ export function EditableNodeLabel({
<input
spellCheck={"false"} // Let's not confuse FEEL name validation with the browser's grammar check.
style={{
- ...fontStyle,
+ ...fontCssProperties,
...(isValid ? {} : invalidInlineFeelNameStyle),
}}
onMouseDownCapture={(e) => e.stopPropagation()} // Make sure mouse events stay inside the node.
@@ -233,7 +234,7 @@ export function EditableNodeLabel({
<span
style={{
whiteSpace: "pre-wrap",
- ...fontStyle,
+ ...fontCssProperties,
...(isValid ? {} : invalidInlineFeelNameStyle),
}}
>
@@ -251,8 +252,12 @@ export function EditableNodeLabel({
}
export function useEditableNodeLabel(id: string | undefined) {
- const focus = useDmnEditorStore((s) => s.focus);
- const [isEditingLabel, setEditingLabel] = useState(!!id && !!focus.consumableId && focus.consumableId === id);
+ const dmnEditorStoreApi = useDmnEditorStoreApi();
+
+ const [isEditingLabel, setEditingLabel] = useState(
+ !!id && !!dmnEditorStoreApi.getState().focus.consumableId && dmnEditorStoreApi.getState().focus.consumableId === id
+ );
+
const triggerEditing = useCallback<React.EventHandler<React.SyntheticEvent>>((e) => {
e.stopPropagation();
e.preventDefault();
@@ -269,5 +274,8 @@ export function useEditableNodeLabel(id: string | undefined) {
[triggerEditing]
);
- return { isEditingLabel, setEditingLabel, triggerEditing, triggerEditingIfEnter };
+ return useMemo(
+ () => ({ isEditingLabel, setEditingLabel, triggerEditing, triggerEditingIfEnter }),
+ [isEditingLabel, triggerEditing, triggerEditingIfEnter]
+ );
}
diff --git a/packages/dmn-editor/src/diagram/nodes/InfoNodePanel.tsx b/packages/dmn-editor/src/diagram/nodes/InfoNodePanel.tsx
index b857675261f..47e3c0bc22f 100644
--- a/packages/dmn-editor/src/diagram/nodes/InfoNodePanel.tsx
+++ b/packages/dmn-editor/src/diagram/nodes/InfoNodePanel.tsx
@@ -20,7 +20,7 @@
import * as React from "react";
import { Label } from "@patternfly/react-core/dist/js/components/Label";
import { InfoIcon } from "@patternfly/react-icons/dist/js/icons/info-icon";
-import { useDmnEditorStoreApi } from "../../store/Store";
+import { useDmnEditorStoreApi } from "../../store/StoreContext";
export function InfoNodePanel(props: { isVisible: boolean }) {
const dmnEditorStoreApi = useDmnEditorStoreApi();
diff --git a/packages/dmn-editor/src/diagram/nodes/NodeSvgs.tsx b/packages/dmn-editor/src/diagram/nodes/NodeSvgs.tsx
index 29e5baaf1b8..5fbc941da67 100644
--- a/packages/dmn-editor/src/diagram/nodes/NodeSvgs.tsx
+++ b/packages/dmn-editor/src/diagram/nodes/NodeSvgs.tsx
@@ -229,7 +229,7 @@ export const DecisionServiceNodeSvg = React.forwardRef<
width={width}
height={height}
strokeWidth={strokeWidth}
- fill={isCollapsed ? DEFAULT_NODE_FILL : fillColor ?? "transparent"}
+ fill={fillColor ?? "transparent"}
stroke={strokeColor ?? DEFAULT_NODE_STROKE_COLOR}
strokeLinejoin={"round"}
rx={"40"}
@@ -360,7 +360,7 @@ export const GroupNodeSvg = React.forwardRef<SVGRectElement, NodeSvgProps & { st
}
);
-export const UnknownNodeSvg = (_props: NodeSvgProps & { strokeDasharray?: string }) => {
+export function UnknownNodeSvg(_props: NodeSvgProps & { strokeDasharray?: string }) {
const { strokeWidth, x, y, width, height, props } = normalize(_props);
const strokeDasharray = props.strokeDasharray ?? "2,4";
return (
@@ -379,7 +379,7 @@ export const UnknownNodeSvg = (_props: NodeSvgProps & { strokeDasharray?: string
/>
</>
);
-};
+}
function NodeCollectionMarker(__props: NodeSvgProps & { anchor: "top" | "bottom" }) {
const { strokeWidth, x, y, width, height, fillColor, strokeColor, props } = normalize(__props);
diff --git a/packages/dmn-editor/src/diagram/nodes/Nodes.tsx b/packages/dmn-editor/src/diagram/nodes/Nodes.tsx
index 1082f3aa905..09174032ce6 100644
--- a/packages/dmn-editor/src/diagram/nodes/Nodes.tsx
+++ b/packages/dmn-editor/src/diagram/nodes/Nodes.tsx
@@ -17,6 +17,7 @@
* under the License.
*/
+import { DmnBuiltInDataType } from "@kie-tools/boxed-expression-component/dist/api";
import {
DMN15__tBusinessKnowledgeModel,
DMN15__tDecision,
@@ -28,20 +29,32 @@ import {
DMN15__tTextAnnotation,
DMNDI15__DMNShape,
} from "@kie-tools/dmn-marshaller/dist/schemas/dmn-1_5/ts-gen/types";
+import { XmlQName } from "@kie-tools/xml-parser-ts/dist/qNames";
+import { drag } from "d3-drag";
+import { select } from "d3-selection";
import * as React from "react";
import { useCallback, useEffect, useMemo, useRef } from "react";
import * as RF from "reactflow";
+import { OnCreateDataType, OnTypeRefChange } from "../../dataTypes/TypeRefSelector";
+import { addTopLevelItemDefinition } from "../../mutations/addTopLevelItemDefinition";
import { renameDrgElement, renameGroupNode, updateTextAnnotation } from "../../mutations/renameNode";
-import { DmnEditorTab, DropTargetNode, SnapGrid, useDmnEditorStore, useDmnEditorStoreApi } from "../../store/Store";
+import { updateDecisionServiceDividerLine } from "../../mutations/updateDecisionServiceDividerLine";
+import { DmnEditorTab, SnapGrid, State } from "../../store/Store";
+import { useDmnEditorStore, useDmnEditorStoreApi } from "../../store/StoreContext";
+import { Unpacked } from "../../tsExt/tsExt";
import { snapShapeDimensions } from "../SnapGrid";
-import { DECISION_SERVICE_COLLAPSED_DIMENSIONS } from "./DefaultSizes";
import { PositionalNodeHandles } from "../connections/PositionalNodeHandles";
import { NodeType, containment, outgoingStructure } from "../connections/graphStructure";
import { EDGE_TYPES } from "../edges/EdgeTypes";
+import { DmnDiagramEdgeData } from "../edges/Edges";
+import { getContainmentRelationship, getDecisionServiceDividerLineLocalY } from "../maths/DmnMaths";
+import { useIsHovered } from "../useIsHovered";
import { DataTypeNodePanel } from "./DataTypeNodePanel";
+import { DECISION_SERVICE_COLLAPSED_DIMENSIONS, MIN_NODE_SIZES } from "./DefaultSizes";
import { EditExpressionNodePanel } from "./EditExpressionNodePanel";
import { EditableNodeLabel, OnEditableNodeLabelChange, useEditableNodeLabel } from "./EditableNodeLabel";
import { InfoNodePanel } from "./InfoNodePanel";
+import { getNodeLabelPosition, useNodeStyle } from "./NodeStyle";
import {
BkmNodeSvg,
DecisionNodeSvg,
@@ -54,20 +67,8 @@ import {
} from "./NodeSvgs";
import { NODE_TYPES } from "./NodeTypes";
import { OutgoingStuffNodePanel } from "./OutgoingStuffNodePanel";
-import { useIsHovered } from "../useIsHovered";
-import { getContainmentRelationship, getDecisionServiceDividerLineLocalY } from "../maths/DmnMaths";
-import { useDmnEditorDerivedStore } from "../../store/DerivedStore";
-import { DmnDiagramEdgeData } from "../edges/Edges";
-import { XmlQName } from "@kie-tools/xml-parser-ts/dist/qNames";
-import { Unpacked } from "../../tsExt/tsExt";
-import { OnCreateDataType, OnTypeRefChange } from "../../dataTypes/TypeRefSelector";
-import { MIN_NODE_SIZES } from "./DefaultSizes";
-import { select } from "d3-selection";
-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 { getNodeLabelPosition, useNodeStyle } from "./NodeStyle";
+import { propsHaveSameValuesDeep } from "../memoization/memoization";
+import { useExternalModels } from "../../includedModels/DmnEditorDependenciesContext";
export type ElementFilter<E extends { __$$element: string }, Filter extends string> = E extends any
? E["__$$element"] extends Filter
@@ -103,23 +104,28 @@ export const InputDataNode = React.memo(
type,
id,
}: RF.NodeProps<DmnDiagramNodeData<DMN15__tInputData & { __$$element: "inputData" }>>) => {
+ const renderCount = useRef<number>(0);
+ renderCount.current++;
+
const ref = useRef<HTMLDivElement>(null);
const isExternal = !!dmnObjectQName.prefix;
- const isResizing = useNodeResizing(id);
-
- const diagram = useDmnEditorStore((s) => s.diagram);
- const isHovered = (useIsHovered(ref) || isResizing) && diagram.draggingNodes.length === 0;
- const { isEditingLabel, setEditingLabel, triggerEditing, triggerEditingIfEnter } = useEditableNodeLabel(
- inputData["@_id"]
+ const snapGrid = useDmnEditorStore((s) => s.diagram.snapGrid);
+ const enableCustomNodeStyles = useDmnEditorStore((s) => s.diagram.overlays.enableCustomNodeStyles);
+ const isHovered = useIsHovered(ref);
+ const isResizing = useNodeResizing(id);
+ const shouldActLikeHovered = useDmnEditorStore(
+ (s) => (isHovered || isResizing) && s.diagram.draggingNodes.length === 0
);
- useHoveredNodeAlwaysOnTop(ref, zIndex, isHovered, dragging, selected, isEditingLabel);
+
+ const { isEditingLabel, setEditingLabel, triggerEditing, triggerEditingIfEnter } = useEditableNodeLabel(id);
+ useHoveredNodeAlwaysOnTop(ref, zIndex, shouldActLikeHovered, dragging, selected, isEditingLabel);
const dmnEditorStoreApi = useDmnEditorStoreApi();
- const { isTargeted, isValidConnectionTarget, isConnecting } = useConnectionTargetStatus(id, isHovered);
- const className = useNodeClassName(diagram.dropTargetNode, isConnecting, isValidConnectionTarget, id);
- const nodeDimensions = useNodeDimensions(type as NodeType, diagram.snapGrid, id, shape, isExternal);
+ const { isTargeted, isValidConnectionTarget } = useConnectionTargetStatus(id, shouldActLikeHovered);
+ const className = useNodeClassName(isValidConnectionTarget, id);
+ const nodeDimensions = useNodeDimensions(type as NodeType, snapGrid, shape, isExternal);
const setName = useCallback<OnEditableNodeLabelChange>(
(newName: string) => {
@@ -141,26 +147,34 @@ export const InputDataNode = React.memo(
[dmnEditorStoreApi, index, inputData]
);
- const { allFeelVariableUniqueNames, allTopLevelItemDefinitionUniqueNames, allDataTypesById } =
- useDmnEditorDerivedStore();
+ const getAllFeelVariableUniqueNames = useCallback((s: State) => s.computed(s).getAllFeelVariableUniqueNames(), []);
+
const onCreateDataType = useDataTypeCreationCallbackForNodes(index, inputData["@_name"]);
const { fontCssProperties, shapeStyle } = useNodeStyle({
dmnStyle: shape["di:Style"],
nodeType: type as NodeType,
- isEnabled: diagram.overlays.enableStyles,
+ isEnabled: enableCustomNodeStyles,
});
- const isCollection = useMemo(() => {
+ const additionalClasses = `${className} ${dmnObjectQName.prefix ? "external" : ""}`;
+
+ const { externalModelsByNamespace } = useExternalModels();
+
+ const isCollection = useDmnEditorStore((s) => {
+ const { allDataTypesById, allTopLevelItemDefinitionUniqueNames } = s
+ .computed(s)
+ .getDataTypes(externalModelsByNamespace);
+
return (
allDataTypesById.get(allTopLevelItemDefinitionUniqueNames.get(inputData.variable?.["@_typeRef"] ?? "") ?? "")
?.itemDefinition?.["@_isCollection"] ?? false
);
- }, [allDataTypesById, allTopLevelItemDefinitionUniqueNames, inputData.variable]);
+ });
return (
<>
- <svg className={`kie-dmn-editor--node-shape ${className} ${dmnObjectQName.prefix ? "external" : ""}`}>
+ <svg className={`kie-dmn-editor--node-shape ${additionalClasses}`}>
<InputDataNodeSvg
isCollection={isCollection}
{...nodeDimensions}
@@ -174,17 +188,17 @@ export const InputDataNode = React.memo(
<PositionalNodeHandles isTargeted={isTargeted && isValidConnectionTarget} nodeId={id} />
<div
ref={ref}
- className={`kie-dmn-editor--node kie-dmn-editor--input-data-node ${className} ${
- dmnObjectQName.prefix ? "external" : ""
- }`}
+ className={`kie-dmn-editor--node kie-dmn-editor--input-data-node ${additionalClasses}`}
tabIndex={-1}
onDoubleClick={triggerEditing}
onKeyDown={triggerEditingIfEnter}
>
- <InfoNodePanel isVisible={!isTargeted && isHovered} />
+ {/* {`render count: ${renderCount.current}`}
+ <br /> */}
+ <InfoNodePanel isVisible={!isTargeted && shouldActLikeHovered} />
<OutgoingStuffNodePanel
- isVisible={!isConnecting && !isTargeted && isHovered}
+ isVisible={!isTargeted && shouldActLikeHovered}
nodeTypes={outgoingStructure[NODE_TYPES.inputData].nodes}
edgeTypes={outgoingStructure[NODE_TYPES.inputData].edges}
/>
@@ -196,22 +210,22 @@ export const InputDataNode = React.memo(
position={getNodeLabelPosition(type as NodeType)}
value={inputData["@_label"] ?? inputData["@_name"]}
onChange={setName}
- allUniqueNames={allFeelVariableUniqueNames}
+ onGetAllUniqueNames={getAllFeelVariableUniqueNames}
shouldCommitOnBlur={true}
fontCssProperties={fontCssProperties}
/>
- {isHovered && (
+ {shouldActLikeHovered && (
<NodeResizerHandle
nodeType={type as NodeType}
- snapGrid={diagram.snapGrid}
+ snapGrid={snapGrid}
nodeId={id}
nodeShapeIndex={shape.index}
/>
)}
<DataTypeNodePanel
- isVisible={!isTargeted && isHovered}
+ isVisible={!isTargeted && shouldActLikeHovered}
variable={inputData.variable}
- namespace={dmnObjectNamespace}
+ dmnObjectNamespace={dmnObjectNamespace}
shape={shape}
onCreate={onCreateDataType}
onChange={onTypeRefChange}
@@ -219,7 +233,8 @@ export const InputDataNode = React.memo(
</div>
</>
);
- }
+ },
+ propsHaveSameValuesDeep
);
export const DecisionNode = React.memo(
@@ -231,23 +246,29 @@ export const DecisionNode = React.memo(
type,
id,
}: RF.NodeProps<DmnDiagramNodeData<DMN15__tDecision & { __$$element: "decision" }>>) => {
+ const renderCount = useRef<number>(0);
+ renderCount.current++;
+
const ref = useRef<HTMLDivElement>(null);
const isExternal = !!dmnObjectQName.prefix;
- const isResizing = useNodeResizing(id);
-
- const diagram = useDmnEditorStore((s) => s.diagram);
- const isHovered = (useIsHovered(ref) || isResizing) && diagram.draggingNodes.length === 0;
+ const snapGrid = useDmnEditorStore((s) => s.diagram.snapGrid);
+ const enableCustomNodeStyles = useDmnEditorStore((s) => s.diagram.overlays.enableCustomNodeStyles);
+ const isHovered = useIsHovered(ref);
+ const isResizing = useNodeResizing(id);
+ const shouldActLikeHovered = useDmnEditorStore(
+ (s) => (isHovered || isResizing) && s.diagram.draggingNodes.length === 0
+ );
const { isEditingLabel, setEditingLabel, triggerEditing, triggerEditingIfEnter } = useEditableNodeLabel(
decision["@_id"]
);
- useHoveredNodeAlwaysOnTop(ref, zIndex, isHovered, dragging, selected, isEditingLabel);
+ useHoveredNodeAlwaysOnTop(ref, zIndex, shouldActLikeHovered, dragging, selected, isEditingLabel);
const dmnEditorStoreApi = useDmnEditorStoreApi();
- const { isTargeted, isValidConnectionTarget, isConnecting } = useConnectionTargetStatus(id, isHovered);
- const className = useNodeClassName(diagram.dropTargetNode, isConnecting, isValidConnectionTarget, id);
- const nodeDimensions = useNodeDimensions(type as NodeType, diagram.snapGrid, id, shape, isExternal);
+ const { isTargeted, isValidConnectionTarget } = useConnectionTargetStatus(id, shouldActLikeHovered);
+ const className = useNodeClassName(isValidConnectionTarget, id);
+ const nodeDimensions = useNodeDimensions(type as NodeType, snapGrid, shape, isExternal);
const setName = useCallback<OnEditableNodeLabelChange>(
(newName: string) => {
dmnEditorStoreApi.setState((state) => {
@@ -268,27 +289,34 @@ export const DecisionNode = React.memo(
[decision, dmnEditorStoreApi, index]
);
- const { allFeelVariableUniqueNames, allTopLevelItemDefinitionUniqueNames, allDataTypesById } =
- useDmnEditorDerivedStore();
+ const getAllFeelVariableUniqueNames = useCallback((s: State) => s.computed(s).getAllFeelVariableUniqueNames(), []);
const onCreateDataType = useDataTypeCreationCallbackForNodes(index, decision["@_name"]);
const { fontCssProperties, shapeStyle } = useNodeStyle({
dmnStyle: shape["di:Style"],
nodeType: type as NodeType,
- isEnabled: diagram.overlays.enableStyles,
+ isEnabled: enableCustomNodeStyles,
});
- const isCollection = useMemo(() => {
+ const additionalClasses = `${className} ${dmnObjectQName.prefix ? "external" : ""}`;
+
+ const { externalModelsByNamespace } = useExternalModels();
+
+ const isCollection = useDmnEditorStore((s) => {
+ const { allDataTypesById, allTopLevelItemDefinitionUniqueNames } = s
+ .computed(s)
+ .getDataTypes(externalModelsByNamespace);
+
return (
allDataTypesById.get(allTopLevelItemDefinitionUniqueNames.get(decision.variable?.["@_typeRef"] ?? "") ?? "")
?.itemDefinition?.["@_isCollection"] ?? false
);
- }, [allDataTypesById, allTopLevelItemDefinitionUniqueNames, decision.variable]);
+ });
return (
<>
- <svg className={`kie-dmn-editor--node-shape ${className} ${dmnObjectQName.prefix ? "external" : ""}`}>
+ <svg className={`kie-dmn-editor--node-shape ${additionalClasses}`}>
<DecisionNodeSvg
isCollection={isCollection}
{...nodeDimensions}
@@ -304,18 +332,19 @@ export const DecisionNode = React.memo(
<div
ref={ref}
- className={`kie-dmn-editor--node kie-dmn-editor--decision-node ${className} ${
- dmnObjectQName.prefix ? "external" : ""
- }`}
+ className={`kie-dmn-editor--node kie-dmn-editor--decision-node ${additionalClasses}`}
tabIndex={-1}
onDoubleClick={triggerEditing}
onKeyDown={triggerEditingIfEnter}
>
- <InfoNodePanel isVisible={!isTargeted && isHovered} />
-
- {!isExternal && <EditExpressionNodePanel isVisible={!isTargeted && isHovered} id={decision["@_id"]!} />}
+ {/* {`render count: ${renderCount.current}`}
+ <br /> */}
+ <InfoNodePanel isVisible={!isTargeted && shouldActLikeHovered} />
+ {!isExternal && (
+ <EditExpressionNodePanel isVisible={!isTargeted && shouldActLikeHovered} id={decision["@_id"]!} />
+ )}
<OutgoingStuffNodePanel
- isVisible={!isConnecting && !isTargeted && isHovered}
+ isVisible={!isTargeted && shouldActLikeHovered}
nodeTypes={outgoingStructure[NODE_TYPES.decision].nodes}
edgeTypes={outgoingStructure[NODE_TYPES.decision].edges}
/>
@@ -327,22 +356,22 @@ export const DecisionNode = React.memo(
position={getNodeLabelPosition(type as NodeType)}
value={decision["@_label"] ?? decision["@_name"]}
onChange={setName}
- allUniqueNames={allFeelVariableUniqueNames}
+ onGetAllUniqueNames={getAllFeelVariableUniqueNames}
shouldCommitOnBlur={true}
fontCssProperties={fontCssProperties}
/>
- {isHovered && (
+ {shouldActLikeHovered && (
<NodeResizerHandle
nodeType={type as NodeType}
- snapGrid={diagram.snapGrid}
+ snapGrid={snapGrid}
nodeId={id}
nodeShapeIndex={shape.index}
/>
)}
<DataTypeNodePanel
- isVisible={!isTargeted && isHovered}
+ isVisible={!isTargeted && shouldActLikeHovered}
variable={decision.variable}
- namespace={dmnObjectNamespace}
+ dmnObjectNamespace={dmnObjectNamespace}
shape={shape}
onChange={onTypeRefChange}
onCreate={onCreateDataType}
@@ -350,7 +379,8 @@ export const DecisionNode = React.memo(
</div>
</>
);
- }
+ },
+ propsHaveSameValuesDeep
);
export const BkmNode = React.memo(
@@ -362,23 +392,29 @@ export const BkmNode = React.memo(
type,
id,
}: RF.NodeProps<DmnDiagramNodeData<DMN15__tBusinessKnowledgeModel & { __$$element: "businessKnowledgeModel" }>>) => {
+ const renderCount = useRef<number>(0);
+ renderCount.current++;
+
const ref = useRef<HTMLDivElement>(null);
const isExternal = !!dmnObjectQName.prefix;
- const isResizing = useNodeResizing(id);
-
- const diagram = useDmnEditorStore((s) => s.diagram);
- const isHovered = (useIsHovered(ref) || isResizing) && diagram.draggingNodes.length === 0;
+ const snapGrid = useDmnEditorStore((s) => s.diagram.snapGrid);
+ const enableCustomNodeStyles = useDmnEditorStore((s) => s.diagram.overlays.enableCustomNodeStyles);
+ const isHovered = useIsHovered(ref);
+ const isResizing = useNodeResizing(id);
+ const shouldActLikeHovered = useDmnEditorStore(
+ (s) => (isHovered || isResizing) && s.diagram.draggingNodes.length === 0
+ );
const { isEditingLabel, setEditingLabel, triggerEditing, triggerEditingIfEnter } = useEditableNodeLabel(
bkm["@_id"]
);
- useHoveredNodeAlwaysOnTop(ref, zIndex, isHovered, dragging, selected, isEditingLabel);
+ useHoveredNodeAlwaysOnTop(ref, zIndex, shouldActLikeHovered, dragging, selected, isEditingLabel);
const dmnEditorStoreApi = useDmnEditorStoreApi();
- const { isTargeted, isValidConnectionTarget, isConnecting } = useConnectionTargetStatus(id, isHovered);
- const className = useNodeClassName(diagram.dropTargetNode, isConnecting, isValidConnectionTarget, id);
- const nodeDimensions = useNodeDimensions(type as NodeType, diagram.snapGrid, id, shape, isExternal);
+ const { isTargeted, isValidConnectionTarget } = useConnectionTargetStatus(id, shouldActLikeHovered);
+ const className = useNodeClassName(isValidConnectionTarget, id);
+ const nodeDimensions = useNodeDimensions(type as NodeType, snapGrid, shape, isExternal);
const setName = useCallback<OnEditableNodeLabelChange>(
(newName: string) => {
dmnEditorStoreApi.setState((state) => {
@@ -399,19 +435,21 @@ export const BkmNode = React.memo(
[bkm, dmnEditorStoreApi, index]
);
- const { allFeelVariableUniqueNames } = useDmnEditorDerivedStore();
+ const getAllFeelVariableUniqueNames = useCallback((s: State) => s.computed(s).getAllFeelVariableUniqueNames(), []);
const onCreateDataType = useDataTypeCreationCallbackForNodes(index, bkm["@_name"]);
const { fontCssProperties, shapeStyle } = useNodeStyle({
dmnStyle: shape["di:Style"],
nodeType: type as NodeType,
- isEnabled: diagram.overlays.enableStyles,
+ isEnabled: enableCustomNodeStyles,
});
+ const additionalClasses = `${className} ${dmnObjectQName.prefix ? "external" : ""}`;
+
return (
<>
- <svg className={`kie-dmn-editor--node-shape ${className} ${dmnObjectQName.prefix ? "external" : ""}`}>
+ <svg className={`kie-dmn-editor--node-shape ${additionalClasses}`}>
<BkmNodeSvg
{...nodeDimensions}
x={0}
@@ -426,18 +464,17 @@ export const BkmNode = React.memo(
<div
ref={ref}
- className={`kie-dmn-editor--node kie-dmn-editor--bkm-node ${className} ${
- dmnObjectQName.prefix ? "external" : ""
- }`}
+ className={`kie-dmn-editor--node kie-dmn-editor--bkm-node ${additionalClasses}`}
tabIndex={-1}
onDoubleClick={triggerEditing}
onKeyDown={triggerEditingIfEnter}
>
- <InfoNodePanel isVisible={!isTargeted && isHovered} />
-
- {!isExternal && <EditExpressionNodePanel isVisible={!isTargeted && isHovered} id={bkm["@_id"]!} />}
+ {/* {`render count: ${renderCount.current}`}
+ <br /> */}
+ <InfoNodePanel isVisible={!isTargeted && shouldActLikeHovered} />
+ {!isExternal && <EditExpressionNodePanel isVisible={!isTargeted && shouldActLikeHovered} id={bkm["@_id"]!} />}
<OutgoingStuffNodePanel
- isVisible={!isConnecting && !isTargeted && isHovered}
+ isVisible={!isTargeted && shouldActLikeHovered}
nodeTypes={outgoingStructure[NODE_TYPES.bkm].nodes}
edgeTypes={outgoingStructure[NODE_TYPES.bkm].edges}
/>
@@ -449,22 +486,22 @@ export const BkmNode = React.memo(
position={getNodeLabelPosition(type as NodeType)}
value={bkm["@_label"] ?? bkm["@_name"]}
onChange={setName}
- allUniqueNames={allFeelVariableUniqueNames}
+ onGetAllUniqueNames={getAllFeelVariableUniqueNames}
shouldCommitOnBlur={true}
fontCssProperties={fontCssProperties}
/>
- {isHovered && (
+ {shouldActLikeHovered && (
<NodeResizerHandle
nodeType={type as NodeType}
- snapGrid={diagram.snapGrid}
+ snapGrid={snapGrid}
nodeId={id}
nodeShapeIndex={shape.index}
/>
)}
<DataTypeNodePanel
- isVisible={!isTargeted && isHovered}
+ isVisible={!isTargeted && shouldActLikeHovered}
variable={bkm.variable}
- namespace={dmnObjectNamespace}
+ dmnObjectNamespace={dmnObjectNamespace}
shape={shape}
onChange={onTypeRefChange}
onCreate={onCreateDataType}
@@ -472,7 +509,8 @@ export const BkmNode = React.memo(
</div>
</>
);
- }
+ },
+ propsHaveSameValuesDeep
);
export const KnowledgeSourceNode = React.memo(
@@ -484,23 +522,30 @@ export const KnowledgeSourceNode = React.memo(
type,
id,
}: RF.NodeProps<DmnDiagramNodeData<DMN15__tKnowledgeSource & { __$$element: "knowledgeSource" }>>) => {
+ const renderCount = useRef<number>(0);
+ renderCount.current++;
+
const ref = useRef<HTMLDivElement>(null);
const isExternal = !!dmnObjectQName.prefix;
- const isResizing = useNodeResizing(id);
- const diagram = useDmnEditorStore((s) => s.diagram);
- const isHovered = (useIsHovered(ref) || isResizing) && diagram.draggingNodes.length === 0;
+ const snapGrid = useDmnEditorStore((s) => s.diagram.snapGrid);
+ const enableCustomNodeStyles = useDmnEditorStore((s) => s.diagram.overlays.enableCustomNodeStyles);
+ const isHovered = useIsHovered(ref);
+ const isResizing = useNodeResizing(id);
+ const shouldActLikeHovered = useDmnEditorStore(
+ (s) => (isHovered || isResizing) && s.diagram.draggingNodes.length === 0
+ );
const { isEditingLabel, setEditingLabel, triggerEditing, triggerEditingIfEnter } = useEditableNodeLabel(
knowledgeSource["@_id"]
);
- useHoveredNodeAlwaysOnTop(ref, zIndex, isHovered, dragging, selected, isEditingLabel);
+ useHoveredNodeAlwaysOnTop(ref, zIndex, shouldActLikeHovered, dragging, selected, isEditingLabel);
const dmnEditorStoreApi = useDmnEditorStoreApi();
- const { isTargeted, isValidConnectionTarget, isConnecting } = useConnectionTargetStatus(id, isHovered);
- const className = useNodeClassName(diagram.dropTargetNode, isConnecting, isValidConnectionTarget, id);
- const nodeDimensions = useNodeDimensions(type as NodeType, diagram.snapGrid, id, shape, isExternal);
+ const { isTargeted, isValidConnectionTarget } = useConnectionTargetStatus(id, shouldActLikeHovered);
+ const className = useNodeClassName(isValidConnectionTarget, id);
+ const nodeDimensions = useNodeDimensions(type as NodeType, snapGrid, shape, isExternal);
const setName = useCallback<OnEditableNodeLabelChange>(
(newName: string) => {
dmnEditorStoreApi.setState((state) => {
@@ -510,17 +555,19 @@ export const KnowledgeSourceNode = React.memo(
[dmnEditorStoreApi, index]
);
- const { allFeelVariableUniqueNames } = useDmnEditorDerivedStore();
+ const getAllFeelVariableUniqueNames = useCallback((s: State) => s.computed(s).getAllFeelVariableUniqueNames(), []);
const { fontCssProperties, shapeStyle } = useNodeStyle({
dmnStyle: shape["di:Style"],
nodeType: type as NodeType,
- isEnabled: diagram.overlays.enableStyles,
+ isEnabled: enableCustomNodeStyles,
});
+ const additionalClasses = `${className} ${dmnObjectQName.prefix ? "external" : ""}`;
+
return (
<>
- <svg className={`kie-dmn-editor--node-shape ${className} ${dmnObjectQName.prefix ? "external" : ""}`}>
+ <svg className={`kie-dmn-editor--node-shape ${additionalClasses}`}>
<KnowledgeSourceNodeSvg
{...nodeDimensions}
x={0}
@@ -535,16 +582,16 @@ export const KnowledgeSourceNode = React.memo(
<div
ref={ref}
- className={`kie-dmn-editor--node kie-dmn-editor--knowledge-source-node ${className} ${
- dmnObjectQName.prefix ? "external" : ""
- }`}
+ className={`kie-dmn-editor--node kie-dmn-editor--knowledge-source-node ${additionalClasses}`}
tabIndex={-1}
onDoubleClick={triggerEditing}
onKeyDown={triggerEditingIfEnter}
>
- <InfoNodePanel isVisible={!isTargeted && isHovered} />
+ {/* {`render count: ${renderCount.current}`}
+ <br /> */}
+ <InfoNodePanel isVisible={!isTargeted && shouldActLikeHovered} />
<OutgoingStuffNodePanel
- isVisible={!isConnecting && !isTargeted && isHovered}
+ isVisible={!isTargeted && shouldActLikeHovered}
nodeTypes={outgoingStructure[NODE_TYPES.knowledgeSource].nodes}
edgeTypes={outgoingStructure[NODE_TYPES.knowledgeSource].edges}
/>
@@ -557,14 +604,14 @@ export const KnowledgeSourceNode = React.memo(
value={knowledgeSource["@_label"] ?? knowledgeSource["@_name"]}
onChange={setName}
skipValidation={true}
- allUniqueNames={allFeelVariableUniqueNames}
+ onGetAllUniqueNames={getAllFeelVariableUniqueNames}
shouldCommitOnBlur={true}
fontCssProperties={fontCssProperties}
/>
- {isHovered && (
+ {shouldActLikeHovered && (
<NodeResizerHandle
nodeType={type as NodeType}
- snapGrid={diagram.snapGrid}
+ snapGrid={snapGrid}
nodeId={id}
nodeShapeIndex={shape.index}
/>
@@ -572,7 +619,8 @@ export const KnowledgeSourceNode = React.memo(
</div>
</>
);
- }
+ },
+ propsHaveSameValuesDeep
);
export const TextAnnotationNode = React.memo(
@@ -584,23 +632,30 @@ export const TextAnnotationNode = React.memo(
type,
id,
}: RF.NodeProps<DmnDiagramNodeData<DMN15__tTextAnnotation & { __$$element: "textAnnotation" }>>) => {
+ const renderCount = useRef<number>(0);
+ renderCount.current++;
+
const ref = useRef<HTMLDivElement>(null);
const isExternal = !!dmnObjectQName.prefix;
- const isResizing = useNodeResizing(id);
- const diagram = useDmnEditorStore((s) => s.diagram);
- const isHovered = (useIsHovered(ref) || isResizing) && diagram.draggingNodes.length === 0;
+ const snapGrid = useDmnEditorStore((s) => s.diagram.snapGrid);
+ const enableCustomNodeStyles = useDmnEditorStore((s) => s.diagram.overlays.enableCustomNodeStyles);
+ const isHovered = useIsHovered(ref);
+ const isResizing = useNodeResizing(id);
+ const shouldActLikeHovered = useDmnEditorStore(
+ (s) => (isHovered || isResizing) && s.diagram.draggingNodes.length === 0
+ );
const { isEditingLabel, setEditingLabel, triggerEditing, triggerEditingIfEnter } = useEditableNodeLabel(
textAnnotation["@_id"]
);
- useHoveredNodeAlwaysOnTop(ref, zIndex, isHovered, dragging, selected, isEditingLabel);
+ useHoveredNodeAlwaysOnTop(ref, zIndex, shouldActLikeHovered, dragging, selected, isEditingLabel);
const dmnEditorStoreApi = useDmnEditorStoreApi();
- const { isTargeted, isValidConnectionTarget, isConnecting } = useConnectionTargetStatus(id, isHovered);
- const className = useNodeClassName(diagram.dropTargetNode, isConnecting, isValidConnectionTarget, id);
- const nodeDimensions = useNodeDimensions(type as NodeType, diagram.snapGrid, id, shape, isExternal);
+ const { isTargeted, isValidConnectionTarget } = useConnectionTargetStatus(id, shouldActLikeHovered);
+ const className = useNodeClassName(isValidConnectionTarget, id);
+ const nodeDimensions = useNodeDimensions(type as NodeType, snapGrid, shape, isExternal);
const setText = useCallback(
(newText: string) => {
dmnEditorStoreApi.setState((state) => {
@@ -610,17 +665,19 @@ export const TextAnnotationNode = React.memo(
[dmnEditorStoreApi, index]
);
- const { allFeelVariableUniqueNames } = useDmnEditorDerivedStore();
+ const getAllFeelVariableUniqueNames = useCallback((s: State) => s.computed(s).getAllFeelVariableUniqueNames(), []);
const { fontCssProperties, shapeStyle } = useNodeStyle({
dmnStyle: shape["di:Style"],
nodeType: type as NodeType,
- isEnabled: diagram.overlays.enableStyles,
+ isEnabled: enableCustomNodeStyles,
});
+ const additionalClasses = `${className} ${dmnObjectQName.prefix ? "external" : ""}`;
+
return (
<>
- <svg className={`kie-dmn-editor--node-shape ${className} ${dmnObjectQName.prefix ? "external" : ""}`}>
+ <svg className={`kie-dmn-editor--node-shape ${additionalClasses}`}>
<TextAnnotationNodeSvg
{...nodeDimensions}
x={0}
@@ -635,16 +692,16 @@ export const TextAnnotationNode = React.memo(
<div
ref={ref}
- className={`kie-dmn-editor--node kie-dmn-editor--text-annotation-node ${className} ${
- dmnObjectQName.prefix ? "external" : ""
- }`}
+ className={`kie-dmn-editor--node kie-dmn-editor--text-annotation-node ${additionalClasses}`}
tabIndex={-1}
onDoubleClick={triggerEditing}
onKeyDown={triggerEditingIfEnter}
>
- <InfoNodePanel isVisible={!isTargeted && isHovered} />
+ {/* {`render count: ${renderCount.current}`}
+ <br /> */}
+ <InfoNodePanel isVisible={!isTargeted && shouldActLikeHovered} />
<OutgoingStuffNodePanel
- isVisible={!isConnecting && !isTargeted && isHovered}
+ isVisible={!isTargeted && shouldActLikeHovered}
nodeTypes={outgoingStructure[NODE_TYPES.textAnnotation].nodes}
edgeTypes={outgoingStructure[NODE_TYPES.textAnnotation].edges}
/>
@@ -658,14 +715,14 @@ export const TextAnnotationNode = React.memo(
value={textAnnotation["@_label"] ?? textAnnotation.text?.__$$text}
onChange={setText}
skipValidation={true}
- allUniqueNames={allFeelVariableUniqueNames}
+ onGetAllUniqueNames={getAllFeelVariableUniqueNames}
shouldCommitOnBlur={true}
fontCssProperties={fontCssProperties}
/>
- {isHovered && (
+ {shouldActLikeHovered && (
<NodeResizerHandle
nodeType={type as NodeType}
- snapGrid={diagram.snapGrid}
+ snapGrid={snapGrid}
nodeId={id}
nodeShapeIndex={shape.index}
/>
@@ -673,7 +730,8 @@ export const TextAnnotationNode = React.memo(
</div>
</>
);
- }
+ },
+ propsHaveSameValuesDeep
);
export const DecisionServiceNode = React.memo(
@@ -685,24 +743,31 @@ export const DecisionServiceNode = React.memo(
type,
id,
}: RF.NodeProps<DmnDiagramNodeData<DMN15__tDecisionService & { __$$element: "decisionService" }>>) => {
+ const renderCount = useRef<number>(0);
+ renderCount.current++;
+
const ref = useRef<SVGRectElement>(null);
const isExternal = !!dmnObjectQName.prefix;
+ const snapGrid = useDmnEditorStore((s) => s.diagram.snapGrid);
+ const enableCustomNodeStyles = useDmnEditorStore((s) => s.diagram.overlays.enableCustomNodeStyles);
+ const isHovered = useIsHovered(ref);
const isResizing = useNodeResizing(id);
-
- const diagram = useDmnEditorStore((s) => s.diagram);
- const isHovered = (useIsHovered(ref) || isResizing) && diagram.draggingNodes.length === 0;
+ const shouldActLikeHovered = useDmnEditorStore(
+ (s) => (isHovered || isResizing) && s.diagram.draggingNodes.length === 0
+ );
+ const isDropTarget = useDmnEditorStore((s) => s.diagram.dropTargetNode?.id === id);
const { isEditingLabel, setEditingLabel, triggerEditing, triggerEditingIfEnter } = useEditableNodeLabel(
decisionService["@_id"]
);
- useHoveredNodeAlwaysOnTop(ref, zIndex, isHovered, dragging, selected, isEditingLabel);
+ useHoveredNodeAlwaysOnTop(ref, zIndex, shouldActLikeHovered, dragging, selected, isEditingLabel);
const dmnEditorStoreApi = useDmnEditorStoreApi();
- const { isTargeted, isValidConnectionTarget, isConnecting } = useConnectionTargetStatus(id, isHovered);
- const className = useNodeClassName(diagram.dropTargetNode, isConnecting, isValidConnectionTarget, id);
+ const { isTargeted, isValidConnectionTarget } = useConnectionTargetStatus(id, shouldActLikeHovered);
+ const className = useNodeClassName(isValidConnectionTarget, id);
- const nodeDimensions = useNodeDimensions(type as NodeType, diagram.snapGrid, id, shape, isExternal);
+ const nodeDimensions = useNodeDimensions(type as NodeType, snapGrid, shape, isExternal);
const setName = useCallback<OnEditableNodeLabelChange>(
(newName: string) => {
dmnEditorStoreApi.setState((state) => {
@@ -742,7 +807,7 @@ export const DecisionServiceNode = React.memo(
[decisionService, dmnEditorStoreApi, index]
);
- const { allFeelVariableUniqueNames, dmnShapesByHref } = useDmnEditorDerivedStore();
+ const getAllFeelVariableUniqueNames = useCallback((s: State) => s.computed(s).getAllFeelVariableUniqueNames(), []);
const dividerLineRef = useRef<SVGPathElement>(null);
@@ -760,25 +825,25 @@ export const DecisionServiceNode = React.memo(
const dragHandler = drag<SVGCircleElement, unknown>()
.on("start", () => {
dmnEditorStoreApi.setState((state) =>
- state.dispatch.diagram.setDividerLineStatus(state, id, { moving: true })
+ state.dispatch(state).diagram.setDividerLineStatus(id, { moving: true })
);
})
.on("drag", (e) => {
dmnEditorStoreApi.setState((state) => {
updateDecisionServiceDividerLine({
definitions: state.dmn.model.definitions,
- drdIndex: diagram.drdIndex,
- dmnShapesByHref,
+ drdIndex: state.diagram.drdIndex,
+ dmnShapesByHref: state.computed(state).indexes().dmnShapesByHref,
drgElementIndex: index,
shapeIndex: shape.index,
localYPosition: e.y,
- snapGrid: diagram.snapGrid,
+ snapGrid: state.diagram.snapGrid,
});
});
})
.on("end", (e) => {
dmnEditorStoreApi.setState((state) =>
- state.dispatch.diagram.setDividerLineStatus(state, id, { moving: false })
+ state.dispatch(state).diagram.setDividerLineStatus(id, { moving: false })
);
});
@@ -786,26 +851,19 @@ export const DecisionServiceNode = React.memo(
return () => {
selection.on(".drag", null);
};
- }, [
- decisionService,
- diagram.drdIndex,
- diagram.snapGrid,
- dmnEditorStoreApi,
- dmnShapesByHref,
- id,
- index,
- shape.index,
- ]);
+ }, [decisionService, dmnEditorStoreApi, id, index, shape.index]);
const { fontCssProperties, shapeStyle } = useNodeStyle({
dmnStyle: shape["di:Style"],
nodeType: type as NodeType,
- isEnabled: diagram.overlays.enableStyles,
+ isEnabled: enableCustomNodeStyles,
});
+ const additionalClasses = `${className} ${dmnObjectQName.prefix ? "external" : ""}`;
+
return (
<>
- <svg className={`kie-dmn-editor--node-shape ${className} ${dmnObjectQName.prefix ? "external" : ""}`}>
+ <svg className={`kie-dmn-editor--node-shape ${additionalClasses}`}>
<DecisionServiceNodeSvg
dividerLineRef={dividerLineRef}
ref={ref}
@@ -817,7 +875,7 @@ export const DecisionServiceNode = React.memo(
strokeColor={shapeStyle.strokeColor}
isReadonly={false}
isCollapsed={isCollapsed}
- showSectionLabels={diagram.dropTargetNode?.id === id}
+ showSectionLabels={isDropTarget}
dividerLineLocalY={getDecisionServiceDividerLineLocalY(shape)}
/>
</svg>
@@ -825,17 +883,16 @@ export const DecisionServiceNode = React.memo(
<PositionalNodeHandles isTargeted={isTargeted && isValidConnectionTarget} nodeId={id} />
<div
- className={`kie-dmn-editor--node kie-dmn-editor--decision-service-node ${className} ${
- dmnObjectQName.prefix ? "external" : ""
- }`}
+ className={`kie-dmn-editor--node kie-dmn-editor--decision-service-node ${additionalClasses}`}
tabIndex={-1}
onDoubleClick={triggerEditing}
onKeyDown={triggerEditingIfEnter}
>
+ {/* {`render count: ${renderCount.current}`}
+ <br /> */}
<InfoNodePanel isVisible={!isTargeted && selected && !dragging} />
-
<OutgoingStuffNodePanel
- isVisible={!isConnecting && !isTargeted && selected && !dragging}
+ isVisible={!isTargeted && selected && !dragging}
nodeTypes={outgoingStructure[NODE_TYPES.decisionService].nodes}
edgeTypes={outgoingStructure[NODE_TYPES.decisionService].edges}
/>
@@ -847,14 +904,14 @@ export const DecisionServiceNode = React.memo(
setEditing={setEditingLabel}
value={decisionService["@_label"] ?? decisionService["@_name"]}
onChange={setName}
- allUniqueNames={allFeelVariableUniqueNames}
+ onGetAllUniqueNames={getAllFeelVariableUniqueNames}
shouldCommitOnBlur={true}
fontCssProperties={fontCssProperties}
/>
{selected && !dragging && !isCollapsed && (
<NodeResizerHandle
nodeType={type as NodeType}
- snapGrid={diagram.snapGrid}
+ snapGrid={snapGrid}
nodeId={id}
nodeShapeIndex={shape.index}
/>
@@ -863,7 +920,7 @@ export const DecisionServiceNode = React.memo(
<DataTypeNodePanel
isVisible={!isTargeted && selected && !dragging}
variable={decisionService.variable}
- namespace={dmnObjectNamespace}
+ dmnObjectNamespace={dmnObjectNamespace}
shape={shape}
onCreate={onCreateDataType}
onChange={onTypeRefChange}
@@ -871,7 +928,8 @@ export const DecisionServiceNode = React.memo(
</div>
</>
);
- }
+ },
+ propsHaveSameValuesDeep
);
export const GroupNode = React.memo(
@@ -883,22 +941,28 @@ export const GroupNode = React.memo(
type,
id,
}: RF.NodeProps<DmnDiagramNodeData<DMN15__tGroup & { __$$element: "group" }>>) => {
+ const renderCount = useRef<number>(0);
+ renderCount.current++;
+
const ref = useRef<SVGRectElement>(null);
const isExternal = !!dmnObjectQName.prefix;
- const isResizing = useNodeResizing(id);
-
- const diagram = useDmnEditorStore((s) => s.diagram);
- const isHovered = (useIsHovered(ref) || isResizing) && diagram.draggingNodes.length === 0;
+ const snapGrid = useDmnEditorStore((s) => s.diagram.snapGrid);
+ const enableCustomNodeStyles = useDmnEditorStore((s) => s.diagram.overlays.enableCustomNodeStyles);
+ const isHovered = useIsHovered(ref);
+ const isResizing = useNodeResizing(id);
+ const shouldActLikeHovered = useDmnEditorStore(
+ (s) => (isHovered || isResizing) && s.diagram.draggingNodes.length === 0
+ );
const dmnEditorStoreApi = useDmnEditorStoreApi();
const reactFlow = RF.useReactFlow<DmnDiagramNodeData, DmnDiagramEdgeData>();
const { isEditingLabel, setEditingLabel, triggerEditing, triggerEditingIfEnter } = useEditableNodeLabel(
group["@_id"]
);
- const { isTargeted, isValidConnectionTarget, isConnecting } = useConnectionTargetStatus(id, isHovered);
- const className = useNodeClassName(diagram.dropTargetNode, isConnecting, isValidConnectionTarget, id);
- const nodeDimensions = useNodeDimensions(type as NodeType, diagram.snapGrid, id, shape, isExternal);
+ const { isTargeted, isValidConnectionTarget } = useConnectionTargetStatus(id, shouldActLikeHovered);
+ const className = useNodeClassName(isValidConnectionTarget, id);
+ const nodeDimensions = useNodeDimensions(type as NodeType, snapGrid, shape, isExternal);
const setName = useCallback<OnEditableNodeLabelChange>(
(newName: string) => {
dmnEditorStoreApi.setState((state) => {
@@ -933,17 +997,17 @@ export const GroupNode = React.memo(
};
}, [dmnEditorStoreApi, reactFlow, shape]);
- const { allFeelVariableUniqueNames } = useDmnEditorDerivedStore();
-
const { fontCssProperties, shapeStyle } = useNodeStyle({
dmnStyle: shape["di:Style"],
nodeType: type as NodeType,
- isEnabled: diagram.overlays.enableStyles,
+ isEnabled: enableCustomNodeStyles,
});
+ const additionalClasses = `${className} ${dmnObjectQName.prefix ? "external" : ""}`;
+
return (
<>
- <svg className={`kie-dmn-editor--node-shape ${className} ${dmnObjectQName.prefix ? "external" : ""}`}>
+ <svg className={`kie-dmn-editor--node-shape ${additionalClasses}`}>
<GroupNodeSvg
ref={ref}
{...nodeDimensions}
@@ -956,15 +1020,15 @@ export const GroupNode = React.memo(
</svg>
<div
- className={`kie-dmn-editor--node kie-dmn-editor--group-node ${className} ${
- dmnObjectQName.prefix ? "external" : ""
- }`}
+ className={`kie-dmn-editor--node kie-dmn-editor--group-node ${additionalClasses}`}
tabIndex={-1}
onDoubleClick={triggerEditing}
onKeyDown={triggerEditingIfEnter}
>
+ {/* {`render count: ${renderCount.current}`}
+ <br /> */}
<OutgoingStuffNodePanel
- isVisible={!isConnecting && !isTargeted && selected && !dragging}
+ isVisible={!isTargeted && selected && !dragging}
nodeTypes={outgoingStructure[NODE_TYPES.group].nodes}
edgeTypes={outgoingStructure[NODE_TYPES.group].edges}
/>
@@ -978,14 +1042,14 @@ export const GroupNode = React.memo(
value={group["@_label"] ?? group["@_name"]}
onChange={setName}
skipValidation={true}
- allUniqueNames={allFeelVariableUniqueNames}
+ onGetAllUniqueNames={useCallback(() => new Map(), [])}
shouldCommitOnBlur={true}
fontCssProperties={fontCssProperties}
/>
{selected && !dragging && (
<NodeResizerHandle
nodeType={type as NodeType}
- snapGrid={diagram.snapGrid}
+ snapGrid={snapGrid}
nodeId={id}
nodeShapeIndex={shape.index}
/>
@@ -993,7 +1057,8 @@ export const GroupNode = React.memo(
</div>
</>
);
- }
+ },
+ propsHaveSameValuesDeep
);
export const UnknownNode = React.memo(
@@ -1005,16 +1070,23 @@ export const UnknownNode = React.memo(
type,
id,
}: RF.NodeProps<DmnDiagramNodeData<null>>) => {
+ const renderCount = useRef<number>(0);
+ renderCount.current++;
+
const ref = useRef<HTMLDivElement>(null);
const isExternal = !!dmnObjectQName.prefix;
- const isResizing = useNodeResizing(id);
- const diagram = useDmnEditorStore((s) => s.diagram);
- const isHovered = (useIsHovered(ref) || isResizing) && diagram.draggingNodes.length === 0;
+ const snapGrid = useDmnEditorStore((s) => s.diagram.snapGrid);
+ const enableCustomNodeStyles = useDmnEditorStore((s) => s.diagram.overlays.enableCustomNodeStyles);
+ const isHovered = useIsHovered(ref);
+ const isResizing = useNodeResizing(id);
+ const shouldActLikeHovered = useDmnEditorStore(
+ (s) => (isHovered || isResizing) && s.diagram.draggingNodes.length === 0
+ );
- const { isTargeted, isValidConnectionTarget, isConnecting } = useConnectionTargetStatus(id, isHovered);
- const className = useNodeClassName(diagram.dropTargetNode, isConnecting, isValidConnectionTarget, id);
- const nodeDimensions = useNodeDimensions(type as NodeType, diagram.snapGrid, id, shape, isExternal);
+ const { isTargeted, isValidConnectionTarget } = useConnectionTargetStatus(id, shouldActLikeHovered);
+ const className = useNodeClassName(isValidConnectionTarget, id);
+ const nodeDimensions = useNodeDimensions(type as NodeType, snapGrid, shape, isExternal);
return (
<>
@@ -1025,7 +1097,9 @@ export const UnknownNode = React.memo(
<RF.Handle key={"unknown"} id={"unknown"} type={"source"} style={{ opacity: 0 }} position={RF.Position.Top} />
<div ref={ref} className={`kie-dmn-editor--node kie-dmn-editor--unknown-node ${className}`} tabIndex={-1}>
- <InfoNodePanel isVisible={!isTargeted && isHovered} />
+ {/* {`render count: ${renderCount.current}`}
+ <br /> */}
+ <InfoNodePanel isVisible={!isTargeted && shouldActLikeHovered} />
<EditableNodeLabel
namedElement={undefined}
@@ -1036,13 +1110,13 @@ export const UnknownNode = React.memo(
value={`? `}
onChange={() => {}}
skipValidation={false}
- allUniqueNames={new Map()}
+ onGetAllUniqueNames={useCallback(() => new Map(), [])}
shouldCommitOnBlur={true}
/>
{selected && !dragging && (
<NodeResizerHandle
nodeType={type as NodeType}
- snapGrid={diagram.snapGrid}
+ snapGrid={snapGrid}
nodeId={id}
nodeShapeIndex={shape.index}
/>
@@ -1050,7 +1124,8 @@ export const UnknownNode = React.memo(
</div>
</>
);
- }
+ },
+ propsHaveSameValuesDeep
);
///
@@ -1095,41 +1170,31 @@ export function NodeResizerHandle(props: {
}
function useNodeResizing(id: string): boolean {
- const node = RF.useStore((s) => s.nodeInternals.get(id));
- if (!node) {
- throw new Error("Can't use nodeInternals of non-existent node " + id);
- }
-
- return node.resizing ?? false;
+ return RF.useStore((s) => s.nodeInternals.get(id)?.resizing ?? false);
}
+
function useNodeDimensions(
type: NodeType,
snapGrid: SnapGrid,
- id: string,
shape: DMNDI15__DMNShape,
isExternal: boolean
): RF.Dimensions {
- const node = RF.useStore((s) => s.nodeInternals.get(id));
- if (!node) {
- throw new Error("Can't use nodeInternals of non-existent node " + id);
- }
-
if (type === NODE_TYPES.decisionService && (isExternal || shape["@_isCollapsed"])) {
return DECISION_SERVICE_COLLAPSED_DIMENSIONS;
}
- const minSizes = MIN_NODE_SIZES[node.type as NodeType](snapGrid);
+ const minSizes = MIN_NODE_SIZES[type](snapGrid);
return {
- width: node.width ?? snapShapeDimensions(snapGrid, shape, minSizes).width,
- height: node.height ?? snapShapeDimensions(snapGrid, shape, minSizes).height,
+ width: snapShapeDimensions(snapGrid, shape, minSizes).width,
+ height: snapShapeDimensions(snapGrid, shape, minSizes).height,
};
}
function useHoveredNodeAlwaysOnTop(
ref: React.RefObject<HTMLDivElement | SVGElement>,
layer: number,
- isHovered: boolean,
+ shouldActLikeHovered: boolean,
dragging: boolean,
selected: boolean,
isEditing: boolean
@@ -1140,67 +1205,73 @@ function useHoveredNodeAlwaysOnTop(
ref.current?.focus();
}
if (ref.current) {
- ref.current.parentElement!.style.zIndex = `${isHovered || dragging ? layer + 1000 + 1 : layer}`;
+ ref.current.parentElement!.style.zIndex = `${shouldActLikeHovered || dragging ? layer + 1000 + 1 : layer}`;
}
}, 0);
- }, [dragging, isHovered, ref, selected, layer, isEditing]);
+ }, [dragging, shouldActLikeHovered, ref, selected, layer, isEditing]);
}
export function useConnection(nodeId: string) {
const connectionNodeId = RF.useStore((s) => s.connectionNodeId);
- const connectionHandleId = RF.useStore((s) => s.connectionHandleId);
const connectionHandleType = RF.useStore((s) => s.connectionHandleType);
- const edgeIdBeingUpdated = useDmnEditorStore((s) => s.diagram.edgeIdBeingUpdated);
- const { edgesById } = useDmnEditorDerivedStore();
- const edge = edgeIdBeingUpdated ? edgesById.get(edgeIdBeingUpdated) : null;
const source = connectionNodeId;
const target = nodeId;
- const sourceHandle = connectionHandleId ?? edge?.type ?? null;
- const connection = {
- source: connectionHandleType === "source" ? source : target,
- target: connectionHandleType === "source" ? target : source,
- sourceHandle,
- targetHandle: null, // We don't use targetHandles, as target handles are only different in position, not in semantic.
- };
+ const edgeIdBeingUpdated = useDmnEditorStore((s) => s.diagram.edgeIdBeingUpdated);
+ const sourceHandle = RF.useStore(
+ (s) => s.connectionHandleId ?? s.edges.find((e) => e.id === edgeIdBeingUpdated)?.type ?? null
+ );
+
+ const connection = useMemo(
+ () => ({
+ source: connectionHandleType === "source" ? source : target,
+ target: connectionHandleType === "source" ? target : source,
+ sourceHandle,
+ targetHandle: null, // We don't use targetHandles, as target handles are only different in position, not in semantic.
+ }),
+ [connectionHandleType, source, sourceHandle, target]
+ );
return connection;
}
-export function useConnectionTargetStatus(nodeId: string, isHovered: boolean) {
- const connectionNodeId = RF.useStore((s) => s.connectionNodeId);
- const isValidConnection = RF.useStore((s) => s.isValidConnection);
+export function useConnectionTargetStatus(nodeId: string, shouldActLikeHovered: boolean) {
+ const isTargeted = RF.useStore((s) => !!s.connectionNodeId && s.connectionNodeId !== nodeId && shouldActLikeHovered);
const connection = useConnection(nodeId);
-
- return {
- isTargeted: !!connectionNodeId && connectionNodeId !== nodeId && isHovered,
- isValidConnectionTarget: isValidConnection?.(connection) ?? false,
- isConnecting: !!connectionNodeId,
- };
+ const isValidConnectionTarget = RF.useStore((s) => s.isValidConnection?.(connection) ?? false);
+
+ return useMemo(
+ () => ({
+ isTargeted,
+ isValidConnectionTarget,
+ }),
+ [isTargeted, isValidConnectionTarget]
+ );
}
-export function useNodeClassName(
- dropTargetNode: DropTargetNode,
- isConnecting: boolean,
- isValidConnectionTarget: boolean,
- nodeId: string
-) {
- const { isDropTargetNodeValidForSelection } = useDmnEditorDerivedStore();
- const connectionNodeId = RF.useStore((s) => s.connectionNodeId);
+export function useNodeClassName(isValidConnectionTarget: boolean, nodeId: string) {
+ const isDropTarget = useDmnEditorStore(
+ (s) => s.diagram.dropTargetNode?.id === nodeId && containment.get(s.diagram.dropTargetNode?.type as NodeType)
+ );
+ const { externalModelsByNamespace } = useExternalModels();
+ const isDropTargetNodeValidForSelection = useDmnEditorStore((s) =>
+ s.computed(s).isDropTargetNodeValidForSelection(externalModelsByNamespace)
+ );
+ const isConnectionNodeId = RF.useStore((s) => s.connectionNodeId === nodeId);
const connection = useConnection(nodeId);
const isEdgeConnection = !!Object.values(EDGE_TYPES).find((s) => s === connection.sourceHandle);
const isNodeConnection = !!Object.values(NODE_TYPES).find((s) => s === connection.sourceHandle);
- if (isNodeConnection && isConnecting && connectionNodeId !== nodeId) {
+ if (isNodeConnection && !isConnectionNodeId) {
return "dimmed";
}
- if (isEdgeConnection && isConnecting && (!isValidConnectionTarget || connectionNodeId === nodeId)) {
+ if (isEdgeConnection && (!isValidConnectionTarget || isConnectionNodeId)) {
return "dimmed";
}
- if (dropTargetNode?.id === nodeId && containment.get(dropTargetNode.type as NodeType)) {
+ if (isDropTarget) {
return isDropTargetNodeValidForSelection ? "drop-target" : "drop-target-invalid";
}
diff --git a/packages/dmn-editor/src/draggable/Draggable.tsx b/packages/dmn-editor/src/draggable/Draggable.tsx
index 7ae45ddb526..cdffd6aeac4 100644
--- a/packages/dmn-editor/src/draggable/Draggable.tsx
+++ b/packages/dmn-editor/src/draggable/Draggable.tsx
@@ -80,9 +80,6 @@ export function DragAndDrop({
const [valuesKeys, setValuesKeys] = useState((values ?? [])?.map((_) => generateUuid()));
useLayoutEffect(() => {
- if (isDisabled) {
- return;
- }
setValuesCopy((prev) => {
if (values?.length !== prev.length) {
setValuesKeys((values ?? [])?.map((_) => generateUuid()));
diff --git a/packages/dmn-editor/src/externalNodes/DmnObjectListItem.tsx b/packages/dmn-editor/src/externalNodes/DmnObjectListItem.tsx
index 928af9ec626..335c5f16c39 100644
--- a/packages/dmn-editor/src/externalNodes/DmnObjectListItem.tsx
+++ b/packages/dmn-editor/src/externalNodes/DmnObjectListItem.tsx
@@ -24,9 +24,10 @@ import { TypeRefLabel } from "../dataTypes/TypeRefLabel";
import { NodeIcon } from "../icons/Icons";
import { getNodeTypeFromDmnObject } from "../diagram/maths/DmnMaths";
import { buildFeelQNameFromNamespace } from "../feel/buildFeelQName";
-import { useDmnEditorDerivedStore } from "../store/DerivedStore";
import { Flex } from "@patternfly/react-core/dist/js/layouts/Flex";
import { DmnBuiltInDataType } from "@kie-tools/boxed-expression-component/dist/api";
+import { useDmnEditorStore } from "../store/StoreContext";
+import { useExternalModels } from "../includedModels/DmnEditorDependenciesContext";
export function DmnObjectListItem({
dmnObject,
@@ -39,7 +40,11 @@ export function DmnObjectListItem({
namespace: string;
relativeToNamespace: string;
}) {
- const { importsByNamespace, allTopLevelDataTypesByFeelName } = useDmnEditorDerivedStore();
+ const importsByNamespace = useDmnEditorStore((s) => s.computed(s).importsByNamespace());
+ const { externalModelsByNamespace } = useExternalModels();
+ const allTopLevelDataTypesByFeelName = useDmnEditorStore(
+ (s) => s.computed(s).getDataTypes(externalModelsByNamespace).allTopLevelDataTypesByFeelName
+ );
if (!dmnObject) {
return <>{dmnObjectHref}</>;
}
diff --git a/packages/dmn-editor/src/externalNodes/ExternalNodesPanel.tsx b/packages/dmn-editor/src/externalNodes/ExternalNodesPanel.tsx
index ef89960b535..07fb04aaa20 100644
--- a/packages/dmn-editor/src/externalNodes/ExternalNodesPanel.tsx
+++ b/packages/dmn-editor/src/externalNodes/ExternalNodesPanel.tsx
@@ -19,9 +19,9 @@
import * as React from "react";
import { useCallback, useState } from "react";
-import { DiagramNodesPanel, DmnEditorTab, useDmnEditorStoreApi } from "../store/Store";
+import { DiagramNodesPanel, DmnEditorTab } from "../store/Store";
+import { useDmnEditorStore, useDmnEditorStoreApi } from "../store/StoreContext";
import { Flex } from "@patternfly/react-core/dist/js/layouts/Flex";
-import { useDmnEditorDerivedStore } from "../store/DerivedStore";
import { buildXmlHref } from "../xml/xmlHrefs";
import {
EmptyState,
@@ -39,6 +39,7 @@ import { EMPTY_IMPORT_NAME_NAMESPACE_IDENTIFIER } from "../includedModels/Includ
import { useDmnEditor } from "../DmnEditorContext";
import { SearchInput } from "@patternfly/react-core/dist/js/components/SearchInput";
import { TimesIcon } from "@patternfly/react-icons/dist/js/icons/times-icon";
+import { useExternalModels } from "../includedModels/DmnEditorDependenciesContext";
export type ExternalNode = {
externalDrgElementNamespace: string;
@@ -50,7 +51,12 @@ export const MIME_TYPE_FOR_DMN_EDITOR_EXTERNAL_NODES_FROM_INCLUDED_MODELS =
export function ExternalNodesPanel() {
const dmnEditorStoreApi = useDmnEditorStoreApi();
- const { dmnShapesByHref, externalDmnsByNamespace, importsByNamespace } = useDmnEditorDerivedStore();
+ const importsByNamespace = useDmnEditorStore((s) => s.computed(s).importsByNamespace());
+ const { externalModelsByNamespace } = useExternalModels();
+ const externalDmnsByNamespace = useDmnEditorStore(
+ (s) => s.computed(s).getExternalModelTypesByNamespace(externalModelsByNamespace).dmns
+ );
+ const dmnShapesByHref = useDmnEditorStore((s) => s.computed(s).indexes().dmnShapesByHref);
const { onRequestToResolvePath } = useDmnEditor();
const onDragStart = useCallback((event: React.DragEvent, externalNode: ExternalNode) => {
diff --git a/packages/dmn-editor/src/feel/InlineFeelNameInput.tsx b/packages/dmn-editor/src/feel/InlineFeelNameInput.tsx
index 2e0085d645d..0c8e8a4d7fb 100644
--- a/packages/dmn-editor/src/feel/InlineFeelNameInput.tsx
+++ b/packages/dmn-editor/src/feel/InlineFeelNameInput.tsx
@@ -19,9 +19,10 @@
import * as React from "react";
import { useCallback, useEffect, useRef, useState } from "react";
-import { DMN15_SPEC } from "../Dmn15Spec";
-import { UniqueNameIndex } from "../Dmn15Spec";
+import { DMN15_SPEC, UniqueNameIndex } from "../Dmn15Spec";
import { useFocusableElement } from "../focus/useFocusableElement";
+import { State } from "../store/Store";
+import { useDmnEditorStoreApi } from "../store/StoreContext";
export type OnInlineFeelNameRenamed = (newName: string) => void;
@@ -52,7 +53,7 @@ export function InlineFeelNameInput({
isReadonly: boolean;
isPlain: boolean;
shouldCommitOnBlur: boolean;
- allUniqueNames: UniqueNameIndex;
+ allUniqueNames: (s: State) => UniqueNameIndex;
placeholder?: string;
saveInvalidValue?: boolean;
validate?: typeof DMN15_SPEC.namedElement.isValidName;
@@ -75,14 +76,16 @@ export function InlineFeelNameInput({
}, 0);
}, []);
- const [isValid, setValid] = useState(_validate(id, name, allUniqueNames));
+ const dmnEditorStoreApi = useDmnEditorStoreApi();
+
+ const [isValid, setValid] = useState(_validate(id, name, allUniqueNames(dmnEditorStoreApi.getState())));
const updateIsValidFlag = useCallback(
(name: string) => {
- const isValid = _validate(id, name, allUniqueNames);
+ const isValid = _validate(id, name, allUniqueNames(dmnEditorStoreApi.getState()));
setValid(isValid);
return isValid;
},
- [_validate, allUniqueNames, id]
+ [_validate, allUniqueNames, dmnEditorStoreApi, id]
);
useEffect(() => {
diff --git a/packages/dmn-editor/src/focus/useFocusableElement.ts b/packages/dmn-editor/src/focus/useFocusableElement.ts
index d4b0823497b..3c8853a195f 100644
--- a/packages/dmn-editor/src/focus/useFocusableElement.ts
+++ b/packages/dmn-editor/src/focus/useFocusableElement.ts
@@ -18,16 +18,17 @@
*/
import { useEffect } from "react";
-import { useDmnEditorStore, useDmnEditorStoreApi } from "../store/Store";
+import { useDmnEditorStore, useDmnEditorStoreApi } from "../store/StoreContext";
export function useFocusableElement(
ref: React.RefObject<HTMLInputElement>,
id: string | undefined,
before?: (cb: () => void) => void
) {
- const focus = useDmnEditorStore((s) => s.focus);
const dmnEditorStoreApi = useDmnEditorStoreApi();
+ const shoudFocus = useDmnEditorStore((s) => s.focus.consumableId === id);
+
useEffect(() => {
if (!id) {
return;
@@ -41,8 +42,8 @@ export function useFocusableElement(
});
};
- if (focus.consumableId === id && ref.current) {
+ if (shoudFocus && ref.current) {
before?.(cb) ?? cb();
}
- }, [before, dmnEditorStoreApi, focus.consumableId, id, ref]);
+ }, [before, dmnEditorStoreApi, id, ref, shoudFocus]);
}
diff --git a/packages/dmn-editor/src/includedModels/DmnEditorDependenciesContext.tsx b/packages/dmn-editor/src/includedModels/DmnEditorDependenciesContext.tsx
index c2cae5a95b2..462b5f2fac8 100644
--- a/packages/dmn-editor/src/includedModels/DmnEditorDependenciesContext.tsx
+++ b/packages/dmn-editor/src/includedModels/DmnEditorDependenciesContext.tsx
@@ -18,7 +18,7 @@
*/
import * as React from "react";
-import { useContext } from "react";
+import { useContext, useMemo } from "react";
import {
OnRequestExternalModelsAvailableToInclude,
OnRequestExternalModelByPath,
@@ -41,5 +41,18 @@ export function DmnEditorExternalModelsContextProvider(
_props: React.PropsWithChildren<DmnEditorExternalModelsContextType>
) {
const { children, ...props } = _props;
- return <DmnEditorExternalModelsContext.Provider value={props}>{children}</DmnEditorExternalModelsContext.Provider>;
+
+ const value = useMemo<DmnEditorExternalModelsContextType>(() => {
+ return {
+ externalModelsByNamespace: props.externalModelsByNamespace,
+ onRequestExternalModelByPath: props.onRequestExternalModelByPath,
+ onRequestExternalModelsAvailableToInclude: props.onRequestExternalModelsAvailableToInclude,
+ };
+ }, [
+ props.externalModelsByNamespace,
+ props.onRequestExternalModelByPath,
+ props.onRequestExternalModelsAvailableToInclude,
+ ]);
+
+ return <DmnEditorExternalModelsContext.Provider value={value}>{children}</DmnEditorExternalModelsContext.Provider>;
}
diff --git a/packages/dmn-editor/src/includedModels/IncludedModels.tsx b/packages/dmn-editor/src/includedModels/IncludedModels.tsx
index 3c4ecf7bc8e..03737d2a50c 100644
--- a/packages/dmn-editor/src/includedModels/IncludedModels.tsx
+++ b/packages/dmn-editor/src/includedModels/IncludedModels.tsx
@@ -19,7 +19,6 @@
import * as React from "react";
import { generateUuid } from "@kie-tools/boxed-expression-component/dist/api";
-import { ns as dmn15ns } from "@kie-tools/dmn-marshaller/dist/schemas/dmn-1_5/ts-gen/meta";
import { ns as dmn12ns } from "@kie-tools/dmn-marshaller/dist/schemas/dmn-1_2/ts-gen/meta";
import { DMN15__tImport } from "@kie-tools/dmn-marshaller/dist/schemas/dmn-1_5/ts-gen/types";
import { Button, ButtonVariant } from "@patternfly/react-core/dist/js/components/Button";
@@ -43,8 +42,7 @@ import { InlineFeelNameInput, OnInlineFeelNameRenamed } from "../feel/InlineFeel
import { addImport } from "../mutations/addImport";
import { deleteImport } from "../mutations/deleteImport";
import { renameImport } from "../mutations/renameImport";
-import { useDmnEditorDerivedStore } from "../store/DerivedStore";
-import { useDmnEditorStore, useDmnEditorStoreApi } from "../store/Store";
+import { useDmnEditorStore, useDmnEditorStoreApi } from "../store/StoreContext";
import { KIE_UNKNOWN_NAMESPACE } from "../kie/kie";
import { ExternalModelLabel } from "./ExternalModelLabel";
import { useExternalModels } from "./DmnEditorDependenciesContext";
@@ -56,6 +54,7 @@ import { Dropdown, DropdownItem, KebabToggle } from "@patternfly/react-core/dist
import { TrashIcon } from "@patternfly/react-icons/dist/js/icons/trash-icon";
import { useInViewSelect } from "../responsiveness/useInViewSelect";
import { useCancelableEffect } from "@kie-tools-core/react-hooks/dist/useCancelableEffect";
+import { State } from "../store/Store";
export const EMPTY_IMPORT_NAME_NAMESPACE_IDENTIFIER = "<Default>";
@@ -66,12 +65,11 @@ const namespaceForNewImportsByFileExtension: Record<string, string> = {
export function IncludedModels() {
const dmnEditorStoreApi = useDmnEditorStoreApi();
- const thisDmn = useDmnEditorStore((s) => s.dmn);
- const thisDmnsImports = useMemo(() => thisDmn.model.definitions.import ?? [], [thisDmn.model.definitions.import]);
+ const thisDmnsImports = useDmnEditorStore((s) => s.dmn.model.definitions.import ?? []);
const { externalContextDescription, externalContextName, dmnEditorRootElementRef, onRequestToResolvePath } =
useDmnEditor();
- const { importsByNamespace, allFeelVariableUniqueNames } = useDmnEditorDerivedStore();
+ const importsByNamespace = useDmnEditorStore((s) => s.computed(s).importsByNamespace());
const { externalModelsByNamespace, onRequestExternalModelsAvailableToInclude, onRequestExternalModelByPath } =
useExternalModels();
@@ -126,10 +124,11 @@ export function IncludedModels() {
}, []);
const add = useCallback(() => {
+ const s = dmnEditorStoreApi.getState();
if (
!selectedPathRelativeToThisDmn ||
!selectedModel ||
- !DMN15_SPEC.IMPORT.name.isValid(generateUuid(), importName, allFeelVariableUniqueNames)
+ !DMN15_SPEC.IMPORT.name.isValid(generateUuid(), importName, s.computed(s).getAllFeelVariableUniqueNames())
) {
return;
}
@@ -166,7 +165,7 @@ export function IncludedModels() {
}, 5000); // Give it time for the `externalModelsByNamespace` object to be reassembled externally.
cancel();
- }, [selectedPathRelativeToThisDmn, selectedModel, importName, allFeelVariableUniqueNames, dmnEditorStoreApi, cancel]);
+ }, [dmnEditorStoreApi, selectedPathRelativeToThisDmn, selectedModel, importName, cancel]);
const [modelPathRelativeToThisDmn, setModelPathsRelativeToThisDmn] = useState<string[] | undefined>(undefined);
useCancelableEffect(
@@ -234,6 +233,8 @@ export function IncludedModels() {
const selectToggleRef = useRef<HTMLButtonElement>(null);
const inViewSelect = useInViewSelect(dmnEditorRootElementRef, selectToggleRef);
+ const getAllUniqueNames = useCallback((s: State) => s.computed(s).getAllFeelVariableUniqueNames(), []);
+
return (
<>
<Modal
@@ -343,7 +344,7 @@ export function IncludedModels() {
shouldCommitOnBlur={true}
className={"pf-c-form-control"}
onRenamed={setImportName}
- allUniqueNames={allFeelVariableUniqueNames}
+ allUniqueNames={getAllUniqueNames}
/>
</FormGroup>
<br />
@@ -436,6 +437,7 @@ function IncludedModelCard({
isReadonly: boolean;
}) {
const dmnEditorStoreApi = useDmnEditorStoreApi();
+
const { onRequestToJumpToPath, onRequestToResolvePath } = useDmnEditor();
const remove = useCallback(
@@ -447,15 +449,21 @@ function IncludedModelCard({
[dmnEditorStoreApi]
);
- const { allFeelVariableUniqueNames, allTopLevelDataTypesByFeelName } = useDmnEditorDerivedStore();
+ const { externalModelsByNamespace } = useExternalModels();
const rename = useCallback<OnInlineFeelNameRenamed>(
(newName) => {
dmnEditorStoreApi.setState((state) => {
- renameImport({ definitions: state.dmn.model.definitions, index, newName, allTopLevelDataTypesByFeelName });
+ renameImport({
+ definitions: state.dmn.model.definitions,
+ index,
+ newName,
+ allTopLevelDataTypesByFeelName: state.computed(state).getDataTypes(externalModelsByNamespace)
+ .allTopLevelDataTypesByFeelName,
+ });
});
},
- [allTopLevelDataTypesByFeelName, dmnEditorStoreApi, index]
+ [dmnEditorStoreApi, externalModelsByNamespace, index]
);
const extension = useMemo(() => {
@@ -521,7 +529,7 @@ function IncludedModelCard({
<InlineFeelNameInput
placeholder={EMPTY_IMPORT_NAME_NAMESPACE_IDENTIFIER}
isPlain={true}
- allUniqueNames={allFeelVariableUniqueNames}
+ allUniqueNames={useCallback((s) => s.computed(s).getAllFeelVariableUniqueNames(), [])}
id={_import["@_id"]!}
name={_import["@_name"]}
isReadonly={false}
@@ -576,15 +584,21 @@ function UnknownIncludedModelCard({
[dmnEditorStoreApi]
);
- const { allFeelVariableUniqueNames, allTopLevelDataTypesByFeelName } = useDmnEditorDerivedStore();
+ const { externalModelsByNamespace } = useExternalModels();
const rename = useCallback<OnInlineFeelNameRenamed>(
(newName) => {
dmnEditorStoreApi.setState((state) => {
- renameImport({ definitions: state.dmn.model.definitions, index, newName, allTopLevelDataTypesByFeelName });
+ renameImport({
+ definitions: state.dmn.model.definitions,
+ index,
+ newName,
+ allTopLevelDataTypesByFeelName: state.computed(state).getDataTypes(externalModelsByNamespace)
+ .allTopLevelDataTypesByFeelName,
+ });
});
},
- [allTopLevelDataTypesByFeelName, dmnEditorStoreApi, index]
+ [dmnEditorStoreApi, externalModelsByNamespace, index]
);
const extension = useMemo(() => {
@@ -635,7 +649,7 @@ function UnknownIncludedModelCard({
<InlineFeelNameInput
placeholder={EMPTY_IMPORT_NAME_NAMESPACE_IDENTIFIER}
isPlain={true}
- allUniqueNames={allFeelVariableUniqueNames}
+ allUniqueNames={useCallback((s) => s.computed(s).getAllFeelVariableUniqueNames(), [])}
id={_import["@_id"]!}
name={_import["@_name"]}
isReadonly={false}
diff --git a/packages/dmn-editor/src/mutations/addConnectedNode.ts b/packages/dmn-editor/src/mutations/addConnectedNode.ts
index d20a718927b..e356a302a68 100644
--- a/packages/dmn-editor/src/mutations/addConnectedNode.ts
+++ b/packages/dmn-editor/src/mutations/addConnectedNode.ts
@@ -29,7 +29,7 @@ import {
import { EdgeType, NodeType } from "../diagram/connections/graphStructure";
import { AutoPositionedEdgeMarker } from "../diagram/edges/AutoPositionedEdgeMarker";
import { EDGE_TYPES } from "../diagram/edges/EdgeTypes";
-import { getBoundsCenterPoint } from "../diagram/maths/DmnMaths";
+import { getDmnBoundsCenterPoint } from "../diagram/maths/DmnMaths";
import { NODE_TYPES } from "../diagram/nodes/NodeTypes";
import { switchExpression } from "@kie-tools-core/switch-expression-ts";
import { NodeNature, nodeNatures } from "./NodeNature";
@@ -171,7 +171,7 @@ export function addConnectedNode({
"@_dmnElementRef": newEdgeId,
"@_sourceElement": sourceNode.shapeId,
"@_targetElement": newShapeId,
- "di:waypoint": [getBoundsCenterPoint(sourceNode.bounds), getBoundsCenterPoint(newNode.bounds)],
+ "di:waypoint": [getDmnBoundsCenterPoint(sourceNode.bounds), getDmnBoundsCenterPoint(newNode.bounds)],
});
repopulateInputDataAndDecisionsOnAllDecisionServices({ definitions });
diff --git a/packages/dmn-editor/src/mutations/addEdge.ts b/packages/dmn-editor/src/mutations/addEdge.ts
index f328d071d32..ff98be9a09d 100644
--- a/packages/dmn-editor/src/mutations/addEdge.ts
+++ b/packages/dmn-editor/src/mutations/addEdge.ts
@@ -38,6 +38,7 @@ import { addOrGetDrd } from "./addOrGetDrd";
import { Unpacked } from "../tsExt/tsExt";
import { repopulateInputDataAndDecisionsOnAllDecisionServices } from "./repopulateInputDataAndDecisionsOnDecisionService";
import { DmnDiagramNodeData } from "../diagram/nodes/Nodes";
+import { AutoPositionedEdgeMarker } from "../diagram/edges/AutoPositionedEdgeMarker";
export function addEdge({
definitions,
@@ -64,7 +65,12 @@ export function addEdge({
shapeId: string | undefined;
index: number;
};
- edge: { type: EdgeType; targetHandle: PositionalNodeHandleId; sourceHandle: PositionalNodeHandleId };
+ edge: {
+ type: EdgeType;
+ targetHandle: PositionalNodeHandleId;
+ sourceHandle: PositionalNodeHandleId;
+ autoPositionedEdgeMarker: AutoPositionedEdgeMarker | undefined;
+ };
keepWaypoints: boolean;
}) {
if (!_checkIsValidConnection(sourceNode, targetNode, edge.type)) {
@@ -168,7 +174,9 @@ export function addEdge({
const newDmnEdge: Unpacked<typeof diagramElements> = {
__$$element: "dmndi:DMNEdge",
- "@_id": withoutDiscreteAutoPosinitioningMarker(removedDmnEdge?.["@_id"] ?? generateUuid()),
+ "@_id":
+ withoutDiscreteAutoPosinitioningMarker(removedDmnEdge?.["@_id"] ?? generateUuid()) +
+ (edge.autoPositionedEdgeMarker ?? ""),
"@_dmnElementRef": existingEdgeId ?? newEdgeId,
"@_sourceElement": sourceNode.shapeId,
"@_targetElement": targetNode.shapeId,
diff --git a/packages/dmn-editor/src/mutations/deleteEdge.ts b/packages/dmn-editor/src/mutations/deleteEdge.ts
index bdae048fcc2..925779ea486 100644
--- a/packages/dmn-editor/src/mutations/deleteEdge.ts
+++ b/packages/dmn-editor/src/mutations/deleteEdge.ts
@@ -27,52 +27,78 @@ import { addOrGetDrd } from "./addOrGetDrd";
import { DmnDiagramEdgeData } from "../diagram/edges/Edges";
import { repopulateInputDataAndDecisionsOnAllDecisionServices } from "./repopulateInputDataAndDecisionsOnDecisionService";
+export enum EdgeDeletionMode {
+ FORM_DRG_AND_ALL_DRDS,
+ FROM_CURRENT_DRD_ONLY,
+}
+
export function deleteEdge({
definitions,
drdIndex,
edge,
+ mode,
}: {
definitions: DMN15__tDefinitions;
drdIndex: number;
edge: { id: string; dmnObject: DmnDiagramEdgeData["dmnObject"] };
+ mode: EdgeDeletionMode;
}) {
- const { diagramElements } = addOrGetDrd({ definitions, drdIndex });
+ if (edge.dmnObject.namespace !== definitions["@_namespace"]) {
+ console.debug("DMN MUTATION: Can't delete an edge that's from an external node.");
+ return { dmnEdge: undefined };
+ }
- const dmnObjects: DMN15__tDefinitions["artifact"] | DMN15__tDefinitions["drgElement"] =
+ const dmnObjects: DMN15__tDefinitions["drgElement" | "artifact"] =
switchExpression(edge?.dmnObject.type, {
association: definitions.artifact,
+ group: definitions.artifact,
default: definitions.drgElement,
}) ?? [];
const dmnObjectIndex = dmnObjects.findIndex((d) => d["@_id"] === edge.dmnObject.id);
if (dmnObjectIndex < 0) {
- throw new Error(`Can't find DMN element with ID ${edge.dmnObject.id}`);
+ throw new Error(`DMN MUTATION: Can't find DMN element with ID ${edge.dmnObject.id}`);
}
- const requirements =
- switchExpression(edge?.dmnObject.requirementType, {
- // Casting to DMN15__tDecision because if has all types of requirement, but not necessarily that's true.
- informationRequirement: (dmnObjects[dmnObjectIndex] as DMN15__tDecision).informationRequirement,
- knowledgeRequirement: (dmnObjects[dmnObjectIndex] as DMN15__tDecision).knowledgeRequirement,
- authorityRequirement: (dmnObjects[dmnObjectIndex] as DMN15__tDecision).authorityRequirement,
- association: dmnObjects,
- }) ?? [];
+ if (mode === EdgeDeletionMode.FORM_DRG_AND_ALL_DRDS) {
+ const requirements =
+ switchExpression(edge?.dmnObject.requirementType, {
+ // Casting to DMN15__tDecision because if has all types of requirement, but not necessarily that's true.
+ informationRequirement: (dmnObjects[dmnObjectIndex] as DMN15__tDecision).informationRequirement,
+ knowledgeRequirement: (dmnObjects[dmnObjectIndex] as DMN15__tDecision).knowledgeRequirement,
+ authorityRequirement: (dmnObjects[dmnObjectIndex] as DMN15__tDecision).authorityRequirement,
+ association: dmnObjects,
+ }) ?? [];
- // Deleting the requirement
- const requirementIndex = (requirements ?? []).findIndex((d) => d["@_id"] === edge.id);
- if (requirementIndex >= 0) {
- requirements?.splice(requirementIndex, 1);
+ // Deleting the requirement
+ const requirementIndex = (requirements ?? []).findIndex((d) => d["@_id"] === edge.id);
+ if (requirementIndex >= 0) {
+ requirements?.splice(requirementIndex, 1);
+ }
}
// Deleting the DMNEdge's
- let dmnEdge: DMNDI15__DMNEdge | undefined;
- const dmnEdgeIndex = (diagramElements ?? []).findIndex((d) => d["@_dmnElementRef"] === edge.id);
- if (dmnEdgeIndex >= 0) {
- dmnEdge = diagramElements[dmnEdgeIndex];
- diagramElements?.splice(dmnEdgeIndex, 1);
+ let deletedDmnEdgeOnCurrentDrd: DMNDI15__DMNEdge | undefined;
+
+ const drdCount = (definitions["dmndi:DMNDI"]?.["dmndi:DMNDiagram"] ?? []).length;
+ for (let i = 0; i < drdCount; i++) {
+ const { diagramElements } = addOrGetDrd({ definitions, drdIndex: i });
+
+ if (mode === EdgeDeletionMode.FROM_CURRENT_DRD_ONLY && i !== drdIndex) {
+ continue;
+ }
+
+ const dmnEdgeIndex = (diagramElements ?? []).findIndex((d) => d["@_dmnElementRef"] === edge.id);
+ if (dmnEdgeIndex >= 0) {
+ if (i === drdIndex) {
+ deletedDmnEdgeOnCurrentDrd = diagramElements[dmnEdgeIndex];
+ }
+
+ diagramElements?.splice(dmnEdgeIndex, 1);
+ }
}
repopulateInputDataAndDecisionsOnAllDecisionServices({ definitions });
- return { dmnEdge };
+ return { deletedDmnEdgeOnCurrentDrd };
}
diff --git a/packages/dmn-editor/src/mutations/deleteNode.ts b/packages/dmn-editor/src/mutations/deleteNode.ts
index ab7898262c9..363fd2f4627 100644
--- a/packages/dmn-editor/src/mutations/deleteNode.ts
+++ b/packages/dmn-editor/src/mutations/deleteNode.ts
@@ -17,65 +17,174 @@
* under the License.
*/
-import { DMN15__tDefinitions } from "@kie-tools/dmn-marshaller/dist/schemas/dmn-1_5/ts-gen/types";
+import { DMN15__tDefinitions, DMNDI15__DMNShape } from "@kie-tools/dmn-marshaller/dist/schemas/dmn-1_5/ts-gen/types";
import { NodeNature } from "./NodeNature";
import { addOrGetDrd } from "./addOrGetDrd";
import { repopulateInputDataAndDecisionsOnAllDecisionServices } from "./repopulateInputDataAndDecisionsOnDecisionService";
import { XmlQName, buildXmlQName } from "@kie-tools/xml-parser-ts/dist/qNames";
import { getNewDmnIdRandomizer } from "../idRandomizer/dmnIdRandomizer";
+import { buildXmlHref } from "../xml/xmlHrefs";
+import { Unpacked } from "../tsExt/tsExt";
+import { DrgEdge } from "../diagram/graph/graph";
+import { EdgeDeletionMode, deleteEdge } from "./deleteEdge";
+
+export enum NodeDeletionMode {
+ FORM_DRG_AND_ALL_DRDS,
+ FROM_CURRENT_DRD_ONLY,
+}
export function deleteNode({
definitions,
+ drgEdges,
drdIndex,
nodeNature,
dmnObjectId,
dmnObjectQName,
+ dmnObjectNamespace,
+ mode,
}: {
definitions: DMN15__tDefinitions;
+ drgEdges: DrgEdge[];
drdIndex: number;
nodeNature: NodeNature;
+ dmnObjectNamespace: string | undefined;
dmnObjectId: string | undefined;
dmnObjectQName: XmlQName;
-}) {
- const { diagramElements, widthsExtension } = addOrGetDrd({ definitions, drdIndex });
+ mode: NodeDeletionMode;
+}): {
+ deletedDmnObject: Unpacked<DMN15__tDefinitions["drgElement" | "artifact"]> | undefined;
+ deletedDmnShapeOnCurrentDrd: DMNDI15__DMNShape | undefined;
+} {
+ if (
+ mode === NodeDeletionMode.FROM_CURRENT_DRD_ONLY &&
+ !canRemoveNodeFromDrdOnly({
+ definitions,
+ drdIndex,
+ dmnObjectNamespace,
+ dmnObjectId,
+ })
+ ) {
+ console.warn("DMN MUTATION: Cannot hide a Decision that's contained by a Decision Service from a DRD.");
+ return { deletedDmnObject: undefined, deletedDmnShapeOnCurrentDrd: undefined };
+ }
- // Edges need to be deleted by a separate call to `deleteEdge` prior to this.
+ // A DRD doesn't necessarily renders all edges of the DRG, so we need to look for what DRG edges to delete when deleting a node from any DRD.
+ if (mode === NodeDeletionMode.FORM_DRG_AND_ALL_DRDS) {
+ const nodeId = buildXmlHref({ namespace: dmnObjectNamespace, id: dmnObjectId! });
+ for (let i = 0; i < drgEdges.length; i++) {
+ const drgEdge = drgEdges[i];
+ // Only delete edges that end at or start from the node being deleted.
+ if (drgEdge.sourceId === nodeId || drgEdge.targetId === nodeId) {
+ deleteEdge({
+ definitions,
+ drdIndex,
+ mode: EdgeDeletionMode.FORM_DRG_AND_ALL_DRDS,
+ edge: {
+ id: drgEdge.id,
+ dmnObject: drgEdge.dmnObject,
+ },
+ });
+ }
+ }
+ }
- // delete the DMNShape
- const shapeDmnElementRef = buildXmlQName(dmnObjectQName);
- diagramElements?.splice(
- (diagramElements ?? []).findIndex((d) => d["@_dmnElementRef"] === shapeDmnElementRef),
- 1
- );
+ let dmnObject: Unpacked<DMN15__tDefinitions["drgElement" | "artifact"]> | undefined;
// External or unknown nodes don't have a dmnObject associated with it, just the shape..
if (!dmnObjectQName.prefix) {
// Delete the dmnObject itself
if (nodeNature === NodeNature.ARTIFACT) {
- definitions.artifact?.splice(
- (definitions.artifact ?? []).findIndex((a) => a["@_id"] === dmnObjectId),
- 1
- );
+ if (mode === NodeDeletionMode.FORM_DRG_AND_ALL_DRDS) {
+ const nodeIndex = (definitions.artifact ?? []).findIndex((a) => a["@_id"] === dmnObjectId);
+ dmnObject = definitions.artifact?.splice(nodeIndex, 1)?.[0];
+ }
} else if (nodeNature === NodeNature.DRG_ELEMENT) {
- const deleted = definitions.drgElement?.splice(
- (definitions.drgElement ?? []).findIndex((d) => d["@_id"] === dmnObjectId),
- 1
- );
-
- const deletedIdsOnDrgElementTree = getNewDmnIdRandomizer()
- .ack({ json: deleted, type: "DMN15__tDefinitions", attr: "drgElement" })
- .getOriginalIds();
-
- // Delete widths
- widthsExtension["kie:ComponentWidths"] = widthsExtension["kie:ComponentWidths"]?.filter(
- (w) => !deletedIdsOnDrgElementTree.has(w["@_dmnElementRef"]!)
- );
+ const nodeIndex = (definitions.drgElement ?? []).findIndex((d) => d["@_id"] === dmnObjectId);
+ dmnObject =
+ mode === NodeDeletionMode.FORM_DRG_AND_ALL_DRDS
+ ? definitions.drgElement?.splice(nodeIndex, 1)?.[0]
+ : definitions.drgElement?.[nodeIndex];
} else if (nodeNature === NodeNature.UNKNOWN) {
// Ignore. There's no dmnObject here.
} else {
throw new Error(`Unknown node nature '${nodeNature}'.`);
}
+
+ if (!dmnObject) {
+ throw new Error(`DMN MUTATION: Can't delete DMN object that doesn't exist: ID=${dmnObjectId}`);
+ }
+ }
+
+ const shapeDmnElementRef = buildXmlQName(dmnObjectQName);
+
+ // Deleting the DMNShape's
+ let deletedDmnShapeOnCurrentDrd: DMNDI15__DMNShape | undefined;
+
+ const deletedIdsOnDmnObjectTree = dmnObject
+ ? getNewDmnIdRandomizer()
+ .ack({ json: [dmnObject], type: "DMN15__tDefinitions", attr: "drgElement" })
+ .getOriginalIds()
+ : new Set<string>();
+
+ const drdCount = (definitions["dmndi:DMNDI"]?.["dmndi:DMNDiagram"] ?? []).length;
+ for (let i = 0; i < drdCount; i++) {
+ if (mode === NodeDeletionMode.FROM_CURRENT_DRD_ONLY && i !== drdIndex) {
+ continue;
+ }
+
+ const { diagramElements, widthsExtension } = addOrGetDrd({ definitions, drdIndex: i });
+ const dmnShapeIndex = (diagramElements ?? []).findIndex((d) => d["@_dmnElementRef"] === shapeDmnElementRef);
+ if (dmnShapeIndex >= 0) {
+ if (i === drdIndex) {
+ deletedDmnShapeOnCurrentDrd = diagramElements[dmnShapeIndex];
+ }
+
+ diagramElements?.splice(dmnShapeIndex, 1);
+ }
+
+ // Delete widths
+ widthsExtension["kie:ComponentWidths"] = widthsExtension["kie:ComponentWidths"]?.filter(
+ (w) => !deletedIdsOnDmnObjectTree.has(w["@_dmnElementRef"]!) // Only works because xsd:IDs are exactly the same as QNames when the QName is not prefixed.
+ );
}
repopulateInputDataAndDecisionsOnAllDecisionServices({ definitions });
+
+ return {
+ deletedDmnObject: mode === NodeDeletionMode.FORM_DRG_AND_ALL_DRDS ? dmnObject : undefined,
+ deletedDmnShapeOnCurrentDrd,
+ };
+}
+
+export function canRemoveNodeFromDrdOnly({
+ definitions,
+ drdIndex,
+ dmnObjectNamespace,
+ dmnObjectId,
+}: {
+ dmnObjectNamespace: string | undefined;
+ dmnObjectId: string | undefined;
+ definitions: DMN15__tDefinitions;
+ drdIndex: number;
+}) {
+ const { diagramElements } = addOrGetDrd({ definitions, drdIndex });
+
+ const dmnObjectHref = buildXmlHref({ namespace: dmnObjectNamespace, id: dmnObjectId! });
+
+ const containingDecisionService = definitions.drgElement?.find(
+ (drgElement) =>
+ drgElement.__$$element === "decisionService" &&
+ [...(drgElement.encapsulatedDecision ?? []), ...(drgElement.outputDecision ?? [])].some(
+ (dd) => dd["@_href"] === dmnObjectHref
+ )
+ );
+
+ const isContainingDecisionServicePresentInTheDrd =
+ containingDecisionService &&
+ diagramElements.some(
+ (s) =>
+ s.__$$element === "dmndi:DMNShape" &&
+ s["@_dmnElementRef"] === buildXmlQName({ type: "xml-qname", localPart: containingDecisionService["@_id"]! })
+ );
+ return !containingDecisionService || !isContainingDecisionServicePresentInTheDrd;
}
diff --git a/packages/dmn-editor/src/overlaysPanel/OverlaysPanel.tsx b/packages/dmn-editor/src/overlaysPanel/OverlaysPanel.tsx
index 38929f3b82b..f964ee90256 100644
--- a/packages/dmn-editor/src/overlaysPanel/OverlaysPanel.tsx
+++ b/packages/dmn-editor/src/overlaysPanel/OverlaysPanel.tsx
@@ -22,7 +22,7 @@ import { Switch } from "@patternfly/react-core/dist/js/components/Switch";
import { Form, FormGroup } from "@patternfly/react-core/dist/js/components/Form";
import { Divider } from "@patternfly/react-core/dist/js/components/Divider";
import { Slider } from "@patternfly/react-core/dist/js/components/Slider";
-import { useDmnEditorStore, useDmnEditorStoreApi } from "../store/Store";
+import { useDmnEditorStore, useDmnEditorStoreApi } from "../store/StoreContext";
const MIN_SNAP = 5;
const MAX_SNAP = 50;
@@ -115,17 +115,6 @@ export function OverlaysPanel() {
}
/>
</FormGroup>
- {/* <FormGroup label={"Custom node styles"}>
- <Switch
- aria-label={"ustom node styles"}
- isChecked={diagram.overlays.enableCustomNodeStyles}
- onChange={(newValue) =>
- dmnEditorStoreApi.setState((state) => {
- state.diagram.overlays.enableCustomNodeStyles = newValue;
- })
- }
- />
- </FormGroup> */}
<FormGroup label={"Show data type toolbar on nodes"}>
<Switch
aria-label={"Show data type toolbar on nodes"}
@@ -140,10 +129,10 @@ export function OverlaysPanel() {
<FormGroup label={"Enable styles"}>
<Switch
aria-label={"Show data type toolbar on nodes"}
- isChecked={diagram.overlays.enableStyles}
+ isChecked={diagram.overlays.enableCustomNodeStyles}
onChange={(newValue) =>
dmnEditorStoreApi.setState((state) => {
- state.diagram.overlays.enableStyles = newValue;
+ state.diagram.overlays.enableCustomNodeStyles = newValue;
})
}
/>
diff --git a/packages/dmn-editor/src/propertiesPanel/BeePropertiesPanel.tsx b/packages/dmn-editor/src/propertiesPanel/BeePropertiesPanel.tsx
index de9b031d68b..1321b95629b 100644
--- a/packages/dmn-editor/src/propertiesPanel/BeePropertiesPanel.tsx
+++ b/packages/dmn-editor/src/propertiesPanel/BeePropertiesPanel.tsx
@@ -24,26 +24,30 @@ import {
DrawerHead,
DrawerPanelContent,
} from "@patternfly/react-core/dist/js/components/Drawer";
-import { useDmnEditorStore, useDmnEditorStoreApi } from "../store/Store";
+import { useDmnEditorStore, useDmnEditorStoreApi } from "../store/StoreContext";
import { useMemo } from "react";
-import { useDmnEditorDerivedStore } from "../store/DerivedStore";
import { buildXmlHref } from "../xml/xmlHrefs";
import { SingleNodeProperties } from "./SingleNodeProperties";
+import { useExternalModels } from "../includedModels/DmnEditorDependenciesContext";
export function BeePropertiesPanel() {
const dmnEditorStoreApi = useDmnEditorStoreApi();
const { selectedObjectId, activeDrgElementId } = useDmnEditorStore((s) => s.boxedExpressionEditor);
- const { nodesById } = useDmnEditorDerivedStore();
+ const { externalModelsByNamespace } = useExternalModels();
+ const node = useDmnEditorStore((s) =>
+ activeDrgElementId
+ ? s
+ .computed(s)
+ .getDiagramData(externalModelsByNamespace)
+ .nodesById.get(buildXmlHref({ id: activeDrgElementId }))
+ : undefined
+ );
const shouldDisplayDecisionOrBkmProps = useMemo(
() => selectedObjectId === undefined || (selectedObjectId && selectedObjectId === activeDrgElementId),
[activeDrgElementId, selectedObjectId]
);
- const node = useMemo(() => {
- return activeDrgElementId ? nodesById.get(buildXmlHref({ id: activeDrgElementId })) : undefined;
- }, [activeDrgElementId, nodesById]);
-
return (
<>
{node && (
diff --git a/packages/dmn-editor/src/propertiesPanel/BkmProperties.tsx b/packages/dmn-editor/src/propertiesPanel/BkmProperties.tsx
index dd346dcfc25..1383fe45b67 100644
--- a/packages/dmn-editor/src/propertiesPanel/BkmProperties.tsx
+++ b/packages/dmn-editor/src/propertiesPanel/BkmProperties.tsx
@@ -24,12 +24,12 @@ import { FormGroup } from "@patternfly/react-core/dist/js/components/Form";
import { TextArea } from "@patternfly/react-core/dist/js/components/TextArea";
import { DocumentationLinksFormGroup } from "./DocumentationLinksFormGroup";
import { TypeRefSelector } from "../dataTypes/TypeRefSelector";
-import { useDmnEditorStore, useDmnEditorStoreApi } from "../store/Store";
+import { useDmnEditorStore, useDmnEditorStoreApi } from "../store/StoreContext";
import { renameDrgElement } from "../mutations/renameNode";
import { InlineFeelNameInput } from "../feel/InlineFeelNameInput";
-import { useDmnEditorDerivedStore } from "../store/DerivedStore";
import { useDmnEditor } from "../DmnEditorContext";
import { useResolvedTypeRef } from "../dataTypes/useResolvedTypeRef";
+import { useCallback } from "react";
export function BkmProperties({
bkm,
@@ -45,7 +45,6 @@ export function BkmProperties({
const thisDmnsNamespace = useDmnEditorStore((s) => s.dmn.model.definitions["@_namespace"]);
const isReadonly = !!namespace && namespace !== thisDmnsNamespace;
- const { allFeelVariableUniqueNames } = useDmnEditorDerivedStore();
const { dmnEditorRootElementRef } = useDmnEditor();
const resolvedTypeRef = useResolvedTypeRef(bkm.variable?.["@_typeRef"], namespace);
@@ -70,7 +69,7 @@ export function BkmProperties({
});
});
}}
- allUniqueNames={allFeelVariableUniqueNames}
+ allUniqueNames={useCallback((s) => s.computed(s).getAllFeelVariableUniqueNames(), [])}
/>
</FormGroup>
diff --git a/packages/dmn-editor/src/propertiesPanel/DecisionProperties.tsx b/packages/dmn-editor/src/propertiesPanel/DecisionProperties.tsx
index ccb6ed3a04d..4a571eb9cb0 100644
--- a/packages/dmn-editor/src/propertiesPanel/DecisionProperties.tsx
+++ b/packages/dmn-editor/src/propertiesPanel/DecisionProperties.tsx
@@ -24,12 +24,12 @@ import { FormGroup } from "@patternfly/react-core/dist/js/components/Form";
import { TextArea } from "@patternfly/react-core/dist/js/components/TextArea";
import { DocumentationLinksFormGroup } from "./DocumentationLinksFormGroup";
import { TypeRefSelector } from "../dataTypes/TypeRefSelector";
-import { useDmnEditorStore, useDmnEditorStoreApi } from "../store/Store";
+import { useDmnEditorStore, useDmnEditorStoreApi } from "../store/StoreContext";
import { renameDrgElement } from "../mutations/renameNode";
import { InlineFeelNameInput } from "../feel/InlineFeelNameInput";
-import { useDmnEditorDerivedStore } from "../store/DerivedStore";
import { useDmnEditor } from "../DmnEditorContext";
import { useResolvedTypeRef } from "../dataTypes/useResolvedTypeRef";
+import { useCallback } from "react";
export function DecisionProperties({
decision,
@@ -45,8 +45,6 @@ export function DecisionProperties({
const thisDmnsNamespace = useDmnEditorStore((s) => s.dmn.model.definitions["@_namespace"]);
const isReadonly = !!namespace && namespace !== thisDmnsNamespace;
- const { allFeelVariableUniqueNames } = useDmnEditorDerivedStore();
-
const { dmnEditorRootElementRef } = useDmnEditor();
const resolvedTypeRef = useResolvedTypeRef(decision.variable?.["@_typeRef"], namespace);
@@ -71,7 +69,7 @@ export function DecisionProperties({
});
});
}}
- allUniqueNames={allFeelVariableUniqueNames}
+ allUniqueNames={useCallback((s) => s.computed(s).getAllFeelVariableUniqueNames(), [])}
/>
</FormGroup>
diff --git a/packages/dmn-editor/src/propertiesPanel/DecisionServiceProperties.tsx b/packages/dmn-editor/src/propertiesPanel/DecisionServiceProperties.tsx
index 9c839201f64..3ae80477c27 100644
--- a/packages/dmn-editor/src/propertiesPanel/DecisionServiceProperties.tsx
+++ b/packages/dmn-editor/src/propertiesPanel/DecisionServiceProperties.tsx
@@ -28,16 +28,16 @@ import { FormGroup } from "@patternfly/react-core/dist/js/components/Form";
import { TextArea } from "@patternfly/react-core/dist/js/components/TextArea";
import { DocumentationLinksFormGroup } from "./DocumentationLinksFormGroup";
import { TypeRefSelector } from "../dataTypes/TypeRefSelector";
-import { useDmnEditorStore, useDmnEditorStoreApi } from "../store/Store";
-import { useMemo } from "react";
+import { useDmnEditorStore, useDmnEditorStoreApi } from "../store/StoreContext";
+import { useCallback, useMemo } from "react";
import { buildXmlHref, parseXmlHref } from "../xml/xmlHrefs";
import { DmnObjectListItem } from "../externalNodes/DmnObjectListItem";
import { renameDrgElement } from "../mutations/renameNode";
import { InlineFeelNameInput } from "../feel/InlineFeelNameInput";
-import { useDmnEditorDerivedStore } from "../store/DerivedStore";
import { Divider } from "@patternfly/react-core/dist/js/components/Divider";
import { useDmnEditor } from "../DmnEditorContext";
import { useResolvedTypeRef } from "../dataTypes/useResolvedTypeRef";
+import { useExternalModels } from "../includedModels/DmnEditorDependenciesContext";
export type AllKnownDrgElementsByHref = Map<
string,
@@ -56,7 +56,10 @@ export function DecisionServiceProperties({
const { setState } = useDmnEditorStoreApi();
const thisDmn = useDmnEditorStore((s) => s.dmn);
- const { externalDmnsByNamespace, allFeelVariableUniqueNames } = useDmnEditorDerivedStore();
+ const { externalModelsByNamespace } = useExternalModels();
+ const externalDmnsByNamespace = useDmnEditorStore(
+ (s) => s.computed(s).getExternalModelTypesByNamespace(externalModelsByNamespace).dmns
+ );
const allDrgElementsByHref = useMemo(() => {
const ret: AllKnownDrgElementsByHref = new Map();
@@ -107,7 +110,7 @@ export function DecisionServiceProperties({
});
});
}}
- allUniqueNames={allFeelVariableUniqueNames}
+ allUniqueNames={useCallback((s) => s.computed(s).getAllFeelVariableUniqueNames(), [])}
/>
</FormGroup>
diff --git a/packages/dmn-editor/src/propertiesPanel/DiagramPropertiesPanel.tsx b/packages/dmn-editor/src/propertiesPanel/DiagramPropertiesPanel.tsx
index 0c87e4b5111..2a6992c4302 100644
--- a/packages/dmn-editor/src/propertiesPanel/DiagramPropertiesPanel.tsx
+++ b/packages/dmn-editor/src/propertiesPanel/DiagramPropertiesPanel.tsx
@@ -21,13 +21,17 @@ import * as React from "react";
import { DrawerHead, DrawerPanelContent } from "@patternfly/react-core/dist/js/components/Drawer";
import { GlobalDiagramProperties } from "./GlobalDiagramProperties";
-import "./DiagramPropertiesPanel.css";
-import { useDmnEditorDerivedStore } from "../store/DerivedStore";
import { SingleNodeProperties } from "./SingleNodeProperties";
import { MultipleNodeProperties } from "./MultipleNodeProperties";
+import { useDmnEditorStore } from "../store/StoreContext";
+import { useExternalModels } from "../includedModels/DmnEditorDependenciesContext";
+import "./DiagramPropertiesPanel.css";
export function DiagramPropertiesPanel() {
- const { selectedNodesById } = useDmnEditorDerivedStore();
+ const { externalModelsByNamespace } = useExternalModels();
+ const selectedNodesById = useDmnEditorStore(
+ (s) => s.computed(s).getDiagramData(externalModelsByNamespace).selectedNodesById
+ );
return (
<DrawerPanelContent
diff --git a/packages/dmn-editor/src/propertiesPanel/DocumentationLinksFormGroup.tsx b/packages/dmn-editor/src/propertiesPanel/DocumentationLinksFormGroup.tsx
index ce7c1699f10..b0deae57865 100644
--- a/packages/dmn-editor/src/propertiesPanel/DocumentationLinksFormGroup.tsx
+++ b/packages/dmn-editor/src/propertiesPanel/DocumentationLinksFormGroup.tsx
@@ -19,7 +19,6 @@
import * as React from "react";
import { useMemo, useState, useCallback, useRef, useEffect } from "react";
-import "./DocumentationLinksFormGroup.css";
import { KIE__tAttachment } from "@kie-tools/dmn-marshaller/dist/schemas/kie-1_0/ts-gen/types";
import { Namespaced } from "@kie-tools/xml-parser-ts";
import { Text, TextVariants } from "@patternfly/react-core/dist/js/components/Text";
@@ -33,6 +32,7 @@ import { AngleRightIcon } from "@patternfly/react-icons/dist/js/icons/angle-righ
import { InlineFeelNameInput, invalidInlineFeelNameStyle } from "../feel/InlineFeelNameInput";
import { generateUuid } from "@kie-tools/boxed-expression-component/dist/api";
import { Draggable, DragAndDrop, useDraggableItemContext } from "../draggable/Draggable";
+import "./DocumentationLinksFormGroup.css";
const PLACEHOLDER_URL_TITLE = "Enter a title...";
const PLACEHOLDER_URL = "http://";
@@ -374,7 +374,7 @@ function DocumentationLinksInput({
// reset the changedByToogle
updatedOnToogle.current = false;
}}
- allUniqueNames={allUniqueNames}
+ allUniqueNames={() => allUniqueNames}
validate={validateTitle}
autoFocus={parentAutoFocus || autoFocus}
onKeyDown={(e) => {
@@ -400,7 +400,7 @@ function DocumentationLinksInput({
// reset the changedByToogle
updatedOnToogle.current = false;
}}
- allUniqueNames={allUniqueNames}
+ allUniqueNames={() => allUniqueNames}
validate={validateUrl}
saveInvalidValue={true}
onKeyDown={(e) => {
diff --git a/packages/dmn-editor/src/propertiesPanel/FontOptions.tsx b/packages/dmn-editor/src/propertiesPanel/FontOptions.tsx
index 26fd6f9854f..d8ba5278f6c 100644
--- a/packages/dmn-editor/src/propertiesPanel/FontOptions.tsx
+++ b/packages/dmn-editor/src/propertiesPanel/FontOptions.tsx
@@ -22,7 +22,8 @@ import { useState, useMemo, useCallback, useEffect } from "react";
import { FormSection } from "@patternfly/react-core/dist/js/components/Form";
import { PencilAltIcon } from "@patternfly/react-icons/dist/js/icons/pencil-alt-icon";
import { PropertiesPanelHeader } from "./PropertiesPanelHeader";
-import { State, useDmnEditorStoreApi } from "../store/Store";
+import { State } from "../store/Store";
+import { useDmnEditorStore, useDmnEditorStoreApi } from "../store/StoreContext";
import { NumberInput } from "@patternfly/react-core/dist/js/components/NumberInput";
import { DMNDI15__DMNShape } from "@kie-tools/dmn-marshaller/dist/schemas/dmn-1_5/ts-gen/types";
import { addOrGetDrd } from "../mutations/addOrGetDrd";
@@ -30,12 +31,11 @@ import { ToggleGroup, ToggleGroupItem } from "@patternfly/react-core/dist/js/com
import { Select, SelectVariant, SelectOption } from "@patternfly/react-core/dist/js/components/Select";
import { useInViewSelect } from "../responsiveness/useInViewSelect";
import { useDmnEditor } from "../DmnEditorContext";
-import { useDmnEditorDerivedStore } from "../store/DerivedStore";
-import "./FontOptions.css";
import { Button, ButtonVariant } from "@patternfly/react-core/dist/js/components/Button";
import { UndoAltIcon } from "@patternfly/react-icons/dist/js/icons/undo-alt-icon";
import { Tooltip } from "@patternfly/react-core/dist/js/components/Tooltip";
import { ColorPicker } from "./ColorPicker";
+import "./FontOptions.css";
// https://www.w3schools.com/cssref/css_websafe_fonts.php
// Array of [name, family]
@@ -67,7 +67,7 @@ export function FontOptions({ startExpanded, nodeIds }: { startExpanded: boolean
const dmnEditorStoreApi = useDmnEditorStoreApi();
const [isStyleSectionExpanded, setStyleSectionExpanded] = useState<boolean>(startExpanded);
- const { dmnShapesByHref } = useDmnEditorDerivedStore();
+ const dmnShapesByHref = useDmnEditorStore((s) => s.computed(s).indexes().dmnShapesByHref);
const shapes = useMemo(() => nodeIds.map((nodeId) => dmnShapesByHref.get(nodeId)), [dmnShapesByHref, nodeIds]);
const shapesStyle = useMemo(() => shapes.map((shape) => shape?.["di:Style"]), [shapes]);
@@ -208,7 +208,7 @@ export function FontOptions({ startExpanded, nodeIds }: { startExpanded: boolean
(newColor: string) => {
setTemporaryFontColor(newColor.replace("#", ""));
editShapeStyle((shapes, state) => {
- state!.diagram.editingStyle = true;
+ state!.diagram.isEditingStyle = true;
});
},
[editShapeStyle]
@@ -226,7 +226,7 @@ export function FontOptions({ startExpanded, nodeIds }: { startExpanded: boolean
green !== shape?.["di:Style"]?.["dmndi:FontColor"]?.["@_green"] &&
blue !== shape?.["di:Style"]?.["dmndi:FontColor"]?.["@_blue"]
) {
- state!.diagram.editingStyle = false;
+ state!.diagram.isEditingStyle = false;
shape!["di:Style"]!["dmndi:FontColor"] ??= { "@_blue": 0, "@_green": 0, "@_red": 0 };
shape!["di:Style"]!["dmndi:FontColor"]["@_red"] = red;
shape!["di:Style"]!["dmndi:FontColor"]["@_green"] = green;
@@ -243,7 +243,7 @@ export function FontOptions({ startExpanded, nodeIds }: { startExpanded: boolean
const onReset = useCallback(() => {
setTemporaryFontColor("000000");
editShapeStyle((shapes, state) => {
- state!.diagram.editingStyle = false;
+ state!.diagram.isEditingStyle = false;
shapes.forEach((shape) => {
shape["di:Style"]!["@_fontBold"] = undefined;
shape["di:Style"]!["@_fontItalic"] = undefined;
diff --git a/packages/dmn-editor/src/propertiesPanel/GlobalDiagramProperties.tsx b/packages/dmn-editor/src/propertiesPanel/GlobalDiagramProperties.tsx
index e5091ffb5c7..cf12d52da12 100644
--- a/packages/dmn-editor/src/propertiesPanel/GlobalDiagramProperties.tsx
+++ b/packages/dmn-editor/src/propertiesPanel/GlobalDiagramProperties.tsx
@@ -23,7 +23,7 @@ import { Form, FormSection, FormGroup } from "@patternfly/react-core/dist/js/com
import { TextArea } from "@patternfly/react-core/dist/js/components/TextArea";
import { TextInput } from "@patternfly/react-core/dist/js/components/TextInput";
import { DataSourceIcon } from "@patternfly/react-icons/dist/js/icons/data-source-icon";
-import { useDmnEditorStore, useDmnEditorStoreApi } from "../store/Store";
+import { useDmnEditorStore, useDmnEditorStoreApi } from "../store/StoreContext";
import { InlineFeelNameInput } from "../feel/InlineFeelNameInput";
import { useState } from "react";
import { Modal, ModalVariant } from "@patternfly/react-core/dist/js/components/Modal";
@@ -85,7 +85,7 @@ export function GlobalDiagramProperties() {
state.dmn.model.definitions["@_name"] = newName;
});
}}
- allUniqueNames={new Map()} // Right now, there's no way to know what are the unique names of all DMNs in the scope. So we let any name go.
+ allUniqueNames={() => new Map()} // Right now, there's no way to know what are the unique names of all DMNs in the scope. So we let any name go.
/>
</FormGroup>
<FormGroup label="Description">
diff --git a/packages/dmn-editor/src/propertiesPanel/GroupProperties.tsx b/packages/dmn-editor/src/propertiesPanel/GroupProperties.tsx
index 29c4480a267..3e4f20bded7 100644
--- a/packages/dmn-editor/src/propertiesPanel/GroupProperties.tsx
+++ b/packages/dmn-editor/src/propertiesPanel/GroupProperties.tsx
@@ -23,7 +23,7 @@ import { ClipboardCopy } from "@patternfly/react-core/dist/js/components/Clipboa
import { FormGroup } from "@patternfly/react-core/dist/js/components/Form";
import { TextArea } from "@patternfly/react-core/dist/js/components/TextArea";
import { TextInput } from "@patternfly/react-core/dist/js/components/TextInput";
-import { useDmnEditorStoreApi } from "../store/Store";
+import { useDmnEditorStoreApi } from "../store/StoreContext";
import { renameGroupNode } from "../mutations/renameNode";
export function GroupProperties({ group, index }: { group: DMN15__tGroup; index: number }) {
diff --git a/packages/dmn-editor/src/propertiesPanel/InputDataProperties.tsx b/packages/dmn-editor/src/propertiesPanel/InputDataProperties.tsx
index 1ebe6abd1f0..8a5d8b7c0d4 100644
--- a/packages/dmn-editor/src/propertiesPanel/InputDataProperties.tsx
+++ b/packages/dmn-editor/src/propertiesPanel/InputDataProperties.tsx
@@ -24,12 +24,12 @@ import { FormGroup } from "@patternfly/react-core/dist/js/components/Form";
import { TextArea } from "@patternfly/react-core/dist/js/components/TextArea";
import { DocumentationLinksFormGroup } from "./DocumentationLinksFormGroup";
import { TypeRefSelector } from "../dataTypes/TypeRefSelector";
-import { useDmnEditorStore, useDmnEditorStoreApi } from "../store/Store";
+import { useDmnEditorStore, useDmnEditorStoreApi } from "../store/StoreContext";
import { renameDrgElement } from "../mutations/renameNode";
import { InlineFeelNameInput } from "../feel/InlineFeelNameInput";
-import { useDmnEditorDerivedStore } from "../store/DerivedStore";
import { useDmnEditor } from "../DmnEditorContext";
import { useResolvedTypeRef } from "../dataTypes/useResolvedTypeRef";
+import { useCallback } from "react";
export function InputDataProperties({
inputData,
@@ -45,8 +45,6 @@ export function InputDataProperties({
const thisDmnsNamespace = useDmnEditorStore((s) => s.dmn.model.definitions["@_namespace"]);
const isReadonly = !!namespace && namespace !== thisDmnsNamespace;
- const { allFeelVariableUniqueNames } = useDmnEditorDerivedStore();
-
const { dmnEditorRootElementRef } = useDmnEditor();
const resolvedTypeRef = useResolvedTypeRef(inputData.variable?.["@_typeRef"], namespace);
@@ -71,7 +69,7 @@ export function InputDataProperties({
});
});
}}
- allUniqueNames={allFeelVariableUniqueNames}
+ allUniqueNames={useCallback((s) => s.computed(s).getAllFeelVariableUniqueNames(), [])}
/>
</FormGroup>
<FormGroup label="Data type">
diff --git a/packages/dmn-editor/src/propertiesPanel/KnowledgeSourceProperties.tsx b/packages/dmn-editor/src/propertiesPanel/KnowledgeSourceProperties.tsx
index 401333ac07f..36af3c30a17 100644
--- a/packages/dmn-editor/src/propertiesPanel/KnowledgeSourceProperties.tsx
+++ b/packages/dmn-editor/src/propertiesPanel/KnowledgeSourceProperties.tsx
@@ -24,10 +24,10 @@ import { FormGroup } from "@patternfly/react-core/dist/js/components/Form";
import { TextArea } from "@patternfly/react-core/dist/js/components/TextArea";
import { TextInput } from "@patternfly/react-core/dist/js/components/TextInput";
import { DocumentationLinksFormGroup } from "./DocumentationLinksFormGroup";
-import { useDmnEditorStore, useDmnEditorStoreApi } from "../store/Store";
+import { useDmnEditorStore, useDmnEditorStoreApi } from "../store/StoreContext";
import { renameDrgElement } from "../mutations/renameNode";
import { InlineFeelNameInput } from "../feel/InlineFeelNameInput";
-import { useDmnEditorDerivedStore } from "../store/DerivedStore";
+import { useCallback } from "react";
export function KnowledgeSourceProperties({
knowledgeSource,
@@ -43,8 +43,6 @@ export function KnowledgeSourceProperties({
const thisDmnsNamespace = useDmnEditorStore((s) => s.dmn.model.definitions["@_namespace"]);
const isReadonly = !!namespace && namespace !== thisDmnsNamespace;
- const { allFeelVariableUniqueNames } = useDmnEditorDerivedStore();
-
return (
<>
<FormGroup label="Name">
@@ -65,7 +63,7 @@ export function KnowledgeSourceProperties({
});
});
}}
- allUniqueNames={allFeelVariableUniqueNames}
+ allUniqueNames={useCallback((s) => s.computed(s).getAllFeelVariableUniqueNames(), [])}
/>
</FormGroup>
diff --git a/packages/dmn-editor/src/propertiesPanel/MultipleNodeProperties.tsx b/packages/dmn-editor/src/propertiesPanel/MultipleNodeProperties.tsx
index 75fcf33c9c1..31815bc8aad 100644
--- a/packages/dmn-editor/src/propertiesPanel/MultipleNodeProperties.tsx
+++ b/packages/dmn-editor/src/propertiesPanel/MultipleNodeProperties.tsx
@@ -23,7 +23,7 @@ import { Flex } from "@patternfly/react-core/dist/js/layouts/Flex";
import { Form, FormSection } from "@patternfly/react-core/dist/js/components/Form";
import { Text, TextContent, TextVariants } from "@patternfly/react-core/dist/js/components/Text";
import { FontOptions } from "./FontOptions";
-import { useDmnEditorStoreApi } from "../store/Store";
+import { useDmnEditorStoreApi } from "../store/StoreContext";
import { Button, ButtonVariant } from "@patternfly/react-core/dist/js/components/Button";
import { TimesIcon } from "@patternfly/react-icons/dist/js/icons/times-icon";
import { Truncate } from "@patternfly/react-core/dist/js/components/Truncate";
diff --git a/packages/dmn-editor/src/propertiesPanel/ShapeOptions.tsx b/packages/dmn-editor/src/propertiesPanel/ShapeOptions.tsx
index 78d3c6f7ac2..0b0bbdcb786 100644
--- a/packages/dmn-editor/src/propertiesPanel/ShapeOptions.tsx
+++ b/packages/dmn-editor/src/propertiesPanel/ShapeOptions.tsx
@@ -23,9 +23,9 @@ import { FormGroup, FormSection } from "@patternfly/react-core/dist/js/component
import { TextInput } from "@patternfly/react-core/dist/js/components/TextInput";
import { CubeIcon } from "@patternfly/react-icons/dist/js/icons/cube-icon";
import { PropertiesPanelHeader } from "./PropertiesPanelHeader";
-import { useDmnEditorDerivedStore } from "../store/DerivedStore";
import { DC__Bounds, DMNDI15__DMNShape } from "@kie-tools/dmn-marshaller/dist/schemas/dmn-1_5/ts-gen/types";
-import { State, useDmnEditorStoreApi } from "../store/Store";
+import { State } from "../store/Store";
+import { useDmnEditorStore, useDmnEditorStoreApi } from "../store/StoreContext";
import { addOrGetDrd } from "../mutations/addOrGetDrd";
import { ArrowsAltVIcon } from "@patternfly/react-icons/dist/js/icons/arrows-alt-v-icon";
import { ArrowsAltHIcon } from "@patternfly/react-icons/dist/js/icons/arrows-alt-h-icon";
@@ -34,7 +34,6 @@ import UndoAltIcon from "@patternfly/react-icons/dist/js/icons/undo-alt-icon";
import { ColorPicker } from "./ColorPicker";
import { ToggleGroup, ToggleGroupItem } from "@patternfly/react-core/dist/js/components/ToggleGroup";
import "./ShapeOptions.css";
-import { Color } from "../diagram/nodes/NodeStyle";
export function ShapeOptions({
startExpanded,
@@ -49,7 +48,7 @@ export function ShapeOptions({
}) {
const [isShapeSectionExpanded, setShapeSectionExpanded] = useState<boolean>(startExpanded);
const dmnEditorStoreApi = useDmnEditorStoreApi();
- const { dmnShapesByHref } = useDmnEditorDerivedStore();
+ const dmnShapesByHref = useDmnEditorStore((s) => s.computed(s).indexes().dmnShapesByHref);
const shapes = useMemo(() => nodeIds.map((nodeId) => dmnShapesByHref.get(nodeId)), [dmnShapesByHref, nodeIds]);
// it only edits the first selected node
@@ -146,7 +145,7 @@ export function ShapeOptions({
(newColor: string) => {
setTemporaryStrokeColor(newColor.replace("#", ""));
editShapeStyle((shapes, state) => {
- state!.diagram.editingStyle = true;
+ state!.diagram.isEditingStyle = true;
});
},
[editShapeStyle]
@@ -164,7 +163,7 @@ export function ShapeOptions({
green !== shape?.["di:Style"]?.["dmndi:StrokeColor"]?.["@_green"] &&
blue !== shape?.["di:Style"]?.["dmndi:StrokeColor"]?.["@_blue"]
) {
- state!.diagram.editingStyle = false;
+ state!.diagram.isEditingStyle = false;
shape!["di:Style"]!["dmndi:StrokeColor"] ??= { "@_blue": 0, "@_green": 0, "@_red": 0 };
shape!["di:Style"]!["dmndi:StrokeColor"]["@_red"] = red;
shape!["di:Style"]!["dmndi:StrokeColor"]["@_green"] = green;
@@ -183,7 +182,7 @@ export function ShapeOptions({
(newColor: string) => {
setTemporaryFillColor(newColor.replace("#", ""));
editShapeStyle((shapes, state) => {
- state!.diagram.editingStyle = true;
+ state!.diagram.isEditingStyle = true;
});
},
[editShapeStyle]
@@ -201,7 +200,7 @@ export function ShapeOptions({
green !== shape?.["di:Style"]?.["dmndi:FillColor"]?.["@_green"] &&
blue !== shape?.["di:Style"]?.["dmndi:FillColor"]?.["@_blue"]
) {
- state!.diagram.editingStyle = false;
+ state!.diagram.isEditingStyle = false;
shape!["di:Style"]!["dmndi:FillColor"] ??= { "@_blue": 255, "@_green": 255, "@_red": 255 };
shape!["di:Style"]!["dmndi:FillColor"]["@_red"] = red;
shape!["di:Style"]!["dmndi:FillColor"]["@_green"] = green;
diff --git a/packages/dmn-editor/src/propertiesPanel/SingleNodeProperties.tsx b/packages/dmn-editor/src/propertiesPanel/SingleNodeProperties.tsx
index 182dfac85f9..cd258c487c5 100644
--- a/packages/dmn-editor/src/propertiesPanel/SingleNodeProperties.tsx
+++ b/packages/dmn-editor/src/propertiesPanel/SingleNodeProperties.tsx
@@ -30,8 +30,7 @@ import { DecisionServiceProperties } from "./DecisionServiceProperties";
import { KnowledgeSourceProperties } from "./KnowledgeSourceProperties";
import { TextAnnotationProperties } from "./TextAnnotationProperties";
import { useMemo } from "react";
-import { useDmnEditorStoreApi } from "../store/Store";
-import { useDmnEditorDerivedStore } from "../store/DerivedStore";
+import { useDmnEditorStore, useDmnEditorStoreApi } from "../store/StoreContext";
import { NODE_TYPES } from "../diagram/nodes/NodeTypes";
import {
DMN15__tBusinessKnowledgeModel,
@@ -49,17 +48,15 @@ import { Button, ButtonVariant } from "@patternfly/react-core/dist/js/components
import { TimesIcon } from "@patternfly/react-icons/dist/js/icons/times-icon";
import { PropertiesPanelHeader } from "./PropertiesPanelHeader";
import { UnknownProperties } from "./UnknownProperties";
+import { useExternalModels } from "../includedModels/DmnEditorDependenciesContext";
import "./SingleNodeProperties.css";
export function SingleNodeProperties({ nodeId }: { nodeId: string }) {
const dmnEditorStoreApi = useDmnEditorStoreApi();
- const { nodesById } = useDmnEditorDerivedStore();
+ const { externalModelsByNamespace } = useExternalModels();
+ const node = useDmnEditorStore((s) => s.computed(s).getDiagramData(externalModelsByNamespace).nodesById.get(nodeId));
const [isSectionExpanded, setSectionExpanded] = useState<boolean>(true);
- const node = useMemo(() => {
- return nodesById.get(nodeId);
- }, [nodeId, nodesById]);
-
if (!node) {
return <>Node not found: {nodeId}</>;
}
diff --git a/packages/dmn-editor/src/propertiesPanel/TextAnnotationProperties.tsx b/packages/dmn-editor/src/propertiesPanel/TextAnnotationProperties.tsx
index 2151cf9e97e..574ab6c6733 100644
--- a/packages/dmn-editor/src/propertiesPanel/TextAnnotationProperties.tsx
+++ b/packages/dmn-editor/src/propertiesPanel/TextAnnotationProperties.tsx
@@ -23,7 +23,7 @@ import { ClipboardCopy } from "@patternfly/react-core/dist/js/components/Clipboa
import { FormGroup } from "@patternfly/react-core/dist/js/components/Form";
import { TextArea } from "@patternfly/react-core/dist/js/components/TextArea";
import { TextInput } from "@patternfly/react-core/dist/js/components/TextInput";
-import { useDmnEditorStoreApi } from "../store/Store";
+import { useDmnEditorStoreApi } from "../store/StoreContext";
import { updateTextAnnotation } from "../mutations/renameNode";
export function TextAnnotationProperties({
diff --git a/packages/dmn-editor/src/propertiesPanel/UnknownProperties.tsx b/packages/dmn-editor/src/propertiesPanel/UnknownProperties.tsx
index 73aea18a00b..a94148e4c3b 100644
--- a/packages/dmn-editor/src/propertiesPanel/UnknownProperties.tsx
+++ b/packages/dmn-editor/src/propertiesPanel/UnknownProperties.tsx
@@ -20,19 +20,21 @@
import * as React from "react";
import { Alert, AlertVariant } from "@patternfly/react-core/dist/js/components/Alert/Alert";
import { DMN15__tDefinitions, DMNDI15__DMNShape } from "@kie-tools/dmn-marshaller/dist/schemas/dmn-1_5/ts-gen/types";
-import { useDmnEditorStore } from "../store/Store";
+import { useDmnEditorStore } from "../store/StoreContext";
import { useMemo } from "react";
import { Unpacked } from "../tsExt/tsExt";
-import { useDmnEditorDerivedStore } from "../store/DerivedStore";
import { XmlQName, buildXmlQName } from "@kie-tools/xml-parser-ts/dist/qNames";
import { Button, ButtonVariant } from "@patternfly/react-core/dist/js/components/Button";
import { useDmnEditor } from "../DmnEditorContext";
import { Divider } from "@patternfly/react-core/dist/js/components/Divider";
+import { useExternalModels } from "../includedModels/DmnEditorDependenciesContext";
export function UnknownProperties(props: { shape: DMNDI15__DMNShape; dmnElementRefQName: XmlQName }) {
const thisDmn = useDmnEditorStore((s) => s.dmn);
-
- const { externalDmnsByNamespace } = useDmnEditorDerivedStore();
+ const { externalModelsByNamespace } = useExternalModels();
+ const externalDmnsByNamespace = useDmnEditorStore(
+ (s) => s.computed(s).getExternalModelTypesByNamespace(externalModelsByNamespace).dmns
+ );
const { onRequestToJumpToPath } = useDmnEditor();
const content = useMemo(() => {
diff --git a/packages/dmn-editor/src/store/ComputedStateCache.ts b/packages/dmn-editor/src/store/ComputedStateCache.ts
new file mode 100644
index 00000000000..84d4b7c0644
--- /dev/null
+++ b/packages/dmn-editor/src/store/ComputedStateCache.ts
@@ -0,0 +1,76 @@
+/*
+ * 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.
+ */
+
+export type TypeOrReturnType<T> = T extends (...args: any[]) => any ? ReturnType<T> : T;
+
+export type CacheEntry<T> = {
+ value: TypeOrReturnType<T> | undefined;
+ dependencies: readonly any[];
+};
+
+export type Cache<T> = {
+ [K in keyof T]: CacheEntry<T[K]>;
+};
+
+let r: number = 0;
+// let h: number = 0;
+// let m: number = 0;
+
+export class ComputedStateCache<T extends Record<string, any>> {
+ private readonly cache: Cache<T>;
+
+ constructor(initialValues: Cache<T>) {
+ this.cache = { ...initialValues };
+ }
+
+ public cached<K extends keyof T, D extends readonly any[]>(
+ key: K,
+ delegate: (...dependencies: D) => TypeOrReturnType<T[K]>,
+ dependencies: D
+ ): TypeOrReturnType<T[K]> {
+ r++;
+
+ const cachedDeps = this.cache[key]?.dependencies ?? [];
+
+ let depsAreEqual = cachedDeps.length === dependencies.length;
+ if (depsAreEqual) {
+ for (let i = 0; i < cachedDeps.length; i++) {
+ if (!Object.is(cachedDeps[i], dependencies[i])) {
+ depsAreEqual = false;
+ }
+ }
+ } else {
+ // console.debug(`${r}: COMPUTED STORE CACHE: (Miss) Deps don't have the same length... (${String(key)})`);
+ }
+
+ if (depsAreEqual) {
+ // console.debug(`${r}: COMPUTED STORE CACHE: Hit ${++h}! (${String(key)})`);
+ return this.cache[key].value!;
+ } else {
+ // console.debug(`${r}: COMPUTED STORE CACHE: (Miss) Deps have different values... (${String(key)})`);
+ }
+
+ // console.debug(`${r}: COMPUTED STORE CACHE: Miss (${++m}})... (${String(key)})`);
+
+ const v = delegate(...dependencies);
+ this.cache[key].dependencies = dependencies;
+ this.cache[key].value = v;
+ return v;
+ }
+}
diff --git a/packages/dmn-editor/src/store/DerivedStore.tsx b/packages/dmn-editor/src/store/DerivedStore.tsx
deleted file mode 100644
index b35b706fb1b..00000000000
--- a/packages/dmn-editor/src/store/DerivedStore.tsx
+++ /dev/null
@@ -1,311 +0,0 @@
-/*
- * 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 { useMemo } from "react";
-import { isValidContainment as _isValidContainment } from "../diagram/connections/isValidContainment";
-import { NodeType } from "../diagram/connections/graphStructure";
-import { useDmnEditorStore } from "./Store";
-import { useDiagramData } from "./useDiagramData";
-import {
- DMN15__tImport,
- DMN15__tItemDefinition,
- DMNDI15__DMNEdge,
- DMNDI15__DMNShape,
-} from "@kie-tools/dmn-marshaller/dist/schemas/dmn-1_5/ts-gen/types";
-import { DmnDiagramNodeData } from "../diagram/nodes/Nodes";
-import { DmnDiagramEdgeData } from "../diagram/edges/Edges";
-import { DataTypeIndex, DataType } from "../dataTypes/DataTypes";
-import { buildFeelQNameFromNamespace } from "../feel/buildFeelQName";
-import { useExternalModels } from "../includedModels/DmnEditorDependenciesContext";
-import { UniqueNameIndex } from "../Dmn15Spec";
-import { ExternalPmmlsIndex, ExternalDmnsIndex } from "../DmnEditor";
-import { builtInFeelTypeNames } from "../dataTypes/BuiltInFeelTypes";
-import { getNamespaceOfDmnImport } from "../includedModels/importNamespaces";
-
-export type DerivedStore = {
- selectedNodeTypes: Set<NodeType>;
- isDropTargetNodeValidForSelection: boolean;
- isDiagramEditingInProgress: boolean;
- nodes: RF.Node[];
- edges: RF.Edge[];
- nodesById: Map<string, RF.Node<DmnDiagramNodeData>>;
- edgesById: Map<string, RF.Edge<DmnDiagramEdgeData>>;
- selectedNodesById: Map<string, RF.Node<DmnDiagramNodeData>>;
- selectedEdgesById: Map<string, RF.Edge<DmnDiagramEdgeData>>;
- importsByNamespace: Map<string, DMN15__tImport>;
- dataTypesTree: DataType[];
- allDataTypesById: DataTypeIndex;
- allTopLevelDataTypesByFeelName: DataTypeIndex;
- dmnEdgesByDmnElementRef: Map<string, DMNDI15__DMNEdge & { index: number }>;
- dmnShapesByHref: Map<string, DMNDI15__DMNShape & { index: number }>;
- drgElementsWithoutVisualRepresentationOnCurrentDrd: string[];
- allFeelVariableUniqueNames: UniqueNameIndex;
- allTopLevelItemDefinitionUniqueNames: UniqueNameIndex;
- externalDmnsByNamespace: ExternalDmnsIndex;
- externalPmmlsByNamespace: ExternalPmmlsIndex;
-};
-
-const DmnEditorDerivedStoreContext = React.createContext<DerivedStore>({} as any);
-
-export function useDmnEditorDerivedStore() {
- return React.useContext(DmnEditorDerivedStoreContext);
-}
-
-export function DmnEditorDerivedStoreContextProvider(props: React.PropsWithChildren<{}>) {
- const diagram = useDmnEditorStore((s) => s.diagram);
- const thisDmn = useDmnEditorStore((s) => s.dmn);
- const thisDmnsImports = useMemo(() => thisDmn.model.definitions.import ?? [], [thisDmn.model.definitions.import]);
-
- const thisDmnsImportsByNamespace = useMemo(() => {
- const ret = new Map<string, DMN15__tImport>();
- for (let i = 0; i < thisDmnsImports.length; i++) {
- ret.set(thisDmnsImports[i]["@_namespace"], thisDmnsImports[i]);
- }
- return ret;
- }, [thisDmnsImports]);
-
- const { externalModelsByNamespace } = useExternalModels();
-
- const { dmns: externalDmnsByNamespace, pmmls: externalPmmlsByNamespace } = useMemo<{
- dmns: ExternalDmnsIndex;
- pmmls: ExternalPmmlsIndex;
- }>(() => {
- return thisDmnsImports.reduce<{ dmns: ExternalDmnsIndex; pmmls: ExternalPmmlsIndex }>(
- (acc, _import) => {
- const externalModel = externalModelsByNamespace?.[getNamespaceOfDmnImport({ dmnImport: _import })];
- if (!externalModel) {
- console.warn(
- `DMN DIAGRAM: Can't index external model with namespace '${_import["@_namespace"]}' because it doesn't exist on the external models list.`
- );
- return acc;
- }
-
- if (externalModel.type === "dmn") {
- acc.dmns.set(_import["@_namespace"], externalModel);
- } else if (externalModel.type === "pmml") {
- acc.pmmls.set(_import["@_namespace"], externalModel);
- } else {
- console.warn("DMN EDITOR: Unknown external model type", externalModel);
- }
-
- return acc;
- },
- { dmns: new Map(), pmmls: new Map() }
- );
- }, [externalModelsByNamespace, thisDmnsImports]);
-
- const {
- nodes,
- edges,
- nodesById,
- edgesById,
- dmnEdgesByDmnElementRef,
- dmnShapesByHref,
- selectedNodesById,
- selectedEdgesById,
- selectedNodeTypes,
- drgElementsWithoutVisualRepresentationOnCurrentDrd,
- } = useDiagramData(externalDmnsByNamespace);
-
- const { dataTypesTree, allDataTypesById, allTopLevelDataTypesByFeelName } = useMemo(() => {
- const allDataTypesById: DataTypeIndex = new Map();
- const allTopLevelDataTypesByFeelName: DataTypeIndex = new Map();
-
- const externalDmnsDataTypeTree = [...externalDmnsByNamespace.values()].flatMap((externalDmn) => {
- return buildDataTypesTree(
- externalDmn.model.definitions.itemDefinition ?? [],
- thisDmnsImportsByNamespace,
- allDataTypesById,
- allTopLevelDataTypesByFeelName,
- undefined,
- new Set(),
- externalDmn.model.definitions["@_namespace"],
- thisDmn.model.definitions["@_namespace"]
- );
- });
-
- // Purposefully do thisDmn's after. This will make sure thisDmn's ItemDefintiions
- // take precedence over any external ones imported to the default namespace.
- const thisDmnsDataTypeTree = buildDataTypesTree(
- thisDmn.model.definitions.itemDefinition ?? [],
- thisDmnsImportsByNamespace,
- allDataTypesById,
- allTopLevelDataTypesByFeelName,
- undefined,
- new Set(),
- thisDmn.model.definitions["@_namespace"],
- thisDmn.model.definitions["@_namespace"]
- );
-
- return {
- dataTypesTree: [...thisDmnsDataTypeTree, ...externalDmnsDataTypeTree],
- allDataTypesById,
- allTopLevelDataTypesByFeelName,
- };
- }, [externalDmnsByNamespace, thisDmn.model.definitions, thisDmnsImportsByNamespace]);
-
- const allTopLevelItemDefinitionUniqueNames = useMemo(() => {
- const ret: UniqueNameIndex = new Map();
-
- for (const [k, v] of allTopLevelDataTypesByFeelName.entries()) {
- ret.set(k, v.itemDefinition["@_id"]!);
- }
-
- for (const type of builtInFeelTypeNames) {
- ret.set(type, type);
- }
-
- return ret;
- }, [allTopLevelDataTypesByFeelName]);
-
- const allFeelVariableUniqueNames = useMemo(() => {
- const ret: UniqueNameIndex = new Map();
-
- const drgElements = thisDmn.model.definitions.drgElement ?? [];
- for (let i = 0; i < drgElements.length; i++) {
- const drgElement = drgElements[i];
- ret.set(drgElement["@_name"]!, drgElement["@_id"]!);
- }
-
- for (let i = 0; i < thisDmnsImports.length; i++) {
- const _import = thisDmnsImports[i];
- ret.set(_import["@_name"], _import["@_id"]!);
- }
-
- return ret;
- }, [thisDmn.model.definitions.drgElement, thisDmnsImports]);
-
- const isDropTargetNodeValidForSelection =
- !!diagram.dropTargetNode &&
- _isValidContainment({
- nodeTypes: selectedNodeTypes,
- inside: diagram.dropTargetNode.type as NodeType,
- dmnObjectQName: diagram.dropTargetNode.data.dmnObjectQName,
- });
-
- const isDiagramEditingInProgress =
- diagram.draggingNodes.length > 0 ||
- diagram.resizingNodes.length > 0 ||
- diagram.draggingWaypoints.length > 0 ||
- diagram.movingDividerLines.length > 0 ||
- diagram.editingStyle;
-
- const value = useMemo(
- () => ({
- selectedNodeTypes,
- isDropTargetNodeValidForSelection,
- isDiagramEditingInProgress,
- nodes,
- edges,
- nodesById,
- edgesById,
- selectedNodesById,
- selectedEdgesById,
- importsByNamespace: thisDmnsImportsByNamespace,
- dmnEdgesByDmnElementRef,
- dmnShapesByHref,
- dataTypesTree,
- allDataTypesById,
- allTopLevelDataTypesByFeelName,
- allFeelVariableUniqueNames,
- allTopLevelItemDefinitionUniqueNames,
- externalDmnsByNamespace,
- externalPmmlsByNamespace,
- drgElementsWithoutVisualRepresentationOnCurrentDrd,
- }),
- [
- selectedNodeTypes,
- isDropTargetNodeValidForSelection,
- isDiagramEditingInProgress,
- nodes,
- edges,
- nodesById,
- edgesById,
- selectedNodesById,
- selectedEdgesById,
- thisDmnsImportsByNamespace,
- dmnEdgesByDmnElementRef,
- dmnShapesByHref,
- dataTypesTree,
- allDataTypesById,
- allTopLevelDataTypesByFeelName,
- allFeelVariableUniqueNames,
- allTopLevelItemDefinitionUniqueNames,
- externalDmnsByNamespace,
- externalPmmlsByNamespace,
- drgElementsWithoutVisualRepresentationOnCurrentDrd,
- ]
- );
-
- return <DmnEditorDerivedStoreContext.Provider value={value}>{props.children}</DmnEditorDerivedStoreContext.Provider>;
-}
-
-function buildDataTypesTree(
- items: DMN15__tItemDefinition[],
- importsByNamespace: Map<string, DMN15__tImport>,
- allDataTypesById: DataTypeIndex,
- allTopLevelDataTypesByFeelName: DataTypeIndex,
- parentId: string | undefined,
- parents: Set<string>,
- namespace: string,
- relativeToNamespace: string
-) {
- const dataTypesTree: DataType[] = [];
-
- for (let i = 0; i < items.length; i++) {
- const itemDefinition = items[i];
-
- const feelName = buildFeelQNameFromNamespace({
- importsByNamespace,
- namedElement: itemDefinition,
- namespace,
- relativeToNamespace,
- }).full;
-
- const dataType: DataType = {
- itemDefinition,
- index: i,
- parentId,
- parents,
- feelName,
- namespace,
- children: buildDataTypesTree(
- itemDefinition.itemComponent ?? [],
- importsByNamespace,
- allDataTypesById,
- allTopLevelDataTypesByFeelName,
- itemDefinition["@_id"],
- new Set([...parents, itemDefinition["@_id"]!]),
- namespace,
- relativeToNamespace
- ),
- };
-
- dataTypesTree.push(dataType);
- allDataTypesById.set(itemDefinition["@_id"]!, dataType);
-
- if (parentId === undefined) {
- allTopLevelDataTypesByFeelName.set(feelName, dataType);
- }
- }
-
- return dataTypesTree;
-}
diff --git a/packages/dmn-editor/src/store/Store.ts b/packages/dmn-editor/src/store/Store.ts
index 99db6143919..a02b2de50e2 100644
--- a/packages/dmn-editor/src/store/Store.ts
+++ b/packages/dmn-editor/src/store/Store.ts
@@ -17,13 +17,24 @@
* under the License.
*/
-import { createContext, useContext } from "react";
-import * as RF from "reactflow";
-import { StoreApi, UseBoundStore, create } from "zustand";
-import { WithImmer, immer } from "zustand/middleware/immer";
-import { useStoreWithEqualityFn } from "zustand/traditional";
import { DmnLatestModel } from "@kie-tools/dmn-marshaller";
+import { DMN15__tImport } from "@kie-tools/dmn-marshaller/dist/schemas/dmn-1_5/ts-gen/types";
+import { enableMapSet } from "immer";
+import * as RF from "reactflow";
+import { create } from "zustand";
+import { immer } from "zustand/middleware/immer";
+import { ExternalModelsIndex } from "../DmnEditor";
import { DmnDiagramNodeData } from "../diagram/nodes/Nodes";
+import { ComputedStateCache } from "./ComputedStateCache";
+import { computeAllFeelVariableUniqueNames } from "./computed/computeAllFeelVariableUniqueNames";
+import { computeDataTypes } from "./computed/computeDataTypes";
+import { computeDiagramData } from "./computed/computeDiagramData";
+import { computeExternalModelsByType } from "./computed/computeExternalModelsByType";
+import { computeImportsByNamespace } from "./computed/computeImportsByNamespace";
+import { computeIndexes } from "./computed/computeIndexes";
+import { computeIsDropTargetNodeValidForSelection } from "./computed/computeIsDropTargetNodeValidForSelection";
+
+enableMapSet(); // Necessary because `Computed` has a lot of Maps and Sets.
export interface DmnEditorDiagramNodeStatus {
selected: boolean;
@@ -54,7 +65,8 @@ export enum DiagramNodesPanel {
export type DropTargetNode = undefined | RF.Node<DmnDiagramNodeData>;
export interface State {
- dispatch: Dispatch;
+ dispatch: (s: State) => Dispatch;
+ computed: (s: State) => Computed;
dmn: { model: DmnLatestModel };
focus: {
consumableId: string | undefined;
@@ -92,9 +104,8 @@ export interface State {
overlays: {
enableNodeHierarchyHighlight: boolean;
enableExecutionHitsHighlights: boolean;
- enableCustomNodeStyles: boolean;
enableDataTypesToolbarOnNodes: boolean;
- enableStyles: boolean;
+ enableCustomNodeStyles: boolean;
};
snapGrid: SnapGrid;
_selectedNodes: Array<string>;
@@ -103,26 +114,44 @@ export interface State {
resizingNodes: Array<string>;
draggingWaypoints: Array<string>;
movingDividerLines: Array<string>;
- editingStyle: boolean;
+ isEditingStyle: boolean;
};
}
+// Read this to understand why we need computed as part of the store.
+// https://github.com/pmndrs/zustand/issues/132#issuecomment-1120467721
+export type Computed = {
+ isDiagramEditingInProgress(): boolean;
+
+ importsByNamespace(): Map<string, DMN15__tImport>;
+
+ indexes(): ReturnType<typeof computeIndexes>;
+
+ getDiagramData(e: ExternalModelsIndex | undefined): ReturnType<typeof computeDiagramData>;
+
+ isDropTargetNodeValidForSelection(e: ExternalModelsIndex | undefined): boolean;
+
+ getExternalModelTypesByNamespace: (
+ e: ExternalModelsIndex | undefined
+ ) => ReturnType<typeof computeExternalModelsByType>;
+
+ getDataTypes(e: ExternalModelsIndex | undefined): ReturnType<typeof computeDataTypes>;
+
+ getAllFeelVariableUniqueNames(): ReturnType<typeof computeAllFeelVariableUniqueNames>;
+};
+
export type Dispatch = {
dmn: {
reset: (model: State["dmn"]["model"]) => void;
};
boxedExpressionEditor: {
- open: (state: State, id: string) => void;
- close: (state: State) => void;
+ open: (id: string) => void;
+ close: () => void;
};
diagram: {
- setNodeStatus: (state: State, nodeId: string, status: Partial<DmnEditorDiagramNodeStatus>) => void;
- setEdgeStatus: (state: State, edgeId: string, status: Partial<DmnEditorDiagramEdgeStatus>) => void;
- setDividerLineStatus: (
- state: State,
- decisionServiceId: string,
- status: Partial<DmnEditorDiagramDividerLineStatus>
- ) => void;
+ setNodeStatus: (nodeId: string, status: Partial<DmnEditorDiagramNodeStatus>) => void;
+ setEdgeStatus: (edgeId: string, status: Partial<DmnEditorDiagramEdgeStatus>) => void;
+ setDividerLineStatus: (decisionServiceId: string, status: Partial<DmnEditorDiagramDividerLineStatus>) => void;
};
};
@@ -132,37 +161,7 @@ export enum DmnEditorTab {
INCLUDED_MODELS,
}
-export const NODE_LAYERS = {
- GROUP_NODE: 0,
- NODES: 1000, // We need a difference > 1000 here, since ReactFlow will add 1000 to the z-index when a node is selected.
- DECISION_SERVICE_NODE: 2000, // We need a difference > 1000 here, since ReactFlow will add 1000 to the z-index when a node is selected.
- NESTED_NODES: 4000,
-};
-
-type ExtractState = StoreApi<State> extends { getState: () => infer T } ? T : never;
-
-export function useDmnEditorStore<StateSlice = ExtractState>(
- selector: (state: State) => StateSlice,
- equalityFn?: (a: StateSlice, b: StateSlice) => boolean
-) {
- const store = useContext(DmnEditorStoreApiContext);
-
- if (store === null) {
- throw new Error("Can't use DMN Editor Store outside of the DmnEditor component.");
- }
-
- return useStoreWithEqualityFn(store, selector, equalityFn);
-}
-
-export function useDmnEditorStoreApi() {
- return useContext(DmnEditorStoreApiContext);
-}
-
-export const DmnEditorStoreApiContext = createContext<StoreApiType>({} as any);
-
-export type StoreApiType = UseBoundStore<WithImmer<StoreApi<State>>>;
-
-export const defaultStaticState = () => ({
+export const defaultStaticState = (): Omit<State, "dmn" | "dispatch" | "computed"> => ({
boxedExpressionEditor: {
activeDrgElementId: undefined,
selectedObjectId: undefined,
@@ -173,6 +172,13 @@ export const defaultStaticState = () => ({
navigation: {
tab: DmnEditorTab.EDITOR,
},
+ focus: {
+ consumableId: undefined,
+ },
+ dataTypesEditor: {
+ activeItemDefinitionId: undefined,
+ expandedItemComponentIds: [],
+ },
diagram: {
drdIndex: 0,
edgeIdBeingUpdated: undefined,
@@ -185,7 +191,6 @@ export const defaultStaticState = () => ({
overlaysPanel: {
isOpen: false,
},
-
openNodesPanel: DiagramNodesPanel.NONE,
drdSelector: {
isOpen: false,
@@ -193,118 +198,184 @@ export const defaultStaticState = () => ({
overlays: {
enableNodeHierarchyHighlight: false,
enableExecutionHitsHighlights: false,
- enableCustomNodeStyles: false,
+ enableCustomNodeStyles: true,
enableDataTypesToolbarOnNodes: true,
- enableStyles: true,
},
- snapGrid: { isEnabled: true, x: 20, y: 20 },
+ snapGrid: {
+ isEnabled: true,
+ x: 20,
+ y: 20,
+ },
_selectedNodes: [],
_selectedEdges: [],
draggingNodes: [],
resizingNodes: [],
draggingWaypoints: [],
movingDividerLines: [],
- editingStyle: false,
+ isEditingStyle: false,
},
});
-export function createDmnEditorStore(model: State["dmn"]["model"]) {
+export function createDmnEditorStore(model: State["dmn"]["model"], computedCache: ComputedStateCache<Computed>) {
return create(
- immer<State>((set, get) => ({
+ immer<State>(() => ({
dmn: {
model,
},
- focus: {
- consumableId: undefined,
- },
- dataTypesEditor: {
- activeItemDefinitionId: undefined,
- expandedItemComponentIds: [],
- },
...defaultStaticState(),
- dispatch: {
- dmn: {
- reset: (model) => {
- set((state) => {
- state.diagram._selectedNodes = [];
- state.diagram.draggingNodes = [];
- state.diagram.resizingNodes = [];
- state.navigation.tab = DmnEditorTab.EDITOR;
- state.boxedExpressionEditor.activeDrgElementId = undefined;
- state.boxedExpressionEditor.selectedObjectId = undefined;
- });
+ dispatch(s: State) {
+ return {
+ dmn: {
+ reset: () => {
+ s.diagram._selectedNodes = [];
+ s.diagram.draggingNodes = [];
+ s.diagram.resizingNodes = [];
+ s.navigation.tab = DmnEditorTab.EDITOR;
+ s.boxedExpressionEditor.activeDrgElementId = undefined;
+ s.boxedExpressionEditor.selectedObjectId = undefined;
+ },
},
- },
- boxedExpressionEditor: {
- open: (state, id) => {
- state.boxedExpressionEditor.activeDrgElementId = id;
- state.boxedExpressionEditor.selectedObjectId = undefined;
- state.boxedExpressionEditor.propertiesPanel.isOpen = state.diagram.propertiesPanel.isOpen;
+ boxedExpressionEditor: {
+ open: (id) => {
+ s.boxedExpressionEditor.activeDrgElementId = id;
+ s.boxedExpressionEditor.selectedObjectId = undefined;
+ s.boxedExpressionEditor.propertiesPanel.isOpen = s.diagram.propertiesPanel.isOpen;
+ },
+ close: () => {
+ s.diagram.propertiesPanel.isOpen = s.boxedExpressionEditor.propertiesPanel.isOpen;
+ s.boxedExpressionEditor.activeDrgElementId = undefined;
+ s.boxedExpressionEditor.selectedObjectId = undefined;
+ },
},
- close: (state) => {
- state.diagram.propertiesPanel.isOpen = state.boxedExpressionEditor.propertiesPanel.isOpen;
- state.boxedExpressionEditor.activeDrgElementId = undefined;
- state.boxedExpressionEditor.selectedObjectId = undefined;
- },
- },
- diagram: {
- setNodeStatus: (prev, nodeId, newStatus) => {
- //selected
- if (newStatus.selected !== undefined) {
- if (newStatus.selected) {
- prev.diagram._selectedNodes.push(nodeId);
- } else {
- prev.diagram._selectedNodes = prev.diagram._selectedNodes.filter((s) => s !== nodeId);
+ diagram: {
+ setNodeStatus: (nodeId, newStatus) => {
+ //selected
+ if (newStatus.selected !== undefined) {
+ if (newStatus.selected) {
+ s.diagram._selectedNodes.push(nodeId);
+ } else {
+ s.diagram._selectedNodes = s.diagram._selectedNodes.filter((s) => s !== nodeId);
+ }
}
- }
- //dragging
- if (newStatus.dragging !== undefined) {
- if (newStatus.dragging) {
- prev.diagram.draggingNodes.push(nodeId);
- } else {
- prev.diagram.draggingNodes = prev.diagram.draggingNodes.filter((s) => s !== nodeId);
+ //dragging
+ if (newStatus.dragging !== undefined) {
+ if (newStatus.dragging) {
+ s.diagram.draggingNodes.push(nodeId);
+ } else {
+ s.diagram.draggingNodes = s.diagram.draggingNodes.filter((s) => s !== nodeId);
+ }
}
- }
- // resizing
- if (newStatus.resizing !== undefined) {
- if (newStatus.resizing) {
- prev.diagram.resizingNodes.push(nodeId);
- } else {
- prev.diagram.resizingNodes = prev.diagram.resizingNodes.filter((s) => s !== nodeId);
+ // resizing
+ if (newStatus.resizing !== undefined) {
+ if (newStatus.resizing) {
+ s.diagram.resizingNodes.push(nodeId);
+ } else {
+ s.diagram.resizingNodes = s.diagram.resizingNodes.filter((s) => s !== nodeId);
+ }
}
- }
- },
- setEdgeStatus: (prev, edgeId, newStatus) => {
- //selected
- if (newStatus.selected !== undefined) {
- if (newStatus.selected) {
- prev.diagram._selectedEdges.push(edgeId);
- } else {
- prev.diagram._selectedEdges = prev.diagram._selectedEdges.filter((s) => s !== edgeId);
+ },
+ setEdgeStatus: (edgeId, newStatus) => {
+ //selected
+ if (newStatus.selected !== undefined) {
+ if (newStatus.selected) {
+ s.diagram._selectedEdges.push(edgeId);
+ } else {
+ s.diagram._selectedEdges = s.diagram._selectedEdges.filter((s) => s !== edgeId);
+ }
}
- }
- //dragging
- if (newStatus.draggingWaypoint !== undefined) {
- if (newStatus.draggingWaypoint) {
- prev.diagram.draggingWaypoints.push(edgeId);
- } else {
- prev.diagram.draggingWaypoints = prev.diagram.draggingWaypoints.filter((s) => s !== edgeId);
+ //dragging
+ if (newStatus.draggingWaypoint !== undefined) {
+ if (newStatus.draggingWaypoint) {
+ s.diagram.draggingWaypoints.push(edgeId);
+ } else {
+ s.diagram.draggingWaypoints = s.diagram.draggingWaypoints.filter((s) => s !== edgeId);
+ }
}
- }
- },
- setDividerLineStatus: (prev, decisionServiceId, newStatus) => {
- //dragging
- if (newStatus.moving !== undefined) {
- if (newStatus.moving) {
- prev.diagram.movingDividerLines.push(decisionServiceId);
- } else {
- prev.diagram.movingDividerLines = prev.diagram.movingDividerLines.filter(
- (s) => s !== decisionServiceId
- );
+ },
+ setDividerLineStatus: (decisionServiceId, newStatus) => {
+ //dragging
+ if (newStatus.moving !== undefined) {
+ if (newStatus.moving) {
+ s.diagram.movingDividerLines.push(decisionServiceId);
+ } else {
+ s.diagram.movingDividerLines = s.diagram.movingDividerLines.filter((s) => s !== decisionServiceId);
+ }
}
- }
+ },
},
- },
+ };
+ },
+ computed(s: State) {
+ return {
+ isDiagramEditingInProgress: () => {
+ return computedCache.cached(
+ "isDiagramEditingInProgress",
+ (
+ draggingNodesCount: number,
+ resizingNodesCount: number,
+ draggingWaypointsCount: number,
+ movingDividerLinesCount: number,
+ isisEditingStyle: boolean
+ ) =>
+ draggingNodesCount > 0 ||
+ resizingNodesCount > 0 ||
+ draggingWaypointsCount > 0 ||
+ movingDividerLinesCount > 0 ||
+ isisEditingStyle,
+ [
+ s.diagram.draggingNodes.length,
+ s.diagram.resizingNodes.length,
+ s.diagram.draggingWaypoints.length,
+ s.diagram.movingDividerLines.length,
+ s.diagram.isEditingStyle,
+ ]
+ );
+ },
+
+ indexes: () => {
+ return computedCache.cached("indexes", computeIndexes, [s.dmn.model.definitions, s.diagram.drdIndex]);
+ },
+
+ importsByNamespace: () => {
+ return computedCache.cached("importsByNamespace", computeImportsByNamespace, [
+ s.dmn.model.definitions.import,
+ ]);
+ },
+
+ isDropTargetNodeValidForSelection: (externalModelsByNamespace: ExternalModelsIndex | undefined) =>
+ computedCache.cached("isDropTargetNodeValidForSelection", computeIsDropTargetNodeValidForSelection, [
+ s.diagram.dropTargetNode,
+ s.computed(s).getDiagramData(externalModelsByNamespace),
+ ]),
+
+ getDataTypes: (externalModelsByNamespace: ExternalModelsIndex | undefined) =>
+ computedCache.cached("getDataTypes", computeDataTypes, [
+ s.dmn.model.definitions["@_namespace"],
+ s.dmn.model.definitions.itemDefinition,
+ s.computed(s).getExternalModelTypesByNamespace(externalModelsByNamespace),
+ s.computed(s).importsByNamespace(),
+ ]),
+
+ getAllFeelVariableUniqueNames: () =>
+ computedCache.cached("getAllFeelVariableUniqueNames", computeAllFeelVariableUniqueNames, [
+ s.dmn.model.definitions.drgElement,
+ s.dmn.model.definitions.import,
+ ]),
+
+ getExternalModelTypesByNamespace: (externalModelsByNamespace: ExternalModelsIndex | undefined) =>
+ computedCache.cached("getExternalModelTypesByNamespace", computeExternalModelsByType, [
+ s.dmn.model.definitions.import,
+ externalModelsByNamespace,
+ ]),
+
+ getDiagramData: (externalModelsByNamespace: ExternalModelsIndex | undefined) =>
+ computedCache.cached("getDiagramData", computeDiagramData, [
+ s.diagram,
+ s.dmn.model.definitions,
+ s.computed(s).getExternalModelTypesByNamespace(externalModelsByNamespace),
+ s.computed(s).indexes(),
+ ]),
+ };
},
}))
);
diff --git a/packages/dmn-editor/src/store/StoreContext.ts b/packages/dmn-editor/src/store/StoreContext.ts
new file mode 100644
index 00000000000..9edb82d8ac3
--- /dev/null
+++ b/packages/dmn-editor/src/store/StoreContext.ts
@@ -0,0 +1,47 @@
+/*
+ * 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 { createContext, useContext } from "react";
+import { StoreApi, UseBoundStore } from "zustand";
+import { WithImmer } from "zustand/middleware/immer";
+import { useStoreWithEqualityFn } from "zustand/traditional";
+import { State } from "./Store";
+
+type ExtractState = StoreApi<State> extends { getState: () => infer T } ? T : never;
+
+export type StoreApiType = UseBoundStore<WithImmer<StoreApi<State>>>;
+
+export const DmnEditorStoreApiContext = createContext<StoreApiType>({} as any);
+
+export function useDmnEditorStore<StateSlice = ExtractState>(
+ selector: (state: State) => StateSlice,
+ equalityFn?: (a: StateSlice, b: StateSlice) => boolean
+) {
+ const store = useContext(DmnEditorStoreApiContext);
+
+ if (store === null) {
+ throw new Error("Can't use DMN Editor Store outside of the DmnEditor component.");
+ }
+
+ return useStoreWithEqualityFn(store, selector, equalityFn);
+}
+
+export function useDmnEditorStoreApi() {
+ return useContext(DmnEditorStoreApiContext);
+}
diff --git a/packages/dmn-editor/src/focus/useFocusableElement.ts b/packages/dmn-editor/src/store/computed/computeAllFeelVariableUniqueNames.ts
similarity index 53%
copy from packages/dmn-editor/src/focus/useFocusableElement.ts
copy to packages/dmn-editor/src/store/computed/computeAllFeelVariableUniqueNames.ts
index d4b0823497b..e64538f84f3 100644
--- a/packages/dmn-editor/src/focus/useFocusableElement.ts
+++ b/packages/dmn-editor/src/store/computed/computeAllFeelVariableUniqueNames.ts
@@ -17,32 +17,27 @@
* under the License.
*/
-import { useEffect } from "react";
-import { useDmnEditorStore, useDmnEditorStoreApi } from "../store/Store";
+import { UniqueNameIndex } from "../../Dmn15Spec";
+import { State } from "../Store";
-export function useFocusableElement(
- ref: React.RefObject<HTMLInputElement>,
- id: string | undefined,
- before?: (cb: () => void) => void
+export function computeAllFeelVariableUniqueNames(
+ drgElements: State["dmn"]["model"]["definitions"]["drgElement"],
+ imports: State["dmn"]["model"]["definitions"]["import"]
) {
- const focus = useDmnEditorStore((s) => s.focus);
- const dmnEditorStoreApi = useDmnEditorStoreApi();
+ const ret: UniqueNameIndex = new Map();
- useEffect(() => {
- if (!id) {
- return;
- }
+ drgElements ??= [];
+ imports ??= [];
- const cb = () => {
- ref.current?.select();
+ for (let i = 0; i < drgElements.length; i++) {
+ const drgElement = drgElements[i];
+ ret.set(drgElement["@_name"]!, drgElement["@_id"]!);
+ }
- dmnEditorStoreApi.setState((state) => {
- state.focus.consumableId = undefined;
- });
- };
+ for (let i = 0; i < imports.length; i++) {
+ const _import = imports[i];
+ ret.set(_import["@_name"], _import["@_id"]!);
+ }
- if (focus.consumableId === id && ref.current) {
- before?.(cb) ?? cb();
- }
- }, [before, dmnEditorStoreApi, focus.consumableId, id, ref]);
+ return ret;
}
diff --git a/packages/dmn-editor/src/store/computed/computeDataTypes.ts b/packages/dmn-editor/src/store/computed/computeDataTypes.ts
new file mode 100644
index 00000000000..a54e6db33da
--- /dev/null
+++ b/packages/dmn-editor/src/store/computed/computeDataTypes.ts
@@ -0,0 +1,131 @@
+/*
+ * 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 { DMN15__tImport, DMN15__tItemDefinition } from "@kie-tools/dmn-marshaller/dist/schemas/dmn-1_5/ts-gen/types";
+import { UniqueNameIndex } from "../../Dmn15Spec";
+import { builtInFeelTypeNames } from "../../dataTypes/BuiltInFeelTypes";
+import { DataType, DataTypeIndex } from "../../dataTypes/DataTypes";
+import { buildFeelQNameFromNamespace } from "../../feel/buildFeelQName";
+import { TypeOrReturnType } from "../ComputedStateCache";
+import { Computed, State } from "../Store";
+
+export function computeDataTypes(
+ namespace: State["dmn"]["model"]["definitions"]["@_namespace"],
+ itemDefinitions: State["dmn"]["model"]["definitions"]["itemDefinition"],
+ externalModelTypesByNamespace: TypeOrReturnType<Computed["getExternalModelTypesByNamespace"]>,
+ thisDmnsImportsByNamespace: TypeOrReturnType<Computed["importsByNamespace"]>
+) {
+ const allDataTypesById: DataTypeIndex = new Map();
+ const allTopLevelDataTypesByFeelName: DataTypeIndex = new Map();
+
+ const externalDmnsDataTypeTree = [...externalModelTypesByNamespace.dmns.values()].flatMap((externalDmn) => {
+ return buildDataTypesTree(
+ externalDmn.model.definitions.itemDefinition ?? [],
+ thisDmnsImportsByNamespace,
+ allDataTypesById,
+ allTopLevelDataTypesByFeelName,
+ undefined,
+ new Set(),
+ externalDmn.model.definitions["@_namespace"],
+ namespace
+ );
+ });
+
+ // Purposefully do thisDmn's after. This will make sure thisDmn's ItemDefintiions
+ // take precedence over any external ones imported to the default namespace.
+ const thisDmnsDataTypeTree = buildDataTypesTree(
+ itemDefinitions ?? [],
+ thisDmnsImportsByNamespace,
+ allDataTypesById,
+ allTopLevelDataTypesByFeelName,
+ undefined,
+ new Set(),
+ namespace,
+ namespace
+ );
+
+ const allTopLevelItemDefinitionUniqueNames: UniqueNameIndex = new Map();
+
+ for (const [k, v] of allTopLevelDataTypesByFeelName.entries()) {
+ allTopLevelItemDefinitionUniqueNames.set(k, v.itemDefinition["@_id"]!);
+ }
+
+ for (const type of builtInFeelTypeNames) {
+ allTopLevelItemDefinitionUniqueNames.set(type, type);
+ }
+
+ return {
+ dataTypesTree: [...thisDmnsDataTypeTree, ...externalDmnsDataTypeTree],
+ allDataTypesById,
+ allTopLevelDataTypesByFeelName,
+ allTopLevelItemDefinitionUniqueNames,
+ };
+}
+
+export function buildDataTypesTree(
+ items: DMN15__tItemDefinition[],
+ importsByNamespace: Map<string, DMN15__tImport>,
+ allDataTypesById: DataTypeIndex,
+ allTopLevelDataTypesByFeelName: DataTypeIndex,
+ parentId: string | undefined,
+ parents: Set<string>,
+ namespace: string,
+ relativeToNamespace: string
+) {
+ const dataTypesTree: DataType[] = [];
+
+ for (let i = 0; i < items.length; i++) {
+ const itemDefinition = items[i];
+
+ const feelName = buildFeelQNameFromNamespace({
+ importsByNamespace,
+ namedElement: itemDefinition,
+ namespace,
+ relativeToNamespace,
+ }).full;
+
+ const dataType: DataType = {
+ itemDefinition,
+ index: i,
+ parentId,
+ parents,
+ feelName,
+ namespace,
+ children: buildDataTypesTree(
+ itemDefinition.itemComponent ?? [],
+ importsByNamespace,
+ allDataTypesById,
+ allTopLevelDataTypesByFeelName,
+ itemDefinition["@_id"],
+ new Set([...parents, itemDefinition["@_id"]!]),
+ namespace,
+ relativeToNamespace
+ ),
+ };
+
+ dataTypesTree.push(dataType);
+ allDataTypesById.set(itemDefinition["@_id"]!, dataType);
+
+ if (parentId === undefined) {
+ allTopLevelDataTypesByFeelName.set(feelName, dataType);
+ }
+ }
+
+ return dataTypesTree;
+}
diff --git a/packages/dmn-editor/src/store/computed/computeDiagramData.ts b/packages/dmn-editor/src/store/computed/computeDiagramData.ts
new file mode 100644
index 00000000000..bc71ae64dd0
--- /dev/null
+++ b/packages/dmn-editor/src/store/computed/computeDiagramData.ts
@@ -0,0 +1,432 @@
+/*
+ * 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 { DMN15__tDefinitions } from "@kie-tools/dmn-marshaller/dist/schemas/dmn-1_5/ts-gen/types";
+import { XmlQName } from "@kie-tools/xml-parser-ts/dist/qNames";
+import * as RF from "reactflow";
+import { KIE_DMN_UNKNOWN_NAMESPACE } from "../../Dmn15Spec";
+import { snapShapeDimensions, snapShapePosition } from "../../diagram/SnapGrid";
+import { EdgeType, NodeType } from "../../diagram/connections/graphStructure";
+import { EDGE_TYPES } from "../../diagram/edges/EdgeTypes";
+import { DmnDiagramEdgeData } from "../../diagram/edges/Edges";
+import { DrgEdge, EdgeVisitor, NodeVisitor, getAdjMatrix, traverse } from "../../diagram/graph/graph";
+import { getNodeTypeFromDmnObject } from "../../diagram/maths/DmnMaths";
+import { DECISION_SERVICE_COLLAPSED_DIMENSIONS, MIN_NODE_SIZES } from "../../diagram/nodes/DefaultSizes";
+import { ___NASTY_HACK_FOR_SAFARI_to_force_redrawing_svgs_and_avoid_repaint_glitches } from "../../diagram/nodes/NodeSvgs";
+import { NODE_TYPES } from "../../diagram/nodes/NodeTypes";
+import { DmnDiagramNodeData, NodeDmnObjects } from "../../diagram/nodes/Nodes";
+import { Unpacked } from "../../tsExt/tsExt";
+import { buildXmlHref, parseXmlHref } from "../../xml/xmlHrefs";
+import { TypeOrReturnType } from "../ComputedStateCache";
+import { Computed, State } from "../Store";
+
+export const NODE_LAYERS = {
+ GROUP_NODE: 0,
+ NODES: 1000, // We need a difference > 1000 here, since ReactFlow will add 1000 to the z-index when a node is selected.
+ DECISION_SERVICE_NODE: 2000, // We need a difference > 1000 here, since ReactFlow will add 1000 to the z-index when a node is selected.
+ NESTED_NODES: 4000,
+};
+
+type AckEdge = (args: {
+ id: string;
+ dmnObject: DmnDiagramEdgeData["dmnObject"];
+ type: EdgeType;
+ source: string;
+ target: string;
+}) => RF.Edge<DmnDiagramEdgeData>;
+
+type AckNode = (
+ dmnObjectQName: XmlQName,
+ dmnObject: NodeDmnObjects,
+ index: number
+) => RF.Node<DmnDiagramNodeData> | undefined;
+
+export function computeDiagramData(
+ diagram: State["diagram"],
+ definitions: State["dmn"]["model"]["definitions"],
+ externalModelTypesByNamespace: TypeOrReturnType<Computed["getExternalModelTypesByNamespace"]>,
+ indexes: TypeOrReturnType<Computed["indexes"]>
+) {
+ // console.time("nodes");
+ ___NASTY_HACK_FOR_SAFARI_to_force_redrawing_svgs_and_avoid_repaint_glitches.flag =
+ !___NASTY_HACK_FOR_SAFARI_to_force_redrawing_svgs_and_avoid_repaint_glitches.flag;
+
+ const drgElementsWithoutVisualRepresentationOnCurrentDrd: string[] = [];
+
+ const selectedNodesById = new Map<string, RF.Node<DmnDiagramNodeData>>();
+ const selectedEdgesById = new Map<string, RF.Edge<DmnDiagramEdgeData>>();
+ const selectedNodeTypes = new Set<NodeType>();
+ const nodesById = new Map<string, RF.Node<DmnDiagramNodeData>>();
+ const edgesById = new Map<string, RF.Edge<DmnDiagramEdgeData>>();
+ const parentIdsById = new Map<string, DmnDiagramNodeData>();
+
+ const { selectedNodes, draggingNodes, resizingNodes, selectedEdges } = {
+ selectedNodes: new Set(diagram._selectedNodes),
+ draggingNodes: new Set(diagram.draggingNodes),
+ resizingNodes: new Set(diagram.resizingNodes),
+ selectedEdges: new Set(diagram._selectedEdges),
+ };
+
+ // console.time("edges");
+ const edges: RF.Edge<DmnDiagramEdgeData>[] = [];
+
+ const drgEdges: DrgEdge[] = [];
+
+ const ackEdge: AckEdge = ({ id, type, dmnObject, source, target }) => {
+ const data = {
+ dmnObject,
+ dmnEdge: id ? indexes.dmnEdgesByDmnElementRef.get(id) : undefined,
+ dmnShapeSource: indexes.dmnShapesByHref.get(source),
+ dmnShapeTarget: indexes.dmnShapesByHref.get(target),
+ };
+
+ const edge: RF.Edge<DmnDiagramEdgeData> = {
+ data,
+ id,
+ type,
+ source,
+ target,
+ selected: selectedEdges.has(id),
+ };
+
+ edgesById.set(edge.id, edge);
+ if (edge.selected) {
+ selectedEdgesById.set(edge.id, edge);
+ }
+
+ edges.push(edge);
+
+ drgEdges.push({ id, sourceId: source, targetId: target, dmnObject });
+
+ return edge;
+ };
+
+ // requirements
+ ackRequirementEdges(definitions["@_namespace"], definitions["@_namespace"], definitions.drgElement, ackEdge);
+
+ // associations
+ (definitions.artifact ?? []).forEach((dmnObject, index) => {
+ if (dmnObject.__$$element !== "association") {
+ return;
+ }
+
+ ackEdge({
+ id: dmnObject["@_id"]!,
+ dmnObject: {
+ namespace: definitions["@_namespace"],
+ type: dmnObject.__$$element,
+ id: dmnObject["@_id"]!,
+ requirementType: "association",
+ index,
+ },
+ type: EDGE_TYPES.association,
+ source: dmnObject.sourceRef?.["@_href"],
+ target: dmnObject.targetRef?.["@_href"],
+ });
+ });
+
+ // console.timeEnd("edges");
+ const ackNode: AckNode = (dmnObjectQName, dmnObject, index) => {
+ const type = getNodeTypeFromDmnObject(dmnObject);
+ if (!type) {
+ return undefined;
+ }
+
+ // If the QName is composite, we try and get the namespace from the XML namespace declarations. If it's not found, we use `UNKNOWN_DMN_NAMESPACE`
+ // If the QName is simple, we simply say that the namespace is undefined, which is the same as the default namespace.
+ const dmnObjectNamespace = dmnObjectQName.prefix
+ ? definitions[`@_xmlns:${dmnObjectQName.prefix}`] ?? KIE_DMN_UNKNOWN_NAMESPACE
+ : undefined;
+
+ const id = buildXmlHref({ namespace: dmnObjectNamespace, id: dmnObjectQName.localPart });
+
+ const _shape = indexes.dmnShapesByHref.get(id);
+ if (!_shape) {
+ drgElementsWithoutVisualRepresentationOnCurrentDrd.push(id);
+ return undefined;
+ }
+
+ const { dmnElementRefQName, ...shape } = _shape;
+
+ const data: DmnDiagramNodeData = {
+ dmnObjectNamespace,
+ dmnObjectQName,
+ dmnObject,
+ shape,
+ index,
+ parentRfNode: undefined,
+ };
+
+ const newNode: RF.Node<DmnDiagramNodeData> = {
+ id,
+ type,
+ selected: selectedNodes.has(id),
+ dragging: draggingNodes.has(id),
+ resizing: resizingNodes.has(id),
+ position: snapShapePosition(diagram.snapGrid, shape),
+ data,
+ zIndex: NODE_LAYERS.NODES,
+ style: { ...snapShapeDimensions(diagram.snapGrid, shape, MIN_NODE_SIZES[type](diagram.snapGrid)) },
+ };
+
+ if (dmnObject?.__$$element === "decisionService") {
+ const containedDecisions = [...(dmnObject.outputDecision ?? []), ...(dmnObject.encapsulatedDecision ?? [])];
+ for (let i = 0; i < containedDecisions.length; i++) {
+ parentIdsById.set(containedDecisions[i]["@_href"], data);
+ }
+ if (shape["@_isCollapsed"] || !!dmnObjectNamespace) {
+ newNode.style = {
+ ...newNode.style,
+ ...DECISION_SERVICE_COLLAPSED_DIMENSIONS,
+ };
+ }
+ }
+
+ nodesById.set(newNode.id, newNode);
+ if (newNode.selected) {
+ selectedNodesById.set(newNode.id, newNode);
+ selectedNodeTypes.add(newNode.type as NodeType);
+ }
+ return newNode;
+ };
+
+ const localNodes: RF.Node<DmnDiagramNodeData>[] = [
+ ...(definitions.drgElement ?? []).flatMap((dmnObject, index) => {
+ const newNode = ackNode({ type: "xml-qname", localPart: dmnObject["@_id"]! }, dmnObject, index);
+ return newNode ? [newNode] : [];
+ }),
+ ...(definitions.artifact ?? []).flatMap((dmnObject, index) => {
+ if (dmnObject.__$$element === "association") {
+ return [];
+ }
+
+ const newNode = ackNode({ type: "xml-qname", localPart: dmnObject["@_id"]! }, dmnObject, index);
+ return newNode ? [newNode] : [];
+ }),
+ ];
+
+ // Assign parents & z-index to NODES
+ for (let i = 0; i < localNodes.length; i++) {
+ const parent = parentIdsById.get(localNodes[i].id);
+ if (parent) {
+ localNodes[i].data.parentRfNode = nodesById.get(
+ buildXmlHref({ namespace: parent.dmnObjectNamespace, id: parent.dmnObjectQName.localPart })
+ );
+ localNodes[i].extent = undefined; // Allows the node to be dragged freely outside of parent's bounds.
+ localNodes[i].zIndex = NODE_LAYERS.NESTED_NODES;
+ }
+
+ if (localNodes[i].type === NODE_TYPES.group) {
+ localNodes[i].zIndex = NODE_LAYERS.GROUP_NODE;
+ } else if (localNodes[i].type === NODE_TYPES.decisionService) {
+ localNodes[i].zIndex = NODE_LAYERS.DECISION_SERVICE_NODE;
+ }
+ }
+
+ const externalDrgElementsByIdByNamespace = [...externalModelTypesByNamespace.dmns.entries()].reduce(
+ (acc, [namespace, externalDmn]) => {
+ // Taking advantage of the loop to add the edges here...
+ ackRequirementEdges(
+ definitions["@_namespace"],
+ externalDmn.model.definitions["@_namespace"],
+ externalDmn.model.definitions.drgElement,
+ ackEdge
+ );
+
+ return acc.set(
+ namespace,
+ (externalDmn.model.definitions.drgElement ?? []).reduce(
+ (acc, e, index) => acc.set(e["@_id"]!, { element: e, index }),
+ new Map<string, { index: number; element: Unpacked<DMN15__tDefinitions["drgElement"]> }>()
+ )
+ );
+ },
+ new Map<string, Map<string, { index: number; element: Unpacked<DMN15__tDefinitions["drgElement"]> }>>()
+ );
+
+ const externalNodes = [...indexes.dmnShapesByHref.entries()].flatMap(([href, shape]) => {
+ if (nodesById.get(href)) {
+ return [];
+ }
+
+ if (!nodesById.get(href) && !indexes.hrefsOfDmnElementRefsOfShapesPointingToExternalDmnObjects.has(href)) {
+ // Unknown local node.
+ console.warn(`DMN DIAGRAM: Found a shape that references a local DRG element that doesn't exist.`, shape);
+ const newNode = ackNode(shape.dmnElementRefQName, null, -1);
+ return newNode ? [newNode] : [];
+ }
+
+ const namespace = definitions[`@_xmlns:${shape.dmnElementRefQName.prefix}`];
+ if (!namespace) {
+ console.warn(
+ `DMN DIAGRAM: Found a shape that references an external node with a namespace that is not declared at this DMN.`,
+ shape
+ );
+ const newNode = ackNode(shape.dmnElementRefQName, null, -1);
+ return newNode ? [newNode] : [];
+ }
+
+ const externalDrgElementsById = externalDrgElementsByIdByNamespace.get(namespace);
+ if (!externalDrgElementsById) {
+ console.warn(
+ `DMN DIAGRAM: Found a shape that references an external node from a namespace that is not provided on this DMN's external DMNs mapping.`,
+ shape
+ );
+ const newNode = ackNode(shape.dmnElementRefQName, null, -1);
+ return newNode ? [newNode] : [];
+ }
+
+ const externalDrgElement = externalDrgElementsById.get(shape.dmnElementRefQName.localPart);
+ if (!externalDrgElement) {
+ console.warn(`DMN DIAGRAM: Found a shape that references a non-existent node from an external DMN.`, shape);
+ const newNode = ackNode(shape.dmnElementRefQName, null, -1);
+ return newNode ? [newNode] : [];
+ }
+
+ const newNode = ackNode(shape.dmnElementRefQName, externalDrgElement.element, externalDrgElement.index);
+ return newNode ? [newNode] : [];
+ });
+
+ // Groups are always at the back. Decision Services after groups, then everything else.
+ const sortedNodes = [...localNodes, ...externalNodes]
+ .sort((a, b) => Number(b.type === NODE_TYPES.decisionService) - Number(a.type === NODE_TYPES.decisionService))
+ .sort((a, b) => Number(b.type === NODE_TYPES.group) - Number(a.type === NODE_TYPES.group));
+
+ // Selected edges go to the end of the array. This is necessary because z-index doesn't work on SVGs.
+ const sortedEdges = edges
+ .filter((e) => nodesById.has(e.source) && nodesById.has(e.target))
+ .sort((a, b) => Number(selectedEdges.has(a.id)) - Number(selectedEdges.has(b.id)));
+
+ // console.timeEnd("nodes");
+ if (diagram.overlays.enableNodeHierarchyHighlight) {
+ assignClassesToHighlightedHierarchyNodes(diagram._selectedNodes, nodesById, edgesById, drgEdges);
+ }
+
+ return {
+ drgEdges,
+ nodes: sortedNodes,
+ edges: sortedEdges,
+ edgesById,
+ nodesById,
+ selectedNodeTypes,
+ selectedNodesById,
+ selectedEdgesById,
+ drgElementsWithoutVisualRepresentationOnCurrentDrd,
+ };
+}
+
+function ackRequirementEdges(
+ thisDmnsNamespace: string,
+ drgElementsNamespace: string,
+ drgElements: DMN15__tDefinitions["drgElement"],
+ ackEdge: AckEdge
+) {
+ const namespace = drgElementsNamespace === thisDmnsNamespace ? "" : drgElementsNamespace;
+
+ for (const dmnObject of drgElements ?? []) {
+ // information requirements
+ if (dmnObject.__$$element === "decision") {
+ (dmnObject.informationRequirement ?? []).forEach((ir, index) => {
+ const irHref = parseXmlHref((ir.requiredDecision ?? ir.requiredInput)!["@_href"]);
+ ackEdge({
+ id: ir["@_id"]!,
+ dmnObject: {
+ namespace: drgElementsNamespace,
+ type: dmnObject.__$$element,
+ id: dmnObject["@_id"]!,
+ requirementType: "informationRequirement",
+ index,
+ },
+ type: EDGE_TYPES.informationRequirement,
+ source: buildXmlHref({ namespace: irHref.namespace ?? namespace, id: irHref.id }),
+ target: buildXmlHref({ namespace, id: dmnObject["@_id"]! }),
+ });
+ });
+ }
+ // knowledge requirements
+ if (dmnObject.__$$element === "decision" || dmnObject.__$$element === "businessKnowledgeModel") {
+ (dmnObject.knowledgeRequirement ?? []).forEach((kr, index) => {
+ const krHref = parseXmlHref(kr.requiredKnowledge["@_href"]);
+ ackEdge({
+ id: kr["@_id"]!,
+ dmnObject: {
+ namespace: drgElementsNamespace,
+ type: dmnObject.__$$element,
+ id: dmnObject["@_id"]!,
+ requirementType: "knowledgeRequirement",
+ index,
+ },
+ type: EDGE_TYPES.knowledgeRequirement,
+ source: buildXmlHref({ namespace: krHref.namespace ?? namespace, id: krHref.id }),
+ target: buildXmlHref({ namespace, id: dmnObject["@_id"]! }),
+ });
+ });
+ }
+ // authority requirements
+ if (
+ dmnObject.__$$element === "decision" ||
+ dmnObject.__$$element === "businessKnowledgeModel" ||
+ dmnObject.__$$element === "knowledgeSource"
+ ) {
+ (dmnObject.authorityRequirement ?? []).forEach((ar, index) => {
+ const arHref = parseXmlHref((ar.requiredInput ?? ar.requiredDecision ?? ar.requiredAuthority)!["@_href"]);
+ ackEdge({
+ id: ar["@_id"]!,
+ dmnObject: {
+ namespace: drgElementsNamespace,
+ type: dmnObject.__$$element,
+ id: dmnObject["@_id"]!,
+ requirementType: "authorityRequirement",
+ index,
+ },
+ type: EDGE_TYPES.authorityRequirement,
+ source: buildXmlHref({ namespace: arHref.namespace ?? namespace, id: arHref.id }),
+ target: buildXmlHref({ namespace, id: dmnObject["@_id"]! }),
+ });
+ });
+ }
+ }
+}
+
+export function assignClassesToHighlightedHierarchyNodes(
+ selected: string[],
+ nodesById: Map<string, RF.Node>,
+ edgesById: Map<string, RF.Edge>,
+ drgEdges: DrgEdge[]
+) {
+ const nodeVisitor: NodeVisitor = (nodeId, traversalDirection) => {
+ const node = nodesById.get(nodeId);
+ if (node) {
+ node.className = `hierarchy ${traversalDirection}`;
+ }
+ };
+
+ const edgeVisitor: EdgeVisitor = (edge, traversalDirection) => {
+ const rfEdge = edgesById.get(edge.id);
+ if (rfEdge) {
+ rfEdge.className = `hierarchy ${traversalDirection}`;
+ }
+ };
+
+ const __selectedSet = new Set(selected);
+ const __adjMatrix = getAdjMatrix(drgEdges);
+
+ traverse(__adjMatrix, __selectedSet, selected, "up", nodeVisitor, edgeVisitor);
+ traverse(__adjMatrix, __selectedSet, selected, "down", nodeVisitor, edgeVisitor); // Traverse "down" after "up" because when there's a cycle, highlighting a node as a dependency is preferable.
+}
diff --git a/packages/dmn-editor/src/store/computed/computeExternalModelsByType.ts b/packages/dmn-editor/src/store/computed/computeExternalModelsByType.ts
new file mode 100644
index 00000000000..b62fec76e2b
--- /dev/null
+++ b/packages/dmn-editor/src/store/computed/computeExternalModelsByType.ts
@@ -0,0 +1,50 @@
+/*
+ * 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 { ExternalDmnsIndex, ExternalModelsIndex, ExternalPmmlsIndex } from "../../DmnEditor";
+import { getNamespaceOfDmnImport } from "../../includedModels/importNamespaces";
+import { State } from "../Store";
+
+export function computeExternalModelsByType(
+ imports: State["dmn"]["model"]["definitions"]["import"],
+ externalModelsByNamespace: ExternalModelsIndex | undefined
+) {
+ return (imports ?? []).reduce<{ dmns: ExternalDmnsIndex; pmmls: ExternalPmmlsIndex }>(
+ (acc, _import) => {
+ const externalModel = externalModelsByNamespace?.[getNamespaceOfDmnImport({ dmnImport: _import })];
+ if (!externalModel) {
+ console.warn(
+ `DMN DIAGRAM: Can't index external model with namespace '${_import["@_namespace"]}' because it doesn't exist on the external models list.`
+ );
+ return acc;
+ }
+
+ if (externalModel.type === "dmn") {
+ acc.dmns.set(_import["@_namespace"], externalModel);
+ } else if (externalModel.type === "pmml") {
+ acc.pmmls.set(_import["@_namespace"], externalModel);
+ } else {
+ console.warn("DMN EDITOR: Unknown external model type", externalModel);
+ }
+
+ return acc;
+ },
+ { dmns: new Map(), pmmls: new Map() }
+ );
+}
diff --git a/packages/dmn-editor/src/focus/useFocusableElement.ts b/packages/dmn-editor/src/store/computed/computeImportsByNamespace.ts
similarity index 52%
copy from packages/dmn-editor/src/focus/useFocusableElement.ts
copy to packages/dmn-editor/src/store/computed/computeImportsByNamespace.ts
index d4b0823497b..994cf3ce945 100644
--- a/packages/dmn-editor/src/focus/useFocusableElement.ts
+++ b/packages/dmn-editor/src/store/computed/computeImportsByNamespace.ts
@@ -17,32 +17,15 @@
* under the License.
*/
-import { useEffect } from "react";
-import { useDmnEditorStore, useDmnEditorStoreApi } from "../store/Store";
+import { DMN15__tImport } from "@kie-tools/dmn-marshaller/dist/schemas/dmn-1_5/ts-gen/types";
+import { State } from "../Store";
-export function useFocusableElement(
- ref: React.RefObject<HTMLInputElement>,
- id: string | undefined,
- before?: (cb: () => void) => void
-) {
- const focus = useDmnEditorStore((s) => s.focus);
- const dmnEditorStoreApi = useDmnEditorStoreApi();
+export function computeImportsByNamespace(imports: State["dmn"]["model"]["definitions"]["import"]) {
+ imports ??= [];
- useEffect(() => {
- if (!id) {
- return;
- }
-
- const cb = () => {
- ref.current?.select();
-
- dmnEditorStoreApi.setState((state) => {
- state.focus.consumableId = undefined;
- });
- };
-
- if (focus.consumableId === id && ref.current) {
- before?.(cb) ?? cb();
- }
- }, [before, dmnEditorStoreApi, focus.consumableId, id, ref]);
+ const ret = new Map<string, DMN15__tImport>();
+ for (let i = 0; i < imports.length; i++) {
+ ret.set(imports[i]["@_namespace"], imports[i]);
+ }
+ return ret;
}
diff --git a/packages/dmn-editor/src/store/computed/computeIndexes.ts b/packages/dmn-editor/src/store/computed/computeIndexes.ts
new file mode 100644
index 00000000000..9e5cd35e677
--- /dev/null
+++ b/packages/dmn-editor/src/store/computed/computeIndexes.ts
@@ -0,0 +1,68 @@
+/*
+ * 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 { DMNDI15__DMNEdge, DMNDI15__DMNShape } from "@kie-tools/dmn-marshaller/dist/schemas/dmn-1_5/ts-gen/types";
+import { XmlQName, parseXmlQName } from "@kie-tools/xml-parser-ts/dist/qNames";
+import { KIE_DMN_UNKNOWN_NAMESPACE } from "../../Dmn15Spec";
+import { buildXmlHref } from "../../xml/xmlHrefs";
+import { State } from "../Store";
+
+export function computeIndexes(
+ definitions: State["dmn"]["model"]["definitions"],
+ drdIndex: State["diagram"]["drdIndex"]
+) {
+ const dmnEdgesByDmnElementRef = new Map<string, DMNDI15__DMNEdge & { index: number }>();
+ const dmnShapesByHref = new Map<string, DMNDI15__DMNShape & { index: number; dmnElementRefQName: XmlQName }>();
+ const hrefsOfDmnElementRefsOfShapesPointingToExternalDmnObjects = new Set<string>();
+
+ const diagramElements = definitions["dmndi:DMNDI"]?.["dmndi:DMNDiagram"]?.[drdIndex]["dmndi:DMNDiagramElement"] ?? [];
+ for (let i = 0; i < diagramElements.length; i++) {
+ const e = diagramElements[i];
+ // DMNEdge
+ if (e.__$$element === "dmndi:DMNEdge") {
+ dmnEdgesByDmnElementRef.set(e["@_dmnElementRef"], { ...e, index: i });
+ }
+
+ // DMNShape
+ else if (e.__$$element === "dmndi:DMNShape") {
+ let href: string;
+ // @_dmnElementRef is a xsd:QName, meaning it can be prefixed with a namespace name.
+ // If we find the namespace as a namespace declaration on the `definitions` object, then this shape represents a node from an included model.
+ // Therefore, we need to add it to `dmnShapesForExternalNodesByDmnRefId`, so we can draw these nodes.
+ // Do not skip adding it to the regular `dmnShapesByHref`, as nodes will query this.
+ const dmnElementRefQName = parseXmlQName(e["@_dmnElementRef"]);
+ if (dmnElementRefQName.prefix) {
+ const namespace = definitions[`@_xmlns:${dmnElementRefQName.prefix}`] ?? KIE_DMN_UNKNOWN_NAMESPACE;
+ href = buildXmlHref({ namespace, id: dmnElementRefQName.localPart });
+ hrefsOfDmnElementRefsOfShapesPointingToExternalDmnObjects.add(href);
+ } else {
+ href = buildXmlHref({ id: dmnElementRefQName.localPart });
+ }
+
+ dmnShapesByHref.set(href, { ...e, index: i, dmnElementRefQName });
+ } else {
+ // Ignore anything that is unknown.
+ }
+ }
+ return {
+ dmnEdgesByDmnElementRef,
+ dmnShapesByHref,
+ hrefsOfDmnElementRefsOfShapesPointingToExternalDmnObjects,
+ };
+}
diff --git a/packages/dmn-editor/src/diagram/nodes/EditExpressionNodePanel.tsx b/packages/dmn-editor/src/store/computed/computeIsDropTargetNodeValidForSelection.ts
similarity index 53%
copy from packages/dmn-editor/src/diagram/nodes/EditExpressionNodePanel.tsx
copy to packages/dmn-editor/src/store/computed/computeIsDropTargetNodeValidForSelection.ts
index 7de7bc3cc69..c9bd4c8ac30 100644
--- a/packages/dmn-editor/src/diagram/nodes/EditExpressionNodePanel.tsx
+++ b/packages/dmn-editor/src/store/computed/computeIsDropTargetNodeValidForSelection.ts
@@ -17,28 +17,20 @@
* under the License.
*/
-import * as React from "react";
-import { Label } from "@patternfly/react-core/dist/js/components/Label";
-import { useDmnEditorStore, useDmnEditorStoreApi } from "../../store/Store";
-
-export function EditExpressionNodePanel(props: { isVisible: boolean; id: string }) {
- const dispatch = useDmnEditorStore((s) => s.dispatch);
- const dmnEditorStoreApi = useDmnEditorStoreApi();
+import { NodeType } from "../../diagram/connections/graphStructure";
+import { isValidContainment } from "../../diagram/connections/isValidContainment";
+import { Computed, State } from "../Store";
+export function computeIsDropTargetNodeValidForSelection(
+ dropTargetNode: State["diagram"]["dropTargetNode"],
+ diagramData: ReturnType<Computed["getDiagramData"]>
+) {
return (
- <>
- {props.isVisible && (
- <Label
- onClick={() =>
- dmnEditorStoreApi.setState((state) => {
- dispatch.boxedExpressionEditor.open(state, props.id);
- })
- }
- className={"kie-dmn-editor--edit-expression-node-panel"}
- >
- Edit
- </Label>
- )}
- </>
+ !!dropTargetNode &&
+ isValidContainment({
+ nodeTypes: diagramData.selectedNodeTypes,
+ inside: dropTargetNode.type as NodeType,
+ dmnObjectQName: dropTargetNode.data.dmnObjectQName,
+ })
);
}
diff --git a/packages/dmn-editor/src/diagram/nodes/EditExpressionNodePanel.tsx b/packages/dmn-editor/src/store/computed/initial.ts
similarity index 51%
copy from packages/dmn-editor/src/diagram/nodes/EditExpressionNodePanel.tsx
copy to packages/dmn-editor/src/store/computed/initial.ts
index 7de7bc3cc69..6b497a254a9 100644
--- a/packages/dmn-editor/src/diagram/nodes/EditExpressionNodePanel.tsx
+++ b/packages/dmn-editor/src/store/computed/initial.ts
@@ -17,28 +17,40 @@
* under the License.
*/
-import * as React from "react";
-import { Label } from "@patternfly/react-core/dist/js/components/Label";
-import { useDmnEditorStore, useDmnEditorStoreApi } from "../../store/Store";
+import { Cache } from "../ComputedStateCache";
+import { Computed } from "../Store";
-export function EditExpressionNodePanel(props: { isVisible: boolean; id: string }) {
- const dispatch = useDmnEditorStore((s) => s.dispatch);
- const dmnEditorStoreApi = useDmnEditorStoreApi();
-
- return (
- <>
- {props.isVisible && (
- <Label
- onClick={() =>
- dmnEditorStoreApi.setState((state) => {
- dispatch.boxedExpressionEditor.open(state, props.id);
- })
- }
- className={"kie-dmn-editor--edit-expression-node-panel"}
- >
- Edit
- </Label>
- )}
- </>
- );
-}
+export const INITIAL_COMPUTED_CACHE: Cache<Computed> = {
+ isDiagramEditingInProgress: {
+ value: undefined,
+ dependencies: [],
+ },
+ importsByNamespace: {
+ value: undefined,
+ dependencies: [],
+ },
+ indexes: {
+ value: undefined,
+ dependencies: [],
+ },
+ getDiagramData: {
+ value: undefined,
+ dependencies: [],
+ },
+ isDropTargetNodeValidForSelection: {
+ value: undefined,
+ dependencies: [],
+ },
+ getExternalModelTypesByNamespace: {
+ value: undefined,
+ dependencies: [],
+ },
+ getDataTypes: {
+ value: undefined,
+ dependencies: [],
+ },
+ getAllFeelVariableUniqueNames: {
+ value: undefined,
+ dependencies: [],
+ },
+};
diff --git a/packages/dmn-editor/src/store/useDiagramData.tsx b/packages/dmn-editor/src/store/useDiagramData.tsx
deleted file mode 100644
index 3ca1beaf800..00000000000
--- a/packages/dmn-editor/src/store/useDiagramData.tsx
+++ /dev/null
@@ -1,474 +0,0 @@
-/*
- * 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 {
- DMN15__tDefinitions,
- DMNDI15__DMNEdge,
- DMNDI15__DMNShape,
-} from "@kie-tools/dmn-marshaller/dist/schemas/dmn-1_5/ts-gen/types";
-import { useMemo } from "react";
-import * as RF from "reactflow";
-import { NODE_LAYERS, useDmnEditorStore } from "./Store";
-import { snapShapeDimensions, snapShapePosition } from "../diagram/SnapGrid";
-import { DECISION_SERVICE_COLLAPSED_DIMENSIONS } from "../diagram/nodes/DefaultSizes";
-import { EdgeType, NodeType } from "../diagram/connections/graphStructure";
-import { EDGE_TYPES } from "../diagram/edges/EdgeTypes";
-import { DmnDiagramEdgeData } from "../diagram/edges/Edges";
-import { NODE_TYPES } from "../diagram/nodes/NodeTypes";
-import { DmnDiagramNodeData, NodeDmnObjects } from "../diagram/nodes/Nodes";
-import { getNodeTypeFromDmnObject } from "../diagram/maths/DmnMaths";
-import { XmlQName, parseXmlQName } from "@kie-tools/xml-parser-ts/dist/qNames";
-import { buildXmlHref } from "../xml/xmlHrefs";
-import { ___NASTY_HACK_FOR_SAFARI_to_force_redrawing_svgs_and_avoid_repaint_glitches } from "../diagram/nodes/NodeSvgs";
-import { ExternalDmnsIndex } from "../DmnEditor";
-import { MIN_NODE_SIZES } from "../diagram/nodes/DefaultSizes";
-import { Unpacked } from "../tsExt/tsExt";
-import { NodeVisitor, EdgeVisitor, getAdjMatrix, traverse } from "../diagram/graph/graph";
-import { KIE_DMN_UNKNOWN_NAMESPACE } from "../Dmn15Spec";
-
-export const diagramColors = {
- hierarchyUp: "#0083a4",
- hierarchyDown: "#003fa4",
- selected: "#006ba4",
-};
-
-export function useDiagramData(externalDmnsByNamespace: ExternalDmnsIndex) {
- ___NASTY_HACK_FOR_SAFARI_to_force_redrawing_svgs_and_avoid_repaint_glitches.flag =
- !___NASTY_HACK_FOR_SAFARI_to_force_redrawing_svgs_and_avoid_repaint_glitches.flag;
-
- const thisDmn = useDmnEditorStore((s) => s.dmn);
- const diagram = useDmnEditorStore((s) => s.diagram);
-
- const { dmnEdgesByDmnElementRef, dmnShapesByHref, hrefsOfDmnElementRefsOfShapesPointingToExternalDmnObjects } =
- useMemo(() => {
- const dmnEdgesByDmnElementRef = new Map<string, DMNDI15__DMNEdge & { index: number }>();
- const dmnShapesByHref = new Map<string, DMNDI15__DMNShape & { index: number; dmnElementRefQName: XmlQName }>();
- const hrefsOfDmnElementRefsOfShapesPointingToExternalDmnObjects = new Set<string>();
-
- const diagramElements =
- thisDmn.model.definitions["dmndi:DMNDI"]?.["dmndi:DMNDiagram"]?.[diagram.drdIndex]["dmndi:DMNDiagramElement"] ??
- [];
- for (let i = 0; i < diagramElements.length; i++) {
- const e = diagramElements[i];
- // DMNEdge
- if (e.__$$element === "dmndi:DMNEdge") {
- dmnEdgesByDmnElementRef.set(e["@_dmnElementRef"], { ...e, index: i });
- }
-
- // DMNShape
- else if (e.__$$element === "dmndi:DMNShape") {
- let href: string;
- // @_dmnElementRef is a xsd:QName, meaning it can be prefixed with a namespace name.
- // If we find the namespace as a namespace declaration on the `definitions` object, then this shape represents a node from an included model.
- // Therefore, we need to add it to `dmnShapesForExternalNodesByDmnRefId`, so we can draw these nodes.
- // Do not skip adding it to the regular `dmnShapesByHref`, as nodes will query this.
- const dmnElementRefQName = parseXmlQName(e["@_dmnElementRef"]);
- if (dmnElementRefQName.prefix) {
- const namespace =
- thisDmn.model.definitions[`@_xmlns:${dmnElementRefQName.prefix}`] ?? KIE_DMN_UNKNOWN_NAMESPACE;
- href = buildXmlHref({ namespace, id: dmnElementRefQName.localPart });
- hrefsOfDmnElementRefsOfShapesPointingToExternalDmnObjects.add(href);
- } else {
- href = buildXmlHref({ id: dmnElementRefQName.localPart });
- }
-
- dmnShapesByHref.set(href, { ...e, index: i, dmnElementRefQName });
- } else {
- // Ignore anything that is unknown.
- }
- }
- return {
- dmnEdgesByDmnElementRef,
- dmnShapesByHref,
- hrefsOfDmnElementRefsOfShapesPointingToExternalDmnObjects,
- };
- }, [diagram.drdIndex, thisDmn.model.definitions]);
-
- const {
- nodes,
- edges,
- nodesById,
- edgesById,
- selectedNodeTypes,
- selectedNodesById,
- selectedEdgesById,
- drgElementsWithoutVisualRepresentationOnCurrentDrd,
- } = useMemo(() => {
- // console.time("nodes");
-
- const drgElementsWithoutVisualRepresentationOnCurrentDrd: string[] = [];
-
- const selectedNodesById = new Map<string, RF.Node<DmnDiagramNodeData>>();
- const selectedEdgesById = new Map<string, RF.Edge<DmnDiagramEdgeData>>();
- const selectedNodeTypes = new Set<NodeType>();
- const nodesById = new Map<string, RF.Node<DmnDiagramNodeData>>();
- const edgesById = new Map<string, RF.Edge<DmnDiagramEdgeData>>();
- const parentIdsById = new Map<string, DmnDiagramNodeData>();
-
- const { selectedNodes, draggingNodes, resizingNodes, selectedEdges } = {
- selectedNodes: new Set(diagram._selectedNodes),
- draggingNodes: new Set(diagram.draggingNodes),
- resizingNodes: new Set(diagram.resizingNodes),
- selectedEdges: new Set(diagram._selectedEdges),
- };
-
- function ackEdge({
- id,
- type,
- dmnObject,
- source,
- target,
- }: {
- id: string;
- dmnObject: DmnDiagramEdgeData["dmnObject"];
- type: EdgeType;
- source: string;
- target: string;
- }): RF.Edge<DmnDiagramEdgeData> {
- const data = {
- dmnObject,
- dmnEdge: id ? dmnEdgesByDmnElementRef.get(id) : undefined,
- dmnShapeSource: dmnShapesByHref.get(source),
- dmnShapeTarget: dmnShapesByHref.get(target),
- };
-
- const edge: RF.Edge<DmnDiagramEdgeData> = {
- data,
- id,
- type,
- source,
- target,
- selected: selectedEdges.has(id),
- };
- edgesById.set(edge.id, edge);
- if (edge.selected) {
- selectedEdgesById.set(edge.id, edge);
- }
- return edge;
- }
-
- // console.time("edges");
-
- const edges: RF.Edge<DmnDiagramEdgeData>[] = [
- // information requirements
- ...(thisDmn.model.definitions.drgElement ?? []).reduce<RF.Edge<DmnDiagramEdgeData>[]>((acc, dmnObject) => {
- if (dmnObject.__$$element === "decision") {
- acc.push(
- ...(dmnObject.informationRequirement ?? []).map((ir, index) =>
- ackEdge({
- id: ir["@_id"] ?? "",
- dmnObject: {
- type: dmnObject.__$$element,
- id: dmnObject["@_id"] ?? "",
- requirementType: "informationRequirement",
- index,
- },
- type: EDGE_TYPES.informationRequirement,
- source: (ir.requiredDecision ?? ir.requiredInput)!["@_href"],
- target: buildXmlHref({ id: dmnObject["@_id"]! }),
- })
- )
- );
- }
- // knowledge requirements
- if (dmnObject.__$$element === "decision" || dmnObject.__$$element === "businessKnowledgeModel") {
- acc.push(
- ...(dmnObject.knowledgeRequirement ?? []).map((kr, index) =>
- ackEdge({
- id: kr["@_id"] ?? "",
- dmnObject: {
- type: dmnObject.__$$element,
- id: dmnObject["@_id"] ?? "",
- requirementType: "knowledgeRequirement",
- index,
- },
- type: EDGE_TYPES.knowledgeRequirement,
- source: kr.requiredKnowledge["@_href"],
- target: buildXmlHref({ id: dmnObject["@_id"]! }),
- })
- )
- );
- }
- // authority requirements
- if (
- dmnObject.__$$element === "decision" ||
- dmnObject.__$$element === "businessKnowledgeModel" ||
- dmnObject.__$$element === "knowledgeSource"
- ) {
- acc.push(
- ...(dmnObject.authorityRequirement ?? []).map((ar, index) =>
- ackEdge({
- id: ar["@_id"] ?? "",
- dmnObject: {
- type: dmnObject.__$$element,
- id: dmnObject["@_id"] ?? "",
- requirementType: "authorityRequirement",
- index,
- },
- type: EDGE_TYPES.authorityRequirement,
- source: (ar.requiredInput ?? ar.requiredDecision ?? ar.requiredAuthority)!["@_href"],
- target: buildXmlHref({ id: dmnObject["@_id"]! }),
- })
- )
- );
- }
- return acc;
- }, []),
- // associations
- ...(thisDmn.model.definitions.artifact ?? []).flatMap((dmnObject, index) =>
- dmnObject.__$$element === "association"
- ? [
- ackEdge({
- id: dmnObject["@_id"] ?? "",
- dmnObject: {
- type: dmnObject.__$$element,
- id: dmnObject["@_id"] ?? "",
- requirementType: "association",
- index,
- },
- type: EDGE_TYPES.association,
- source: dmnObject.sourceRef?.["@_href"],
- target: dmnObject.targetRef?.["@_href"],
- }),
- ]
- : []
- ),
- ];
-
- // Selected edges go to the end of the array. This is necessary because z-index doesn't work on SVGs.
- const sortedEdges = edges.sort((a, b) => Number(selectedEdges.has(a.id)) - Number(selectedEdges.has(b.id)));
-
- // console.timeEnd("edges");
-
- function ackNode(dmnObjectQName: XmlQName, dmnObject: NodeDmnObjects, index: number) {
- const type = getNodeTypeFromDmnObject(dmnObject);
- if (!type) {
- return undefined;
- }
-
- // If the QName is composite, we try and get the namespace from the XML namespace declarations. If it's not found, we use `UNKNOWN_DMN_NAMESPACE`
- // If the QName is simple, we simply say that the namespace is undefined, which is the same as the default namespace.
- const dmnObjectNamespace = dmnObjectQName.prefix
- ? thisDmn.model.definitions[`@_xmlns:${dmnObjectQName.prefix}`] ?? KIE_DMN_UNKNOWN_NAMESPACE
- : undefined;
-
- const id = buildXmlHref({ namespace: dmnObjectNamespace, id: dmnObjectQName.localPart });
-
- const _shape = dmnShapesByHref.get(id);
- if (!_shape) {
- drgElementsWithoutVisualRepresentationOnCurrentDrd.push(id);
- return undefined;
- }
-
- const { dmnElementRefQName, ...shape } = _shape;
-
- const data: DmnDiagramNodeData = {
- dmnObjectNamespace,
- dmnObjectQName,
- dmnObject,
- shape,
- index,
- parentRfNode: undefined,
- };
-
- const newNode: RF.Node<DmnDiagramNodeData> = {
- id,
- type,
- selected: selectedNodes.has(id),
- dragging: draggingNodes.has(id),
- resizing: resizingNodes.has(id),
- position: snapShapePosition(diagram.snapGrid, shape),
- data,
- zIndex: NODE_LAYERS.NODES,
- style: { ...snapShapeDimensions(diagram.snapGrid, shape, MIN_NODE_SIZES[type](diagram.snapGrid)) },
- };
-
- if (dmnObject?.__$$element === "decisionService") {
- const containedDecisions = [...(dmnObject.outputDecision ?? []), ...(dmnObject.encapsulatedDecision ?? [])];
- for (let i = 0; i < containedDecisions.length; i++) {
- parentIdsById.set(containedDecisions[i]["@_href"], data);
- }
- if (shape["@_isCollapsed"] || !!dmnObjectNamespace) {
- newNode.style = {
- ...newNode.style,
- ...DECISION_SERVICE_COLLAPSED_DIMENSIONS,
- };
- }
- }
-
- nodesById.set(newNode.id, newNode);
- if (newNode.selected) {
- selectedNodesById.set(newNode.id, newNode);
- selectedNodeTypes.add(newNode.type as NodeType);
- }
- return newNode;
- }
-
- const localNodes: RF.Node<DmnDiagramNodeData>[] = [
- ...(thisDmn.model.definitions.drgElement ?? []).flatMap((dmnObject, index) => {
- const newNode = ackNode({ type: "xml-qname", localPart: dmnObject["@_id"]! }, dmnObject, index);
- 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] : [];
- }),
- ];
-
- // Assign parents & z-index to NODES
- for (let i = 0; i < localNodes.length; i++) {
- const parent = parentIdsById.get(localNodes[i].id);
- if (parent) {
- localNodes[i].data.parentRfNode = nodesById.get(
- buildXmlHref({ namespace: parent.dmnObjectNamespace, id: parent.dmnObjectQName.localPart })
- );
- localNodes[i].extent = undefined; // Allows the node to be dragged freely outside of parent's bounds.
- localNodes[i].zIndex = NODE_LAYERS.NESTED_NODES;
- }
-
- if (localNodes[i].type === NODE_TYPES.group) {
- localNodes[i].zIndex = NODE_LAYERS.GROUP_NODE;
- } else if (localNodes[i].type === NODE_TYPES.decisionService) {
- localNodes[i].zIndex = NODE_LAYERS.DECISION_SERVICE_NODE;
- }
- }
-
- const externalNodes = [...dmnShapesByHref.entries()].flatMap(([href, shape]) => {
- if (nodesById.get(href)) {
- return [];
- }
-
- if (!nodesById.get(href) && !hrefsOfDmnElementRefsOfShapesPointingToExternalDmnObjects.has(href)) {
- // Unknown local node.
- console.warn(`DMN DIAGRAM: Found a shape that references a local DRG element that doesn't exist.`, shape);
- const newNode = ackNode(shape.dmnElementRefQName, null, -1);
- return newNode ? [newNode] : [];
- }
-
- const namespace = thisDmn.model.definitions[`@_xmlns:${shape.dmnElementRefQName.prefix}`];
- if (!namespace) {
- console.warn(
- `DMN DIAGRAM: Found a shape that references an external node with a namespace that is not declared at this DMN.`,
- shape
- );
- const newNode = ackNode(shape.dmnElementRefQName, null, -1);
- return newNode ? [newNode] : [];
- }
-
- const externalDmn = externalDmnsByNamespace.get(namespace);
- if (!externalDmn) {
- console.warn(
- `DMN DIAGRAM: Found a shape that references an external node from a namespace that is not provided on this DMN's external DMNs mapping.`,
- shape
- );
- const newNode = ackNode(shape.dmnElementRefQName, null, -1);
- return newNode ? [newNode] : [];
- }
-
- const externalDrgElementsById = (externalDmn.model.definitions.drgElement ?? []).reduce(
- (acc, e, index) => acc.set(e["@_id"]!, { element: e, index }),
- new Map<string, { index: number; element: Unpacked<DMN15__tDefinitions["drgElement"]> }>()
- );
-
- const externalDrgElement = externalDrgElementsById.get(shape.dmnElementRefQName.localPart);
- if (!externalDrgElement) {
- console.warn(`DMN DIAGRAM: Found a shape that references a non-existent node from an external DMN.`, shape);
- const newNode = ackNode(shape.dmnElementRefQName, null, -1);
- return newNode ? [newNode] : [];
- }
-
- const newNode = ackNode(shape.dmnElementRefQName, externalDrgElement.element, externalDrgElement.index);
- return newNode ? [newNode] : [];
- });
-
- // Groups are always at the back. Decision Services after groups, then everything else.
- const sortedNodes = [...localNodes, ...externalNodes]
- .sort((a, b) => Number(b.type === NODE_TYPES.decisionService) - Number(a.type === NODE_TYPES.decisionService))
- .sort((a, b) => Number(b.type === NODE_TYPES.group) - Number(a.type === NODE_TYPES.group));
-
- // console.timeEnd("nodes");
-
- if (diagram.overlays.enableNodeHierarchyHighlight) {
- assignClassesToHighlightedHierarchyNodes([...selectedNodesById.keys()], nodesById, edges);
- }
-
- return {
- nodes: sortedNodes,
- edges: sortedEdges,
- edgesById,
- nodesById,
- selectedNodeTypes,
- selectedNodesById,
- selectedEdgesById,
- drgElementsWithoutVisualRepresentationOnCurrentDrd,
- };
- }, [
- diagram._selectedNodes,
- diagram.draggingNodes,
- diagram.resizingNodes,
- diagram._selectedEdges,
- diagram.overlays.enableNodeHierarchyHighlight,
- diagram.snapGrid,
- thisDmn.model.definitions,
- hrefsOfDmnElementRefsOfShapesPointingToExternalDmnObjects,
- dmnEdgesByDmnElementRef,
- dmnShapesByHref,
- externalDmnsByNamespace,
- ]);
-
- return {
- drgElementsWithoutVisualRepresentationOnCurrentDrd,
- dmnShapesByHref,
- dmnEdgesByDmnElementRef,
- nodesById,
- edgesById,
- nodes,
- edges,
- selectedNodeTypes,
- selectedNodesById,
- selectedEdgesById,
- };
-}
-
-export function assignClassesToHighlightedHierarchyNodes(
- selected: string[],
- nodes: Map<string, RF.Node>,
- edges: RF.Edge[]
-) {
- const nodeVisitor: NodeVisitor = (nodeId, traversalDirection) => {
- const node = nodes.get(nodeId);
- if (node) {
- node.className = `hierarchy ${traversalDirection}`;
- }
- };
-
- const edgeVisitor: EdgeVisitor = (edge, traversalDirection) => {
- edge.className = `hierarchy ${traversalDirection}`;
- };
-
- const __selectedSet = new Set(selected);
- const __adjMatrix = getAdjMatrix(edges);
-
- traverse(__adjMatrix, __selectedSet, selected, "up", nodeVisitor, edgeVisitor);
- traverse(__adjMatrix, __selectedSet, selected, "down", nodeVisitor, edgeVisitor); // Traverse "down" after "up" because when there's a cycle, highlighting a node as a dependency is preferable.
-}
diff --git a/packages/dmn-editor/src/svg/DmnDiagramSvg.tsx b/packages/dmn-editor/src/svg/DmnDiagramSvg.tsx
index 7dc91246484..14ed32fd83b 100644
--- a/packages/dmn-editor/src/svg/DmnDiagramSvg.tsx
+++ b/packages/dmn-editor/src/svg/DmnDiagramSvg.tsx
@@ -27,7 +27,7 @@ import {
KnowledgeRequirementPath,
} from "../diagram/edges/Edges";
import { DmnDiagramNodeData } from "../diagram/nodes/Nodes";
-import { SnapGrid, State } from "../store/Store";
+import { Computed, SnapGrid, State } from "../store/Store";
import { EdgeMarkers } from "../diagram/edges/EdgeMarkers";
import { EDGE_TYPES } from "../diagram/edges/EdgeTypes";
import { getSnappedMultiPointAnchoredEdgePath } from "../diagram/edges/getSnappedMultiPointAnchoredEdgePath";
@@ -54,8 +54,8 @@ import {
} 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";
+import { TypeOrReturnType } from "../store/ComputedStateCache";
export function DmnDiagramSvg({
nodes,
@@ -68,7 +68,7 @@ export function DmnDiagramSvg({
edges: RF.Edge<DmnDiagramEdgeData>[];
snapGrid: SnapGrid;
thisDmn: State["dmn"];
- importsByNamespace: DerivedStore["importsByNamespace"];
+ importsByNamespace: TypeOrReturnType<Computed["importsByNamespace"]>;
}) {
const { nodesSvg, nodesById } = useMemo(() => {
const nodesById = new Map<string, RF.Node<DmnDiagramNodeData>>();
@@ -102,105 +102,103 @@ export function DmnDiagramSvg({
}).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>
- </>
+ <g data-kie-dmn-node-id={node.id} key={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 }}
+ dy={`calc(1.5em * ${i})`}
+ {...getNodeLabelSvgTextAlignmentProps(node, getNodeLabelPosition(node.type as NodeType))}
+ >
+ {labelLine}
+ </Text>
+ ))}
+ </>
+ </g>
);
});
@@ -211,23 +209,35 @@ export function DmnDiagramSvg({
<>
<EdgeMarkers />
{edges.map((e) => {
+ const s = nodesById?.get(e.source);
+ const t = nodesById?.get(e.target);
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),
+ sourceNodeBounds: {
+ x: s?.positionAbsolute?.x,
+ y: s?.positionAbsolute?.y,
+ width: s?.width,
+ height: s?.height,
+ },
+ targetNodeBounds: {
+ x: t?.positionAbsolute?.x,
+ y: t?.positionAbsolute?.y,
+ width: t?.width,
+ height: t?.height,
+ },
});
return (
- <>
+ <React.Fragment key={e.id}>
{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} />}
- </>
+ </React.Fragment>
);
})}
{nodesSvg}
diff --git a/packages/dmn-editor/tsconfig.json b/packages/dmn-editor/tsconfig.json
index 6a8d695ed19..e1728dbdb5f 100644
--- a/packages/dmn-editor/tsconfig.json
+++ b/packages/dmn-editor/tsconfig.json
@@ -1,8 +1,10 @@
{
"extends": "@kie-tools/tsconfig/tsconfig.json",
"compilerOptions": {
- "outDir": "dist",
- "esModuleInterop": true
+ "module": "ES6",
+ "moduleResolution": "Node",
+ "esModuleInterop": true,
+ "outDir": "./dist"
},
"include": ["./src"]
}
diff --git a/packages/dmn-feel-antlr4-parser/src/parser/VariablesRepository.ts b/packages/dmn-feel-antlr4-parser/src/parser/VariablesRepository.ts
index 5b9366d7884..c26f7ba35f1 100644
--- a/packages/dmn-feel-antlr4-parser/src/parser/VariablesRepository.ts
+++ b/packages/dmn-feel-antlr4-parser/src/parser/VariablesRepository.ts
@@ -513,7 +513,7 @@ export class VariablesRepository {
break;
default:
- throw new Error("Unknown or not supported type for expression.");
+ // throw new Error("Unknown or not supported type for expression.");
}
}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 119a03261d6..c5f21896cf6 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -3352,6 +3352,12 @@ importers:
d3-selection:
specifier: ^3.0.0
version: 3.0.0
+ elkjs:
+ specifier: ^0.9.1
+ version: 0.9.1
+ fast-deep-equal:
+ specifier: ^3.1.3
+ version: 3.1.3
immer:
specifier: ^10.0.3
version: 10.0.3(patch_hash=utu5oov26wz5mjuays57tp3ybu)
@@ -30078,6 +30084,11 @@ packages:
{ integrity: sha512-L6uRgvZTH+4OF5NE/MBbzQx/WYpru1xCBE9respNj6qznEewGUIfhzmm7horWWxbNO2M0WckQypGctR8lH79xQ== }
dev: false
+ /elkjs@0.9.1:
+ resolution:
+ { integrity: sha512-JWKDyqAdltuUcyxaECtYG6H4sqysXSLeoXuGUBfRNESMTkj+w+qdb0jya8Z/WI0jVd03WQtCGhS6FOFtlhD5FQ== }
+ dev: false
+
/ellipsize@0.2.0:
resolution:
{ integrity: sha512-InJhblLPZbBjw3N49knOWonfprgKPLKGySmG6bGHi7WsD5OkXIIlLkU4AguROmaMZ0v1BRdo267wEc0Pexw8ww== }
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@kie.apache.org
For additional commands, e-mail: commits-help@kie.apache.org