//React
import React, { useEffect, useRef, useState } from "react";
//Components
import SaveChangesModal from "./SaveChangesModal";
//Redux
//Styles
import { Accordion, Button, ButtonGroup, Col, Row } from "react-bootstrap";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
faDownload,
faRedo,
faSave,
faTable,
faUndo,
} from "@fortawesome/free-solid-svg-icons";
import { Resizable } from "react-resizable";
//DMN editor
import DmnModeler from "dmn-js/lib/Modeler";
import { DrdLinting } from "dmn-js-dmnlint";
import dmnlintConfig from "../.dmnlintrc";
import { useDropzone } from "react-dropzone";
import newDmn from "../resources/newDmn.dmn";
//Other
import convert from "xml-js";
import PropTypes from "prop-types";
/**
* Business rule activity. It provides a DMN editor.
*
* @component
*/
const BusinessRuleActivity = ({ task, updateTask }) => {
/**
* DMN XML state hook.
* @constant
*
* @type {[string, function]}
*/
const [dmnXML, setDmnXML] = useState();
useEffect(() => {
if (task.attributes["dascontract:activity-properties"]) {
const xmlModel = convert.json2xml(
JSON.parse(task.attributes["dascontract:activity-properties"]),
{
compact: false,
spaces: 4,
}
);
setDmnXML(xmlModel);
}
}, []);
//DMN editor
/**
* DMN modeler reference hook.
* @constant
*
* @type {Object}
*/
const dmnModelerRef = useRef();
/**
* Canvas HTML element for DMN modeler reference hook.
* @constant
*
* @type {Object}
*/
const canvasRef = useRef();
/**
* Command stack of modeler reference hook. Enables handling of undo/redo.
* @constant
*
* @type {Object}
*/
const commandStackRef = useRef();
/**
* Resizable component height state hook.
* @constant
*
* @type {[number, function]}
*/
const [viewerHeight, setViewerHeight] = useState(1000); // *0.5 at inline style to reduce resizing speed.
/**
* Updates the value of "viewerHeight" when user drags the corner of Resizable component.
*
* @param {{number}} size Updated size of Resizable component.
*/
const handleResize = ({ size }) => {
setViewerHeight(size.height);
};
const handleKeyDown = (e) => {
if (e.ctrlKey && e.shiftKey && (e.key === "Z" || e.key === "z")) {
commandStackRef.current.redo();
} else if (e.ctrlKey && (e.key === "Z" || e.key === "z")) {
commandStackRef.current.undo();
}
};
const handleUndo = () => {
commandStackRef.current.undo();
};
const handleRedo = () => {
commandStackRef.current.redo();
};
//File load
/**
* Import error indicator state hook.
* @constant
*
* @type {[boolean, function]}
*/
const [importError, setImportError] = useState(false);
/**
* Successful import indicator state hook.
* @constant
*
* @type {[boolean, function]}
*/
const [successfulImport, setSuccessfulImport] = useState(false);
/**
* Problem cause description state hook.
* @constant
*
* @type {[string, function]}
*/
const [problemCause, setProblemCause] = useState();
/**
* Handles file drop. Displays a modal to confirm replacement of model.
*/
const onDrop = () => {
toggleModalButtonRef.current.click();
};
/**
* Handles accepted file drop.
*/
const onDropAccepted = () => {
try {
if (acceptedFiles) {
const file = acceptedFiles[0];
const reader = new FileReader();
reader.onload = () => {
const content = reader.result;
setFileName(file.name);
setDmnXML(content);
openDmn(content);
};
reader.readAsText(file);
}
} catch (err) {
console.error("Error happened opening file: ", err);
}
};
/**
* Dropzone hook.
*/
const { acceptedFiles, getRootProps, getInputProps } = useDropzone({
noClick: true,
/*onDropAccepted: onDropAccepted,*/
onDrop: onDrop,
});
/**
* Attempts to open a diagram in XML with DMN modeler.
*
* @param {string} xml
*/
const openDmn = async (dmnXML) => {
await dmnModelerRef.current
.importXML(dmnXML)
.then(({ warnings }) => {
if (warnings.length) {
console.log("Import with warnings: ", warnings);
} else {
console.log("Import successful.");
}
/*commandStackRef.current = dmnModelerRef.current.get("commandStack");*/
setSuccessfulImport(true);
setImportError(false);
})
.catch((err) => {
setSuccessfulImport(false);
setImportError(true);
setProblemCause(err.message);
});
};
/**
* Attempts to create a diagram in XML with DMN modeler.
*
* @param {string} xml
*/
const createNewDmn = () => {
try {
let rawFile = new XMLHttpRequest();
rawFile.open("GET", newDmn, false);
rawFile.onreadystatechange = function () {
if (rawFile.readyState === 4) {
if (rawFile.status === 200 || rawFile.status == 0) {
setFileName("newDmn.dmn");
setDmnXML(rawFile.responseText);
openDmn(rawFile.responseText);
}
}
};
rawFile.send(null);
} catch (err) {
console.error("Error happened opening model: ", err);
}
};
useEffect(() => {
if (!dmnModelerRef.current) {
dmnModelerRef.current = new DmnModeler({
container: canvasRef.current,
width: "100%",
common: {
linting: dmnlintConfig,
},
drd: {
additionalModules: [DrdLinting],
},
});
}
}, []);
useEffect(() => {
if (dmnXML) {
openDmn(dmnXML);
}
}, [acceptedFiles, dmnXML]);
//Model save
/**
* Toggle saved modal reference hook.
* @constant
*
* @type {Object}
*/
const toggleModalButtonRef = useRef();
/**
* File name state hook.
* @constant
*
* @type {[string, function]}
*/
const [fileName, setFileName] = useState("");
/**
* Attempts to save the current state of the modeler as "loadedContract" in the store.
*/
const saveModel = async () => {
try {
dmnModelerRef.current.saveXML().then(({ xml }) => {
const jsonModel = JSON.stringify(
convert.xml2json(xml, {
compact: false,
spaces: 4,
})
);
updateTask(task, jsonModel);
});
} catch (err) {
console.error("Error happened saving model: ", err);
}
};
//File save
/**
* Link for SVG reference hook.
* @constant
*
* @type {Object}
*/
const linkSaveSVGRef = useRef();
/**
* Link for XML reference hook.
* @constant
*
* @type {Object}
*/
const linkSaveXMLRef = useRef();
/**
* Attempts to save the current XML from task attributes.
*/
const saveXML = async () => {
try {
saveModel();
await dmnModelerRef.current.saveXML({ format: true }).then(({ xml }) => {
encodeDownload(
linkSaveXMLRef.current,
`${task.attributes.id}.dmn`,
xml
);
linkSaveXMLRef.current.click();
});
} catch (err) {
console.error("Error happened saving XML: ", err);
}
};
/**
* Attempts to save the current SVG from task attributes.
*/
const saveSVG = async () => {
try {
saveModel();
await dmnModelerRef.current.saveSVG().then(({ svg }) => {
encodeDownload(
linkSaveSVGRef.current,
`${task.attributes.id}.dmn.svg`,
svg
);
linkSaveSVGRef.current.click();
});
} catch (err) {
console.error("Error happened saving SVG: ", err);
}
};
/**
* Encodes the download of files.
*
* @param {Object} link
* @param {string} name
* @param {*} data
*/
const encodeDownload = (link, name, data) => {
let encodedData = encodeURIComponent(data);
if (data) {
link.href = "data:application/bpmn20-xml;charset=UTF-8," + encodedData;
link.download = name;
}
};
//TODO: Markup can be modified for react-bootstrap.
return (
<Col
sm="12"
{...getRootProps({ className: "dropzone" })}
/*onKeyDown={handleKeyDown}*/
>
<input {...getInputProps()} />
<SaveChangesModal
context="replaceDiagram"
toggleModalButtonRef={toggleModalButtonRef}
callback={onDropAccepted}
/>
<a ref={linkSaveXMLRef} className="hidden" target="_blank" />
<a ref={linkSaveSVGRef} className="hidden" target="_blank" />
<Accordion className="accordion-inline">
<Accordion.Item eventKey={`${task.attributes && task.attributes.id}`}>
<Accordion.Header>
<FontAwesomeIcon className="mx-2" icon={faTable} />
{task.attributes && task.attributes.name} (
{task.attributes && task.attributes.id})
</Accordion.Header>
<Accordion.Body>
<Resizable height={viewerHeight} onResize={handleResize}>
<Row
className={`dmn-container ${
importError && "with-import-error"
} ${successfulImport && "with-diagram"} mb-2`}
style={{ height: viewerHeight * 0.5 + "px" }}
>
<Col>
<div className="message intro">
<div className="px-4 py-5 my-5 text-center">
<div className="col-lg-6 mx-auto">
<p className="lead mb-4 note">
Drop a DMN file from your desktop or
<span
className="link-primary"
style={{ cursor: "pointer" }}
onClick={createNewDmn}
>
{" "}
create a new DMN model{" "}
</span>
to get started.
</p>
</div>
</div>
</div>
<div className="message import-error">
<div className="px-4 py-5 my-5 text-center">
<div className="col-lg-6 mx-auto">
<p className="lead mb-4 note">
Ooops, we could not display the DMN model. Drop
another file or{" "}
<span
className="link-primary"
style={{ cursor: "pointer" }}
onClick={createNewDmn}
>
{" "}
create a new DMN model
</span>
.
</p>
<div
className={`${
!problemCause && "d-none"
} lead mb-4 note`}
>
<span>Cause of the problem: </span>
<pre>{problemCause}</pre>
</div>
</div>
</div>
</div>
<div
ref={canvasRef}
className="dmn-canvas"
style={{ height: viewerHeight * 0.5 + "px" }}
/>
</Col>
</Row>
</Resizable>
<ButtonGroup
className={`${
(!successfulImport || importError) && "d-none"
} dmn-save-buttons m-2`}
>
<Button variant="outline-primary" size="m" onClick={saveModel}>
<FontAwesomeIcon icon={faSave} />
</Button>
<Button variant="outline-primary" size="m" onClick={saveXML}>
<FontAwesomeIcon icon={faDownload} /> XML
</Button>
{/*<Button
variant="outline-secondary"
size="m" onClick={saveSVG}>
<FontAwesomeIcon icon={faDownload}/> SVG
</Button>
<Button
variant="outline-secondary"
size="m" onClick={handleUndo}>
<FontAwesomeIcon icon={faUndo}/>
</Button>
<Button
variant="outline-secondary"
size="m" onClick={handleRedo}>
<FontAwesomeIcon icon={faRedo}/>
</Button>*/}
</ButtonGroup>
</Accordion.Body>
</Accordion.Item>
</Accordion>
</Col>
);
};
BusinessRuleActivity.propTypes = {
/**
* Task data.
*/
task: PropTypes.object.isRequired,
/**
* Callback to update the value of task in the parent component.
*/
updateTask: PropTypes.func.isRequired,
};
export default BusinessRuleActivity;
Source