Skip to content

Commit

Permalink
Handles 3rd level of included models
Browse files Browse the repository at this point in the history
  • Loading branch information
danielzhe committed Sep 2, 2024
1 parent 227ee5a commit 3d8abb5
Show file tree
Hide file tree
Showing 8 changed files with 295 additions and 82 deletions.
106 changes: 104 additions & 2 deletions packages/dmn-editor-envelope/src/DmnEditorRoot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -563,6 +563,94 @@ function ExternalModelsManager({
};
}, [thisDmnsNormalizedPosixPathRelativeToTheWorkspaceRoot]);

const getIncludedNamespacesFromModel = useCallback((model: Normalized<DmnLatestModel>) => {
return (model.definitions.import ?? [])
.map((i) => getNamespaceOfDmnImport({ dmnImport: i }))
.join(NAMESPACES_EFFECT_SEPARATOR);
}, []);

const getDmnsByNamespace = useCallback((resources: (ResourceContent | undefined)[]) => {
const ret = new Map<string, ResourceContent>();
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<DmnLatestModel>,
externalModelsIndex: DmnEditor.ExternalModelsIndex,
resourcesByNamespace: Map<string, ResourceContent>,
loadedDmnsByPathRelativeToTheWorkspaceRoot: Set<string>,
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;
Expand Down Expand Up @@ -594,7 +682,7 @@ function ExternalModelsManager({
const externalModelsIndex: DmnEditor.ExternalModelsIndex = {};

const namespacesSet = new Set(namespaces.split(NAMESPACES_EFFECT_SEPARATOR));

const loadedDmnsByPathRelativeToTheWorkspaceRoot = new Set<string>();
for (let i = 0; i < resources.length; i++) {
const resource = resources[i];
if (!resource) {
Expand Down Expand Up @@ -623,6 +711,7 @@ function ExternalModelsManager({
);
}

loadedDmnsByPathRelativeToTheWorkspaceRoot.add(resource.normalizedPosixPathRelativeToTheWorkspaceRoot);
externalModelsIndex[namespace] = {
normalizedPosixPathRelativeToTheOpenFile,
model: normalize(getMarshaller(content, { upgradeTo: "latest" }).parser.parse()),
Expand All @@ -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);
}
Expand Down
3 changes: 1 addition & 2 deletions packages/dmn-editor/src/diagram/DrgNodesPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,7 @@ export function DrgNodesPanel() {
const containingDecisionServiceHrefsByDecisionHrefsRelativeToThisDmn = useMemo(
() =>
computeContainingDecisionServiceHrefsByDecisionHrefs({
drgElements: thisDmnsDrgElements,
drgElementsNamespace: thisDmnsNamespace,
drgElementsNamespaceByNamespace: new Map([[thisDmnsNamespace, thisDmnsDrgElements]]),
thisDmnsNamespace: thisDmnsNamespace,
}),
[thisDmnsDrgElements, thisDmnsNamespace]
Expand Down
136 changes: 102 additions & 34 deletions packages/dmn-editor/src/externalNodes/DmnObjectListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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 ? (
<p>{displayName}</p>
) : (
<div>{`This ${nodeTypeTooltipDescription} node is from an external model that is not included in this one. Namespace: ${namespace}`}</div>
);
}, [displayName, dmnObject, isNamespaceIncluded, namespace, nodeTypeTooltipDescription]);

const isValid = useDmnEditorStore((s) =>
DMN15_SPEC.namedElement.isValidName(
Expand All @@ -81,32 +139,42 @@ export function DmnObjectListItem({
return !dmnObject ? (
<>{dmnObjectHref}</>
) : (
<Flex
alignItems={{ default: "alignItemsCenter" }}
justifyContent={{ default: "justifyContentFlexStart" }}
spaceItems={{ default: "spaceItemsNone" }}
>
<div style={{ width: "40px", height: "40px", marginRight: 0 }}>
<Icon />
</div>
<div style={{ color: isValid ? undefined : "red" }}>{`${displayName}`}</div>
<div>
{dmnObject.__$$element !== "knowledgeSource" ? (
<>
&nbsp;
<TypeRefLabel
typeRef={dmnObject.variable?.["@_typeRef"]}
relativeToNamespace={namespace}
isCollection={
allTopLevelDataTypesByFeelName.get(dmnObject.variable?.["@_typeRef"] ?? DmnBuiltInDataType.Undefined)
?.itemDefinition["@_isCollection"]
}
/>
</>
) : (
<></>
<Tooltip content={toolTip} isContentLeftAligned={true}>
<Flex
alignItems={{ default: "alignItemsCenter" }}
justifyContent={{ default: "justifyContentFlexStart" }}
spaceItems={{ default: "spaceItemsNone" }}
>
<div style={{ width: "40px", height: "40px", marginRight: 0 }}>
<Icon />
</div>
{!isNamespaceIncluded && (
<div
style={{
backgroundColor: "#f0f0f0",
color: "#6a6e72",
}}
>{`${notIncludedNamespaceDescription}.`}</div>
)}
</div>
</Flex>
<div style={{ color: isValid ? undefined : "red" }}>{`${displayName}`}</div>
<div>
{dmnObject.__$$element !== "knowledgeSource" ? (
<>
&nbsp;
<TypeRefLabel
typeRef={dmnObject.variable?.["@_typeRef"]}
relativeToNamespace={namespace}
isCollection={
allTopLevelDataTypesByFeelName.get(dmnObject.variable?.["@_typeRef"] ?? DmnBuiltInDataType.Undefined)
?.itemDefinition["@_isCollection"]
}
/>
</>
) : (
<></>
)}
</div>
</Flex>
</Tooltip>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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) =>
Expand Down
11 changes: 5 additions & 6 deletions packages/dmn-editor/src/mutations/deleteNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"]}'`
);
}
});
}
Expand Down
Loading

0 comments on commit 3d8abb5

Please sign in to comment.