Source

ActivityDasContractProperties.js

//React
import React, { useEffect, useRef, useState } from "react";
import { Link, useNavigate } from "@reach/router";

//Components
import ProcessViewer from "./ProcessViewer";
import NoTypeActivity from "./NoTypeActivity";
import UserActivityFormFields from "./UserActivityFormFields";
import ScriptActivity from "./ScriptActivity";
import BusinessRuleActivity from "./BusinessRuleActivity";
import SavedToast from "./SavedToast";

//Redux
import { connect } from "react-redux";
import { loadContract } from "../redux/Contracts/contracts-actions";

//Styles
import { Container, Row } from "react-bootstrap";

//Others
import convert from "xml-js";
import PreventUnload from "./PreventUnload";

/**
 * Enables editing activity DasContract properties. Shows a process viewer for reference.
 *
 * @component
 */
const ActivityDasContractProperties = ({
  appStarted,
  loadedContract,
  loadContract,
  loadContractError,
}) => {
  //Data load
  /**
   * Loaded contract in JSON format state hook.
   * @constant
   *
   * @type {[string, function]}
   */
  const [loadedContractJSON, setLoadedContractJSON] = useState();

  useEffect(() => {
    if (loadedContract && !loadContractError) {
      setLoadedContractJSON(
        JSON.parse(
          convert.xml2json(loadedContract.xml, {
            compact: false,
            spaces: 4,
          })
        )
      );
    }

    redirectIfNotReady();
  }, []);

  const navigate = useNavigate();

  /**
   * Redirects the user to "Home" if there's no loaded contract or a load contract error.
   */
  const redirectIfNotReady = () => {
    if (!appStarted) {
      navigate("/");
    } else if (!loadedContract || loadContractError) {
      navigate("/process-editor");
    }
  };

  //Data manipulation
  /**
   * Model elements state hook.
   * @constant
   *
   * @type {[string, function]}
   */
  const [modelElements, setModelElements] = useState();

  /**
   * Activity presence indicator state hook.
   * @constant
   *
   * @type {[string, function]}
   */
  const [thereAreActivities, setThereAreActivities] = useState(false);

  useEffect(() => {
    loadedContractJSON && filterElements();
  }, [loadedContractJSON]);

  /**
   * Filters tasks from "loadedContractJSON" in a destructured array and sets result to "modelElements". Sets the state of the activities presence indicator.
   */
  const filterElements = () => {
    const superElementsArr = loadedContractJSON.elements[0].elements;

    const processArray = superElementsArr.filter((element) => {
      return element.name === "bpmn2:process";
    });

    const elementsArr = processArray[0].elements;

    if (elementsArr) {
      const tasksWithoutType = elementsArr.filter((element) => {
        return element.name === "bpmn2:task";
      });

      const userTasks = elementsArr.filter((element) => {
        return element.name === "bpmn2:userTask";
      });

      const businessRuleTasks = elementsArr.filter((element) => {
        return element.name === "bpmn2:businessRuleTask";
      });

      const scriptTasks = elementsArr.filter((element) => {
        return element.name === "bpmn2:scriptTask";
      });

      const otherElements = elementsArr.filter((element) => {
        return (
          element.name !== "bpmn2:task" &&
          element.name !== "bpmn2:userTask" &&
          element.name !== "bpmn2:businessRuleTask" &&
          element.name !== "bpmn2:scriptTask" &&
          element.name !== "bpmn2:subProcess"
        );
      });

      setModelElements([
        tasksWithoutType,
        userTasks,
        businessRuleTasks,
        scriptTasks,
        otherElements,
      ]);

      if (
        tasksWithoutType.length ||
        userTasks.length ||
        businessRuleTasks.length ||
        scriptTasks.length
      ) {
        setThereAreActivities(true);
      }
    } else {
      setThereAreActivities(false);
    }
  };

  /**
   * Updated a task.
   *
   * @param {Object} modelElementToUpdate Model element to update in "modelElements".
   * @param {Object} newData New data for the model element to update.
   */
  const updateTask = (modelElementToUpdate, newData) => {
    let newModelElements;

    newModelElements = [...modelElements];

    newModelElements.forEach((newModelElementType) => {
      newModelElementType.forEach((newModelElement) => {
        if (
          newModelElement.attributes.id === modelElementToUpdate.attributes.id
        ) {
          newModelElement.attributes["dascontract:activity-properties"] =
            newData;
        }
      });
    });

    setModelElements(newModelElements);

    if (loadedContractJSON) {
      const jointModelElementTypes = joinArray(modelElements);

      const newLoadedContractJSON = loadedContractJSON;

      const superElementsArray = newLoadedContractJSON.elements[0].elements;
      const elementsArrayIndex = superElementsArray.findIndex((element) => {
        return element.name === "bpmn2:process";
      });

      newLoadedContractJSON.elements[0].elements[elementsArrayIndex].elements =
        jointModelElementTypes;

      setLoadedContractJSON(newLoadedContractJSON);

      updateLoadedContract();
    }
  };

  /**
   * Joins a destructured array into a "joint" array.
   *
   * @param {Array} arr Array to be joined.
   * @returns
   */
  const joinArray = (arr) => {
    const jointArray = [];

    arr.forEach((arrElement) => {
      jointArray.push(...arrElement);
    });

    return jointArray;
  };

  /**
   * Toggle saved modal reference hook.
   * @constant
   *
   * @type {Object}
   */
  const toggleSavedToastRef = useRef(() => {});

  /**
   * Updates "loadedContract" from the store with the data in "loadedContractJSON".
   */
  const updateLoadedContract = () => {
    const newXML = convert.json2xml(loadedContractJSON, {
      compact: false,
      spaces: 4,
    });

    loadContract({
      ...loadedContract,
      xml: newXML,
    });

    toggleSavedToastRef.current();
  };

  //TODO: Markup can be modified for react-bootstrap.
  return (
    <Container className={`${loadContractError && "d-none"}`} fluid={true}>
      {appStarted && <PreventUnload />}
      {appStarted && <SavedToast toggle={toggleSavedToastRef} ms={5000} />}

      <Row>
        <ProcessViewer />
      </Row>
      <Row className={`${thereAreActivities && "d-none"} mt-2`}>
        <div className="px-4 py-5 my-2 text-center">
          <div className="col-lg-6 mx-auto">
            <p className="lead mb-4 note">
              Add some activities at the{" "}
              <Link to={"/process-editor"}>process editor</Link> to begin.
            </p>
          </div>
        </div>
      </Row>
      <Row className="mt-2">
        {modelElements &&
          modelElements[0].map((task) => {
            return <NoTypeActivity key={task.attributes.id} task={task} />;
          })}
      </Row>
      <Row className="mt-2">
        {modelElements &&
          modelElements[1].map((task) => {
            return (
              <UserActivityFormFields
                key={task.attributes.id}
                task={task}
                updateTask={updateTask}
                loadedContractJSON={loadedContractJSON}
              />
            );
          })}
      </Row>
      <Row className="mt-2">
        {modelElements &&
          modelElements[3].map((task) => {
            return (
              <ScriptActivity
                key={task.attributes.id}
                task={task}
                updateTask={updateTask}
              />
            );
          })}
      </Row>

      <Row className="mt-2">
        {modelElements &&
          modelElements[2].map((task) => {
            return (
              <BusinessRuleActivity
                key={task.attributes.id}
                task={task}
                updateTask={updateTask}
              />
            );
          })}
      </Row>
    </Container>
  );
};

const mapStateToProps = (state) => {
  return {
    appStarted: state.ui.appStarted,
    loadedContract: state.contracts.loadedContract,
    loadContractError: state.contracts.loadContractError,
  };
};

const mapDispatchToProps = (dispatch) => {
  return {
    loadContract: (contract) => dispatch(loadContract(contract)),
  };
};

ActivityDasContractProperties.propTypes = {
  /**
   * App started indicator from store.
   */
  appStarted: PropTypes.bool,

  /**
   * Loaded contract from store.
   */
  loadedContract: PropTypes.object,

  /**
   * Action to load a contract in the store.
   */
  loadContract: PropTypes.func.isRequired,

  /**
   * Load contract error indicator from store.
   */
  loadContractError: PropTypes.bool,
};

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(ActivityDasContractProperties);