Create a Simple React JS Workflow App From Scratch – Your Ultimate Guide
Tags: react, dotnet, tutorial, csharp
Published: 2024-05-20 09:33:26

It's time to write our second application, where there will be a list of schemes, processes, and a Workflow Designer with the ability to start a process and see its status. We will use [create-react-app](https://create-react-app.dev/) template to create a simple [React](https://reactjs.org/) application. Open your console and go to the folder `react-example`, then execute following commands:

```bash
npx create-react-app frontend
cd frontend
npm run start
```
You should see something similar in your console:

```text
Compiled successfully!

You can now view frontend in the browser.

  Local:            http://localhost:3000
  On Your Network:  http://192.168.50.125:3000

Note that the development build is not optimized.
To create a production build, use npm run build.

webpack compiled successfully
```
Now open your browser at [http://localhost:3000](http://localhost:3000) (in your case address may be different) and you will see an example React application. Let's start hack it. First we will add a navigation bar to our application. We will use [React Suite library](https://rsuitejs.com/).

Stop frontend application if its running (Ctrl+C). Open your console and execute command:

```bash
npm install rsuite
```
Then run frontend application again:

```bash
npm run start
```
Open frontend application in your favourite IDE. We will use JetBrains IDEA.

**Adding settings to access backend APIs**

Add `settings.js` file under frontend `src` folder with the following content:

frontend/src/settings.js

```javascript
const backendUrl = 'http://localhost:5139';

const settings = {
    workflowUrl: `${backendUrl}/api/workflow`,
    userUrl: `${backendUrl}/api/user`,
    designerUrl: `${backendUrl}/designer/API`
}

export default settings;
```
The `settings` object contains following properties:

1. workflowUrl - URL to access workflow API, see `WorkflowController` class.
2. userUrl - URL to access user API, see `UserController` class.
3. designerUrl - URL to access Workflow Designer API, see `DesignerController` class.

**Adding schemes table**

We will now create a simple table to show the workflow schemes from our API. Add the `Schemes.js` file to the frontend `src` folder with the following content:

frontend/src/Schemes.js

```javascript
import {Table} from "rsuite";
import {useEffect, useState} from "react";
import settings from "./settings";

const {Column, HeaderCell, Cell} = Table;

const Schemes = (props) => {
    const [data, setData] = useState([]);

    useEffect(() => {
        fetch(`${settings.workflowUrl}/schemes`)
            .then(response => response.json())
            .then(data => setData(data))
    }, []);
    
    return <Table data={data}
                  height={400}
                  onRowClick={rowData => props.onRowClick?.(rowData)}>
        <Column flexGrow={1}>
            <HeaderCell>Code</HeaderCell>
            <Cell dataKey="code"/>
        </Column>
        <Column flexGrow={1}>
            <HeaderCell>Tags</HeaderCell>
            <Cell dataKey="tags"/>
        </Column>
    </Table>
}

export default Schemes;
```
The React `Schemes` component simply shows the `data` in the `Table` and retrieves the `data` from the backend in the `useEffect` hook. The component calls the `props.onRowClick` function, if it was passed through props, when the user clicks a table row.

**Adding process instance table**

Add the `Processes.js` file to the frontend `src` folder with the following content:

frontend/src/Processes.js

```javascript
import {Table} from "rsuite";
import {useEffect, useState} from "react";
import settings from "./settings";

const {Column, HeaderCell, Cell} = Table;

const Processes = (props) => {
    const [data, setData] = useState([]);

    useEffect(() => {
        fetch(`${settings.workflowUrl}/instances`)
            .then(response => response.json())
            .then(data => setData(data))
    }, []);

    return <Table data={data}
                  height={400}
                  onRowClick={rowData => props.onRowClick?.(rowData)}>
        <Column flexGrow={1}>
            <HeaderCell>Id</HeaderCell>
            <Cell dataKey="id"/>
        </Column>
        <Column flexGrow={1}>
            <HeaderCell>Scheme</HeaderCell>
            <Cell dataKey="scheme"/>
        </Column>
        <Column flexGrow={1}>
            <HeaderCell>CreationDate</HeaderCell>
            <Cell dataKey="creationDate"/>
        </Column>
        <Column flexGrow={1}>
            <HeaderCell>StateName</HeaderCell>
            <Cell dataKey="stateName"/>
        </Column>
        <Column flexGrow={1}>
            <HeaderCell>ActivityName</HeaderCell>
            <Cell dataKey="activityName"/>
        </Column>
    </Table>
}

export default Processes;
```
The React `Processes` component works the same way as the `Schemes` component, but for process instances. Pay attention to `useState`, `useEffect` hooks and the `onRowClick` property.

**Adding users SelectPicker**

Now we need a component in which we can select the current user who will execute the process commands. Add the `Users.js` file to the frontend `src` folder with the following content:

frontend/src/Users.js

```javascript
import {useEffect, useState} from "react";
import {SelectPicker} from "rsuite";
import settings from "./settings";

const Users = (props) => {
    const [users, setUsers] = useState([]);

    const onChangeUser = user => {
        props.onChangeUser?.(user);
    }

    useEffect(() => {
        fetch(`${settings.userUrl}/all`)
            .then(response => response.json())
            .then(data => {
                setUsers(data);
                onChangeUser(data[0].name)
            })
    }, []);

    const data = users.map(u => {
        const roles = u.roles.join(', ');
        return ({label: `${u.name} (${roles})`, value: u.name})
    });

    return <SelectPicker data={data} style={{width: 224}} menuStyle={{zIndex: 1000}}
                         value={props.currentUser} onChange={onChangeUser}/>
}

export default Users;
```
The `Users` component shows the users in the `SelectPicker` and calls the `props.onChangeUser` function if one was passed. Pay attention to `useState`, `useEffect` hooks.


**Adding component to create schemes and load schemes**

Since Workflow Designer does not have a built-in component for selecting the current scheme, we will create it. Add the `SchemeMenu.js` file to the frontend `src` folder with the following content:

frontend/src/SchemeMenu.js

```javascript
import {Button, ButtonGroup} from "rsuite";
import React from "react";

const SchemeMenu = (props) => {
    const onClick = () => {
        const newCode = prompt('Enter scheme name');
        if (newCode) {
            props.onNewScheme?.(newCode);
        }
    }

    return <ButtonGroup>
        <Button disabled={true}>Scheme name: {props.schemeCode}</Button>
        <Button onClick={onClick}>Create or load scheme</Button>
        <Button onClick={() => props.onCreateProcess?.()}>Create process</Button>
    </ButtonGroup>
}

export default SchemeMenu;
```
The `SchemeMenu` component shows `ButtonGroup` with three buttons:

1. Disabled button with current scheme name from property `props.schemeCode`.

2. A button for creating a new or loading an existing scheme. The Workflow Designer checks if there is a scheme with the entered name, if the scheme exists, then it will load it, otherwise it will create a new one.

3.A button to create a new process instance on selected scheme. This button will call the `props.onCreateProcess` function if it was passed to properties.

The `SchemeMenu` component calls the `props.onNewScheme` function if it was passed to the properties when changing the scheme name.

**Adding component to executing process instance commands**

We need to show the available commands for the selected user and process instance. Let's create a component for this. Add the `ProcessMenu.js` file to the frontend `src` folder with the following content:

frontend/src/ProcessMenu.js

```javascript
import React, {useEffect, useState} from "react";
import {Button, ButtonGroup, FlexboxGrid} from "rsuite";
import FlexboxGridItem from "rsuite/cjs/FlexboxGrid/FlexboxGridItem";
import settings from "./settings";
import Users from "./Users";

const ProcessMenu = (props) => {
    const [commands, setCommands] = useState([]);
    const [currentUser, setCurrentUser] = useState();

    const loadCommands = (processId, user) => {
        fetch(`${settings.workflowUrl}/commands/${processId}/${user}`)
            .then(result => result.json())
            .then(result => {
                setCommands(result.commands)
            })
    }

    const executeCommand = (command) => {
        fetch(`${settings.workflowUrl}/executeCommand/${props.processId}/${command}/${currentUser}`)
            .then(result => result.json())
            .then(() => {
                loadCommands(props.processId, currentUser);
                props.afterCommandExecuted?.();
            });
    }

    useEffect(() => {
        loadCommands(props.processId, currentUser);
    }, [props.processId, currentUser]);

    const buttons = commands.map(c => <Button key={c} onClick={() => executeCommand(c)}>{c}</Button>)

    return <FlexboxGrid>
        <FlexboxGridItem colspan={4}>
            <Users onChangeUser={setCurrentUser} currentUser={currentUser}/>
        </FlexboxGridItem>
        <FlexboxGridItem colspan={12}>
            <ButtonGroup>
                <Button disabled={true}>Commands:</Button>
                {buttons}
            </ButtonGroup>
        </FlexboxGridItem>
    </FlexboxGrid>
}

export default ProcessMenu;
```
The `ProcessMenu` component loads the available commands in the `useEffect` hook for `props.processId` and `currentUser`. Also pay attention to the `loadCommands` and `executeCommand` functions.

**Adding Designer component**

Now we are ready to add a Workflow Designer component. Open your console in `frontend` folder and execute following command:

```bash
npm install @optimajet/workflow-designer-react --legacy-peer-deps
```
This will install latest npm package with a [Workflow Designer](https://www.npmjs.com/package/@optimajet/workflow-designer-react) for React. This npm package is wrapper around [JavaScript Workflow Designer](https://www.npmjs.com/package/@optimajet/workflow-designer).

Add the `Designer.js` file to the frontend `src` folder with the following content:

frontend/src/Designer.js

```javascript
import React, {useRef, useState} from "react";
import {Container} from "rsuite";
import WorkflowDesigner from "@optimajet/workflow-designer-react";
import settings from "./settings";
import SchemeMenu from "./SchemeMenu";
import ProcessMenu from "./ProcessMenu";

const Designer = (props) => {
    const {schemeCode, ...otherProps} = {props}
    const [code, setCode] = useState(props.schemeCode)
    const [processId, setProcessId] = useState(props.processId)
    const designerRef = useRef()

    const designerConfig = {
        renderTo: 'wfdesigner',
        apiurl: settings.designerUrl,
        templatefolder: '/templates/',
        widthDiff: 300,
        heightDiff: 100,
        showSaveButton: !processId
    };

    const createOrLoad = (code) => {
        setCode(code)
        setProcessId(null)
        const data = {
            schemecode: code,
            processid: undefined
        }
        const wfDesigner = designerRef.current.innerDesigner;
        if (wfDesigner.exists(data)) {
            wfDesigner.load(data);
        } else {
            wfDesigner.create(code);
        }
    }

    const refreshDesigner = () => {
        designerRef.current.loadScheme();
    }

    const onCreateProcess = () => {
        fetch(`${settings.workflowUrl}/createInstance/${code}`)
            .then(result => result.json())
            .then(data => {
                setProcessId(data.id)
                const params = {
                    schemecode: code,
                    processid: data.id
                };
                designerRef.current.innerDesigner.load(params, 
                    () => console.log('Process loaded'),
                    error => console.error(error));
            });
    }

    return <Container style={{maxWidth: '80%', overflow: 'hidden'}}>
        {!processId &&
            <SchemeMenu {...otherProps} schemeCode={code}
                        onNewScheme={createOrLoad} onCreateProcess={onCreateProcess}/>
        }
        {!!processId && <ProcessMenu processId={processId} afterCommandExecuted={refreshDesigner}/>}
        <WorkflowDesigner
            schemeCode={code}
            processId={processId}
            designerConfig={designerConfig}
            ref={designerRef}
        />
    </Container>
}

export default Designer;
```
The Designer component does the following:

1. Gets `schemeCode` and `processId` from `props`.
2. Renders the `WorkflowDesigner` component.
3. Renders the `SchemeMenu` component if `processId` is set.
4. Renders the `ProcessMenu` component if `processId` is not set.

The `ProcessMenu` component is displayed when we are in "running process" mode, otherwise the SchemeMenu is displayed.

**Adding a custom activity**

Copy the entire contents of the [templates](https://github.com/optimajet/WorkflowEngine.NET/tree/master/Designer/templates) folder to the `public/templates` folder in your `frontend` project:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4iz6wanynljpeqr5c3k5.png)

Copy the file `public/templates/elements/activity.svg` to `public/templates/elements/weatherActivity.svg`. The `weatherActivity.svg` file is the SVG template that will be displayed on the Canvas.

Copy the file `public/templates/activity.html` to `public/templates/weatherActivity.html`. The `weatherActivity.html` file represents the activity form. Open the `weatherActivity.html` file and change the name of the function `activity_Init` to `weatherActivity_Init`:

frontend/public/templates/weatherActivity.html

```javascript
  ...
  function activity_Init(me) {
  function weatherActivity_Init(me) {
  ...
```
You can learn more about custom activity [here](https://workflowengine.io/documentation/designer-customization/custom-activity).

**Combining all interface components together**

Now we have components to show schemes, process instances and a Workflow Designer. Let's combine these components together to show them on web page. Add the `AppView.js` file to the frontend `src` folder with the following content:

frontend/src/AppView.js

```javascript
import {Container, Content, Header, Nav, Navbar} from "rsuite";
import React, {useState} from "react";
import Schemes from "./Schemes";
import Processes from "./Processes";
import Designer from "./Designer";

const navigationItems = [
    {name: 'Schemes', component: Schemes},
    {name: 'Processes', component: Processes},
    {name: 'Designer', component: Designer}
];

const AppView = () => {
    const [tab, setTab] = useState(navigationItems[0].name);
    const [schemeCode, setSchemeCode] = useState('Test1');
    const [processId, setProcessId] = useState();
    const items = navigationItems.map(
        item => <Nav.Item key={item.name} active={tab === item.name} onClick={() => setTab(item.name)}>{item.name}</Nav.Item>);
    const Child = navigationItems.find(item => item.name === tab)?.component
    const childProps = {
        onRowClick: (data) => {
            if (data.code) {
                setSchemeCode(data.code)
                setProcessId(undefined)
                setTab('Designer')
            } else if (data.id) {
                setSchemeCode(data.scheme);
                setProcessId(data.id);
                setTab('Designer')
            }
        },
        schemeCode: schemeCode,
        processId: processId
    }
    return <Container>
        <Header>
            <Navbar>
                <Nav>
                    {items}
                </Nav>
            </Navbar>
        </Header>
        <Content>
            <Child {...childProps}/>
        </Content>
    </Container>
}

export default AppView;
```
`AppView` component renders three tabs:

1. Schemes - shows workflow schemes.
2. Processes - shows process instances.
3. Designer - shows the Workflow Designer with a scheme or process instance.

When the user clicks on the navigation element, the `setTab` function is executed and changes the active tab. Variable `schemeCode` contains the name of the current scheme, by default 'Test1'. The variable `processId` contains the unique identifier of the current process, by default `undefined`, which means that the process is not running.

Now open your `index.js` script and change its contents (changed lines are highlighted):

frontend/src/index.js

```javascript
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import reportWebVitals from './reportWebVitals';
import 'rsuite/dist/rsuite.min.css';    
import AppView from "./AppView";        

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <AppView/>
  </React.StrictMode>
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
```
We now have a web interface that shows everything we need to display schemes, process instances, draw process schemes, and start processes.

##Adding a process scheme

Now start the backend application, execute the script from the `Backend` folder (if the application was already running - restart it):

```bash
dotnet run --project WorkflowApi
```
Then run the `frontend` application, execute the script from the frontend folder (if the application was already running - restart it):

```bash
npm run start
```
Now we need to load our scheme. To do this, save the following XML to the file `Test1.xml`.

Test1.xml

```xml
<Process Name="Test1" CanBeInlined="false" Tags="" LogEnabled="false">
  <Designer />
  <Actors>
    <Actor Name="User" Rule="CheckRole" Value="User" />
    <Actor Name="Manager" Rule="CheckRole" Value="Manager" />
  </Actors>
  <Commands>
    <Command Name="GetWeatherForecast" />
    <Command Name="SendWeatherForecast" />
    <Command Name="ReRun" />
  </Commands>
  <Activities>
    <Activity Name="InitialActivity" State="InitialActivity" IsInitial="true" IsFinal="false" IsForSetState="true" IsAutoSchemeUpdate="true">
      <Designer X="390" Y="170" Hidden="false" />
    </Activity>
    <Activity Name="WeatherActivity" State="WeatherActivity" IsInitial="false" IsFinal="false" IsForSetState="true" IsAutoSchemeUpdate="true">
      <Annotations>
        <Annotation Name="__customtype"><![CDATA[WeatherActivity]]></Annotation>
      </Annotations>
      <Designer X="730" Y="170" Hidden="false" />
    </Activity>
    <Activity Name="SendEmail" State="SendEmail" IsInitial="false" IsFinal="false" IsForSetState="true" IsAutoSchemeUpdate="true">
      <Annotations>
        <Annotation Name="__customtype"><![CDATA[SendEmail]]></Annotation>
        <Annotation Name="CcList"><![CDATA[[]]]></Annotation>
        <Annotation Name="BccList"><![CDATA[[]]]></Annotation>
        <Annotation Name="ReplyToList"><![CDATA[[]]]></Annotation>
        <Annotation Name="To"><![CDATA[mail@gmail.com]]></Annotation>
        <Annotation Name="Subject"><![CDATA[Weather]]></Annotation>
        <Annotation Name="IsHTML"><![CDATA[true]]></Annotation>
        <Annotation Name="Body"><![CDATA[WeatherDate: @WeatherDate
WeatherTemperature: @WeatherTemperature
Latitude: @Weather.latitude]]></Annotation>
      </Annotations>
      <Designer X="1100" Y="170" Hidden="false" />
    </Activity>
  </Activities>
  <Transitions>
    <Transition Name="InitialActivity_WeatherActivity_1" To="WeatherActivity" From="InitialActivity" Classifier="Direct" AllowConcatenationType="And" RestrictConcatenationType="And" ConditionsConcatenationType="And" DisableParentStateControl="false">
      <Restrictions>
        <Restriction Type="Allow" NameRef="User" />
      </Restrictions>
      <Triggers>
        <Trigger Type="Command" NameRef="GetWeatherForecast" />
      </Triggers>
      <Conditions>
        <Condition Type="Always" />
      </Conditions>
      <Designer Hidden="false" />
    </Transition>
    <Transition Name="WeatherActivity_SendEmail_1" To="SendEmail" From="WeatherActivity" Classifier="Direct" AllowConcatenationType="And" RestrictConcatenationType="And" ConditionsConcatenationType="And" DisableParentStateControl="false">
      <Restrictions>
        <Restriction Type="Allow" NameRef="Manager" />
      </Restrictions>
      <Triggers>
        <Trigger Type="Command" NameRef="SendWeatherForecast" />
      </Triggers>
      <Conditions>
        <Condition Type="Always" />
      </Conditions>
      <Designer Hidden="false" />
    </Transition>
    <Transition Name="SendEmail_InitialActivity_1" To="InitialActivity" From="SendEmail" Classifier="Reverse" AllowConcatenationType="And" RestrictConcatenationType="And" ConditionsConcatenationType="And" DisableParentStateControl="false">
      <Restrictions>
        <Restriction Type="Restrict" NameRef="User" />
      </Restrictions>
      <Triggers>
        <Trigger Type="Command" NameRef="ReRun" />
      </Triggers>
      <Conditions>
        <Condition Type="Always" />
      </Conditions>
      <Designer X="816" Y="342" Hidden="false" />
    </Transition>
  </Transitions>
</Process>
```
Open a browser at [http://localhost:3000](http://localhost:3000) and click the 'Designer' tab. We will add only one process scheme, since the free license does not allow us to make more schemes. 

Click on the `Menu -> File -> Upload scheme` and choose `Test1.xml` files. Then click on Save button to save the scheme.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/cup4lrx6m5vcuolnjihc.png)

Now we have a process scheme:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1rvg6pw7d08prnhwezq6.png)

The last thing we need is to change the email address in the `SendEmail` activity. Write your email address here:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/l9s5e23q3njo1sffm6aw.png)

Then click the 'Save' button and then click the 'Save' button on the toolbar to save the scheme.

##Starting and executing the process

Click the 'Create process' button:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kxz2lj8kgzs4hr68urct.png)

Now the process is in `InitialActivity` (highlighted in yellow) activity. And there is one 'GetWeatherForecast' command available for a user with the 'User' role (Peter, Margaret). If you select John or Sam in select, the 'GetWeatherForecast' command will disappear because they only have the 'Manager' role.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ethmhqw0ammhjrx1yogz.png)

Now click the 'GetWeatherForecast' button to execute the command and the process will switch to the `WeatherActivity` activity. And there is one 'SendWeatherForecast' command available for a user with the 'Manager' role (Peter, John, Sam):

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/m4zep46do818usc93vxb.png)

Click the 'Process info' button (the letter 'P' in the circle) on the toolbar and go to the 'Process Parameters' tab in the window that opens to see the process parameters. Note that here you can see the process parameters that were saved using our custom activity class WeatherActivity.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rm29aw1zvjbydel2wl5n.png)

Select the Sam user and click the 'SendWeatherForecast' button. Now the process is in `SendEmail` activity. And there is one 'ReRun' command available for a user without the 'User' role (John, Sam):

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/z8xg3hndzg9mqp665yw4.png)

If all your settings have been done correctly, you should see something similar to this email in your inbox:

**!EMAIL**
Weather
WeatherDate: 2022-10-07 WeatherTemperature: 5,7 Latitude: 52,52

Select the John user and click the 'Rerun' button. Now the process is in `InitialActivity` activity again:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3h1mpvzi4yuc5ai89am9.png)

You can execute the commands for this process again, but let's go to the 'Schemes' tab (click on it):

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hwfeskjevk9xkhkztqg5.png)

This is a table with schemes, and it has only one row with the scheme name 'Test1'. Click on this row to open the scheme in the Workflow Designer. Click the 'Process info' button (the letter 'P' in the circle) on the toolbar and enter the tags in the 'Tags' field:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vz50fseiof5ucch1ycdx.png)

Then click the 'Save' button and then click the 'Save' button on the toolbar to save the scheme. After that, go to the 'Schemes' tab to view the data in the schemes table.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ik5o3ztglw7py854mr9x.png)

Learn more about scheme tags [here](https://workflowengine.io/documentation/scalability/multitenancy#tags). Go to the 'Processes' tab to view the table with process instances:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/56b446pd3y7mvqc3i3mg.png)

In this table you can see rows with the process instances. The data for this table is obtained through the `WorkflowController.Instances` method in the `WorkflowApi` project. You can click on a row to open a process instance in the Workflow Designer.

##Conclusion

Wow, it's been a long journey! Now you have a simple admin panel where you can see your process schemas, process instances, and Workflow Designer. You can change our simple process - add actions, commands and more. You can also change your Backend project and add more useful stuff. Check out our documentation to learn more.

P.S.

We are taking our first steps in creating content that is useful to the developer community, so we ask you not to judge us too harshly and to help us with advice, comments, and recommendations on how to improve our publications. We welcome any feedback in the comments.