Source

components/UserActivityFormFields.js

//React
import React, { useEffect, useState } from "react";

//Components
import UserActivityArrayFieldTemplate from "./UserActivityArrayFieldTemplate";
import FormDataStateContext from "./FormDataStateContext";

//Styles
import { Accordion, Button, Col, Container } from "react-bootstrap";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faSave, faUser } from "@fortawesome/free-solid-svg-icons";
import { Resizable } from "react-resizable";

//Code editor
import CodeMirror from "@uiw/react-codemirror";
import { javascript } from "@codemirror/lang-javascript";

//Other
import SchemaForm from "@rjsf/bootstrap-4";
import PropTypes from "prop-types";

/**
 * Enables to edit an user activity by form fields provided through react-jsonschema-form.
 *
 * @component
 */
const UserActivityFormFields = ({ task, updateTask, loadedContractJSON }) => {
  /**
   * Form schema state hook.
   * @constant
   *
   * @type {[Object, function]}
   */
  const [schema, setSchema] = useState({});

  /**
   * Form UI schema state hook.
   * @constant
   *
   * @type {[Object, function]}
   */
  const [uiSchema, setUiSchema] = useState({});

  /**
   * Form data state hook.
   * @constant
   *
   * @type {Array}
   */
  const [formData, setFormData] = useState([]);

  useEffect(() => {
    if (task.attributes["dascontract:activity-properties"]) {
      try {
        setFormData(
          JSON.parse(task.attributes["dascontract:activity-properties"])
        );
      } catch (e) {}
    }

    if (loadedContractJSON) {
      setupSchema();

      setUiSchema({
        items: {
          description: {
            "ui:widget": "textarea",
          },
        },
        "ui:order": [
          "id",
          "name",
          "label",
          "description",
          "readonly",
          "binding",
        ],
      });
    }
  }, []);

  /**
   * Resizable component height state hook.
   * @constant
   *
   * @type {[number, function]}
   */
  const [viewerHeight, setViewerHeight] = useState(400);

  /**
   * Updates the value of "viewerHeight" when user drags the corner of Resizable component.
   *
   * @param {{Object}} size Updated size of Resizable component.
   */
  const handleResize = ({ size }) => {
    setViewerHeight(size.height);
  };

  /**
   * Sets up the form schema adding entities present in the DasContract data model.
   */
  const setupSchema = () => {
    const rootProcesses = getRootProcesses();
    const entities = getEntities(rootProcesses);
    console.log(entities);
    setSchema({
      type: "array",
      items: {
        type: "object",
        properties: {
          id: {
            type: "number",
            title: "Number",
            minLength: 1,
            readOnly: true,
          },
          name: {
            type: "string",
            title: "Name",
            minLength: 1,
          },
          label: {
            type: "string",
            title: "Label",
            minLength: 1,
          },
          description: {
            type: "string",
            title: "Description",
          },
          readonly: {
            type: "boolean",
            title: "Readonly?",
          },
          binding: {
            type: "string",
            title: "Entity binding",
            enum: entities,
          },
        },
        required: ["id", "name", "label"],
      },
    });
  };

  /**
   * Obtains the entities of data models.
   *
   * @returns {Array<Object>} Entities of data models.
   */
  const getEntities = (rootProcesses) => {
    const entities = [];

    rootProcesses &&
      rootProcesses.forEach((process) => {
        if (process.attributes["dascontract:data-model"]) {
          const dataModel = JSON.parse(
            process.attributes["dascontract:data-model"]
          );

          dataModel.forEach((entity) => {
            entities.push(
              `${process.attributes.id} - ${
                entity.name ? entity.name : "Untitled"
              } - ${entity.id && entity.id}`
            );
          });
        }
      });

    return entities;
  };

  /**
   * Gets root processes from "loadedContractJSON".
   *
   *@returns {Array<Object>} Root processes array.
   */
  const getRootProcesses = () => {
    const elementsArr = loadedContractJSON.elements[0].elements;

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

  return (
    <Col sm="4">
      <Accordion className="accordion-inline">
        <Accordion.Item eventKey={`${task.attributes && task.attributes.id}`}>
          <Accordion.Header>
            <FontAwesomeIcon className="mx-2" icon={faUser} />
            {task.attributes && task.attributes.name} (
            {task.attributes && task.attributes.id})
          </Accordion.Header>
          <Accordion.Body>
            <Resizable height={viewerHeight} onResize={handleResize}>
              <Container
                className="m-2"
                fluid={true}
                style={{ height: viewerHeight * 0.5 + "px" }}
              >
                <CodeMirror
                  value={JSON.stringify(formData, null, 4)}
                  options={{ lint: true }}
                  height={viewerHeight * 0.5 + "px"}
                  extensions={[javascript()]}
                  editable={false}
                />
              </Container>
            </Resizable>

            <FormDataStateContext.Provider value={[formData, setFormData]}>
              <SchemaForm
                schema={schema}
                uiSchema={uiSchema}
                formData={formData}
                onChange={({ formData }) => setFormData(formData)}
                ArrayFieldTemplate={UserActivityArrayFieldTemplate}
              >
                <Button
                  type="submit"
                  variant="outline-primary"
                  size="m"
                  onClick={() => updateTask(task, JSON.stringify(formData))}
                >
                  <FontAwesomeIcon icon={faSave} />
                </Button>
              </SchemaForm>
            </FormDataStateContext.Provider>
          </Accordion.Body>
        </Accordion.Item>
      </Accordion>
    </Col>
  );
};

UserActivityFormFields.propTypes = {
  /**
   * Task data.
   */
  task: PropTypes.object.isRequired,

  /**
   * Callback to update the value of task in the parent component.
   */
  updateTask: PropTypes.func.isRequired,
};

export default UserActivityFormFields;