packages/dmn-editor-envelope/src/DmnEditorRoot.tsx (726 lines of code) (raw):
/*
* 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 __path from "path";
import * as React 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-marshaller/dist/normalization/normalize";
import { DMN_LATEST_VERSION, DmnLatestModel, DmnMarshaller, getMarshaller } from "@kie-tools/dmn-marshaller";
import { generateUuid } from "@kie-tools/boxed-expression-component/dist/api";
import { ResourceContent, SearchType, WorkspaceChannelApi, WorkspaceEdit } from "@kie-tools-core/workspace/dist/api";
import { DMN15_SPEC } from "@kie-tools/dmn-marshaller/dist/schemas/dmn-1_5/Dmn15Spec";
import { domParser } from "@kie-tools/xml-parser-ts";
import { ns as dmn15ns } from "@kie-tools/dmn-marshaller/dist/schemas/dmn-1_5/ts-gen/meta";
import { XML2PMML } from "@kie-tools/pmml-editor-marshaller";
import { getPmmlNamespace } from "@kie-tools/dmn-editor/dist/pmml/pmml";
import { getNamespaceOfDmnImport } from "@kie-tools/dmn-editor/dist/includedModels/importNamespaces";
import {
imperativePromiseHandle,
PromiseImperativeHandle,
} from "@kie-tools-core/react-hooks/dist/useImperativePromiseHandler";
import { KeyboardShortcutsService } from "@kie-tools-core/keyboard-shortcuts/dist/envelope/KeyboardShortcutsService";
import { Flex } from "@patternfly/react-core/dist/js/layouts/Flex";
import {
EmptyState,
EmptyStateBody,
EmptyStateIcon,
EmptyStateHeader,
} from "@patternfly/react-core/dist/js/components/EmptyState";
import {
JavaCodeCompletionAccessor,
JavaCodeCompletionClass,
} from "@kie-tools-core/vscode-java-code-completion/dist/api";
export const EXTERNAL_MODELS_SEARCH_GLOB_PATTERN = "**/*.{dmn,pmml}";
export const TARGET_DIRECTORY = "target/classes/";
export const EMPTY_DMN = () => `<?xml version="1.0" encoding="UTF-8"?>
<definitions
xmlns="${dmn15ns.get("")}"
expressionLanguage="${DMN15_SPEC.expressionLanguage.default}"
namespace="https://kie.org/dmn/${generateUuid()}"
id="${generateUuid()}"
name="DMN${generateUuid()}">
</definitions>`;
export interface JavaCodeCompletionExposedInteropApi {
getFields(fqcn: string): Promise<JavaCodeCompletionAccessor[]>;
getClasses(query: string): Promise<JavaCodeCompletionClass[]>;
isLanguageServerAvailable(): Promise<boolean>;
}
export type DmnEditorRootProps = {
exposing: (s: DmnEditorRoot) => void;
onNewEdit: (edit: WorkspaceEdit) => void;
onRequestWorkspaceFilesList: WorkspaceChannelApi["kogitoWorkspace_resourceListRequest"];
onRequestWorkspaceFileContent: WorkspaceChannelApi["kogitoWorkspace_resourceContentRequest"];
onOpenFileFromNormalizedPosixPathRelativeToTheWorkspaceRoot: WorkspaceChannelApi["kogitoWorkspace_openFile"];
onOpenedBoxedExpressionEditorNodeChange?: (newOpenedNodeId: string | undefined) => void;
workspaceRootAbsolutePosixPath: string;
keyboardShortcutsService: KeyboardShortcutsService | undefined;
isEvaluationHighlightsSupported?: boolean;
isReadOnly: boolean;
isImportDataTypesFromJavaClassesSupported?: boolean;
javaCodeCompletionService?: JavaCodeCompletionExposedInteropApi;
};
export type DmnEditorRootState = {
marshaller: DmnMarshaller<typeof DMN_LATEST_VERSION> | undefined;
stack: Normalized<DmnLatestModel>[];
pointer: number;
openFileNormalizedPosixPathRelativeToTheWorkspaceRoot: string | undefined;
externalModelsByNamespace: DmnEditor.ExternalModelsIndex;
isReadOnly: boolean;
externalModelsManagerDoneBootstraping: boolean;
keyboardShortcutsRegisterIds: number[];
keyboardShortcutsRegistered: boolean;
error: Error | undefined;
evaluationResultsByNodeId: DmnEditor.EvaluationResultsByNodeId;
};
export class DmnEditorRoot extends React.Component<DmnEditorRootProps, DmnEditorRootState> {
private readonly externalModelsManagerDoneBootstraping = imperativePromiseHandle<void>();
private readonly dmnEditorRef: React.RefObject<DmnEditor.DmnEditorRef>;
constructor(props: DmnEditorRootProps) {
super(props);
props.exposing(this);
this.dmnEditorRef = React.createRef();
this.state = {
externalModelsByNamespace: {},
marshaller: undefined,
stack: [],
pointer: -1,
openFileNormalizedPosixPathRelativeToTheWorkspaceRoot: undefined,
isReadOnly: props.isReadOnly,
externalModelsManagerDoneBootstraping: false,
keyboardShortcutsRegisterIds: [],
keyboardShortcutsRegistered: false,
error: undefined,
evaluationResultsByNodeId: new Map(),
};
}
// Exposed API
public openBoxedExpressionEditor(nodeId: string): void {
this.dmnEditorRef.current?.openBoxedExpressionEditor(nodeId);
}
public showDmnEvaluationResults(evaluationResultsByNodeId: DmnEditor.EvaluationResultsByNodeId): void {
this.setState((prev) => ({ ...prev, evaluationResultsByNodeId: evaluationResultsByNodeId }));
}
public async undo(): Promise<void> {
this.setState((prev) => ({ ...prev, pointer: Math.max(0, prev.pointer - 1) }));
}
public async redo(): Promise<void> {
this.setState((prev) => ({ ...prev, pointer: Math.min(prev.stack.length - 1, prev.pointer + 1) }));
}
public async getDiagramSvg(): Promise<string | undefined> {
return this.dmnEditorRef.current?.getDiagramSvg();
}
public async getContent(): Promise<string> {
if (!this.state.marshaller || !this.model) {
throw new Error(
`DMN EDITOR ROOT: Content has not been set yet. Throwing an error to prevent returning a "default" content.`
);
}
return this.state.marshaller.builder.build(this.model);
}
public async setContent(
openFileNormalizedPosixPathRelativeToTheWorkspaceRoot: string,
content: string
): Promise<void> {
const marshaller = this.getMarshaller(content);
// Save stack
let savedStackPointer: Normalized<DmnLatestModel>[] = [];
// Set the model and path for external models manager.
this.setState((prev) => {
savedStackPointer = [...prev.stack];
return {
stack: [normalize(marshaller.parser.parse())],
openFileNormalizedPosixPathRelativeToTheWorkspaceRoot,
pointer: 0,
};
});
// Wait the external manager models to load.
await this.externalModelsManagerDoneBootstraping.promise;
// Set the values to render the DMN Editor.
this.setState((prev) => {
// External change to the same file.
if (
prev.openFileNormalizedPosixPathRelativeToTheWorkspaceRoot ===
openFileNormalizedPosixPathRelativeToTheWorkspaceRoot
) {
const newStack = savedStackPointer.slice(0, prev.pointer + 1);
return {
marshaller,
openFileNormalizedPosixPathRelativeToTheWorkspaceRoot,
stack: [...newStack, normalize(marshaller.parser.parse())],
isReadOnly: prev.isReadOnly,
pointer: newStack.length,
externalModelsManagerDoneBootstraping: true,
};
}
// Different file opened. Need to reset everything.
else {
return {
marshaller,
openFileNormalizedPosixPathRelativeToTheWorkspaceRoot,
stack: [normalize(marshaller.parser.parse())],
isReadOnly: prev.isReadOnly,
pointer: 0,
externalModelsManagerDoneBootstraping: true,
};
}
});
}
public get model(): Normalized<DmnLatestModel> | undefined {
return this.state.stack[this.state.pointer];
}
// Internal methods
private getMarshaller(content: string) {
try {
return getMarshaller(content || EMPTY_DMN(), { upgradeTo: "latest" });
} catch (e) {
this.setState((s) => ({
...s,
error: e,
}));
throw e;
}
}
private setExternalModelsByNamespace = (externalModelsByNamespace: DmnEditor.ExternalModelsIndex) => {
this.setState((prev) => ({ ...prev, externalModelsByNamespace }));
};
private onModelChange: DmnEditor.OnDmnModelChange = (model) => {
this.setState(
(prev) => {
const newStack = prev.stack.slice(0, prev.pointer + 1);
return {
...prev,
stack: [...newStack, model],
pointer: newStack.length,
};
},
() =>
this.props.onNewEdit({
id: `${this.state.openFileNormalizedPosixPathRelativeToTheWorkspaceRoot}__${generateUuid()}`,
})
);
};
private onRequestExternalModelsAvailableToInclude: DmnEditor.OnRequestExternalModelsAvailableToInclude = async () => {
if (!this.state.openFileNormalizedPosixPathRelativeToTheWorkspaceRoot) {
return [];
}
const list = await this.props.onRequestWorkspaceFilesList({
pattern: EXTERNAL_MODELS_SEARCH_GLOB_PATTERN,
opts: { type: SearchType.TRAVERSAL },
});
return list.normalizedPosixPathsRelativeToTheWorkspaceRoot.flatMap((p) =>
// Do not show this DMN on the list and filter out assets into target/classes directory
p === this.state.openFileNormalizedPosixPathRelativeToTheWorkspaceRoot || p.includes(TARGET_DIRECTORY)
? []
: __path.relative(__path.dirname(this.state.openFileNormalizedPosixPathRelativeToTheWorkspaceRoot!), p)
);
};
private onRequestToResolvePathRelativeToTheOpenFile: DmnEditor.OnRequestToResolvePath = (
normalizedPosixPathRelativeToTheOpenFile
) => {
const normalizedPosixPathRelativeToTheWorkspaceRoot = __path
.resolve(
__path.dirname(this.state.openFileNormalizedPosixPathRelativeToTheWorkspaceRoot!),
normalizedPosixPathRelativeToTheOpenFile
)
.substring(1); // Remove leading slash.
return normalizedPosixPathRelativeToTheWorkspaceRoot;
// Example:
// this.state.openFileAbsolutePath = /Users/ljmotta/packages/dmns/Dmn.dmn
// normalizedPosixPathRelativeToTheOpenFile = ../../tmp/Tmp.dmn
// workspaceRootAbsolutePosixPath = /Users/ljmotta
// resolvedAbsolutePath = /Users/ljmotta/tmp/Tmp.dmn
// return (which is the normalizedPosixPathRelativeToTheWorkspaceRoot) = tmp/Tmp.dmn
};
private onRequestExternalModelByPathsRelativeToTheOpenFile: DmnEditor.OnRequestExternalModelByPath = async (
normalizedPosixPathRelativeToTheOpenFile
) => {
const normalizedPosixPathRelativeToTheWorkspaceRoot = this.onRequestToResolvePathRelativeToTheOpenFile(
normalizedPosixPathRelativeToTheOpenFile
);
const resource = await this.props.onRequestWorkspaceFileContent({
normalizedPosixPathRelativeToTheWorkspaceRoot,
opts: { type: "text" },
});
const ext = __path.extname(normalizedPosixPathRelativeToTheOpenFile);
if (ext === ".dmn") {
return {
normalizedPosixPathRelativeToTheOpenFile,
type: "dmn",
model: normalize(getMarshaller(resource?.content ?? "", { upgradeTo: "latest" }).parser.parse()),
svg: "",
};
} else if (ext === ".pmml") {
return {
normalizedPosixPathRelativeToTheOpenFile,
type: "pmml",
model: XML2PMML(resource?.content ?? ""),
};
} else {
throw new Error(`Unknown extension '${ext}'.`);
}
};
private onOpenFileFromPathRelativeToTheOpenFile = (normalizedPosixPathRelativeToTheOpenFile: string) => {
if (!this.state.openFileNormalizedPosixPathRelativeToTheWorkspaceRoot) {
return;
}
this.props.onOpenFileFromNormalizedPosixPathRelativeToTheWorkspaceRoot(
this.onRequestToResolvePathRelativeToTheOpenFile(normalizedPosixPathRelativeToTheOpenFile)
);
};
public componentDidUpdate(
prevProps: Readonly<DmnEditorRootProps>,
prevState: Readonly<DmnEditorRootState>,
snapshot?: any
): void {
if (this.props.keyboardShortcutsService === undefined || this.state.keyboardShortcutsRegistered === true) {
return;
}
const commands = this.dmnEditorRef.current?.getCommands();
if (commands === undefined) {
return;
}
const cancelAction = this.props.keyboardShortcutsService.registerKeyPress("Escape", "Edit | Unselect", async () =>
commands.cancelAction()
);
const deleteSelectionBackspace = this.props.keyboardShortcutsService.registerKeyPress(
"Backspace",
"Edit | Delete selection",
async () => {}
);
const deleteSelectionDelete = this.props.keyboardShortcutsService.registerKeyPress(
"Delete",
"Edit | Delete selection",
async () => {}
);
const selectAll = this.props.keyboardShortcutsService?.registerKeyPress(
"A",
"Edit | Select/Deselect all",
async () => commands.selectAll()
);
const createGroup = this.props.keyboardShortcutsService?.registerKeyPress(
"G",
"Edit | Create group wrapping selection",
async () => {
console.log(" KEY GROUP PRESSED, ", commands);
return commands.createGroup();
}
);
const hideFromDrd = this.props.keyboardShortcutsService?.registerKeyPress("X", "Edit | Hide from DRD", async () =>
commands.hideFromDrd()
);
const copy = this.props.keyboardShortcutsService?.registerKeyPress("Ctrl+C", "Edit | Copy nodes", async () =>
commands.copy()
);
const cut = this.props.keyboardShortcutsService?.registerKeyPress("Ctrl+X", "Edit | Cut nodes", async () =>
commands.cut()
);
const paste = this.props.keyboardShortcutsService?.registerKeyPress("Ctrl+V", "Edit | Paste nodes", async () =>
commands.paste()
);
const togglePropertiesPanel = this.props.keyboardShortcutsService?.registerKeyPress(
"I",
"Misc | Open/Close properties panel",
async () => commands.togglePropertiesPanel()
);
const toggleHierarchyHighlight = this.props.keyboardShortcutsService?.registerKeyPress(
"H",
"Misc | Toggle hierarchy highlights",
async () => commands.toggleHierarchyHighlight()
);
const moveUp = this.props.keyboardShortcutsService.registerKeyPress(
"Up",
"Move | Move selection up",
async () => {}
);
const moveDown = this.props.keyboardShortcutsService.registerKeyPress(
"Down",
"Move | Move selection down",
async () => {}
);
const moveLeft = this.props.keyboardShortcutsService.registerKeyPress(
"Left",
"Move | Move selection left",
async () => {}
);
const moveRight = this.props.keyboardShortcutsService.registerKeyPress(
"Right",
"Move | Move selection right",
async () => {}
);
const bigMoveUp = this.props.keyboardShortcutsService.registerKeyPress(
"Shift + Up",
"Move | Move selection up a big distance",
async () => {}
);
const bigMoveDown = this.props.keyboardShortcutsService.registerKeyPress(
"Shift + Down",
"Move | Move selection down a big distance",
async () => {}
);
const bigMoveLeft = this.props.keyboardShortcutsService.registerKeyPress(
"Shift + Left",
"Move | Move selection left a big distance",
async () => {}
);
const bigMoveRight = this.props.keyboardShortcutsService.registerKeyPress(
"Shift + Right",
"Move | Move selection right a big distance",
async () => {}
);
const focusOnBounds = this.props.keyboardShortcutsService?.registerKeyPress(
"B",
"Navigate | Focus on selection",
async () => commands.focusOnSelection()
);
const resetPosition = this.props.keyboardShortcutsService?.registerKeyPress(
"Space",
"Navigate | Reset position to origin",
async () => commands.resetPosition()
);
const pan = this.props.keyboardShortcutsService?.registerKeyPress(
"Right Mouse Button",
"Navigate | Hold and drag to Pan",
async () => {}
);
const zoom = this.props.keyboardShortcutsService?.registerKeyPress(
"Ctrl",
"Navigate | Hold and scroll to zoom in/out",
async () => {}
);
const navigateHorizontally = this.props.keyboardShortcutsService?.registerKeyPress(
"Shift",
"Navigate | Hold and scroll to navigate horizontally",
async () => {}
);
this.setState((prev) => ({
...prev,
keyboardShortcutsRegistered: true,
keyboardShortcutsRegisterIds: [
bigMoveDown,
bigMoveLeft,
bigMoveRight,
bigMoveUp,
cancelAction,
copy,
createGroup,
cut,
deleteSelectionBackspace,
deleteSelectionDelete,
focusOnBounds,
hideFromDrd,
moveDown,
moveLeft,
moveRight,
moveUp,
navigateHorizontally,
pan,
paste,
resetPosition,
selectAll,
toggleHierarchyHighlight,
togglePropertiesPanel,
zoom,
],
}));
}
public componentWillUnmount() {
const keyboardShortcuts = this.dmnEditorRef.current?.getCommands();
if (keyboardShortcuts === undefined) {
return;
}
this.state.keyboardShortcutsRegisterIds.forEach((id) => {
this.props.keyboardShortcutsService?.deregister(id);
});
}
public render() {
return (
<>
{this.state.error && <DmnMarshallerFallbackError error={this.state.error} />}
{this.model && (
<>
<DmnEditor.DmnEditor
ref={this.dmnEditorRef}
originalVersion={this.state.marshaller?.originalVersion}
model={this.model}
externalModelsByNamespace={this.state.externalModelsByNamespace}
evaluationResultsByNodeId={this.state.evaluationResultsByNodeId}
validationMessages={[]}
externalContextName={""}
externalContextDescription={""}
issueTrackerHref={""}
isEvaluationHighlightsSupported={this.props?.isEvaluationHighlightsSupported}
isReadOnly={this.state.isReadOnly}
isImportDataTypesFromJavaClassesSupported={this.props?.isImportDataTypesFromJavaClassesSupported}
javaCodeCompletionService={this.props?.javaCodeCompletionService}
onModelChange={this.onModelChange}
onOpenedBoxedExpressionEditorNodeChange={this.props.onOpenedBoxedExpressionEditorNodeChange}
onRequestExternalModelsAvailableToInclude={this.onRequestExternalModelsAvailableToInclude}
// (begin) All paths coming from inside the DmnEditor component are paths relative to the open file.
onRequestExternalModelByPath={this.onRequestExternalModelByPathsRelativeToTheOpenFile}
onRequestToJumpToPath={this.onOpenFileFromPathRelativeToTheOpenFile}
onRequestToResolvePath={this.onRequestToResolvePathRelativeToTheOpenFile}
// (end)
/>
<ExternalModelsManager
workspaceRootAbsolutePosixPath={this.props.workspaceRootAbsolutePosixPath}
thisDmnsNormalizedPosixPathRelativeToTheWorkspaceRoot={
this.state.openFileNormalizedPosixPathRelativeToTheWorkspaceRoot
}
model={this.model}
onChange={this.setExternalModelsByNamespace}
onRequestWorkspaceFilesList={this.props.onRequestWorkspaceFilesList}
onRequestWorkspaceFileContent={this.props.onRequestWorkspaceFileContent}
externalModelsManagerDoneBootstraping={this.externalModelsManagerDoneBootstraping}
/>
</>
)}
</>
);
}
}
const NAMESPACES_EFFECT_SEPARATOR = " , ";
function ExternalModelsManager({
workspaceRootAbsolutePosixPath,
thisDmnsNormalizedPosixPathRelativeToTheWorkspaceRoot,
model,
onChange,
onRequestWorkspaceFileContent,
onRequestWorkspaceFilesList,
externalModelsManagerDoneBootstraping,
}: {
workspaceRootAbsolutePosixPath: string;
thisDmnsNormalizedPosixPathRelativeToTheWorkspaceRoot: string | undefined;
model: Normalized<DmnLatestModel>;
onChange: (externalModelsByNamespace: DmnEditor.ExternalModelsIndex) => void;
onRequestWorkspaceFileContent: WorkspaceChannelApi["kogitoWorkspace_resourceContentRequest"];
onRequestWorkspaceFilesList: WorkspaceChannelApi["kogitoWorkspace_resourceListRequest"];
externalModelsManagerDoneBootstraping: PromiseImperativeHandle<void>;
}) {
const namespaces = useMemo(
() => getIncludedNamespacesFromModel(model.definitions.import),
[model.definitions.import]
);
const [externalUpdatesCount, setExternalUpdatesCount] = useState(0);
// This is a hack. Every time a file is updates in KIE Sandbox, the Shared Worker emits an event to this BroadcastChannel.
// By listening to it, we can reload the `externalModelsByNamespace` object. This makes the DMN Editor react to external changes,
// Which is very important for multi-file editing.
//
// Now, this mechanism is not ideal. We would ideally only be notified on changes to relevant files, but this sub-system does not exist yet.
// The consequence of this "hack" is some extra reloads.
useEffect(() => {
const bc = new BroadcastChannel("workspaces_files");
bc.onmessage = ({ data }) => {
// Changes to `thisDmn` shouldn't update its references to external models.
// Here, `data?.relativePath` is relative to the workspace root.
if (data?.relativePath === thisDmnsNormalizedPosixPathRelativeToTheWorkspaceRoot) {
return;
}
setExternalUpdatesCount((prev) => prev + 1);
};
return () => {
bc.close();
};
}, [thisDmnsNormalizedPosixPathRelativeToTheWorkspaceRoot]);
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;
}, []);
// This effect actually populates `externalModelsByNamespace` through the `onChange` call.
useEffect(() => {
let canceled = false;
if (!thisDmnsNormalizedPosixPathRelativeToTheWorkspaceRoot) {
return;
}
onRequestWorkspaceFilesList({ pattern: EXTERNAL_MODELS_SEARCH_GLOB_PATTERN, opts: { type: SearchType.TRAVERSAL } })
.then((list) => {
const resources: Array<Promise<ResourceContent | undefined>> = [];
for (let i = 0; i < list.normalizedPosixPathsRelativeToTheWorkspaceRoot.length; i++) {
const normalizedPosixPathRelativeToTheWorkspaceRoot = list.normalizedPosixPathsRelativeToTheWorkspaceRoot[i];
// Do not show this DMN on the list and filter out assets into target/classes directory
if (
normalizedPosixPathRelativeToTheWorkspaceRoot === thisDmnsNormalizedPosixPathRelativeToTheWorkspaceRoot ||
normalizedPosixPathRelativeToTheWorkspaceRoot.includes(TARGET_DIRECTORY)
) {
continue;
}
resources.push(
onRequestWorkspaceFileContent({
normalizedPosixPathRelativeToTheWorkspaceRoot,
opts: { type: "text" },
})
);
}
return Promise.all(resources);
})
.then((resources) => {
const externalModelsIndex: DmnEditor.ExternalModelsIndex = {};
const namespacesSet = new Set(namespaces.split(NAMESPACES_EFFECT_SEPARATOR));
const loadedDmnsByPathRelativeToTheWorkspaceRoot = new Set<string>();
const dmnsByNamespace = getDmnsByNamespace(resources);
for (let i = 0; i < resources.length; i++) {
const resource = resources[i];
if (!resource) {
continue;
}
const ext = __path.extname(resource.normalizedPosixPathRelativeToTheWorkspaceRoot);
const normalizedPosixPathRelativeToTheOpenFile = __path.relative(
__path.dirname(thisDmnsNormalizedPosixPathRelativeToTheWorkspaceRoot),
resource.normalizedPosixPathRelativeToTheWorkspaceRoot
);
const resourceContent = resource.content ?? "";
// DMN Files
if (ext === ".dmn") {
const namespaceOfTheResourceFile = domParser
.getDomDocument(resourceContent)
.documentElement.getAttribute("namespace");
if (namespaceOfTheResourceFile && namespacesSet.has(namespaceOfTheResourceFile)) {
checkIfNamespaceIsAlreadyLoaded({
externalModelsIndex,
namespaceOfTheResourceFile,
normalizedPosixPathRelativeToTheWorkspaceRoot: resource.normalizedPosixPathRelativeToTheWorkspaceRoot,
});
loadModel({
includedModelContent: resourceContent,
includedModelNamespace: namespaceOfTheResourceFile,
externalModelsIndex,
thisDmnsNormalizedPosixPathRelativeToTheWorkspaceRoot,
loadedDmnsByPathRelativeToTheWorkspaceRoot,
normalizedPosixPathRelativeToTheWorkspaceRoot: resource.normalizedPosixPathRelativeToTheWorkspaceRoot,
resourcesByNamespace: dmnsByNamespace,
});
}
}
// PMML Files
else if (ext === ".pmml") {
const namespace = getPmmlNamespace({ normalizedPosixPathRelativeToTheOpenFile });
if (namespace && namespacesSet.has(namespace)) {
// No need to check for namespaces being equal becuase there can't be two files with the same relativePath.
externalModelsIndex[namespace] = {
normalizedPosixPathRelativeToTheOpenFile,
model: XML2PMML(resourceContent),
type: "pmml",
};
}
}
// Unknown files
else {
throw new Error(`Unknown extension '${ext}'.`);
}
}
if (!canceled) {
onChange(externalModelsIndex);
}
externalModelsManagerDoneBootstraping.resolve();
});
return () => {
canceled = true;
};
}, [
namespaces,
onChange,
onRequestWorkspaceFileContent,
onRequestWorkspaceFilesList,
thisDmnsNormalizedPosixPathRelativeToTheWorkspaceRoot,
externalUpdatesCount,
workspaceRootAbsolutePosixPath,
externalModelsManagerDoneBootstraping,
getDmnsByNamespace,
]);
return <></>;
}
function DmnMarshallerFallbackError({ error }: { error: Error }) {
return (
<Flex justifyContent={{ default: "justifyContentCenter" }} style={{ marginTop: "100px" }}>
<EmptyState style={{ maxWidth: "1280px" }}>
<EmptyStateHeader
titleText="Unable to open file."
icon={<EmptyStateIcon icon={() => <div style={{ fontSize: "3em" }}>😕</div>} />}
headingLevel={"h4"}
/>
<br />
<EmptyStateBody>Error details: {error.message}</EmptyStateBody>
</EmptyState>
</Flex>
);
}
function getIncludedNamespacesFromModel(imports: Normalized<DmnLatestModel["definitions"]["import"]>) {
return (imports ?? []).map((i) => getNamespaceOfDmnImport({ dmnImport: i })).join(NAMESPACES_EFFECT_SEPARATOR);
}
function loadModel(args: {
thisDmnsNormalizedPosixPathRelativeToTheWorkspaceRoot: string;
resourcesByNamespace: Map<string, ResourceContent>;
normalizedPosixPathRelativeToTheWorkspaceRoot: string;
includedModelNamespace: string;
loadedDmnsByPathRelativeToTheWorkspaceRoot: Set<string>;
includedModelContent: string;
externalModelsIndex: DmnEditor.ExternalModelsIndex;
}) {
const normalizedPosixPathRelativeToTheOpenFile = __path.relative(
__path.dirname(args.thisDmnsNormalizedPosixPathRelativeToTheWorkspaceRoot),
args.normalizedPosixPathRelativeToTheWorkspaceRoot
);
const includedModel = normalize(getMarshaller(args.includedModelContent, { upgradeTo: "latest" }).parser.parse());
args.externalModelsIndex[args.includedModelNamespace] = {
normalizedPosixPathRelativeToTheOpenFile,
model: includedModel,
type: "dmn",
svg: "",
};
args.loadedDmnsByPathRelativeToTheWorkspaceRoot.add(args.normalizedPosixPathRelativeToTheWorkspaceRoot);
loadDependentModels({
...args,
model: includedModel,
});
}
// Load all included models from the model and the included models of those models, recursively.
function loadDependentModels(args: {
model: Normalized<DmnLatestModel>;
externalModelsIndex: DmnEditor.ExternalModelsIndex;
resourcesByNamespace: Map<string, ResourceContent>;
loadedDmnsByPathRelativeToTheWorkspaceRoot: Set<string>;
thisDmnsNormalizedPosixPathRelativeToTheWorkspaceRoot: string;
}) {
const includedNamespaces = new Set(
getIncludedNamespacesFromModel(args.model.definitions.import).split(NAMESPACES_EFFECT_SEPARATOR)
);
for (const includedNamespace of includedNamespaces) {
if (!args.resourcesByNamespace.has(includedNamespace)) {
console.warn(
`DMN EDITOR ROOT: The included namespace '${includedNamespace}' for the model '${args.model.definitions["@_id"]}' can not be found.`
);
continue;
}
const resource = args.resourcesByNamespace.get(includedNamespace)!;
if (args.loadedDmnsByPathRelativeToTheWorkspaceRoot.has(resource.normalizedPosixPathRelativeToTheWorkspaceRoot)) {
continue;
}
checkIfNamespaceIsAlreadyLoaded({
externalModelsIndex: args.externalModelsIndex,
namespaceOfTheResourceFile: includedNamespace,
normalizedPosixPathRelativeToTheWorkspaceRoot: resource.normalizedPosixPathRelativeToTheWorkspaceRoot,
});
loadModel({
...args,
includedModelContent: resource.content ?? "",
normalizedPosixPathRelativeToTheWorkspaceRoot: resource.normalizedPosixPathRelativeToTheWorkspaceRoot,
includedModelNamespace: includedNamespace,
});
}
}
function checkIfNamespaceIsAlreadyLoaded(args: {
externalModelsIndex: DmnEditor.ExternalModelsIndex;
namespaceOfTheResourceFile: string;
normalizedPosixPathRelativeToTheWorkspaceRoot: string;
}) {
if (args.externalModelsIndex[args.namespaceOfTheResourceFile]) {
console.warn(
`DMN EDITOR ROOT: Multiple DMN models encountered with the same namespace '${args.namespaceOfTheResourceFile}': '${
args.normalizedPosixPathRelativeToTheWorkspaceRoot
}' and '${
args.externalModelsIndex[args.namespaceOfTheResourceFile]!.normalizedPosixPathRelativeToTheOpenFile
}'. The latter will be considered.`
);
}
}