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 ea6c23d5027 kie-issues#451: Implement autolayout on the new
React-based DMN Editor for DMN files without Diagram information - Part 1
(#2341)
ea6c23d5027 is described below
commit ea6c23d502720b836422bfc3868c364f0e793c2d
Author: Luiz João Motta <[email protected]>
AuthorDate: Fri May 24 00:34:42 2024 -0300
kie-issues#451: Implement autolayout on the new React-based DMN Editor for
DMN files without Diagram information - Part 1 (#2341)
---
packages/dmn-editor/src/DmnEditor.tsx | 4 +
.../dmn-editor/src/autolayout/AutoLayoutHook.ts | 220 +++++++++
.../dmn-editor/src/autolayout/AutolayoutButton.tsx | 542 +--------------------
packages/dmn-editor/src/autolayout/autoLayout.ts | 396 +++++++++++++++
packages/dmn-editor/src/diagram/Diagram.tsx | 142 +++++-
packages/dmn-editor/src/diagram/nodes/Nodes.tsx | 2 +-
packages/dmn-editor/src/mutations/resizeNode.ts | 8 +-
.../mutations/updateDecisionServiceDividerLine.ts | 10 +-
.../src/normalization/autoGenerateDrd.ts | 211 ++++++++
packages/dmn-editor/src/store/Store.ts | 18 +-
.../dmn-editor/stories/dev/DevWebApp.stories.tsx | 27 +
11 files changed, 1046 insertions(+), 534 deletions(-)
diff --git a/packages/dmn-editor/src/DmnEditor.tsx
b/packages/dmn-editor/src/DmnEditor.tsx
index 9f60ae5972d..3232ab0b562 100644
--- a/packages/dmn-editor/src/DmnEditor.tsx
+++ b/packages/dmn-editor/src/DmnEditor.tsx
@@ -250,6 +250,10 @@ export const DmnEditorInternal = ({
if (model === original(state.dmn.model)) {
return;
}
+
+ state.diagram.autoLayout.canAutoGenerateDrd =
+ model.definitions["dmndi:DMNDI"]?.["dmndi:DMNDiagram"] === undefined &&
+ model.definitions.drgElement !== undefined;
state.dmn.model = normalize(model);
dmnModelBeforeEditingRef.current = state.dmn.model;
diff --git a/packages/dmn-editor/src/autolayout/AutoLayoutHook.ts
b/packages/dmn-editor/src/autolayout/AutoLayoutHook.ts
new file mode 100644
index 00000000000..1034b2dcced
--- /dev/null
+++ b/packages/dmn-editor/src/autolayout/AutoLayoutHook.ts
@@ -0,0 +1,220 @@
+/*
+ * 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 Elk from "elkjs/lib/elk.bundled.js";
+import { useCallback } from "react";
+import { PositionalNodeHandleId } from
"../diagram/connections/PositionalNodeHandles";
+import { EdgeType, NodeType } from "../diagram/connections/graphStructure";
+import { NODE_TYPES } from "../diagram/nodes/NodeTypes";
+import { addEdge } from "../mutations/addEdge";
+import { repositionNode } from "../mutations/repositionNode";
+import { resizeNode } from "../mutations/resizeNode";
+import { updateDecisionServiceDividerLine } from
"../mutations/updateDecisionServiceDividerLine";
+import { AutolayoutParentNode, FAKE_MARKER, visitNodeAndNested } from
"./autoLayout";
+import { State } from "../store/Store";
+import { DmnDiagramNodeData } from "../diagram/nodes/Nodes";
+import { DmnDiagramEdgeData } from "../diagram/edges/Edges";
+import { 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 * as RF from "reactflow";
+import { Normalized } from "../normalization/normalize";
+
+export function useAutoLayout() {
+ return useCallback(
+ ({
+ s,
+ autolayouted,
+ parentNodesById,
+ nodesById,
+ edgesById,
+ edges,
+ dmnShapesByHref,
+ }: {
+ s: State;
+ autolayouted: {
+ isHorizontal: boolean;
+ nodes: Elk.ElkNode[] | undefined;
+ edges: Elk.ElkExtendedEdge[] | undefined;
+ };
+ parentNodesById: Map<string, AutolayoutParentNode>;
+ nodesById: Map<string, RF.Node<DmnDiagramNodeData, string | undefined>>;
+ edgesById: Map<string, RF.Edge<DmnDiagramEdgeData>>;
+ edges: RF.Edge<DmnDiagramEdgeData>[];
+ dmnShapesByHref: Map<
+ string,
+ Normalized<DMNDI15__DMNShape> & {
+ index: number;
+ dmnElementRefQName: XmlQName;
+ }
+ >;
+ }) => {
+ // 7. Update all nodes positions skipping empty groups, which will be
positioned manually after all nodes are done being repositioned.
+ 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 = nodesById.get(nodeId)!;
+
+ repositionNode({
+ definitions: s.dmn.model.definitions,
+ drdIndex: s.computed(s).getDrdIndex(),
+ 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 = nodesById.get(nodeId)!;
+
+ resizeNode({
+ definitions: s.dmn.model.definitions,
+ drdIndex: s.computed(s).getDrdIndex(),
+ __readonly_dmnShapesByHref: dmnShapesByHref,
+ snapGrid: s.diagram.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 = 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.computed(s).getDrdIndex(),
+ __readonly_dmnShapesByHref: dmnShapesByHref,
+ drgElementIndex: parentNode.data.index,
+ shapeIndex: parentNode.data.shape.index,
+ snapGrid: s.diagram.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 = edgesById.get(elkEdge.id)!;
+
+ const sourceNode = nodesById.get(elkEdge.sources[0])!;
+ const targetNode = 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.computed(s).getDrdIndex(),
+ 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,
+ });
+ }
+ },
+ []
+ );
+}
diff --git a/packages/dmn-editor/src/autolayout/AutolayoutButton.tsx
b/packages/dmn-editor/src/autolayout/AutolayoutButton.tsx
index e2d88e4aef3..af4d703a8ab 100644
--- a/packages/dmn-editor/src/autolayout/AutolayoutButton.tsx
+++ b/packages/dmn-editor/src/autolayout/AutolayoutButton.tsx
@@ -17,543 +17,53 @@
* 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 OptimizeIcon from "@patternfly/react-icons/dist/js/icons/optimize-icon";
+import { useAutoLayout } from "./AutoLayoutHook";
+import { useDmnEditorStoreApi } from "../store/StoreContext";
+import { autoLayout } from "./autoLayout";
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 { useDmnEditorStore, 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 isAlternativeInputDataShape = useDmnEditorStore((s) =>
s.computed(s).isAlternativeInputDataShape());
- 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 applyAutoLayout = useAutoLayout();
+ const onClick = React.useCallback(async () => {
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,
- isAlternativeInputDataShape,
- 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, isAlternativeInputDataShape });
- 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];
+ const isAlternativeInputDataShape =
state.computed(state).isAlternativeInputDataShape();
+
+ const { autolayouted, parentNodesById } = await autoLayout({
+ snapGrid,
+ nodesById,
+ edgesById,
+ nodes,
+ drgEdges,
+ isAlternativeInputDataShape,
});
- // 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.computed(s).getDrdIndex(),
- 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.computed(s).getDrdIndex(),
- dmnShapesByHref: s.computed(s).indexedDrd().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.computed(s).getDrdIndex(),
- dmnShapesByHref: s.computed(s).indexedDrd().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.computed(s).getDrdIndex(),
- 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,
- });
- }
+ applyAutoLayout({
+ s,
+ dmnShapesByHref: s.computed(s).indexedDrd().dmnShapesByHref,
+ edges: s.computed(s).getDiagramData(externalModelsByNamespace).edges,
+ edgesById:
s.computed(s).getDiagramData(externalModelsByNamespace).edgesById,
+ nodesById:
s.computed(s).getDiagramData(externalModelsByNamespace).nodesById,
+ autolayouted: autolayouted,
+ parentNodesById: parentNodesById,
+ });
});
- }, [dmnEditorStoreApi, externalModelsByNamespace,
isAlternativeInputDataShape]);
+ }, [applyAutoLayout, dmnEditorStoreApi, externalModelsByNamespace]);
return (
- <button className={"kie-dmn-editor--autolayout-panel-toggle-button"}
onClick={onApply} title={"Autolayout (beta)"}>
+ <button className={"kie-dmn-editor--autolayout-panel-toggle-button"}
onClick={onClick} title={"Autolayout (beta)"}>
<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/autolayout/autoLayout.ts
b/packages/dmn-editor/src/autolayout/autoLayout.ts
new file mode 100644
index 00000000000..ebb672d10c3
--- /dev/null
+++ b/packages/dmn-editor/src/autolayout/autoLayout.ts
@@ -0,0 +1,396 @@
+/*
+ * 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 ELK, * as Elk from "elkjs/lib/elk.bundled.js";
+import { NodeType } from "../diagram/connections/graphStructure";
+import { DrgEdge, 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 { SnapGrid } from "../store/Store";
+import { DmnDiagramEdgeData } from "../diagram/edges/Edges";
+import { Edge, Node } from "reactflow";
+import { DmnDiagramNodeData } from "../diagram/nodes/Nodes";
+
+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;
+}
+
+export const FAKE_MARKER = "__$FAKE$__";
+
+export async function autoLayout({
+ snapGrid,
+ nodesById,
+ edgesById,
+ nodes,
+ drgEdges,
+ isAlternativeInputDataShape,
+}: {
+ snapGrid: SnapGrid;
+ nodesById: Map<string, Node<DmnDiagramNodeData, string | undefined>>;
+ edgesById: Map<string, Edge<DmnDiagramEdgeData>>;
+ nodes: Node<DmnDiagramNodeData, string | undefined>[];
+ drgEdges: DrgEdge[];
+ isAlternativeInputDataShape: boolean;
+}) {
+ 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 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,
+ isAlternativeInputDataShape,
+ 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,
isAlternativeInputDataShape });
+ 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);
+ return {
+ autolayouted,
+ parentNodesById,
+ };
+}
+
+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[],
+ };
+}
+
+export 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/diagram/Diagram.tsx
b/packages/dmn-editor/src/diagram/Diagram.tsx
index 6d907005290..57b4d7f0bbc 100644
--- a/packages/dmn-editor/src/diagram/Diagram.tsx
+++ b/packages/dmn-editor/src/diagram/Diagram.tsx
@@ -34,6 +34,7 @@ import {
EmptyStateBody,
EmptyStateIcon,
EmptyStatePrimary,
+ EmptyStateVariant,
} from "@patternfly/react-core/dist/js/components/EmptyState";
import { Label } from "@patternfly/react-core/dist/js/components/Label";
import { Popover } from "@patternfly/react-core/dist/js/components/Popover";
@@ -53,7 +54,7 @@ import {
ExternalNode,
MIME_TYPE_FOR_DMN_EDITOR_EXTERNAL_NODES_FROM_INCLUDED_MODELS,
} from "../externalNodes/ExternalNodesPanel";
-import { NodeNature, nodeNatures } from "../mutations/NodeNature";
+import { nodeNatures } from "../mutations/NodeNature";
import { addConnectedNode } from "../mutations/addConnectedNode";
import { addDecisionToDecisionService } from
"../mutations/addDecisionToDecisionService";
import { addEdge } from "../mutations/addEdge";
@@ -61,12 +62,12 @@ import { addShape } from "../mutations/addShape";
import { addStandaloneNode } from "../mutations/addStandaloneNode";
import { deleteDecisionFromDecisionService } from
"../mutations/deleteDecisionFromDecisionService";
import { EdgeDeletionMode, deleteEdge } from "../mutations/deleteEdge";
-import { NodeDeletionMode, canRemoveNodeFromDrdOnly, deleteNode } from
"../mutations/deleteNode";
+import { NodeDeletionMode, deleteNode } from "../mutations/deleteNode";
import { repositionNode } from "../mutations/repositionNode";
import { resizeNode } from "../mutations/resizeNode";
import { updateExpression } from "../mutations/updateExpression";
import { OverlaysPanel } from "../overlaysPanel/OverlaysPanel";
-import { DiagramLhsPanel, SnapGrid } from "../store/Store";
+import { DiagramLhsPanel, SnapGrid, State } from "../store/Store";
import { useDmnEditorStore, useDmnEditorStoreApi } from
"../store/StoreContext";
import { Unpacked } from "../tsExt/tsExt";
import { buildXmlHref, parseXmlHref } from "../xml/xmlHrefs";
@@ -94,8 +95,6 @@ import {
getContainmentRelationship,
getHandlePosition,
getNodeTypeFromDmnObject,
- getBounds,
- CONTAINER_NODES_DESIRABLE_PADDING,
} from "./maths/DmnMaths";
import { DEFAULT_NODE_SIZES, MIN_NODE_SIZES } from "./nodes/DefaultSizes";
import { NODE_TYPES } from "./nodes/NodeTypes";
@@ -118,7 +117,11 @@ import {
} from "../mutations/addExistingDecisionServiceToDrd";
import { updateExpressionWidths } from "../mutations/updateExpressionWidths";
import { DiagramCommands } from "./DiagramCommands";
+import { autoLayout } from "../autolayout/autoLayout";
+import { useAutoLayout } from "../autolayout/AutoLayoutHook";
+import { autoGenerateDrd } from "../normalization/autoGenerateDrd";
import { Normalized, normalize } from "../normalization/normalize";
+import OptimizeIcon from "@patternfly/react-icons/dist/js/icons/optimize-icon";
const isFirefox = typeof (window as any).InstallTrigger !== "undefined"; //
See
https://stackoverflow.com/questions/9847580/how-to-detect-safari-chrome-ie-firefox-and-opera-browsers
@@ -638,7 +641,7 @@ export const Diagram = React.forwardRef<DiagramRef, {
container: React.RefObject
resizeNode({
definitions: state.dmn.model.definitions,
drdIndex: state.computed(state).getDrdIndex(),
- dmnShapesByHref:
state.computed(state).indexedDrd().dmnShapesByHref,
+ __readonly_dmnShapesByHref:
state.computed(state).indexedDrd().dmnShapesByHref,
snapGrid: state.diagram.snapGrid,
change: {
isExternal: !!node.data.dmnObjectQName.prefix,
@@ -1122,9 +1125,12 @@ export const Diagram = React.forwardRef<DiagramRef, {
container: React.RefObject
const isEmptyStateShowing =
showEmptyState && nodes.length === 0 &&
drgElementsWithoutVisualRepresentationOnCurrentDrdLength === 0;
+ const canAutoGenerateDrd = useDmnEditorStore((s) =>
s.diagram.autoLayout.canAutoGenerateDrd);
+
return (
<>
- {isEmptyStateShowing && <DmnDiagramEmptyState
setShowEmptyState={setShowEmptyState} />}
+ {nodes.length === 0 && canAutoGenerateDrd && <DmnDiagramWithoutDrd />}
+ {isEmptyStateShowing && !canAutoGenerateDrd && <DmnDiagramEmptyState
setShowEmptyState={setShowEmptyState} />}
<DiagramContainerContextProvider container={container}>
<svg style={{ position: "absolute", top: 0, left: 0 }}>
<EdgeMarkers />
@@ -1193,6 +1199,128 @@ export const Diagram = React.forwardRef<DiagramRef, {
container: React.RefObject
}
);
+function DmnDiagramWithoutDrd() {
+ const dmnEditorStoreApi = useDmnEditorStoreApi();
+ const { externalModelsByNamespace } = useExternalModels();
+ const applyAutoLayout = useAutoLayout();
+
+ return (
+ <Bullseye
+ style={{
+ position: "absolute",
+ width: "100%",
+ pointerEvents: "none",
+ zIndex: 1,
+ height: "auto",
+ marginTop: "120px",
+ }}
+ >
+ <div className={"kie-dmn-editor--diagram-empty-state"}>
+ <Button
+ title={"Close"}
+ style={{
+ position: "absolute",
+ top: "8px",
+ right: 0,
+ }}
+ variant={ButtonVariant.plain}
+ icon={<TimesIcon />}
+ onClick={() => {
+ dmnEditorStoreApi.setState((s) => {
+ s.diagram.autoLayout.canAutoGenerateDrd = false;
+ });
+ }}
+ />
+
+ <EmptyState variant={EmptyStateVariant.small}>
+ <EmptyStateIcon icon={BlueprintIcon} />
+ <Title size={"md"} headingLevel={"h4"}>
+ Empty Diagram
+ </Title>
+ <EmptyStateBody>
+ The current DMN does not have any Diagram associated with it. Do
you want to auto-generate it?
+ </EmptyStateBody>
+
+ <EmptyStatePrimary>
+ <Button
+ variant={ButtonVariant.link}
+ icon={<OptimizeIcon />}
+ onClick={async () => {
+ const { computed, ...state } = dmnEditorStoreApi.getState();
+ const dereferencedState: State = { computed,
...JSON.parse(JSON.stringify(state)) };
+
+ const externalModelTypesByNamespace = dereferencedState
+ .computed(dereferencedState)
+ .getExternalModelTypesByNamespace(externalModelsByNamespace);
+
+ autoGenerateDrd({
+ model: dereferencedState.dmn.model,
+ diagram: dereferencedState.diagram,
+ externalModelsByNamespace,
+ externalModelTypesByNamespace,
+ });
+
+ const snapGrid = dereferencedState.diagram.snapGrid;
+ const nodesById = dereferencedState
+ .computed(dereferencedState)
+ .getDiagramData(externalModelsByNamespace).nodesById;
+ const edgesById = dereferencedState
+ .computed(dereferencedState)
+ .getDiagramData(externalModelsByNamespace).edgesById;
+ const nodes = dereferencedState
+ .computed(dereferencedState)
+ .getDiagramData(externalModelsByNamespace).nodes;
+ const edges = dereferencedState
+ .computed(dereferencedState)
+ .getDiagramData(externalModelsByNamespace).edges;
+ const drgEdges = dereferencedState
+ .computed(dereferencedState)
+ .getDiagramData(externalModelsByNamespace).drgEdges;
+ const isAlternativeInputDataShape = dereferencedState
+ .computed(dereferencedState)
+ .isAlternativeInputDataShape();
+ const dmnShapesByHref =
dereferencedState.computed(dereferencedState).indexedDrd().dmnShapesByHref;
+
+ // Auto layout the new DRD
+ const { autolayouted, parentNodesById } = await autoLayout({
+ snapGrid,
+ nodesById,
+ edgesById,
+ nodes,
+ drgEdges,
+ isAlternativeInputDataShape,
+ });
+
+ dmnEditorStoreApi.setState((s) => {
+ s.diagram.autoLayout.canAutoGenerateDrd = false;
+ applyAutoLayout({
+ s: dereferencedState,
+ dmnShapesByHref,
+ edges: edges,
+ edgesById: edgesById,
+ nodesById: nodesById,
+ autolayouted: autolayouted,
+ parentNodesById: parentNodesById,
+ });
+ s.dmn.model = dereferencedState.dmn.model;
+ });
+ }}
+ >
+ Auto-generate Diagram
+ </Button>
+ </EmptyStatePrimary>
+
+ <br />
+ <EmptyStateBody style={{ fontSize: "12px", wordBreak: "break-word"
}}>
+ Auto generating the diagram will automatically place the nodes
with the default size and shape. You can also
+ manually build your diagram using the "DRG Nodes" option
from the palette.
+ </EmptyStateBody>
+ </EmptyState>
+ </div>
+ </Bullseye>
+ );
+}
+
function DmnDiagramEmptyState({
setShowEmptyState,
}: {
diff --git a/packages/dmn-editor/src/diagram/nodes/Nodes.tsx
b/packages/dmn-editor/src/diagram/nodes/Nodes.tsx
index a826ebeebce..b95c96b4bd5 100644
--- a/packages/dmn-editor/src/diagram/nodes/Nodes.tsx
+++ b/packages/dmn-editor/src/diagram/nodes/Nodes.tsx
@@ -957,7 +957,7 @@ export const DecisionServiceNode = React.memo(
updateDecisionServiceDividerLine({
definitions: state.dmn.model.definitions,
drdIndex: state.computed(state).getDrdIndex(),
- dmnShapesByHref:
state.computed(state).indexedDrd().dmnShapesByHref,
+ __readonly_dmnShapesByHref:
state.computed(state).indexedDrd().dmnShapesByHref,
drgElementIndex: index,
shapeIndex: shape.index,
localYPosition: e.y,
diff --git a/packages/dmn-editor/src/mutations/resizeNode.ts
b/packages/dmn-editor/src/mutations/resizeNode.ts
index 8d6a1dda662..6199148f5d3 100644
--- a/packages/dmn-editor/src/mutations/resizeNode.ts
+++ b/packages/dmn-editor/src/mutations/resizeNode.ts
@@ -38,13 +38,13 @@ import { Normalized } from "../normalization/normalize";
export function resizeNode({
definitions,
drdIndex,
- dmnShapesByHref,
+ __readonly_dmnShapesByHref,
snapGrid,
change,
}: {
definitions: Normalized<DMN15__tDefinitions>;
drdIndex: number;
- dmnShapesByHref: Map<string, Normalized<DMNDI15__DMNShape> & { index: number
}>;
+ __readonly_dmnShapesByHref: Map<string, Normalized<DMNDI15__DMNShape> & {
index: number }>;
snapGrid: SnapGrid;
change: {
nodeType: NodeType;
@@ -77,7 +77,7 @@ export function resizeNode({
// We ignore handling the contents of the Decision Service when it is
external
if (!change.isExternal) {
ds.encapsulatedDecision?.forEach((ed) => {
- const edShape = dmnShapesByHref.get(ed["@_href"])!;
+ const edShape = __readonly_dmnShapesByHref.get(ed["@_href"])!;
const dim = snapShapeDimensions(snapGrid, edShape,
MIN_NODE_SIZES[NODE_TYPES.decision]({ snapGrid }));
const pos = snapShapePosition(snapGrid, edShape);
if (pos.x + dim.width > limit.x) {
@@ -91,7 +91,7 @@ export function resizeNode({
// Output Decisions don't limit the resizing vertically, only
horizontally.
ds.outputDecision?.forEach((ed) => {
- const edShape = dmnShapesByHref.get(ed["@_href"])!;
+ const edShape = __readonly_dmnShapesByHref.get(ed["@_href"])!;
const dim = snapShapeDimensions(snapGrid, edShape,
MIN_NODE_SIZES[NODE_TYPES.decision]({ snapGrid }));
const pos = snapShapePosition(snapGrid, edShape);
if (pos.x + dim.width > limit.x) {
diff --git
a/packages/dmn-editor/src/mutations/updateDecisionServiceDividerLine.ts
b/packages/dmn-editor/src/mutations/updateDecisionServiceDividerLine.ts
index 86ba8bd5e2a..5b0b3d2c684 100644
--- a/packages/dmn-editor/src/mutations/updateDecisionServiceDividerLine.ts
+++ b/packages/dmn-editor/src/mutations/updateDecisionServiceDividerLine.ts
@@ -37,7 +37,7 @@ export const DECISION_SERVICE_DIVIDER_LINE_PADDING = 100;
export function updateDecisionServiceDividerLine({
definitions,
drdIndex,
- dmnShapesByHref,
+ __readonly_dmnShapesByHref,
shapeIndex,
localYPosition,
drgElementIndex,
@@ -45,7 +45,7 @@ export function updateDecisionServiceDividerLine({
}: {
definitions: Normalized<DMN15__tDefinitions>;
drdIndex: number;
- dmnShapesByHref: Map<string, Normalized<DMNDI15__DMNShape> & { index: number
}>;
+ __readonly_dmnShapesByHref: Map<string, Normalized<DMNDI15__DMNShape> & {
index: number }>;
shapeIndex: number;
localYPosition: number;
drgElementIndex: number;
@@ -72,13 +72,13 @@ export function updateDecisionServiceDividerLine({
const upperLimit = (ds.outputDecision ?? []).reduce((acc, od) => {
const v =
- snapShapePosition(snapGrid, dmnShapesByHref.get(od["@_href"])!).y +
- snapShapeDimensions(snapGrid, dmnShapesByHref.get(od["@_href"])!,
decisionMinSizes).height;
+ snapShapePosition(snapGrid,
__readonly_dmnShapesByHref.get(od["@_href"])!).y +
+ snapShapeDimensions(snapGrid,
__readonly_dmnShapesByHref.get(od["@_href"])!, decisionMinSizes).height;
return v > acc ? v : acc;
}, snappedPosition.y + DECISION_SERVICE_DIVIDER_LINE_PADDING);
const lowerLimit = (ds.encapsulatedDecision ?? []).reduce((acc, ed) => {
- const v = snapShapePosition(snapGrid,
dmnShapesByHref.get(ed["@_href"])!).y;
+ const v = snapShapePosition(snapGrid,
__readonly_dmnShapesByHref.get(ed["@_href"])!).y;
return v < acc ? v : acc;
}, snappedPosition.y + snappedDimensions.height -
DECISION_SERVICE_DIVIDER_LINE_PADDING);
diff --git a/packages/dmn-editor/src/normalization/autoGenerateDrd.ts
b/packages/dmn-editor/src/normalization/autoGenerateDrd.ts
new file mode 100644
index 00000000000..2053b0c7f9f
--- /dev/null
+++ b/packages/dmn-editor/src/normalization/autoGenerateDrd.ts
@@ -0,0 +1,211 @@
+/*
+ * 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 { ExternalDmnsIndex, ExternalModelsIndex, ExternalPmmlsIndex } from
"../DmnEditor";
+import { computeDiagramData } from "../store/computed/computeDiagramData";
+import { State } from "../store/Store";
+import { MIN_NODE_SIZES } from "../diagram/nodes/DefaultSizes";
+import { getNodeTypeFromDmnObject } from "../diagram/maths/DmnMaths";
+import { DMN15__tDefinitions } from
"@kie-tools/dmn-marshaller/dist/schemas/dmn-1_5/ts-gen/types";
+import { DmnLatestModel } from "@kie-tools/dmn-marshaller";
+import { parseXmlHref } from "../xml/xmlHrefs";
+import { computeIndexedDrd } from "../store/computed/computeIndexes";
+import { getDefaultDrdName } from "../mutations/addOrGetDrd";
+import { addShape } from "../mutations/addShape";
+import { addEdge } from "../mutations/addEdge";
+import { EdgeType, NodeType } from "../diagram/connections/graphStructure";
+import { PositionalNodeHandleId } from
"../diagram/connections/PositionalNodeHandles";
+import { Normalized } from "./normalize";
+
+export async function autoGenerateDrd(args: {
+ model: State["dmn"]["model"];
+ diagram: State["diagram"];
+ externalModelsByNamespace: ExternalModelsIndex | undefined;
+ externalModelTypesByNamespace: {
+ dmns: ExternalDmnsIndex;
+ pmmls: ExternalPmmlsIndex;
+ };
+}) {
+ // Create DRD
+ args.model.definitions["dmndi:DMNDI"] = {
+ ...args.model.definitions["dmndi:DMNDI"],
+ "dmndi:DMNDiagram": [
+ {
+ "@_id": generateUuid(),
+ "@_name": getDefaultDrdName({ drdIndex: 0 }),
+ "@_useAlternativeInputDataShape": false,
+ "dmndi:DMNDiagramElement": [],
+ "di:extension": { "kie:ComponentsWidthsExtension": {
"kie:ComponentWidths": [{}] } },
+ },
+ ],
+ };
+
+ // 1. Add shapes from current DRG
+ args.model.definitions.drgElement?.forEach((drgElement) => {
+ const nodeType = getNodeTypeFromDmnObject(drgElement) ?? "node_unknown";
+ const minNodeSize = MIN_NODE_SIZES[nodeType]({
+ snapGrid: {
+ isEnabled: true,
+ x: 20,
+ y: 20,
+ },
+ isAlternativeInputDataShape: false,
+ });
+
+ addShape({
+ definitions: args.model.definitions,
+ drdIndex: 0,
+ nodeType: nodeType,
+ shape: {
+ "@_id": generateUuid(),
+ "@_dmnElementRef": drgElement["@_id"]!,
+ "dc:Bounds": {
+ "@_x": 0,
+ "@_y": 0,
+ ...minNodeSize,
+ },
+ },
+ });
+ });
+
+ // 2. Add shapes from external models;
+ const definedNamespaces = new Map(
+ Object.keys(args.model.definitions)
+ .filter((keys: keyof Normalized<DMN15__tDefinitions>) =>
String(keys).startsWith("@_xmlns:"))
+ .map((xmlnsKey: keyof Normalized<DMN15__tDefinitions>) => [
+ args.model.definitions[xmlnsKey],
+ xmlnsKey.split("@_xmlns:")[1],
+ ])
+ );
+
+ const updateIndexedDrdWithNodes =
computeIndexedDrd(args.model.definitions["@_namespace"],
args.model.definitions, 0);
+ const { nodesById: updatedNodesByIdWithNodes, drgEdges:
updatedDrgEdgesWithNodes } = computeDiagramData(
+ args.diagram,
+ args.model.definitions,
+ args.externalModelTypesByNamespace,
+ updateIndexedDrdWithNodes,
+ false
+ );
+
+ // Search on edges for any node that isn't on the nodesByIdMap;
+ // Only external nodes should be added to the externalNodesHref;
+ const externalNodesHref = updatedDrgEdgesWithNodes.reduce((acc, drgEdge) => {
+ if (!updatedNodesByIdWithNodes.has(drgEdge.sourceId)) {
+ acc.add(drgEdge.sourceId);
+ }
+ if (!updatedNodesByIdWithNodes.has(drgEdge.targetId)) {
+ acc.add(drgEdge.targetId);
+ }
+ return acc;
+ }, new Set<string>());
+
+ // Add external shapes
+ externalNodesHref.forEach((href) => {
+ const { namespace, id } = parseXmlHref(href);
+ if (namespace) {
+ const externalModel = args.externalModelsByNamespace?.[namespace];
+ if (externalModel && (externalModel.model as
Normalized<DmnLatestModel>).definitions) {
+ const drgElements = (externalModel.model as
Normalized<DmnLatestModel>).definitions.drgElement;
+ const drgElement = drgElements?.filter((drgElement) =>
drgElement["@_id"] === id);
+
+ const nodeType = getNodeTypeFromDmnObject(drgElement![0]) ??
"node_unknown";
+ const minNodeSize = MIN_NODE_SIZES[nodeType]({
+ snapGrid: {
+ isEnabled: true,
+ x: 20,
+ y: 20,
+ },
+ isAlternativeInputDataShape: false,
+ });
+
+ addShape({
+ definitions: args.model.definitions,
+ drdIndex: 0,
+ nodeType: nodeType,
+ shape: {
+ "@_id": generateUuid(),
+ "@_dmnElementRef": `${definedNamespaces.get(namespace)}:${id}`,
+ "dc:Bounds": {
+ "@_x": 0,
+ "@_y": 0,
+ ...minNodeSize,
+ },
+ },
+ });
+ }
+ }
+ });
+
+ // 3. Add edges
+ const updatedIndexedDrdWithExternalNodes = computeIndexedDrd(
+ args.model.definitions["@_namespace"],
+ args.model.definitions,
+ 0
+ );
+ const {
+ nodesById: updatedNodesByIdWithExternalNodes,
+ edgesById: updatedEdgesByIdWithExternalNodes,
+ drgEdges: updatedDrgEdgesWithExternalNodes,
+ } = computeDiagramData(
+ args.diagram,
+ args.model.definitions,
+ args.externalModelTypesByNamespace,
+ updatedIndexedDrdWithExternalNodes,
+ false
+ );
+
+ for (const drgEdge of updatedDrgEdgesWithExternalNodes) {
+ const edge = updatedEdgesByIdWithExternalNodes.get(drgEdge.id);
+ const sourceNode = updatedNodesByIdWithExternalNodes.get(drgEdge.sourceId);
+ const targetNode = updatedNodesByIdWithExternalNodes.get(drgEdge.targetId);
+
+ // Avoid missing nodes. Possible cause is an external model which couldn't
be found.
+ if (!edge || !sourceNode || !targetNode) {
+ continue;
+ }
+
+ addEdge({
+ definitions: args.model.definitions,
+ drdIndex: 0,
+ keepWaypoints: false,
+ 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"],
+ },
+ });
+ }
+}
diff --git a/packages/dmn-editor/src/store/Store.ts
b/packages/dmn-editor/src/store/Store.ts
index 85657b44f8f..21f04b3bf74 100644
--- a/packages/dmn-editor/src/store/Store.ts
+++ b/packages/dmn-editor/src/store/Store.ts
@@ -88,6 +88,9 @@ export interface State {
tab: DmnEditorTab;
};
diagram: {
+ autoLayout: {
+ canAutoGenerateDrd: boolean;
+ };
__unsafeDrdIndex: number;
edgeIdBeingUpdated: string | undefined;
dropTargetNode: DropTargetNode;
@@ -187,6 +190,9 @@ export const defaultStaticState = (): Omit<State, "dmn" |
"dispatch" | "computed
expandedItemComponentIds: [],
},
diagram: {
+ autoLayout: {
+ canAutoGenerateDrd: false,
+ },
__unsafeDrdIndex: 0,
edgeIdBeingUpdated: undefined,
dropTargetNode: undefined,
@@ -221,12 +227,22 @@ export const defaultStaticState = (): Omit<State, "dmn" |
"dispatch" | "computed
});
export function createDmnEditorStore(model: DmnLatestModel, computedCache:
ComputedStateCache<Computed>) {
+ const { diagram, ...defaultState } = defaultStaticState();
return create(
immer<State>(() => ({
dmn: {
model: normalize(model),
},
- ...defaultStaticState(),
+ ...defaultState,
+ diagram: {
+ ...diagram,
+ // A model without DRD and with DRG element can be auto generated
+ autoLayout: {
+ canAutoGenerateDrd:
+ model.definitions["dmndi:DMNDI"]?.["dmndi:DMNDiagram"] ===
undefined &&
+ model.definitions.drgElement !== undefined,
+ },
+ },
dispatch(s: State) {
return {
dmn: {
diff --git a/packages/dmn-editor/stories/dev/DevWebApp.stories.tsx
b/packages/dmn-editor/stories/dev/DevWebApp.stories.tsx
index 3a9c41928ca..262231b2523 100644
--- a/packages/dmn-editor/stories/dev/DevWebApp.stories.tsx
+++ b/packages/dmn-editor/stories/dev/DevWebApp.stories.tsx
@@ -31,6 +31,7 @@ import { loanPreQualificationDmn } from
"../useCases/loanPreQualification/LoanPr
import { DmnEditorWrapper } from "../dmnEditorStoriesWrapper";
import {
DmnEditorProps,
+ DmnEditorRef,
ExternalModelsIndex,
OnDmnModelChange,
OnRequestExternalModelByPath,
@@ -40,6 +41,30 @@ import {
const initialModel = generateEmptyDmn15();
+const emptyDrd = `<?xml version="1.0" encoding="UTF-8" ?>
+<definitions xmlns="https://www.omg.org/spec/DMN/20230324/MODEL/"
xmlns:dmndi="https://www.omg.org/spec/DMN/20230324/DMNDI/"
xmlns:dc="http://www.omg.org/spec/DMN/20180521/DC/"
xmlns:di="http://www.omg.org/spec/DMN/20180521/DI/"
xmlns:kie="https://kie.org/dmn/extensions/1.0"
xmlns:included0="https://kie.org/dmn/_125A5475-65CE-4574-822C-9CB2268F1393"
expressionLanguage="https://www.omg.org/spec/DMN/20230324/FEEL/"
namespace="https://kie.org/dmn/_2B849D68-E816-42F9-898A-1938B5D6B297" id="_
[...]
+ <import id="_8079D96B-F569-4F4E-830B-7462B6AFE492" name="u"
importType="http://www.omg.org/spec/DMN/20180521/MODEL/"
namespace="https://kie.org/dmn/_125A5475-65CE-4574-822C-9CB2268F1393"
locationURI="./Untitled-4.dmn" />
+ <inputData name="My Input" id="_9392B01E-8C6B-4E29-9CC4-21C16EFB2F6B">
+ <variable name="My Input" id="_9483BABF-708A-4357-AD78-18C7A770E292"
typeRef="<Undefined>" />
+ </inputData>
+ <decision name="My Decision" id="_83A0C6FA-0951-4E1E-9DF1-74A9D2A95E98">
+ <variable id="_01C70F45-2955-474A-9FAC-14967ABAF475"
typeRef="<Undefined>" name="My Decision" />
+ <informationRequirement id="_A7EAFD5D-BDF7-4D09-81A9-9C22711847C0">
+ <requiredInput
href="https://kie.org/dmn/_125A5475-65CE-4574-822C-9CB2268F1393#_D9138F6E-E9DA-47AB-8DEF-5CD531B94ABE"
/>
+ </informationRequirement>
+ <informationRequirement id="_E4FE78BB-996B-46C4-9F9B-018163E9017A">
+ <requiredInput href="#_9392B01E-8C6B-4E29-9CC4-21C16EFB2F6B" />
+ </informationRequirement>
+ <informationRequirement id="_E52D5C34-172E-4E33-B2FE-7B2A7AFDF52C">
+ <requiredInput href="#_4072ADC3-E7CF-4D22-8179-7494EE22157C" />
+ </informationRequirement>
+ </decision>
+ <inputData name="Another Input" id="_4072ADC3-E7CF-4D22-8179-7494EE22157C">
+ <variable name="Another Input" id="_7490876B-8FA9-4FEC-B078-7563EF04F52B"
typeRef="<Undefined>" />
+ </inputData>
+</definitions>
+`;
+
function DevWebApp(args: DmnEditorProps) {
const [state, setState] = useState<{
marshaller: DmnMarshaller;
@@ -168,6 +193,8 @@ function DevWebApp(args: DmnEditorProps) {
<button onClick={() =>
onSelectModel(generateEmptyDmn15())}>Empty</button>
<button onClick={() =>
onSelectModel(loanPreQualificationDmn)}>Loan Pre Qualification</button>
+
+ <button onClick={() => onSelectModel(emptyDrd)}>Empty
DRD</button>
|
<button disabled={!isUndoEnabled} style={{ opacity:
isUndoEnabled ? 1 : 0.5 }} onClick={undo}>
{`Undo (${state.pointer})`}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]