DEV Community

Cover image for Create a Simple React JS Workflow App From Scratch – Your Ultimate Guide
Optimajet Limited
Optimajet Limited

Posted on • Edited on

Create a Simple React JS Workflow App From Scratch – Your Ultimate Guide

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 template to create a simple React application. Open your console and go to the folder react-example, then execute following commands:



npx create-react-app frontend
cd frontend
npm run start


Enter fullscreen mode Exit fullscreen mode

You should see something similar in your console:



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


Enter fullscreen mode Exit fullscreen mode

Now open your browser at 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.

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



npm install rsuite


Enter fullscreen mode Exit fullscreen mode

Then run frontend application again:



npm run start


Enter fullscreen mode Exit fullscreen mode

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



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

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

export default settings;


Enter fullscreen mode Exit fullscreen mode

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



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;


Enter fullscreen mode Exit fullscreen mode

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



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;


Enter fullscreen mode Exit fullscreen mode

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



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;


Enter fullscreen mode Exit fullscreen mode

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



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;


Enter fullscreen mode Exit fullscreen mode

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



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;


Enter fullscreen mode Exit fullscreen mode

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:



npm install @optimajet/workflow-designer-react --legacy-peer-deps


Enter fullscreen mode Exit fullscreen mode

This will install latest npm package with a Workflow Designer for React. This npm package is wrapper around JavaScript Workflow Designer.

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

frontend/src/Designer.js



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;


Enter fullscreen mode Exit fullscreen mode

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 folder to the public/templates folder in your frontend project:

Image description

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



  ...
  function activity_Init(me) {
  function weatherActivity_Init(me) {
  ...


Enter fullscreen mode Exit fullscreen mode

You can learn more about custom activity here.

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



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;


Enter fullscreen mode Exit fullscreen mode

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



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();


Enter fullscreen mode Exit fullscreen mode

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):



dotnet run --project WorkflowApi


Enter fullscreen mode Exit fullscreen mode

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



npm run start


Enter fullscreen mode Exit fullscreen mode

Now we need to load our scheme. To do this, save the following XML to the file Test1.xml.

Test1.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>


Enter fullscreen mode Exit fullscreen mode

Open a browser at 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

Now we have a process scheme:

Image description

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

Image description

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

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

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

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

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

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

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

Image description

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

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

Learn more about scheme tags here. Go to the 'Processes' tab to view the table with process instances:

Image description

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.

Top comments (0)