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 530f8fdcbbb kie-issues#2090: DMN Editor: Does not render edge when 
required decision uses namespace in href (#3268)
530f8fdcbbb is described below

commit 530f8fdcbbbc46e27b0eb44b2642104622c15730
Author: Tiago Bento <[email protected]>
AuthorDate: Fri Sep 12 08:48:09 2025 -0400

    kie-issues#2090: DMN Editor: Does not render edge when required decision 
uses namespace in href (#3268)
---
 packages/dmn-editor/src/diagram/graph/graph.ts     |   3 +-
 packages/dmn-editor/src/mutations/deleteEdge.ts    |   4 +-
 .../src/store/computed/computeDiagramData.ts       |  83 +++++++++++++--------
 .../NamespaceInRequiredDecisionHref.mdx            |  25 +++++++
 .../NamespaceInRequiredDecisionHref.stories.tsx    |  78 +++++++++++++++++++
 .../dmn-editor/tests-e2e/__fixtures__/editor.ts    |   6 ++
 .../namespace-in-required-decision-href.png        | Bin 0 -> 22780 bytes
 .../namespace-in-required-decision-href.png        | Bin 0 -> 23419 bytes
 .../webkit/namespace-in-required-decision-href.png | Bin 0 -> 23346 bytes
 .../tests-e2e/checkDmnWithNamespaceInHref.spec.ts  |  33 ++++++++
 10 files changed, 199 insertions(+), 33 deletions(-)

diff --git a/packages/dmn-editor/src/diagram/graph/graph.ts 
b/packages/dmn-editor/src/diagram/graph/graph.ts
index 720db0226ee..186dd54795f 100644
--- a/packages/dmn-editor/src/diagram/graph/graph.ts
+++ b/packages/dmn-editor/src/diagram/graph/graph.ts
@@ -33,7 +33,8 @@ export type DrgEdge = {
   targetId: string;
   id: string;
   dmnObject: {
-    namespace: string;
+    /* Empty or undefined if same as "thisDmn"'s namespace. */
+    normalizedNamespace: undefined | string;
     id: string;
     type: Unpacked<Normalized<DMN_LATEST__tDefinitions>["artifact" | 
"drgElement"]>["__$$element"];
     requirementType: "informationRequirement" | "knowledgeRequirement" | 
"authorityRequirement" | "association";
diff --git a/packages/dmn-editor/src/mutations/deleteEdge.ts 
b/packages/dmn-editor/src/mutations/deleteEdge.ts
index 8302768c60a..52e2e6c041a 100644
--- a/packages/dmn-editor/src/mutations/deleteEdge.ts
+++ b/packages/dmn-editor/src/mutations/deleteEdge.ts
@@ -44,7 +44,7 @@ export function deleteEdge({
   mode: EdgeDeletionMode;
   externalModelsByNamespace: ExternalModelsIndex | undefined;
 }) {
-  if (edge.dmnObject.namespace === definitions["@_namespace"]) {
+  if (!edge.dmnObject.normalizedNamespace) {
     const dmnObjects: Normalized<DMN_LATEST__tDefinitions>["drgElement" | 
"artifact"] =
       switchExpression(edge?.dmnObject.type, {
         association: definitions.artifact,
@@ -60,7 +60,7 @@ export function deleteEdge({
     if (mode === EdgeDeletionMode.FROM_DRG_AND_ALL_DRDS) {
       const requirements =
         switchExpression(edge?.dmnObject.requirementType, {
-          // Casting toDMN_LATEST__tDecision because if has all types of 
requirement, but not necessarily that's true.
+          // Casting to DMN_LATEST__tDecision because if has all types of 
requirement, but not necessarily that's true.
           informationRequirement: (dmnObjects[dmnObjectIndex] as 
Normalized<DMN_LATEST__tDecision>)
             .informationRequirement,
           knowledgeRequirement: (dmnObjects[dmnObjectIndex] as 
Normalized<DMN_LATEST__tDecision>).knowledgeRequirement,
diff --git a/packages/dmn-editor/src/store/computed/computeDiagramData.ts 
b/packages/dmn-editor/src/store/computed/computeDiagramData.ts
index 3982766f9a6..eb817ea9baf 100644
--- a/packages/dmn-editor/src/store/computed/computeDiagramData.ts
+++ b/packages/dmn-editor/src/store/computed/computeDiagramData.ts
@@ -52,7 +52,8 @@ type AckEdge = (args: {
   type: EdgeType;
   source: string;
   target: string;
-  sourceNamespace: string | undefined;
+  /* Empty or undefined if same as "thisDmn"'s namespace. */
+  sourceNormalizedNamespace: string | undefined;
 }) => RF.Edge<DmnDiagramEdgeData>;
 
 type AckNode = (
@@ -96,7 +97,7 @@ export function computeDiagramData(
   const drgEdges: DrgEdge[] = [];
   const drgAdjacencyList: DrgAdjacencyList = new Map();
 
-  const ackEdge: AckEdge = ({ id, type, dmnObject, source, target, 
sourceNamespace }) => {
+  const ackEdge: AckEdge = ({ id, type, dmnObject, source, target, 
sourceNormalizedNamespace }) => {
     const data = {
       dmnObject,
       dmnEdge: id ? indexedDrd.dmnEdgesByDmnElementRef.get(xmlHrefToQName(id, 
definitions)) : undefined,
@@ -113,9 +114,9 @@ export function computeDiagramData(
       selected: selectedEdges.has(id),
     };
 
-    if (sourceNamespace && sourceNamespace !== KIE_UNKNOWN_NAMESPACE) {
-      edgesFromExternalNodesByNamespace.set(sourceNamespace, [
-        ...(edgesFromExternalNodesByNamespace.get(sourceNamespace) ?? []),
+    if (sourceNormalizedNamespace && sourceNormalizedNamespace !== 
KIE_UNKNOWN_NAMESPACE) {
+      edgesFromExternalNodesByNamespace.set(sourceNormalizedNamespace, [
+        ...(edgesFromExternalNodesByNamespace.get(sourceNormalizedNamespace) 
?? []),
         edge,
       ]);
     }
@@ -151,7 +152,7 @@ export function computeDiagramData(
     ackEdge({
       id: dmnObject["@_id"]!,
       dmnObject: {
-        namespace: definitions["@_namespace"],
+        normalizedNamespace: undefined, // normalized as it's always equal to 
`definitions["@_namespace"]`.
         type: dmnObject.__$$element,
         id: dmnObject["@_id"]!,
         requirementType: "association",
@@ -160,7 +161,7 @@ export function computeDiagramData(
       type: EDGE_TYPES.association,
       source: dmnObject.sourceRef?.["@_href"],
       target: dmnObject.targetRef?.["@_href"],
-      sourceNamespace: undefined, // association are always from the current 
namespace
+      sourceNormalizedNamespace: undefined, // association are always from the 
current namespace
     });
   });
 
@@ -413,30 +414,38 @@ function ackRequirementEdges(
   drgElements: Normalized<DMN_LATEST__tDefinitions>["drgElement"],
   ackEdge: AckEdge
 ) {
-  const namespace = drgElementsNamespace === thisDmnsNamespace ? "" : 
drgElementsNamespace;
+  const normalizedDrgElementsNamespace =
+    !drgElementsNamespace || 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"]);
+
+        const normalizedIrSourceNamespace = !irHref.namespace
+          ? normalizedDrgElementsNamespace
+          : irHref.namespace === thisDmnsNamespace
+            ? ""
+            : irHref.namespace;
+
         ackEdge({
-          // HREF format, used as RF.Edge ID
+          // Plain ID or full HREF format, used as RF.Edge ID
           id:
-            drgElementsNamespace === thisDmnsNamespace
-              ? ir["@_id"]
-              : buildXmlHref({ namespace: drgElementsNamespace, id: ir["@_id"] 
}),
+            normalizedDrgElementsNamespace === ""
+              ? ir["@_id"] // prevent the id from having a leading `#`
+              : buildXmlHref({ namespace: normalizedDrgElementsNamespace, id: 
ir["@_id"] }),
           dmnObject: {
-            namespace: drgElementsNamespace,
+            normalizedNamespace: normalizedDrgElementsNamespace, // owner of 
the requirement (edge)
             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"]! }),
-          sourceNamespace: irHref.namespace ?? namespace,
+          source: buildXmlHref({ namespace: normalizedIrSourceNamespace, id: 
irHref.id }),
+          target: buildXmlHref({ namespace: normalizedDrgElementsNamespace, 
id: dmnObject["@_id"]! }),
+          sourceNormalizedNamespace: normalizedIrSourceNamespace,
         });
       });
     }
@@ -444,23 +453,30 @@ function ackRequirementEdges(
     if (dmnObject.__$$element === "decision" || dmnObject.__$$element === 
"businessKnowledgeModel") {
       (dmnObject.knowledgeRequirement ?? []).forEach((kr, index) => {
         const krHref = parseXmlHref(kr.requiredKnowledge["@_href"]);
+
+        const normalizedKrSourceNamespace = !krHref.namespace
+          ? normalizedDrgElementsNamespace
+          : krHref.namespace === thisDmnsNamespace
+            ? ""
+            : krHref.namespace;
+
         ackEdge({
-          // HREF format, used as RF.Edge ID
+          // Plain ID or full HREF format, used as RF.Edge ID
           id:
-            drgElementsNamespace === thisDmnsNamespace
-              ? kr["@_id"]
-              : buildXmlHref({ namespace: drgElementsNamespace, id: kr["@_id"] 
}),
+            normalizedDrgElementsNamespace === ""
+              ? kr["@_id"] // prevent the id from having a leading `#`
+              : buildXmlHref({ namespace: normalizedDrgElementsNamespace, id: 
kr["@_id"] }),
           dmnObject: {
-            namespace: drgElementsNamespace,
+            normalizedNamespace: normalizedDrgElementsNamespace, // owner of 
the requirement (edge)
             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"]! }),
-          sourceNamespace: krHref.namespace ?? namespace,
+          source: buildXmlHref({ namespace: normalizedKrSourceNamespace, id: 
krHref.id }),
+          target: buildXmlHref({ namespace: normalizedDrgElementsNamespace, 
id: dmnObject["@_id"]! }),
+          sourceNormalizedNamespace: normalizedKrSourceNamespace,
         });
       });
     }
@@ -472,19 +488,26 @@ function ackRequirementEdges(
     ) {
       (dmnObject.authorityRequirement ?? []).forEach((ar, index) => {
         const arHref = parseXmlHref((ar.requiredInput ?? ar.requiredDecision 
?? ar.requiredAuthority)!["@_href"]);
+
+        const normalizedArSourceNamespace = !arHref.namespace
+          ? normalizedDrgElementsNamespace
+          : arHref.namespace === thisDmnsNamespace
+            ? ""
+            : arHref.namespace;
+
         ackEdge({
-          id: ar["@_id"]!,
+          id: ar["@_id"],
           dmnObject: {
-            namespace: drgElementsNamespace,
+            normalizedNamespace: normalizedDrgElementsNamespace, // owner of 
the requirement (edge)
             type: dmnObject.__$$element,
-            id: dmnObject["@_id"]!,
+            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"]! }),
-          sourceNamespace: arHref.namespace ?? namespace,
+          source: buildXmlHref({ namespace: normalizedArSourceNamespace, id: 
arHref.id }),
+          target: buildXmlHref({ namespace: normalizedDrgElementsNamespace, 
id: dmnObject["@_id"]! }),
+          sourceNormalizedNamespace: normalizedArSourceNamespace,
         });
       });
     }
diff --git 
a/packages/dmn-editor/stories/useCases/namespaceInRequiredDecisionHref/NamespaceInRequiredDecisionHref.mdx
 
b/packages/dmn-editor/stories/useCases/namespaceInRequiredDecisionHref/NamespaceInRequiredDecisionHref.mdx
new file mode 100644
index 00000000000..34950c8239b
--- /dev/null
+++ 
b/packages/dmn-editor/stories/useCases/namespaceInRequiredDecisionHref/NamespaceInRequiredDecisionHref.mdx
@@ -0,0 +1,25 @@
+{/* 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 { Meta, Story } from "@storybook/blocks";
+import * as NamespaceInRequiredDecisionHref from 
"./NamespaceInRequiredDecisionHref.stories";
+
+<Meta title="MDX/Use cases/Namespace In Required Decision Href" 
of={NamespaceInRequiredDecisionHref} />
+
+## Namespace In Required Decision Href
+
+DMN 1.6 supports referencing required decision with namespace in href.
diff --git 
a/packages/dmn-editor/stories/useCases/namespaceInRequiredDecisionHref/NamespaceInRequiredDecisionHref.stories.tsx
 
b/packages/dmn-editor/stories/useCases/namespaceInRequiredDecisionHref/NamespaceInRequiredDecisionHref.stories.tsx
new file mode 100644
index 00000000000..468b3d3c785
--- /dev/null
+++ 
b/packages/dmn-editor/stories/useCases/namespaceInRequiredDecisionHref/NamespaceInRequiredDecisionHref.stories.tsx
@@ -0,0 +1,78 @@
+/*
+ * 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 type { Meta, StoryObj } from "@storybook/react";
+import { getMarshaller } from "@kie-tools/dmn-marshaller";
+import { Empty } from "../../misc/empty/Empty.stories";
+import { DmnEditor, DmnEditorProps } from "../../../src/DmnEditor";
+import { StorybookDmnEditorProps } from "../../dmnEditorStoriesWrapper";
+
+export const namespaceInRequiredDecisionHref = `<?xml version="1.0" 
encoding="UTF-8" ?>
+<definitions xmlns="https://www.omg.org/spec/DMN/20240513/MODEL/"; 
expressionLanguage="https://www.omg.org/spec/DMN/20240513/FEEL/"; 
namespace="https://kie.org/dmn/_BCA8E83B-E3E6-432C-BFD6-44CD55A76A6C"; 
id="_2BC7DBF7-90C6-448A-B336-05F26D1FEC8E" 
name="DMN_18EBF2E7-B344-47B1-827A-3A2E08B8E9CA" 
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/extens [...]
+  <decision name="Required Decision A" 
id="_7A15C07E-4A30-450D-9796-A2454EC55279">
+    <variable name="Required Decision A" 
id="_B156824F-3ABD-4387-9711-08B8A3A5241C" />
+  </decision>
+  <decision name="Decision in Need" id="_A26B267C-96D9-4E35-8564-DC655DC08E39">
+    <variable name="Decision in Need" 
id="_AED5DD3A-EC35-457B-A13A-C8CC4614C46F" />
+    <informationRequirement id="_4BA30E29-004F-43E2-9402-77C107CA9339">
+      <requiredDecision 
href="https://kie.org/dmn/_BCA8E83B-E3E6-432C-BFD6-44CD55A76A6C#_7A15C07E-4A30-450D-9796-A2454EC55279";
 />
+    </informationRequirement>
+  </decision>
+  <dmndi:DMNDI>
+    <dmndi:DMNDiagram id="_DBBAB955-3E4F-4731-BC13-AD0A71E064D3" name="Default 
DRD" useAlternativeInputDataShape="false">
+      <di:extension>
+        <kie:ComponentsWidthsExtension>
+          <kie:ComponentWidths />
+        </kie:ComponentsWidthsExtension>
+      </di:extension>
+      <dmndi:DMNShape id="_68A19ACB-7EA6-4962-A61F-866D08E3D151" 
dmnElementRef="_7A15C07E-4A30-450D-9796-A2454EC55279" isCollapsed="false" 
isListedInputData="false">
+        <dc:Bounds x="120" y="80" width="160" height="80" />
+      </dmndi:DMNShape>
+      <dmndi:DMNShape id="_B06617DD-EDE9-4D54-8EF9-609545A69743" 
dmnElementRef="_A26B267C-96D9-4E35-8564-DC655DC08E39" isCollapsed="false" 
isListedInputData="false">
+        <dc:Bounds x="420" y="80" width="160" height="80" />
+      </dmndi:DMNShape>
+      <dmndi:DMNEdge id="_51E984B1-E548-4B7F-918A-802C995C7651" 
dmnElementRef="_4BA30E29-004F-43E2-9402-77C107CA9339" 
sourceElement="_68A19ACB-7EA6-4962-A61F-866D08E3D151" 
targetElement="_B06617DD-EDE9-4D54-8EF9-609545A69743">
+        <di:waypoint x="200" y="120" />
+        <di:waypoint x="500" y="120" />
+      </dmndi:DMNEdge>
+    </dmndi:DMNDiagram>
+  </dmndi:DMNDI>
+</definitions>
+`;
+
+const meta: Meta<DmnEditorProps> = {
+  title: "Use cases/Namespace In Required Decision Href",
+  component: DmnEditor,
+  includeStories: /^[A-Z]/,
+};
+
+export default meta;
+type Story = StoryObj<StorybookDmnEditorProps>;
+
+const marshaller = getMarshaller(namespaceInRequiredDecisionHref, { upgradeTo: 
"latest" });
+const model = marshaller.parser.parse();
+
+export const NamespaceInRequiredDecisionHref: Story = {
+  render: Empty.render,
+  args: {
+    model: model,
+    xml: marshaller.builder.build(model),
+  },
+};
diff --git a/packages/dmn-editor/tests-e2e/__fixtures__/editor.ts 
b/packages/dmn-editor/tests-e2e/__fixtures__/editor.ts
index 07cfcb17d4c..bc10e1802ff 100644
--- a/packages/dmn-editor/tests-e2e/__fixtures__/editor.ts
+++ b/packages/dmn-editor/tests-e2e/__fixtures__/editor.ts
@@ -62,4 +62,10 @@ export class Editor {
   public async changeTab(args: { tab: TabName }) {
     await this.page.getByRole("tab", { name: args.tab }).click();
   }
+
+  public async openDecisionWithNamespaceInHref() {
+    await this.page.goto(
+      
`${this.baseURL}/iframe.html?args=&id=use-cases-namespace-in-required-decision-href--namespace-in-required-decision-href&viewMode=story`
+    );
+  }
 }
diff --git 
a/packages/dmn-editor/tests-e2e/__screenshots__/Google-Chrome/namespace-in-required-decision-href.png
 
b/packages/dmn-editor/tests-e2e/__screenshots__/Google-Chrome/namespace-in-required-decision-href.png
new file mode 100644
index 00000000000..c4be1d0be03
Binary files /dev/null and 
b/packages/dmn-editor/tests-e2e/__screenshots__/Google-Chrome/namespace-in-required-decision-href.png
 differ
diff --git 
a/packages/dmn-editor/tests-e2e/__screenshots__/chromium/namespace-in-required-decision-href.png
 
b/packages/dmn-editor/tests-e2e/__screenshots__/chromium/namespace-in-required-decision-href.png
new file mode 100644
index 00000000000..faf2e8bf101
Binary files /dev/null and 
b/packages/dmn-editor/tests-e2e/__screenshots__/chromium/namespace-in-required-decision-href.png
 differ
diff --git 
a/packages/dmn-editor/tests-e2e/__screenshots__/webkit/namespace-in-required-decision-href.png
 
b/packages/dmn-editor/tests-e2e/__screenshots__/webkit/namespace-in-required-decision-href.png
new file mode 100644
index 00000000000..3634c666067
Binary files /dev/null and 
b/packages/dmn-editor/tests-e2e/__screenshots__/webkit/namespace-in-required-decision-href.png
 differ
diff --git a/packages/dmn-editor/tests-e2e/checkDmnWithNamespaceInHref.spec.ts 
b/packages/dmn-editor/tests-e2e/checkDmnWithNamespaceInHref.spec.ts
new file mode 100644
index 00000000000..a7df638d5d2
--- /dev/null
+++ b/packages/dmn-editor/tests-e2e/checkDmnWithNamespaceInHref.spec.ts
@@ -0,0 +1,33 @@
+/*
+ * 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 { expect, test } from "./__fixtures__/base";
+
+test.describe("Namespace in Href - Diagram", () => {
+  test.beforeEach(async ({ editor, nodes }) => {
+    await editor.openDecisionWithNamespaceInHref();
+    await expect(nodes.get({ name: "Required Decision A" })).toBeVisible();
+    await editor.setIsReadOnly(true);
+  });
+  test("should show edges if namespace present in hrefs", async ({ page }) => {
+    await expect(page.getByText("Required Decision A")).toBeAttached();
+    await expect(page.getByText("Decision in Need")).toBeAttached();
+    await 
expect(page).toHaveScreenshot("namespace-in-required-decision-href.png");
+  });
+});


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to