From 3d8abb5f3a64e9a8d077fdc72493203f53dc2816 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Jos=C3=A9=20dos=20Santos?= Date: Mon, 2 Sep 2024 18:46:59 -0300 Subject: [PATCH] Handles 3rd level of included models --- .../dmn-editor-envelope/src/DmnEditorRoot.tsx | 106 +++++++++++++- .../dmn-editor/src/diagram/DrgNodesPanel.tsx | 3 +- .../src/externalNodes/DmnObjectListItem.tsx | 136 +++++++++++++----- .../addExistingDecisionServiceToDrd.ts | 8 +- .../dmn-editor/src/mutations/deleteNode.ts | 11 +- ...eInputDataAndDecisionsOnDecisionService.ts | 23 ++- .../DecisionServiceProperties.tsx | 30 +++- ...gDecisionServiceHrefsByDecisionHrefs.ts.ts | 60 ++++---- 8 files changed, 295 insertions(+), 82 deletions(-) diff --git a/packages/dmn-editor-envelope/src/DmnEditorRoot.tsx b/packages/dmn-editor-envelope/src/DmnEditorRoot.tsx index c6741d3cc3c..386dbace5ea 100644 --- a/packages/dmn-editor-envelope/src/DmnEditorRoot.tsx +++ b/packages/dmn-editor-envelope/src/DmnEditorRoot.tsx @@ -19,7 +19,7 @@ import * as __path from "path"; import * as React from "react"; -import { useEffect, useMemo, useState } from "react"; +import { useCallback, useEffect, useMemo, useState } from "react"; import * as DmnEditor from "@kie-tools/dmn-editor/dist/DmnEditor"; import { normalize, Normalized } from "@kie-tools/dmn-editor/dist/normalization/normalize"; import { DMN_LATEST_VERSION, DmnLatestModel, DmnMarshaller, getMarshaller } from "@kie-tools/dmn-marshaller"; @@ -563,6 +563,94 @@ function ExternalModelsManager({ }; }, [thisDmnsNormalizedPosixPathRelativeToTheWorkspaceRoot]); + const getIncludedNamespacesFromModel = useCallback((model: Normalized) => { + return (model.definitions.import ?? []) + .map((i) => getNamespaceOfDmnImport({ dmnImport: i })) + .join(NAMESPACES_EFFECT_SEPARATOR); + }, []); + + const getDmnsByNamespace = useCallback((resources: (ResourceContent | undefined)[]) => { + const ret = new Map(); + for (let i = 0; i < resources.length; i++) { + const resource = resources[i]; + if (!resource) { + continue; + } + + const content = resource.content ?? ""; + const ext = __path.extname(resource.normalizedPosixPathRelativeToTheWorkspaceRoot); + if (ext === ".dmn") { + const namespace = domParser.getDomDocument(content).documentElement.getAttribute("namespace"); + if (namespace) { + // Check for multiplicity of namespaces on DMN models + if (ret.has(namespace)) { + console.warn( + `DMN EDITOR ROOT: Multiple DMN models encountered with the same namespace '${namespace}': '${ + resource.normalizedPosixPathRelativeToTheWorkspaceRoot + }' and '${ + ret.get(namespace)!.normalizedPosixPathRelativeToTheWorkspaceRoot + }'. The latter will be considered.` + ); + } + + ret.set(namespace, resource); + } + } + } + + return ret; + }, []); + + // Load all included models from the model and the included models of those models, recursively. + const loadDependentModels = useCallback( + ( + model: Normalized, + externalModelsIndex: DmnEditor.ExternalModelsIndex, + resourcesByNamespace: Map, + loadedDmnsByPathRelativeToTheWorkspaceRoot: Set, + thisDmnsNormalizedPosixPathRelativeToTheWorkspaceRoot: string + ) => { + const includedNamespaces = new Set(getIncludedNamespacesFromModel(model).split(NAMESPACES_EFFECT_SEPARATOR)); + + for (const includedNamespace of includedNamespaces) { + if (!resourcesByNamespace.has(includedNamespace)) { + console.warn( + `DMN EDITOR ROOT: The included namespace '${includedNamespace}' for the model '${model.definitions["@_id"]}' can not be found.` + ); + } else { + const resource = resourcesByNamespace.get(includedNamespace)!; + if (loadedDmnsByPathRelativeToTheWorkspaceRoot.has(resource.normalizedPosixPathRelativeToTheWorkspaceRoot)) { + continue; + } + + const normalizedPosixPathRelativeToTheOpenFile = __path.relative( + __path.dirname(thisDmnsNormalizedPosixPathRelativeToTheWorkspaceRoot), + resource.normalizedPosixPathRelativeToTheWorkspaceRoot + ); + const content = resource.content ?? ""; + const includedModel = normalize(getMarshaller(content, { upgradeTo: "latest" }).parser.parse()); + externalModelsIndex[includedNamespace] = { + normalizedPosixPathRelativeToTheOpenFile, + model: includedModel, + type: "dmn", + svg: "", + }; + + loadedDmnsByPathRelativeToTheWorkspaceRoot.add(resource.normalizedPosixPathRelativeToTheWorkspaceRoot); + + loadDependentModels( + includedModel, + externalModelsIndex, + resourcesByNamespace, + loadedDmnsByPathRelativeToTheWorkspaceRoot, + thisDmnsNormalizedPosixPathRelativeToTheWorkspaceRoot + ); + } + } + }, + [getIncludedNamespacesFromModel] + ); + // This effect actually populates `externalModelsByNamespace` through the `onChange` call. useEffect(() => { let canceled = false; @@ -594,7 +682,7 @@ function ExternalModelsManager({ const externalModelsIndex: DmnEditor.ExternalModelsIndex = {}; const namespacesSet = new Set(namespaces.split(NAMESPACES_EFFECT_SEPARATOR)); - + const loadedDmnsByPathRelativeToTheWorkspaceRoot = new Set(); for (let i = 0; i < resources.length; i++) { const resource = resources[i]; if (!resource) { @@ -623,6 +711,7 @@ function ExternalModelsManager({ ); } + loadedDmnsByPathRelativeToTheWorkspaceRoot.add(resource.normalizedPosixPathRelativeToTheWorkspaceRoot); externalModelsIndex[namespace] = { normalizedPosixPathRelativeToTheOpenFile, model: normalize(getMarshaller(content, { upgradeTo: "latest" }).parser.parse()), @@ -645,6 +734,19 @@ function ExternalModelsManager({ } } + const dmnsByNamespace = getDmnsByNamespace(resources); + for (const dmn of dmnsByNamespace.values()) { + const content = dmn.content ?? ""; + const model = normalize(getMarshaller(content, { upgradeTo: "latest" }).parser.parse()); + loadDependentModels( + model, + externalModelsIndex, + dmnsByNamespace, + loadedDmnsByPathRelativeToTheWorkspaceRoot, + thisDmnsNormalizedPosixPathRelativeToTheWorkspaceRoot + ); + } + if (!canceled) { onChange(externalModelsIndex); } diff --git a/packages/dmn-editor/src/diagram/DrgNodesPanel.tsx b/packages/dmn-editor/src/diagram/DrgNodesPanel.tsx index 66b9903f8a4..a60f238c4fb 100644 --- a/packages/dmn-editor/src/diagram/DrgNodesPanel.tsx +++ b/packages/dmn-editor/src/diagram/DrgNodesPanel.tsx @@ -61,8 +61,7 @@ export function DrgNodesPanel() { const containingDecisionServiceHrefsByDecisionHrefsRelativeToThisDmn = useMemo( () => computeContainingDecisionServiceHrefsByDecisionHrefs({ - drgElements: thisDmnsDrgElements, - drgElementsNamespace: thisDmnsNamespace, + drgElementsNamespaceByNamespace: new Map([[thisDmnsNamespace, thisDmnsDrgElements]]), thisDmnsNamespace: thisDmnsNamespace, }), [thisDmnsDrgElements, thisDmnsNamespace] diff --git a/packages/dmn-editor/src/externalNodes/DmnObjectListItem.tsx b/packages/dmn-editor/src/externalNodes/DmnObjectListItem.tsx index 85423717aba..ffee4a3d684 100644 --- a/packages/dmn-editor/src/externalNodes/DmnObjectListItem.tsx +++ b/packages/dmn-editor/src/externalNodes/DmnObjectListItem.tsx @@ -31,6 +31,8 @@ import { useDmnEditorStore } from "../store/StoreContext"; import { useExternalModels } from "../includedModels/DmnEditorDependenciesContext"; import { DMN15_SPEC } from "@kie-tools/dmn-marshaller/dist/schemas/dmn-1_5/Dmn15Spec"; import { Normalized } from "../normalization/normalize"; +import { Tooltip } from "@patternfly/react-core/dist/js/components/Tooltip"; +import { NODE_TYPES } from "../diagram/nodes/NodeTypes"; export function DmnObjectListItem({ dmnObject, @@ -50,14 +52,70 @@ export function DmnObjectListItem({ ); const isAlternativeInputDataShape = useDmnEditorStore((s) => s.computed(s).isAlternativeInputDataShape()); - const displayName = dmnObject - ? buildFeelQNameFromNamespace({ - namedElement: dmnObject, - importsByNamespace, - namespace, - relativeToNamespace, - }).full - : dmnObjectHref; + // The dmnObject represented here can be a node from a 3rd model that is not included in this model. + // For example, consider a "Local Decision Service" with an encapsulated "Decision-A" from "Model A", + // but that "Decision-A" have an "Input-B" that is from "Model B", which is not included in local model. + // + // Model Name: Local Model.dmn + // Nodes: Local Decision Service + // Included Models: Model-A.dmn + // + // Model Name: Model-A.dmn + // Nodes: Decision-A + // Included Models: Model-B.dmn + // + // Model Name: Model-B.dmn + // Nodes: Input-B + // Included Models: [none] + // + // So, the "Local Model" only "knows" the nodes from "Model-A" and NOT from "Model-B". + // That's why we have different logic to build description for "known dmnObjects" and "unknown dmnObjects". + const isNamespaceIncluded = useMemo( + () => namespace === relativeToNamespace || importsByNamespace.has(namespace), + [importsByNamespace, namespace, relativeToNamespace] + ); + + const notIncludedNamespaceDescription = useMemo(() => { + return `${namespace.substring(0, 11)}...${namespace.substring(namespace.length - 4)}`; + }, [namespace]); + + const displayName = useMemo( + () => + dmnObject && isNamespaceIncluded + ? buildFeelQNameFromNamespace({ + namedElement: dmnObject, + importsByNamespace, + namespace, + relativeToNamespace, + }).full + : dmnObject?.["@_name"], + [dmnObject, importsByNamespace, isNamespaceIncluded, namespace, relativeToNamespace] + ); + + const nodeTypeTooltipDescription = useMemo(() => { + if (dmnObject === undefined) { + throw new Error("nodeTypeDescription can't be defined without a DMN object"); + } + const nodeType = getNodeTypeFromDmnObject(dmnObject); + if (nodeType === undefined) { + throw new Error("Can't determine nodeTypeDescription with undefined node type"); + } + if (nodeType === NODE_TYPES.decision) { + return "Decision"; + } else if (nodeType === NODE_TYPES.inputData) { + return "Input Data"; + } else { + return "Unknown"; + } + }, [dmnObject]); + + const toolTip = useMemo(() => { + return dmnObject && isNamespaceIncluded ? ( +

{displayName}

+ ) : ( +
{`This ${nodeTypeTooltipDescription} node is from an external model that is not included in this one. Namespace: ${namespace}`}
+ ); + }, [displayName, dmnObject, isNamespaceIncluded, namespace, nodeTypeTooltipDescription]); const isValid = useDmnEditorStore((s) => DMN15_SPEC.namedElement.isValidName( @@ -81,32 +139,42 @@ export function DmnObjectListItem({ return !dmnObject ? ( <>{dmnObjectHref} ) : ( - -
- -
-
{`${displayName}`}
-
- {dmnObject.__$$element !== "knowledgeSource" ? ( - <> -   - - - ) : ( - <> + + +
+ +
+ {!isNamespaceIncluded && ( +
{`${notIncludedNamespaceDescription}.`}
)} -
-
+
{`${displayName}`}
+
+ {dmnObject.__$$element !== "knowledgeSource" ? ( + <> +   + + + ) : ( + <> + )} +
+ + ); } diff --git a/packages/dmn-editor/src/mutations/addExistingDecisionServiceToDrd.ts b/packages/dmn-editor/src/mutations/addExistingDecisionServiceToDrd.ts index d1142a3f0d6..31a7840ab8c 100644 --- a/packages/dmn-editor/src/mutations/addExistingDecisionServiceToDrd.ts +++ b/packages/dmn-editor/src/mutations/addExistingDecisionServiceToDrd.ts @@ -92,11 +92,15 @@ export function getStrategyToAddExistingDecisionServiceToDrd({ id: __readonly_drgElement["@_id"]!, }); + const drgElementsByNamespace = new Map([[__readonly_namespace, __readonly_definitions.drgElement]]); + __readonly_externalDmnsIndex.forEach((value, key) => { + drgElementsByNamespace.set(key, value.model.definitions.drgElement); + }); + const containingDecisionServiceHrefsByDecisionHrefsRelativeToThisDmn = computeContainingDecisionServiceHrefsByDecisionHrefs({ thisDmnsNamespace: __readonly_namespace, - drgElementsNamespace: __readonly_decisionServiceNamespace, - drgElements: decisionServiceDmnDefinitions.drgElement, + drgElementsNamespaceByNamespace: drgElementsByNamespace, }); const doesThisDrdHaveConflictingDecisionService = containedDecisionHrefsRelativeToThisDmn.some((decisionHref) => diff --git a/packages/dmn-editor/src/mutations/deleteNode.ts b/packages/dmn-editor/src/mutations/deleteNode.ts index b2a49c40590..7ff21881d9a 100644 --- a/packages/dmn-editor/src/mutations/deleteNode.ts +++ b/packages/dmn-editor/src/mutations/deleteNode.ts @@ -212,16 +212,15 @@ export function canRemoveNodeFromDrdOnly({ id: __readonly_dmnObjectId!, }); - const drgElements = - definitions["@_namespace"] === __readonly_dmnObjectNamespace - ? definitions.drgElement ?? [] - : __readonly_externalDmnsIndex.get(__readonly_dmnObjectNamespace)?.model.definitions.drgElement ?? []; + const drgElementsByNamespace = new Map([[__readonly_dmnObjectNamespace, definitions.drgElement]]); + __readonly_externalDmnsIndex.forEach((value, key) => { + drgElementsByNamespace.set(key, value.model.definitions.drgElement); + }); const containingDecisionServiceHrefsByDecisionHrefsRelativeToThisDmn = computeContainingDecisionServiceHrefsByDecisionHrefs({ thisDmnsNamespace: definitions["@_namespace"], - drgElementsNamespace: __readonly_dmnObjectNamespace, - drgElements, + drgElementsNamespaceByNamespace: drgElementsByNamespace, }); const containingDecisionServiceHrefs = diff --git a/packages/dmn-editor/src/mutations/repopulateInputDataAndDecisionsOnDecisionService.ts b/packages/dmn-editor/src/mutations/repopulateInputDataAndDecisionsOnDecisionService.ts index 96552ec56dc..b369905ec05 100644 --- a/packages/dmn-editor/src/mutations/repopulateInputDataAndDecisionsOnDecisionService.ts +++ b/packages/dmn-editor/src/mutations/repopulateInputDataAndDecisionsOnDecisionService.ts @@ -95,11 +95,28 @@ export function repopulateInputDataAndDecisionsOnDecisionService({ if (externalDecision) { (externalDecision.informationRequirement ?? []).flatMap((ir) => { - // We need to add the reference to the external model if (ir.requiredDecision) { - requirements.set(`${href.namespace}${ir.requiredDecision["@_href"]}`, "decisionIr"); + const externalHref = parseXmlHref(ir.requiredDecision["@_href"]); + // If the requiredDecision has namespace, it means that it is pointing to a node in a 3rd model, + // not this one (the local model) neither the model in the `href.namespace`. + if (externalHref.namespace) { + requirements.set(`${ir.requiredDecision["@_href"]}`, "decisionIr"); + } else { + requirements.set(`${href.namespace}${ir.requiredDecision["@_href"]}`, "decisionIr"); + } } else if (ir.requiredInput) { - requirements.set(`${href.namespace}${ir.requiredInput["@_href"]}`, "inputDataIr"); + // If the requiredInput has namespace, it means that it is pointing to a node in a 3rd model, + // not this one (the local model) neither the model in the `href.namespace`. + const externalHref = parseXmlHref(ir.requiredInput["@_href"]); + if (externalHref.namespace) { + requirements.set(`${ir.requiredInput["@_href"]}`, "inputDataIr"); + } else { + requirements.set(`${href.namespace}${ir.requiredInput["@_href"]}`, "inputDataIr"); + } + } else { + throw new Error( + `DMN MUTATION: Invalid information requirement referenced by external DecisionService: '${externalDecision["@_id"]}'` + ); } }); } diff --git a/packages/dmn-editor/src/propertiesPanel/DecisionServiceProperties.tsx b/packages/dmn-editor/src/propertiesPanel/DecisionServiceProperties.tsx index 21f29fed530..7507d9bd4d0 100644 --- a/packages/dmn-editor/src/propertiesPanel/DecisionServiceProperties.tsx +++ b/packages/dmn-editor/src/propertiesPanel/DecisionServiceProperties.tsx @@ -23,6 +23,7 @@ import { DMN15__tDecision, DMN15__tDecisionService, DMN15__tInputData, + DMN15__tDefinitions, } from "@kie-tools/dmn-marshaller/dist/schemas/dmn-1_5/ts-gen/types"; import { ClipboardCopy } from "@patternfly/react-core/dist/js/components/ClipboardCopy"; import { FormGroup } from "@patternfly/react-core/dist/js/components/Form"; @@ -44,6 +45,9 @@ import { buildFeelQNameFromNamespace } from "../feel/buildFeelQName"; import { Alert, AlertVariant } from "@patternfly/react-core/dist/js/components/Alert/Alert"; import { Normalized } from "../normalization/normalize"; import { generateUuid } from "@kie-tools/boxed-expression-component/dist/api"; +import { ExternalDmn, ExternalModel } from "../DmnEditor"; +import { DmnLatestModel } from "../../../dmn-marshaller"; +import { Unpacked } from "../tsExt/tsExt"; import { useSettings } from "../settings/DmnEditorSettingsContext"; export type AllKnownDrgElementsByHref = Map< @@ -70,10 +74,23 @@ export function DecisionServiceProperties({ (s) => s.computed(s).getExternalModelTypesByNamespace(externalModelsByNamespace).dmns ); + const allExternalDmns = Object.entries(externalModelsByNamespace ?? {}).reduce((acc, [namespace, externalModel]) => { + if (!externalModel) { + console.warn(`DMN EDITOR: Could not find model with namespace '${namespace}'. Ignoring.`); + return acc; + } else { + if (externalModel.type === "dmn") { + acc.push(externalModel); + } + + return acc; + } + }, new Array>()); + const allDrgElementsByHref = useMemo(() => { const ret: AllKnownDrgElementsByHref = new Map(); - const allDmns = [{ model: thisDmn.model }, ...externalDmnsByNamespace.values()]; + const allDmns = [{ model: thisDmn.model }, ...allExternalDmns.values()]; for (let i = 0; i < allDmns.length; i++) { const anyDmn = allDmns[i]!; @@ -410,14 +427,14 @@ function DecisionServiceEquivalentFunction({ const dmnObject = allDrgElementsByHref.get(potentialExternalHref); - return dmnObject + return dmnObject && importsByNamespace.has(resolvedNamespace) ? buildFeelQNameFromNamespace({ namedElement: dmnObject, importsByNamespace, namespace: resolvedNamespace, relativeToNamespace: thisDmnsNamespace, }).full - : potentialExternalHref; + : buildDisplayName(dmnObject, resolvedNamespace); }, [allDrgElementsByHref, decisionServiceNamespace, importsByNamespace, thisDmnsNamespace] ); @@ -449,3 +466,10 @@ function DecisionServiceEquivalentFunction({ ); } + +function buildDisplayName( + dmnObject: Unpacked["drgElement"]> | undefined, + namespace: string +) { + return `${namespace.substring(0, 11)}...${namespace.substring(namespace.length - 4)}.${dmnObject?.["@_name"]}`; +} diff --git a/packages/dmn-editor/src/store/computed/computeContainingDecisionServiceHrefsByDecisionHrefs.ts.ts b/packages/dmn-editor/src/store/computed/computeContainingDecisionServiceHrefsByDecisionHrefs.ts.ts index 6365fa7fb88..b2055aa471c 100644 --- a/packages/dmn-editor/src/store/computed/computeContainingDecisionServiceHrefsByDecisionHrefs.ts.ts +++ b/packages/dmn-editor/src/store/computed/computeContainingDecisionServiceHrefsByDecisionHrefs.ts.ts @@ -23,45 +23,45 @@ import { State } from "../Store"; export function computeContainingDecisionServiceHrefsByDecisionHrefs({ thisDmnsNamespace, - drgElementsNamespace, - drgElements, + drgElementsNamespaceByNamespace, }: { thisDmnsNamespace: string; - drgElementsNamespace: string; - drgElements: State["dmn"]["model"]["definitions"]["drgElement"]; + drgElementsNamespaceByNamespace: Map; }) { - drgElements ??= []; + drgElementsNamespaceByNamespace ??= new Map(); const decisionServiceHrefsByDecisionHrefs = new Map(); - for (const drgElement of drgElements) { - const drgElementHref = buildXmlHref({ - namespace: drgElementsNamespace === thisDmnsNamespace ? "" : drgElementsNamespace, - id: drgElement["@_id"]!, - }); - - // Decision - if (drgElement.__$$element === "decision") { - decisionServiceHrefsByDecisionHrefs.set( - drgElementHref, - decisionServiceHrefsByDecisionHrefs.get(drgElementHref) ?? [] - ); - } - // DS - else if (drgElement.__$$element === "decisionService") { - const { containedDecisionHrefsRelativeToThisDmn } = getDecisionServicePropertiesRelativeToThisDmn({ - thisDmnsNamespace, - decisionServiceNamespace: drgElementsNamespace, - decisionService: drgElement, + for (const [drgElementsNamespace, drgElements] of drgElementsNamespaceByNamespace) { + for (const drgElement of drgElements ?? []) { + const drgElementHref = buildXmlHref({ + namespace: drgElementsNamespace === thisDmnsNamespace ? "" : drgElementsNamespace, + id: drgElement["@_id"]!, }); - for (const containedDecisionHref of containedDecisionHrefsRelativeToThisDmn) { - decisionServiceHrefsByDecisionHrefs.set(containedDecisionHref, [ - ...(decisionServiceHrefsByDecisionHrefs.get(containedDecisionHref) ?? []), + // Decision + if (drgElement.__$$element === "decision") { + decisionServiceHrefsByDecisionHrefs.set( drgElementHref, - ]); + decisionServiceHrefsByDecisionHrefs.get(drgElementHref) ?? [] + ); + } + // DS + else if (drgElement.__$$element === "decisionService") { + const { containedDecisionHrefsRelativeToThisDmn } = getDecisionServicePropertiesRelativeToThisDmn({ + thisDmnsNamespace, + decisionServiceNamespace: drgElementsNamespace, + decisionService: drgElement, + }); + + for (const containedDecisionHref of containedDecisionHrefsRelativeToThisDmn) { + decisionServiceHrefsByDecisionHrefs.set(containedDecisionHref, [ + ...(decisionServiceHrefsByDecisionHrefs.get(containedDecisionHref) ?? []), + drgElementHref, + ]); + } + } else { + // Ignore other elements } - } else { - // Ignore other elements } }