eslint is passing w/ burnettk

This commit is contained in:
jasquat 2022-06-27 12:00:15 -04:00
parent b9238739eb
commit c9e118b7c2
25 changed files with 1284 additions and 939 deletions

View File

@ -4,20 +4,27 @@ module.exports = {
es2021: true, es2021: true,
}, },
extends: [ extends: [
"react-app", 'react-app',
"react-app/jest", 'react-app/jest',
"plugin:react/recommended", 'plugin:react/recommended',
"airbnb", 'airbnb',
"plugin:jest/recommended", 'plugin:jest/recommended',
"plugin:prettier/recommended", 'plugin:prettier/recommended',
], ],
parserOptions: { parserOptions: {
ecmaFeatures: { ecmaFeatures: {
jsx: true, jsx: true,
}, },
ecmaVersion: "latest", ecmaVersion: 'latest',
sourceType: "module", sourceType: 'module',
},
plugins: ['react'],
rules: {
'react/jsx-no-bind': 'off',
'jsx-a11y/no-autofocus': 'off',
'jsx-a11y/label-has-associated-control': 'off',
'no-console': 'off',
'react/jsx-filename-extension': [1, { extensions: ['.js', '.jsx'] }],
'react/react-in-jsx-scope': 'off',
}, },
plugins: ["react"],
rules: {},
}; };

1
package-lock.json generated
View File

@ -25,6 +25,7 @@
"dmn-js": "^12.1.1", "dmn-js": "^12.1.1",
"dmn-js-properties-panel": "^1.0.0", "dmn-js-properties-panel": "^1.0.0",
"dmn-js-shared": "^12.1.1", "dmn-js-shared": "^12.1.1",
"prop-types": "^15.8.1",
"react": "^18.2.0", "react": "^18.2.0",
"react-bootstrap": "^2.4.0", "react-bootstrap": "^2.4.0",
"react-datepicker": "^4.8.0", "react-datepicker": "^4.8.0",

View File

@ -20,6 +20,7 @@
"dmn-js": "^12.1.1", "dmn-js": "^12.1.1",
"dmn-js-properties-panel": "^1.0.0", "dmn-js-properties-panel": "^1.0.0",
"dmn-js-shared": "^12.1.1", "dmn-js-shared": "^12.1.1",
"prop-types": "^15.8.1",
"react": "^18.2.0", "react": "^18.2.0",
"react-bootstrap": "^2.4.0", "react-bootstrap": "^2.4.0",
"react-datepicker": "^4.8.0", "react-datepicker": "^4.8.0",

View File

@ -1,5 +1,6 @@
import React from "react"; import React from 'react';
//FIXME: this is currently not actually catching any issues. not sure why import PropTypes from 'prop-types';
class ErrorBoundary extends React.Component { class ErrorBoundary extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
@ -8,26 +9,29 @@ class ErrorBoundary extends React.Component {
static getDerivedStateFromError(error) { static getDerivedStateFromError(error) {
// Update state so the next render will show the fallback UI. // Update state so the next render will show the fallback UI.
return { hasError: true }; return { hasError: true, error };
} }
componentDidCatch(error, errorInfo) { componentDidCatch(error, errorInfo) {
this.setState({
error: error,
errorInfo: errorInfo
})
// You can also log the error to an error reporting service // You can also log the error to an error reporting service
console.log("HELLO: ", error, errorInfo); console.log('HELLO: ', error, errorInfo);
} }
render() { render() {
if (this.state.hasError) { const { hasError } = this.state;
const { children } = this.props;
if (hasError) {
// You can render any custom fallback UI // You can render any custom fallback UI
return <h1>Something went wrong.</h1>; return <h1>Something went wrong.</h1>;
} }
return this.props.children; return children;
} }
} }
export default ErrorBoundary; export default ErrorBoundary;
ErrorBoundary.propTypes = {
children: PropTypes.string.isRequired,
};

View File

@ -1,23 +1,25 @@
import React from "react"; import React from 'react';
import { BACKEND_BASE_URL } from '../config';
import { HOT_AUTH_TOKEN } from '../config';
import axios from 'axios'; import axios from 'axios';
import PropTypes from 'prop-types';
import { BACKEND_BASE_URL, HOT_AUTH_TOKEN } from '../config';
export default class FileInput extends React.Component { export default class FileInput extends React.Component {
constructor(props) { constructor({ processGroupId, processModelId }) {
super(props); super({ processGroupId, processModelId });
this.handleSubmit = this.handleSubmit.bind(this); this.handleSubmit = this.handleSubmit.bind(this);
this.fileInput = React.createRef(); this.fileInput = React.createRef();
this.props = props; this.processGroupId = processGroupId;
this.processModelId = processModelId;
} }
handleSubmit(event) { handleSubmit(event) {
event.preventDefault() event.preventDefault();
const url = `${BACKEND_BASE_URL}/process-models/${this.props.processModel.process_group_id}/${this.props.processModel.id}/file`; const url = `${BACKEND_BASE_URL}/process-models/${this.processGroupId}/${this.processModelId}/file`;
const formData = new FormData(); const formData = new FormData();
formData.append('file', this.fileInput.current.files[0]); formData.append('file', this.fileInput.current.files[0]);
formData.append('fileName', this.fileInput.current.files[0].name); formData.append('fileName', this.fileInput.current.files[0].name);
// this might work if we remove the content-type header
// const headers = { // const headers = {
// 'Authorization': `Bearer ${HOT_AUTH_TOKEN}`, // 'Authorization': `Bearer ${HOT_AUTH_TOKEN}`,
// 'content-type': 'multipart/form-data', // 'content-type': 'multipart/form-data',
@ -38,7 +40,7 @@ export default class FileInput extends React.Component {
const config = { const config = {
headers: { headers: {
'content-type': 'multipart/form-data', 'content-type': 'multipart/form-data',
'Authorization': `Bearer ${HOT_AUTH_TOKEN}`, Authorization: `Bearer ${HOT_AUTH_TOKEN}`,
}, },
}; };
axios.post(url, formData, config).then((response) => { axios.post(url, formData, config).then((response) => {
@ -58,3 +60,8 @@ export default class FileInput extends React.Component {
); );
} }
} }
FileInput.propTypes = {
processGroupId: PropTypes.string.isRequired,
processModelId: PropTypes.string.isRequired,
};

View File

@ -1,93 +1,162 @@
import React from "react"; import React from 'react';
import { Link } from "react-router-dom"; import { Link } from 'react-router-dom';
import { Dropdown, Stack } from 'react-bootstrap' import { Dropdown, Stack } from 'react-bootstrap';
import PropTypes from 'prop-types';
export const DEFAULT_PER_PAGE = 50; export const DEFAULT_PER_PAGE = 50;
export const DEFAULT_PAGE = 1; export const DEFAULT_PAGE = 1;
export default function PaginationForTable(props) { export default function PaginationForTable({
page,
perPage,
pagination,
tableToDisplay,
queryParamString,
path,
}) {
const PER_PAGE_OPTIONS = [2, 10, 50, 100]; const PER_PAGE_OPTIONS = [2, 10, 50, 100];
const buildPerPageDropdown = (() => { const buildPerPageDropdown = () => {
const perPageDropdownRows = PER_PAGE_OPTIONS.map(perPageOption => { const perPageDropdownRows = PER_PAGE_OPTIONS.map((perPageOption) => {
if (perPageOption === props.perPage) { if (perPageOption === perPage) {
return <Dropdown.Item key={perPageOption} href={`${props.path}?page=1&per_page=${perPageOption}`} active>{perPageOption}</Dropdown.Item> return (
} else { <Dropdown.Item
return <Dropdown.Item key={perPageOption} href={`${props.path}?page=1&per_page=${perPageOption}`}>{perPageOption}</Dropdown.Item> key={perPageOption}
href={`${path}?page=1&per_page=${perPageOption}`}
active
>
{perPageOption}
</Dropdown.Item>
);
} }
return (
<Dropdown.Item
key={perPageOption}
href={`${path}?page=1&per_page=${perPageOption}`}
>
{perPageOption}
</Dropdown.Item>
);
}); });
return ( return (
<Stack direction="horizontal" gap={3}> <Stack direction="horizontal" gap={3}>
<Dropdown className="ms-auto" id="pagination-page-dropdown"> <Dropdown className="ms-auto" id="pagination-page-dropdown">
<Dropdown.Toggle id="process-instances-per-page" variant="light border"> <Dropdown.Toggle
Process Instances to Show: {props.perPage} id="process-instances-per-page"
</Dropdown.Toggle> variant="light border"
>
Process Instances to Show: {perPage}
</Dropdown.Toggle>
<Dropdown.Menu variant="light"> <Dropdown.Menu variant="light">{perPageDropdownRows}</Dropdown.Menu>
{perPageDropdownRows} </Dropdown>
</Dropdown.Menu>
</Dropdown>
</Stack> </Stack>
) );
}); };
const buildPaginationNav = (() => { const buildPaginationNav = () => {
let previousPageTag = ""; let previousPageTag = '';
if (props.page === 1) { if (page === 1) {
previousPageTag = ( previousPageTag = (
<li data-qa="pagination-previous-button-inactive" className="page-item disabled" key="previous"><span style={{fontSize:"1.5em"}} className="page-link">&laquo;</span></li> <li
) data-qa="pagination-previous-button-inactive"
className="page-item disabled"
key="previous"
>
<span style={{ fontSize: '1.5em' }} className="page-link">
&laquo;
</span>
</li>
);
} else { } else {
previousPageTag = ( previousPageTag = (
<li className="page-item" key="previous"> <li className="page-item" key="previous">
<Link data-qa="pagination-previous-button" className="page-link" style={{fontSize:"1.5em"}} to={`${props.path}?page=${props.page - 1}&per_page=${props.perPage}${props.queryParamString}`}>&laquo;</Link> <Link
data-qa="pagination-previous-button"
className="page-link"
style={{ fontSize: '1.5em' }}
to={`${path}?page=${
page - 1
}&per_page=${perPage}${queryParamString}`}
>
&laquo;
</Link>
</li> </li>
) );
} }
let nextPageTag = ""; let nextPageTag = '';
if (props.page >= props.pagination.pages) { if (page >= pagination.pages) {
nextPageTag = ( nextPageTag = (
<li data-qa="pagination-next-button-inactive" className="page-item disabled" key="next"><span style={{fontSize:"1.5em"}} className="page-link">&raquo;</span></li> <li
) data-qa="pagination-next-button-inactive"
className="page-item disabled"
key="next"
>
<span style={{ fontSize: '1.5em' }} className="page-link">
&raquo;
</span>
</li>
);
} else { } else {
nextPageTag = ( nextPageTag = (
<li className="page-item" key="next"> <li className="page-item" key="next">
<Link data-qa="pagination-next-button" className="page-link" style={{fontSize:"1.5em"}} to={`${props.path}?page=${props.page + 1}&per_page=${props.perPage}${props.queryParamString}`}>&raquo;</Link> <Link
data-qa="pagination-next-button"
className="page-link"
style={{ fontSize: '1.5em' }}
to={`${path}?page=${
page + 1
}&per_page=${perPage}${queryParamString}`}
>
&raquo;
</Link>
</li> </li>
) );
} }
let startingNumber = ((props.page - 1) * props.perPage) + 1 let startingNumber = (page - 1) * perPage + 1;
let endingNumber = ((props.page) * props.perPage) let endingNumber = page * perPage;
if (endingNumber > props.pagination.total) { if (endingNumber > pagination.total) {
endingNumber = props.pagination.total endingNumber = pagination.total;
} }
if (startingNumber > props.pagination.total) { if (startingNumber > pagination.total) {
startingNumber = props.pagination.total startingNumber = pagination.total;
} }
return ( return (
<Stack direction="horizontal" gap={3}> <Stack direction="horizontal" gap={3}>
<p className="ms-auto">{startingNumber}-{endingNumber} of <span data-qa="total-paginated-items">{props.pagination.total}</span></p> <p className="ms-auto">
{startingNumber}-{endingNumber} of{' '}
<span data-qa="total-paginated-items">{pagination.total}</span>
</p>
<nav aria-label="Page navigation"> <nav aria-label="Page navigation">
<div> <div>
<ul className="pagination"> <ul className="pagination">
{previousPageTag} {previousPageTag}
{nextPageTag} {nextPageTag}
</ul> </ul>
</div> </div>
</nav> </nav>
</Stack> </Stack>
) );
}); };
return( return (
<main> <main>
{buildPaginationNav()} {buildPaginationNav()}
{props.tableToDisplay} {tableToDisplay}
{buildPerPageDropdown()} {buildPerPageDropdown()}
</main> </main>
) );
} }
PaginationForTable.propTypes = {
page: PropTypes.number.isRequired,
perPage: PropTypes.number.isRequired,
pagination: PropTypes.objectOf(PropTypes.number).isRequired,
tableToDisplay: PropTypes.string.isRequired,
queryParamString: PropTypes.string.isRequired,
path: PropTypes.string.isRequired,
};

View File

@ -1,30 +1,67 @@
import { Link } from "react-router-dom"; import { Link } from 'react-router-dom';
import Breadcrumb from 'react-bootstrap/Breadcrumb' import Breadcrumb from 'react-bootstrap/Breadcrumb';
import PropTypes from 'prop-types';
export default function ProcessBreadcrumb(props) { export default function ProcessBreadcrumb({
let processGroupBreadcrumb = '' processModelId,
let processModelBreadcrumb = '' processGroupId,
linkProcessModel,
}) {
let processGroupBreadcrumb = '';
let processModelBreadcrumb = '';
if (props.processModelId) { if (processModelId) {
if (props.linkProcessModel) { if (linkProcessModel) {
processModelBreadcrumb = <Breadcrumb.Item linkAs={Link} linkProps={{ to: `/process-models/${props.processGroupId}/${props.processModelId}` }}>Process Model: {props.processModelId}</Breadcrumb.Item> processModelBreadcrumb = (
<Breadcrumb.Item
linkAs={Link}
linkProps={{
to: `/process-models/${processGroupId}/${processModelId}`,
}}
>
Process Model: {processModelId}
</Breadcrumb.Item>
);
} else { } else {
processModelBreadcrumb = <Breadcrumb.Item active={true}>Process Model: {props.processModelId}</Breadcrumb.Item> processModelBreadcrumb = (
<Breadcrumb.Item active>
Process Model: {processModelId}
</Breadcrumb.Item>
);
} }
processGroupBreadcrumb = <Breadcrumb.Item linkAs={Link} linkProps={{ to: `/process-groups/${props.processGroupId}` }}> processGroupBreadcrumb = (
Process Group: {props.processGroupId} <Breadcrumb.Item
</Breadcrumb.Item> linkAs={Link}
} else if (props.processGroupId) { linkProps={{ to: `/process-groups/${processGroupId}` }}
processGroupBreadcrumb = <Breadcrumb.Item active={true}>Process Group: {props.processGroupId}</Breadcrumb.Item> >
Process Group: {processGroupId}
</Breadcrumb.Item>
);
} else if (processGroupId) {
processGroupBreadcrumb = (
<Breadcrumb.Item active>Process Group: {processGroupId}</Breadcrumb.Item>
);
} }
return ( return (
<main style={{ padding: "1rem 0" }}> <main style={{ padding: '1rem 0' }}>
<Breadcrumb> <Breadcrumb>
<Breadcrumb.Item linkAs={Link} linkProps={{ to: '/' }}>Home</Breadcrumb.Item> <Breadcrumb.Item linkAs={Link} linkProps={{ to: '/' }}>
{processGroupBreadcrumb} Home
{processModelBreadcrumb} </Breadcrumb.Item>
</Breadcrumb> {processGroupBreadcrumb}
{processModelBreadcrumb}
</Breadcrumb>
</main> </main>
); );
} }
ProcessBreadcrumb.propTypes = {
processModelId: PropTypes.string.isRequired,
processGroupId: PropTypes.string.isRequired,
linkProcessModel: PropTypes.bool,
};
ProcessBreadcrumb.defaultProps = {
linkProcessModel: false,
};

View File

@ -1,30 +1,46 @@
import { render, screen } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import { BrowserRouter } from 'react-router-dom';
import ProcessBreadcrumb from './ProcessBreadcrumb'; import ProcessBreadcrumb from './ProcessBreadcrumb';
import {
BrowserRouter,
} from "react-router-dom";
test('renders home link', () => { test('renders home link', () => {
render(<BrowserRouter><ProcessBreadcrumb /></BrowserRouter>); render(
<BrowserRouter>
<ProcessBreadcrumb />
</BrowserRouter>
);
const homeElement = screen.getByText(/Home/); const homeElement = screen.getByText(/Home/);
expect(homeElement).toBeInTheDocument(); expect(homeElement).toBeInTheDocument();
}); });
test('renders process group when given processGroupId', async () => { test('renders process group when given processGroupId', async () => {
render(<BrowserRouter><ProcessBreadcrumb processGroupId='group-a'/></BrowserRouter>); render(
<BrowserRouter>
<ProcessBreadcrumb processGroupId="group-a" />
</BrowserRouter>
);
const processGroupElement = screen.getByText(/group-a/); const processGroupElement = screen.getByText(/group-a/);
expect(processGroupElement).toBeInTheDocument(); expect(processGroupElement).toBeInTheDocument();
const processGroupBreadcrumbs = await screen.findAllByText(/Process Group: group-a/); const processGroupBreadcrumbs = await screen.findAllByText(
/Process Group: group-a/
);
expect(processGroupBreadcrumbs[0]).toHaveClass('breadcrumb-item active'); expect(processGroupBreadcrumbs[0]).toHaveClass('breadcrumb-item active');
}); });
test('renders process model when given processModelId', async () => { test('renders process model when given processModelId', async () => {
render(<BrowserRouter><ProcessBreadcrumb processGroupId='group-b' processModelId='model-c'/></BrowserRouter>); render(
<BrowserRouter>
<ProcessBreadcrumb processGroupId="group-b" processModelId="model-c" />
</BrowserRouter>
);
const processGroupElement = screen.getByText(/group-b/); const processGroupElement = screen.getByText(/group-b/);
expect(processGroupElement).toBeInTheDocument(); expect(processGroupElement).toBeInTheDocument();
const processModelBreadcrumbs = await screen.findAllByText(/Process Model: model-c/); const processModelBreadcrumbs = await screen.findAllByText(
/Process Model: model-c/
);
expect(processModelBreadcrumbs[0]).toHaveClass('breadcrumb-item active'); expect(processModelBreadcrumbs[0]).toHaveClass('breadcrumb-item active');
const processGroupBreadcrumbs = await screen.findAllByText(/Process Group: group-b/); const processGroupBreadcrumbs = await screen.findAllByText(
/Process Group: group-b/
);
expect(processGroupBreadcrumbs[0]).toBeInTheDocument(); expect(processGroupBreadcrumbs[0]).toBeInTheDocument();
// expect(processGroupBreadcrumbs[0]).toHaveClass('breadcrumb-item'); // expect(processGroupBreadcrumbs[0]).toHaveClass('breadcrumb-item');
}); });

View File

@ -1,23 +1,24 @@
const host = window.location.hostname; const host = window.location.hostname;
const hostAndPort = `${host}:7000`; const hostAndPort = `${host}:7000`;
export const BACKEND_BASE_URL = `http://${hostAndPort}/v1.0` export const BACKEND_BASE_URL = `http://${hostAndPort}/v1.0`;
export const HOT_AUTH_TOKEN = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOm51bGx9.krsOjlSilPMu_3r7WkkUfKyr-h3HprXr6R4_FXRXz6Y" export const HOT_AUTH_TOKEN =
'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOm51bGx9.krsOjlSilPMu_3r7WkkUfKyr-h3HprXr6R4_FXRXz6Y';
export const STANDARD_HEADERS = { export const STANDARD_HEADERS = {
headers: new Headers({ headers: new Headers({
'Authorization': `Bearer ${HOT_AUTH_TOKEN}` Authorization: `Bearer ${HOT_AUTH_TOKEN}`,
}) }),
} };
export const PROCESS_STATUSES = [ export const PROCESS_STATUSES = [
"all", 'all',
"not_started", 'not_started',
"user_input_required", 'user_input_required',
"waiting", 'waiting',
"complete", 'complete',
"faulted", 'faulted',
"suspended", 'suspended',
] ];
export const DATE_FORMAT = "yyyy-MM-dd HH:mm:ss" export const DATE_FORMAT = 'yyyy-MM-dd HH:mm:ss';

View File

@ -1,20 +1,20 @@
// https://www.30secondsofcode.org/js/s/slugify // https://www.30secondsofcode.org/js/s/slugify
export const slugifyString = ((str) => { export const slugifyString = (str) => {
return str return str
.toLowerCase() .toLowerCase()
.trim() .trim()
.replace(/[^\w\s-]/g, '') .replace(/[^\w\s-]/g, '')
.replace(/[\s_-]+/g, '-') .replace(/[\s_-]+/g, '-')
.replace(/^-+|-+$/g, ''); .replace(/^-+|-+$/g, '');
}); };
export const convertDateToSeconds = ((date, onChangeFunction) => { export const convertDateToSeconds = (date, onChangeFunction) => {
if (date === null) { if (date === null) {
return; return undefined;
} }
let dateInMilliseconds = date; let dateInMilliseconds = date;
if (typeof(date.getTime) === "function") { if (typeof date.getTime === 'function') {
dateInMilliseconds = date.getTime(); dateInMilliseconds = date.getTime();
} }
const dateInSeconds = Math.floor(dateInMilliseconds / 1000); const dateInSeconds = Math.floor(dateInMilliseconds / 1000);
@ -23,4 +23,6 @@ export const convertDateToSeconds = ((date, onChangeFunction) => {
} else { } else {
return dateInSeconds; return dateInSeconds;
} }
});
return undefined;
};

View File

@ -1,33 +1,27 @@
import React from 'react'; import React from 'react';
import * as ReactDOMClient from 'react-dom/client'; import * as ReactDOMClient from 'react-dom/client';
import logo from './logo.svg'; import { Container } from 'react-bootstrap';
import 'bootstrap/dist/css/bootstrap.css'; import 'bootstrap/dist/css/bootstrap.css';
import './index.css'; import './index.css';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import reportWebVitals from './reportWebVitals'; import reportWebVitals from './reportWebVitals';
import {
BrowserRouter,
Routes,
Route,
} from "react-router-dom";
import ProcessGroups from "./routes/ProcessGroups" import ProcessGroups from './routes/ProcessGroups';
import ProcessGroupShow from "./routes/ProcessGroupShow" import ProcessGroupShow from './routes/ProcessGroupShow';
import ProcessGroupNew from "./routes/ProcessGroupNew" import ProcessGroupNew from './routes/ProcessGroupNew';
import ProcessGroupEdit from "./routes/ProcessGroupEdit" import ProcessGroupEdit from './routes/ProcessGroupEdit';
import ProcessModelShow from "./routes/ProcessModelShow" import ProcessModelShow from './routes/ProcessModelShow';
import ProcessModelEditDiagram from "./routes/ProcessModelEditDiagram" import ProcessModelEditDiagram from './routes/ProcessModelEditDiagram';
import ProcessInstanceList from "./routes/ProcessInstanceList" import ProcessInstanceList from './routes/ProcessInstanceList';
import ProcessInstanceReport from "./routes/ProcessInstanceReport" import ProcessInstanceReport from './routes/ProcessInstanceReport';
import ProcessModelNew from "./routes/ProcessModelNew" import ProcessModelNew from './routes/ProcessModelNew';
import ProcessModelEdit from "./routes/ProcessModelEdit" import ProcessModelEdit from './routes/ProcessModelEdit';
import ProcessInstanceShow from "./routes/ProcessInstanceShow" import ProcessInstanceShow from './routes/ProcessInstanceShow';
import ErrorBoundary from "./components/ErrorBoundary" import ErrorBoundary from './components/ErrorBoundary';
import { Container } from 'react-bootstrap'
import logo from './logo.svg';
const root = ReactDOMClient.createRoot(document.getElementById('root')); const root = ReactDOMClient.createRoot(document.getElementById('root'));
root.render( root.render(
@ -40,18 +34,48 @@ root.render(
<Route path="/" element={<ProcessGroups />} /> <Route path="/" element={<ProcessGroups />} />
<Route path="process-groups" element={<ProcessGroups />} /> <Route path="process-groups" element={<ProcessGroups />} />
<Route path="process-groups/:process_group_id" element={<ProcessGroupShow />} /> <Route
path="process-groups/:process_group_id"
element={<ProcessGroupShow />}
/>
<Route path="process-groups/new" element={<ProcessGroupNew />} /> <Route path="process-groups/new" element={<ProcessGroupNew />} />
<Route path="process-groups/:process_group_id/edit" element={<ProcessGroupEdit />} /> <Route
path="process-groups/:process_group_id/edit"
element={<ProcessGroupEdit />}
/>
<Route path="process-models/:process_group_id/new" element={<ProcessModelNew />} /> <Route
<Route path="process-models/:process_group_id/:process_model_id" element={<ProcessModelShow />} /> path="process-models/:process_group_id/new"
<Route path="process-models/:process_group_id/:process_model_id/file" element={<ProcessModelEditDiagram />} /> element={<ProcessModelNew />}
<Route path="process-models/:process_group_id/:process_model_id/file/:file_name" element={<ProcessModelEditDiagram />} /> />
<Route path="process-models/:process_group_id/:process_model_id/process-instances" element={<ProcessInstanceList />} /> <Route
<Route path="process-models/:process_group_id/:process_model_id/process-instances/report" element={<ProcessInstanceReport />} /> path="process-models/:process_group_id/:process_model_id"
<Route path="process-models/:process_group_id/:process_model_id/edit" element={<ProcessModelEdit />} /> element={<ProcessModelShow />}
<Route path="process-models/:process_group_id/:process_model_id/process-instances/:process_instance_id" element={<ProcessInstanceShow />} /> />
<Route
path="process-models/:process_group_id/:process_model_id/file"
element={<ProcessModelEditDiagram />}
/>
<Route
path="process-models/:process_group_id/:process_model_id/file/:file_name"
element={<ProcessModelEditDiagram />}
/>
<Route
path="process-models/:process_group_id/:process_model_id/process-instances"
element={<ProcessInstanceList />}
/>
<Route
path="process-models/:process_group_id/:process_model_id/process-instances/report"
element={<ProcessInstanceReport />}
/>
<Route
path="process-models/:process_group_id/:process_model_id/edit"
element={<ProcessModelEdit />}
/>
<Route
path="process-models/:process_group_id/:process_model_id/process-instances/:process_instance_id"
element={<ProcessInstanceShow />}
/>
</Routes> </Routes>
</BrowserRouter> </BrowserRouter>
</ErrorBoundary> </ErrorBoundary>

View File

@ -10,34 +10,44 @@ import {
DmnPropertiesProviderModule, DmnPropertiesProviderModule,
} from 'dmn-js-properties-panel'; } from 'dmn-js-properties-panel';
import React, { useRef, useEffect, useState } from "react"; import React, { useRef, useEffect, useState } from 'react';
import { BACKEND_BASE_URL } from './config';
import { HOT_AUTH_TOKEN } from './config';
import spiffworkflowIO from 'bpmn-js-spiffworkflow/app/spiffworkflow/InputOutput'; import spiffworkflowIO from 'bpmn-js-spiffworkflow/app/spiffworkflow/InputOutput';
import spiffworkflowPanel from 'bpmn-js-spiffworkflow/app/spiffworkflow/PropertiesPanel'; import spiffworkflowPanel from 'bpmn-js-spiffworkflow/app/spiffworkflow/PropertiesPanel';
import Button from 'react-bootstrap/Button'; import Button from 'react-bootstrap/Button';
import PropTypes from 'prop-types';
import { HOT_AUTH_TOKEN, BACKEND_BASE_URL } from './config';
import "bpmn-js/dist/assets/diagram-js.css"; import 'bpmn-js/dist/assets/diagram-js.css';
import "bpmn-js/dist/assets/bpmn-font/css/bpmn-embedded.css"; import 'bpmn-js/dist/assets/bpmn-font/css/bpmn-embedded.css';
import "bpmn-js-properties-panel/dist/assets/properties-panel.css" import 'bpmn-js-properties-panel/dist/assets/properties-panel.css';
import './bpmn-js-properties-panel.css'; import './bpmn-js-properties-panel.css';
import "bpmn-js/dist/assets/bpmn-js.css"; import 'bpmn-js/dist/assets/bpmn-js.css';
import "dmn-js/dist/assets/diagram-js.css"; import 'dmn-js/dist/assets/diagram-js.css';
import "dmn-js/dist/assets/dmn-js-decision-table-controls.css"; import 'dmn-js/dist/assets/dmn-js-decision-table-controls.css';
import "dmn-js/dist/assets/dmn-js-decision-table.css"; import 'dmn-js/dist/assets/dmn-js-decision-table.css';
import "dmn-js/dist/assets/dmn-js-drd.css"; import 'dmn-js/dist/assets/dmn-js-drd.css';
import "dmn-js/dist/assets/dmn-js-literal-expression.css"; import 'dmn-js/dist/assets/dmn-js-literal-expression.css';
import "dmn-js/dist/assets/dmn-js-shared.css"; import 'dmn-js/dist/assets/dmn-js-shared.css';
import "dmn-js/dist/assets/dmn-font/css/dmn-embedded.css"; import 'dmn-js/dist/assets/dmn-font/css/dmn-embedded.css';
import "dmn-js-properties-panel/dist/assets/properties-panel.css" import 'dmn-js-properties-panel/dist/assets/properties-panel.css';
import "bpmn-js-spiffworkflow/app/css/app.css" import 'bpmn-js-spiffworkflow/app/css/app.css';
// https://codesandbox.io/s/quizzical-lake-szfyo?file=/src/App.js was a handy reference // https://codesandbox.io/s/quizzical-lake-szfyo?file=/src/App.js was a handy reference
export default function ReactDiagramEditor(props) { export default function ReactDiagramEditor({
const [diagramXML, setDiagramXML] = useState(""); processModelId,
processGroupId,
saveDiagram,
diagramType,
diagramXML,
fileName,
onLaunchScriptEditor,
url,
}) {
const [diagramXMLString, setDiagramXMLString] = useState('');
const [diagramModelerState, setDiagramModelerState] = useState(null); const [diagramModelerState, setDiagramModelerState] = useState(null);
const [performingXmlUpdates, setPerformingXmlUpdates] = useState(false); const [performingXmlUpdates, setPerformingXmlUpdates] = useState(false);
@ -48,8 +58,8 @@ export default function ReactDiagramEditor(props) {
return; return;
} }
document.getElementById("diagram-container").innerHTML = ""; document.getElementById('diagram-container').innerHTML = '';
var temp = document.createElement('template'); const temp = document.createElement('template');
temp.innerHTML = ` temp.innerHTML = `
<div class="content with-diagram" id="js-drop-zone"> <div class="content with-diagram" id="js-drop-zone">
@ -57,61 +67,48 @@ export default function ReactDiagramEditor(props) {
style="border:1px solid #000000; height:90vh; width:90vw; margin:auto;"></div> style="border:1px solid #000000; height:90vh; width:90vw; margin:auto;"></div>
<div class="properties-panel-parent" id="js-properties-panel"></div> <div class="properties-panel-parent" id="js-properties-panel"></div>
</div> </div>
` `;
var frag = temp.content; const frag = temp.content;
document.getElementById("diagram-container").appendChild(frag); document.getElementById('diagram-container').appendChild(frag);
let diagramModeler = null; let diagramModeler = null;
if (props.diagramType === "bpmn") { if (diagramType === 'bpmn') {
diagramModeler = new BpmnModeler({ diagramModeler = new BpmnModeler({
container: "#canvas", container: '#canvas',
keyboard: { keyboard: {
bindTo: document bindTo: document,
}, },
propertiesPanel: { propertiesPanel: {
parent: '#js-properties-panel' parent: '#js-properties-panel',
}, },
additionalModules: [ additionalModules: [
BpmnPropertiesPanelModule, BpmnPropertiesPanelModule,
BpmnPropertiesProviderModule, BpmnPropertiesProviderModule,
spiffworkflowIO, spiffworkflowIO,
spiffworkflowPanel spiffworkflowPanel,
], ],
}); });
} else if (props.diagramType === "dmn") { } else if (diagramType === 'dmn') {
diagramModeler = new DmnModeler({ diagramModeler = new DmnModeler({
container: "#canvas", container: '#canvas',
keyboard: { keyboard: {
bindTo: document bindTo: document,
}, },
drd: { drd: {
propertiesPanel: { propertiesPanel: {
parent: '#js-properties-panel' parent: '#js-properties-panel',
}, },
additionalModules: [ additionalModules: [
DmnPropertiesPanelModule, DmnPropertiesPanelModule,
DmnPropertiesProviderModule, DmnPropertiesProviderModule,
] ],
} },
}); });
} }
setDiagramModelerState(diagramModeler);
diagramModeler.on('launch.script.editor', (event) => {
const {
error,
element,
} = event;
if (error) {
console.log(error);
}
handleLaunchScriptEditor(element);
});
function handleLaunchScriptEditor(element) { function handleLaunchScriptEditor(element) {
const { onLaunchScriptEditor } = props;
if (onLaunchScriptEditor) { if (onLaunchScriptEditor) {
setPerformingXmlUpdates(true); setPerformingXmlUpdates(true);
const modeling = diagramModeler.get('modeling'); const modeling = diagramModeler.get('modeling');
@ -119,108 +116,150 @@ export default function ReactDiagramEditor(props) {
} }
} }
}, [props, diagramModelerState]) setDiagramModelerState(diagramModeler);
diagramModeler.on('launch.script.editor', (event) => {
const { error, element } = event;
if (error) {
console.log(error);
}
handleLaunchScriptEditor(element);
});
}, [diagramModelerState, diagramType, onLaunchScriptEditor]);
useEffect(() => { useEffect(() => {
if (!diagramModelerState) { if (!diagramModelerState) {
return; return undefined;
} }
if (performingXmlUpdates) { if (performingXmlUpdates) {
return; return undefined;
}
function handleError(err) {
console.log('ERROR:', err);
} }
diagramModelerState.on('import.done', (event) => { diagramModelerState.on('import.done', (event) => {
const { const { error } = event;
error,
} = event;
if (error) { if (error) {
return handleError(error); handleError(error);
return;
} }
let modeler = diagramModelerState; let modeler = diagramModelerState;
if (props.diagramType === "dmn") { if (diagramType === 'dmn') {
modeler = diagramModelerState.getActiveViewer(); modeler = diagramModelerState.getActiveViewer();
} }
// only get the canvas if the dmn active viewer is actually // only get the canvas if the dmn active viewer is actually
// a Modeler and not an Editor which is what it will when we are // a Modeler and not an Editor which is what it will when we are
// actively editing a decision table // actively editing a decision table
if (modeler.constructor.name === "Modeler") { if (modeler.constructor.name === 'Modeler') {
modeler.get('canvas').zoom('fit-viewport'); modeler.get('canvas').zoom('fit-viewport');
} }
}); });
var diagramXMLToUse = props.diagramXML || diagramXML
if (diagramXMLToUse) {
if (!diagramXML) {
setDiagramXML(diagramXMLToUse);
}
return displayDiagram(diagramModelerState, diagramXMLToUse);
}
if (!diagramXML) {
if (props.url) {
return fetchDiagramFromURL(props.url);
} else if (props.fileName) {
return fetchDiagramFromJsonAPI(props.process_group_id, props.process_model_id, props.fileName);
} else {
let newDiagramFileName = 'new_bpmn_diagram.bpmn';
if (props.diagramType === "dmn" ) {
newDiagramFileName = 'new_dmn_diagram.dmn';
}
return fetchDiagramFromURL(process.env.PUBLIC_URL + '/' + newDiagramFileName);
}
}
return () => {
diagramModelerState.destroy();
}
function fetchDiagramFromURL(url) {
fetch(url)
.then(response => response.text())
.then(text => setDiagramXML(text))
.catch(err => handleError(err));
}
function fetchDiagramFromJsonAPI(processGroupId, processModelId, fileName) {
fetch(`${BACKEND_BASE_URL}/process-models/${processGroupId}/${processModelId}/file/${fileName}`, {
headers: new Headers({
'Authorization': `Bearer ${HOT_AUTH_TOKEN}`
})
})
.then(response => response.json())
.then(response_json => setDiagramXML(response_json.file_contents))
.catch(err => handleError(err));
}
function handleError(err) {
const { onError } = props;
if (onError) {
onError(err);
}
}
function displayDiagram(diagramModelerToUse, diagramXMLToDisplay) { function displayDiagram(diagramModelerToUse, diagramXMLToDisplay) {
if (alreadyImportedXmlRef.current) { if (alreadyImportedXmlRef.current) {
return; return;
} }
diagramModelerToUse.importXML(diagramXMLToDisplay); diagramModelerToUse.importXML(diagramXMLToDisplay);
alreadyImportedXmlRef.current = true alreadyImportedXmlRef.current = true;
} }
}, [props, diagramXML, diagramModelerState, performingXmlUpdates]);
function fetchDiagramFromURL(urlToUse) {
fetch(urlToUse)
.then((response) => response.text())
.then((text) => setDiagramXMLString(text))
.catch((err) => handleError(err));
}
function fetchDiagramFromJsonAPI() {
fetch(
`${BACKEND_BASE_URL}/process-models/${processGroupId}/${processModelId}/file/${fileName}`,
{
headers: new Headers({
Authorization: `Bearer ${HOT_AUTH_TOKEN}`,
}),
}
)
.then((response) => response.json())
.then((responseJson) => setDiagramXMLString(responseJson.file_contents))
.catch((err) => handleError(err));
}
const diagramXMLToUse = diagramXML || diagramXMLString;
if (diagramXMLToUse) {
if (!diagramXMLString) {
setDiagramXMLString(diagramXMLToUse);
}
displayDiagram(diagramModelerState, diagramXMLToUse);
return undefined;
}
if (!diagramXMLString) {
if (url) {
fetchDiagramFromURL(url);
return undefined;
}
if (fileName) {
fetchDiagramFromJsonAPI();
return undefined;
}
let newDiagramFileName = 'new_bpmn_diagram.bpmn';
if (diagramType === 'dmn') {
newDiagramFileName = 'new_dmn_diagram.dmn';
}
fetchDiagramFromURL(`${process.env.PUBLIC_URL}/${newDiagramFileName}`);
return undefined;
}
return () => {
diagramModelerState.destroy();
};
}, [
diagramModelerState,
diagramType,
diagramXML,
diagramXMLString,
fileName,
performingXmlUpdates,
processGroupId,
processModelId,
url,
]);
function handleSave() { function handleSave() {
diagramModelerState.saveXML({ format: true }) diagramModelerState.saveXML({ format: true }).then((xmlObject) => {
.then(xmlObject => { saveDiagram(xmlObject.xml);
props.saveDiagram(xmlObject.xml); });
})
} }
return ( return (
<div> <div>
<Button onClick={handleSave} variant="danger">Save</Button> <Button onClick={handleSave} variant="danger">
Save
</Button>
</div> </div>
); );
} }
ReactDiagramEditor.propTypes = {
processModelId: PropTypes.string.isRequired,
processGroupId: PropTypes.string.isRequired,
saveDiagram: PropTypes.func.isRequired,
diagramType: PropTypes.string.isRequired,
diagramXML: PropTypes.string,
fileName: PropTypes.string,
onLaunchScriptEditor: PropTypes.func,
url: PropTypes.string,
};
ReactDiagramEditor.defaultProps = {
diagramXML: null,
fileName: null,
onLaunchScriptEditor: null,
url: null,
};

View File

@ -1,4 +1,4 @@
const reportWebVitals = onPerfEntry => { const reportWebVitals = (onPerfEntry) => {
if (onPerfEntry && onPerfEntry instanceof Function) { if (onPerfEntry && onPerfEntry instanceof Function) {
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getCLS(onPerfEntry); getCLS(onPerfEntry);

View File

@ -1,19 +1,21 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from 'react';
import { useParams, useNavigate } from "react-router-dom"; import { useParams, useNavigate } from 'react-router-dom';
import { BACKEND_BASE_URL } from '../config'; import { Button, Stack } from 'react-bootstrap';
import { HOT_AUTH_TOKEN, STANDARD_HEADERS } from '../config'; import { BACKEND_BASE_URL, HOT_AUTH_TOKEN, STANDARD_HEADERS } from '../config';
import ProcessBreadcrumb from '../components/ProcessBreadcrumb' import ProcessBreadcrumb from '../components/ProcessBreadcrumb';
import { Button, Stack } from 'react-bootstrap'
export default function ProcessGroupEdit() { export default function ProcessGroupEdit() {
const [displayName, setDisplayName] = useState(""); const [displayName, setDisplayName] = useState('');
const params = useParams(); const params = useParams();
const navigate = useNavigate(); const navigate = useNavigate();
const [processGroup, setProcessGroup] = useState(null); const [processGroup, setProcessGroup] = useState(null);
useEffect(() => { useEffect(() => {
fetch(`${BACKEND_BASE_URL}/process-groups/${params.process_group_id}`, STANDARD_HEADERS) fetch(
.then(res => res.json()) `${BACKEND_BASE_URL}/process-groups/${params.process_group_id}`,
STANDARD_HEADERS
)
.then((res) => res.json())
.then( .then(
(result) => { (result) => {
setProcessGroup(result); setProcessGroup(result);
@ -22,85 +24,84 @@ export default function ProcessGroupEdit() {
(error) => { (error) => {
console.log(error); console.log(error);
} }
) );
}, [params]); }, [params]);
const updateProcessGroup = ((event) => { const updateProcessGroup = (event) => {
event.preventDefault() event.preventDefault();
fetch(`${BACKEND_BASE_URL}/process-groups/${processGroup.id}`, { fetch(`${BACKEND_BASE_URL}/process-groups/${processGroup.id}`, {
headers: new Headers({ headers: new Headers({
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Authorization': `Bearer ${HOT_AUTH_TOKEN}` Authorization: `Bearer ${HOT_AUTH_TOKEN}`,
}), }),
method: 'PUT', method: 'PUT',
body: JSON.stringify({ body: JSON.stringify({
display_name: displayName, display_name: displayName,
id: processGroup.id, id: processGroup.id,
}), }),
}) }).then(
.then(res => res.json()) () => {
.then( navigate(`/process-groups/${processGroup.id}`);
(result) => { },
navigate(`/process-groups/${processGroup.id}`) // Note: it's important to handle errors here
}, // instead of a catch() block so that we don't swallow
// Note: it's important to handle errors here // exceptions from actual bugs in components.
// instead of a catch() block so that we don't swallow (newError) => {
// exceptions from actual bugs in components. console.log(newError);
(newError) => { }
console.log(newError); );
} };
)
}); const deleteProcessGroup = () => {
const deleteProcessGroup = (() => {
fetch(`${BACKEND_BASE_URL}/process-groups/${processGroup.id}`, { fetch(`${BACKEND_BASE_URL}/process-groups/${processGroup.id}`, {
headers: new Headers({ headers: new Headers({
'Authorization': `Bearer ${HOT_AUTH_TOKEN}` Authorization: `Bearer ${HOT_AUTH_TOKEN}`,
}), }),
method: 'DELETE', method: 'DELETE',
}) }).then(
.then(res => res.json()) () => {
.then( navigate(`/process-groups`);
(result) => { },
navigate(`/process-groups`); (error) => {
}, console.log(error);
(error) => { }
console.log(error); );
} };
)
});
const onDisplayNameChanged = ((newDisplayName) => { const onDisplayNameChanged = (newDisplayName) => {
setDisplayName(newDisplayName); setDisplayName(newDisplayName);
}); };
if (processGroup) { if (processGroup) {
return ( return (
<main style={{ padding: "1rem 0" }}> <main style={{ padding: '1rem 0' }}>
<ProcessBreadcrumb processGroupId={processGroup.id} /> <ProcessBreadcrumb processGroupId={processGroup.id} />
<h2>Edit Process Group: {processGroup.id}</h2> <h2>Edit Process Group: {processGroup.id}</h2>
<form onSubmit={updateProcessGroup}> <form onSubmit={updateProcessGroup}>
<label>Display Name:</label> <label>Display Name:</label>
<input <input
name='display_name' name="display_name"
type='text' type="text"
value={displayName} value={displayName}
onChange={e => onDisplayNameChanged(e.target.value)} onChange={(e) => onDisplayNameChanged(e.target.value)}
/> />
<br /> <br />
<br /> <br />
<Stack direction="horizontal" gap={3}> <Stack direction="horizontal" gap={3}>
<Button type="submit">Submit</Button> <Button type="submit">Submit</Button>
<Button variant="secondary" href={`/process-groups/${processGroup.id}`}>Cancel</Button> <Button
<Button onClick={deleteProcessGroup} variant="danger">Delete Process Group</Button> variant="secondary"
href={`/process-groups/${processGroup.id}`}
>
Cancel
</Button>
<Button onClick={deleteProcessGroup} variant="danger">
Delete Process Group
</Button>
</Stack> </Stack>
</form> </form>
</main> </main>
); );
} else {
return (<></>)
} }
} }

View File

@ -1,71 +1,70 @@
import React, { useState } from "react"; import React, { useState } from 'react';
import { useNavigate } from "react-router-dom"; import { useNavigate } from 'react-router-dom';
import { BACKEND_BASE_URL } from '../config'; import { BACKEND_BASE_URL, HOT_AUTH_TOKEN } from '../config';
import { HOT_AUTH_TOKEN } from '../config'; import ProcessBreadcrumb from '../components/ProcessBreadcrumb';
import ProcessBreadcrumb from '../components/ProcessBreadcrumb' import { slugifyString } from '../helpers';
import { slugifyString } from '../helpers'
export default function ProcessGroupNew() { export default function ProcessGroupNew() {
const [identifier, setIdentifier] = useState(""); const [identifier, setIdentifier] = useState('');
const [idHasBeenUpdatedByUser, setIdHasBeenUpdatedByUser] = useState(false); const [idHasBeenUpdatedByUser, setIdHasBeenUpdatedByUser] = useState(false);
const [displayName, setDisplayName] = useState(""); const [displayName, setDisplayName] = useState('');
const navigate = useNavigate(); const navigate = useNavigate();
const addProcessGroup = ((event) => { const addProcessGroup = (event) => {
event.preventDefault() event.preventDefault();
fetch(`${BACKEND_BASE_URL}/process-groups`, { fetch(`${BACKEND_BASE_URL}/process-groups`, {
headers: new Headers({ headers: new Headers({
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Authorization': `Bearer ${HOT_AUTH_TOKEN}` Authorization: `Bearer ${HOT_AUTH_TOKEN}`,
}), }),
method: 'POST', method: 'POST',
body: JSON.stringify({ body: JSON.stringify({
id: identifier, id: identifier,
display_name: displayName, display_name: displayName,
}), }),
}) }).then(
.then(res => res.json()) () => {
.then( navigate(`/process-groups/${identifier}`);
(result) => { },
navigate(`/process-groups/${identifier}`) // Note: it's important to handle errors here
}, // instead of a catch() block so that we don't swallow
// Note: it's important to handle errors here // exceptions from actual bugs in components.
// instead of a catch() block so that we don't swallow (newError) => {
// exceptions from actual bugs in components. console.log(newError);
(newError) => { }
console.log(newError); );
} };
)
}); const onDisplayNameChanged = (newDisplayName) => {
const onDisplayNameChanged = ((newDisplayName) => {
setDisplayName(newDisplayName); setDisplayName(newDisplayName);
if (!idHasBeenUpdatedByUser) { if (!idHasBeenUpdatedByUser) {
setIdentifier(slugifyString(newDisplayName)); setIdentifier(slugifyString(newDisplayName));
} }
}); };
return ( return (
<main style={{ padding: "1rem 0" }}> <main style={{ padding: '1rem 0' }}>
<ProcessBreadcrumb /> <ProcessBreadcrumb />
<h2>Add Process Group</h2> <h2>Add Process Group</h2>
<form onSubmit={addProcessGroup}> <form onSubmit={addProcessGroup}>
<label>Display Name:</label> <label>Display Name:</label>
<input <input
name='display_name' name="display_name"
type='text' type="text"
value={displayName} value={displayName}
onChange={e => onDisplayNameChanged(e.target.value)} onChange={(e) => onDisplayNameChanged(e.target.value)}
/> />
<br /> <br />
<label>ID:</label> <label>ID:</label>
<input <input
name='id' name="id"
type='text' type="text"
value={identifier} value={identifier}
onChange={e => { setIdentifier(e.target.value); setIdHasBeenUpdatedByUser(true)} } onChange={(e) => {
setIdentifier(e.target.value);
setIdHasBeenUpdatedByUser(true);
}}
/> />
<br /> <br />
<button type="submit">Submit</button> <button type="submit">Submit</button>

View File

@ -1,11 +1,12 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from 'react';
import { Link, useSearchParams } from "react-router-dom"; import { Link, useSearchParams, useParams } from 'react-router-dom';
import { BACKEND_BASE_URL } from '../config'; import { Button, Table, Stack } from 'react-bootstrap';
import { HOT_AUTH_TOKEN } from '../config'; import { BACKEND_BASE_URL, HOT_AUTH_TOKEN } from '../config';
import { useParams } from "react-router-dom"; import ProcessBreadcrumb from '../components/ProcessBreadcrumb';
import ProcessBreadcrumb from '../components/ProcessBreadcrumb' import PaginationForTable, {
import { Button, Table, Stack } from 'react-bootstrap' DEFAULT_PER_PAGE,
import PaginationForTable, { DEFAULT_PER_PAGE, DEFAULT_PAGE } from '../components/PaginationForTable' DEFAULT_PAGE,
} from '../components/PaginationForTable';
export default function ProcessGroupShow() { export default function ProcessGroupShow() {
const params = useParams(); const params = useParams();
@ -16,92 +17,107 @@ export default function ProcessGroupShow() {
const [pagination, setPagination] = useState(null); const [pagination, setPagination] = useState(null);
useEffect(() => { useEffect(() => {
const page = searchParams.get('page') || DEFAULT_PAGE; const page = parseInt(searchParams.get('page') || DEFAULT_PAGE, 10);
const perPage = parseInt(searchParams.get('per_page') || DEFAULT_PER_PAGE); const perPage = parseInt(
searchParams.get('per_page') || DEFAULT_PER_PAGE,
10
);
fetch(`${BACKEND_BASE_URL}/process-groups/${params.process_group_id}`, { fetch(`${BACKEND_BASE_URL}/process-groups/${params.process_group_id}`, {
headers: new Headers({ headers: new Headers({
'Authorization': `Bearer ${HOT_AUTH_TOKEN}` Authorization: `Bearer ${HOT_AUTH_TOKEN}`,
}) }),
}) })
.then(res => res.json()) .then((res) => res.json())
.then( .then(
(result) => { (processGroupResult) => {
setProcessGroup(result); setProcessGroup(processGroupResult);
fetch(`${BACKEND_BASE_URL}/process-groups/${params.process_group_id}/process-models?per_page=${perPage}&page=${page}`, { fetch(
headers: new Headers({ `${BACKEND_BASE_URL}/process-groups/${params.process_group_id}/process-models?per_page=${perPage}&page=${page}`,
'Authorization': `Bearer ${HOT_AUTH_TOKEN}` {
}) headers: new Headers({
}) Authorization: `Bearer ${HOT_AUTH_TOKEN}`,
.then(res => res.json()) }),
}
)
.then((res) => res.json())
.then( .then(
(result) => { (processModelResult) => {
setProcessModels(result.results); setProcessModels(processModelResult.results);
setPagination(result.pagination); setPagination(processModelResult.pagination);
}, },
(error) => { (error) => {
console.log(error); console.log(error);
}) }
);
}, },
(error) => { (error) => {
console.log(error); console.log(error);
} }
) );
}, [params, searchParams]); }, [params, searchParams]);
const buildTable = (() => { const buildTable = () => {
const rows = processModels.map((row,index) => { const rows = processModels.map((row) => {
return ( return (
<tr key={index}> <tr key={row.id}>
<td> <td>
<Link to={`/process-models/${processGroup.id}/${row.id}`}>{row.id}</Link> <Link to={`/process-models/${processGroup.id}/${row.id}`}>
{row.id}
</Link>
</td> </td>
<td>{row.display_name}</td> <td>{row.display_name}</td>
</tr> </tr>
) );
}) });
return( return (
<div> <div>
<h3>Process Models</h3> <h3>Process Models</h3>
<Table striped bordered > <Table striped bordered>
<thead> <thead>
<tr> <tr>
<th>Process Model Id</th> <th>Process Model Id</th>
<th>Display Name</th> <th>Display Name</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>{rows}</tbody>
{rows} </Table>
</tbody>
</Table>
</div> </div>
) );
}); };
if (processGroup && pagination) { if (processGroup && pagination) {
const perPage = parseInt(searchParams.get('per_page') || DEFAULT_PER_PAGE); const perPage = parseInt(
const page = parseInt(searchParams.get('page') || DEFAULT_PAGE); searchParams.get('per_page') || DEFAULT_PER_PAGE,
10
);
const page = parseInt(searchParams.get('page') || DEFAULT_PAGE, 10);
return ( return (
<main style={{ padding: "1rem 0" }}> <main style={{ padding: '1rem 0' }}>
<ProcessBreadcrumb processGroupId={processGroup.id} /> <ProcessBreadcrumb processGroupId={processGroup.id} />
<h2>Process Group: {processGroup.id}</h2> <h2>Process Group: {processGroup.id}</h2>
<ul> <ul>
<Stack direction="horizontal" gap={3}> <Stack direction="horizontal" gap={3}>
<Button href={`/process-models/${processGroup.id}/new`}>Add a process model</Button> <Button href={`/process-models/${processGroup.id}/new`}>
<Button href={`/process-groups/${processGroup.id}/edit`} variant="secondary">Edit process group</Button> Add a process model
</Stack> </Button>
<br /> <Button
<br /> href={`/process-groups/${processGroup.id}/edit`}
<PaginationForTable variant="secondary"
page={page} >
perPage={perPage} Edit process group
pagination={pagination} </Button>
tableToDisplay={buildTable()} </Stack>
path={`/process-groups/${processGroup.id}`} <br />
/> <br />
<PaginationForTable
page={page}
perPage={perPage}
pagination={pagination}
tableToDisplay={buildTable()}
path={`/process-groups/${processGroup.id}`}
/>
</ul> </ul>
</main> </main>
); );
} else {
return (<></>)
} }
} }

View File

@ -1,10 +1,12 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from 'react';
import { Link, useSearchParams } from "react-router-dom"; import { Link, useSearchParams } from 'react-router-dom';
import { BACKEND_BASE_URL } from '../config'; import { Button, Table } from 'react-bootstrap';
import { HOT_AUTH_TOKEN } from '../config'; import { BACKEND_BASE_URL, HOT_AUTH_TOKEN } from '../config';
import ProcessBreadcrumb from '../components/ProcessBreadcrumb' import ProcessBreadcrumb from '../components/ProcessBreadcrumb';
import { Button, Table } from 'react-bootstrap' import PaginationForTable, {
import PaginationForTable, { DEFAULT_PER_PAGE, DEFAULT_PAGE } from '../components/PaginationForTable' DEFAULT_PER_PAGE,
DEFAULT_PAGE,
} from '../components/PaginationForTable';
// Example process group json // Example process group json
// {'admin': False, 'display_name': 'Test Workflows', 'display_order': 0, 'id': 'test_process_group'} // {'admin': False, 'display_name': 'Test Workflows', 'display_order': 0, 'id': 'test_process_group'}
@ -15,14 +17,20 @@ export default function ProcessGroups() {
const [pagination, setPagination] = useState(null); const [pagination, setPagination] = useState(null);
useEffect(() => { useEffect(() => {
const page = searchParams.get('page') || DEFAULT_PAGE; const page = parseInt(searchParams.get('page') || DEFAULT_PAGE, 10);
const perPage = parseInt(searchParams.get('per_page') || DEFAULT_PER_PAGE); const perPage = parseInt(
fetch(`${BACKEND_BASE_URL}/process-groups?per_page=${perPage}&page=${page}`, { searchParams.get('per_page') || DEFAULT_PER_PAGE,
headers: new Headers({ 10
'Authorization': `Bearer ${HOT_AUTH_TOKEN}` );
}) fetch(
}) `${BACKEND_BASE_URL}/process-groups?per_page=${perPage}&page=${page}`,
.then(res => res.json()) {
headers: new Headers({
Authorization: `Bearer ${HOT_AUTH_TOKEN}`,
}),
}
)
.then((res) => res.json())
.then( .then(
(result) => { (result) => {
setProcessGroups(result.results); setProcessGroups(result.results);
@ -31,43 +39,44 @@ export default function ProcessGroups() {
(newError) => { (newError) => {
console.log(newError); console.log(newError);
} }
) );
}, [searchParams]); }, [searchParams]);
const buildTable = (() => { const buildTable = () => {
const rows = processGroups.map((row,index) => { const rows = processGroups.map((row) => {
return ( return (
<tr key={index}> <tr key={row.id}>
<td> <td>
<Link to={`/process-groups/${row.id}`}>{row.id}</Link> <Link to={`/process-groups/${row.id}`}>{row.id}</Link>
</td> </td>
<td>{row.display_name}</td> <td>{row.display_name}</td>
</tr> </tr>
) );
}) });
return( return (
<Table striped bordered > <Table striped bordered>
<thead> <thead>
<tr> <tr>
<th>Process Group Id</th> <th>Process Group Id</th>
<th>Display Name</th> <th>Display Name</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>{rows}</tbody>
{rows}
</tbody>
</Table> </Table>
) );
}); };
if (processGroups?.length > 0) { if (processGroups?.length > 0) {
const perPage = parseInt(searchParams.get('per_page') || DEFAULT_PER_PAGE); const perPage = parseInt(
const page = parseInt(searchParams.get('page') || DEFAULT_PAGE); searchParams.get('per_page') || DEFAULT_PER_PAGE,
10
);
const page = parseInt(searchParams.get('page') || DEFAULT_PAGE, 10);
return ( return (
<main style={{ padding: "1rem 0" }}> <main style={{ padding: '1rem 0' }}>
<ProcessBreadcrumb /> <ProcessBreadcrumb />
<h2>Process Groups</h2> <h2>Process Groups</h2>
<Button href={`/process-groups/new`}>Add a process group</Button> <Button href="/process-groups/new">Add a process group</Button>
<br /> <br />
<br /> <br />
<PaginationForTable <PaginationForTable
@ -75,11 +84,9 @@ export default function ProcessGroups() {
perPage={perPage} perPage={perPage}
pagination={pagination} pagination={pagination}
tableToDisplay={buildTable()} tableToDisplay={buildTable()}
path={`/process-groups`} path="/process-groups"
/> />
</main> </main>
); );
} else {
return (<></>)
} }
} }

View File

@ -1,16 +1,28 @@
import React, { useEffect, useMemo, useState } from "react"; import React, { useEffect, useMemo, useState } from 'react';
import { Link, useNavigate, useParams, useSearchParams } from "react-router-dom"; import {
Link,
useNavigate,
useParams,
useSearchParams,
} from 'react-router-dom';
import { BACKEND_BASE_URL, PROCESS_STATUSES, DATE_FORMAT } from '../config'; import { Button, Table, Stack, Dropdown } from 'react-bootstrap';
import { HOT_AUTH_TOKEN } from '../config'; import DatePicker from 'react-datepicker';
import { format } from 'date-fns';
import {
BACKEND_BASE_URL,
PROCESS_STATUSES,
DATE_FORMAT,
HOT_AUTH_TOKEN,
} from '../config';
import ProcessBreadcrumb from '../components/ProcessBreadcrumb'; import ProcessBreadcrumb from '../components/ProcessBreadcrumb';
import { Button, Table, Stack, Dropdown } from 'react-bootstrap'; import { convertDateToSeconds } from '../helpers';
import DatePicker from "react-datepicker";
import { convertDateToSeconds } from "../helpers";
import PaginationForTable, { DEFAULT_PER_PAGE, DEFAULT_PAGE } from '../components/PaginationForTable' import PaginationForTable, {
import "react-datepicker/dist/react-datepicker.css"; DEFAULT_PER_PAGE,
import { format } from "date-fns"; DEFAULT_PAGE,
} from '../components/PaginationForTable';
import 'react-datepicker/dist/react-datepicker.css';
export default function ProcessInstanceList() { export default function ProcessInstanceList() {
const params = useParams(); const params = useParams();
@ -22,56 +34,71 @@ export default function ProcessInstanceList() {
const oneHourInSeconds = 3600; const oneHourInSeconds = 3600;
const oneMonthInSeconds = oneHourInSeconds * 24 * 30; const oneMonthInSeconds = oneHourInSeconds * 24 * 30;
const [startFrom, setStartFrom] = useState(convertDateToSeconds(new Date()) - oneMonthInSeconds); const [startFrom, setStartFrom] = useState(
const [startTill, setStartTill] = useState(convertDateToSeconds(new Date()) + oneHourInSeconds); convertDateToSeconds(new Date()) - oneMonthInSeconds
const [endFrom, setEndFrom] = useState(convertDateToSeconds(new Date()) - oneMonthInSeconds); );
const [endTill, setEndTill] = useState(convertDateToSeconds(new Date()) + oneHourInSeconds); const [startTill, setStartTill] = useState(
convertDateToSeconds(new Date()) + oneHourInSeconds
);
const [endFrom, setEndFrom] = useState(
convertDateToSeconds(new Date()) - oneMonthInSeconds
);
const [endTill, setEndTill] = useState(
convertDateToSeconds(new Date()) + oneHourInSeconds
);
const [processStatus, setProcessStatus] = useState(PROCESS_STATUSES[0]); const [processStatus, setProcessStatus] = useState(PROCESS_STATUSES[0]);
const parametersToAlwaysFilterBy = useMemo(() => { const parametersToAlwaysFilterBy = useMemo(() => {
return { return {
'start_from': setStartFrom, start_from: setStartFrom,
'start_till': setStartTill, start_till: setStartTill,
'end_from': setEndFrom, end_from: setEndFrom,
'end_till': setEndTill, end_till: setEndTill,
} };
}, [setStartFrom, setStartTill, setEndFrom, setEndTill]); }, [setStartFrom, setStartTill, setEndFrom, setEndTill]);
useEffect(() => { useEffect(() => {
getProcessInstances();
function getProcessInstances() { function getProcessInstances() {
const page = searchParams.get('page') || DEFAULT_PAGE; const page = searchParams.get('page') || DEFAULT_PAGE;
const perPage = parseInt(searchParams.get('per_page') || DEFAULT_PER_PAGE); const perPage = parseInt(
searchParams.get('per_page') || DEFAULT_PER_PAGE,
10
);
let queryParamString = `per_page=${perPage}&page=${page}`; let queryParamString = `per_page=${perPage}&page=${page}`;
for (const paramName in parametersToAlwaysFilterBy) { Object.keys(parametersToAlwaysFilterBy).forEach((paramName) => {
const functionToCall = parametersToAlwaysFilterBy[paramName] const functionToCall = parametersToAlwaysFilterBy[paramName];
let defaultValue = null; let defaultValue = null;
if (paramName.endsWith("_from")) { if (paramName.endsWith('_from')) {
defaultValue = convertDateToSeconds(new Date()) - oneMonthInSeconds; defaultValue = convertDateToSeconds(new Date()) - oneMonthInSeconds;
} else if (paramName.endsWith("_till")) { } else if (paramName.endsWith('_till')) {
defaultValue = convertDateToSeconds(new Date()) + oneHourInSeconds; defaultValue = convertDateToSeconds(new Date()) + oneHourInSeconds;
} }
let searchParamValue = searchParams.get(paramName); const searchParamValue = searchParams.get(paramName);
if (searchParamValue) { if (searchParamValue) {
queryParamString += `&${paramName}=${searchParamValue}`; queryParamString += `&${paramName}=${searchParamValue}`;
functionToCall(searchParamValue); functionToCall(searchParamValue);
} else if (defaultValue) { } else if (defaultValue) {
queryParamString += `&${paramName}=${defaultValue}`; queryParamString += `&${paramName}=${defaultValue}`;
} }
} });
if (searchParams.get('process_status')) { if (searchParams.get('process_status')) {
queryParamString += `&process_status=${searchParams.get('process_status')}`; queryParamString += `&process_status=${searchParams.get(
'process_status'
)}`;
setProcessStatus(searchParams.get('process_status')); setProcessStatus(searchParams.get('process_status'));
} }
fetch(`${BACKEND_BASE_URL}/process-models/${params.process_group_id}/${params.process_model_id}/process-instances?${queryParamString}`, { fetch(
headers: new Headers({ `${BACKEND_BASE_URL}/process-models/${params.process_group_id}/${params.process_model_id}/process-instances?${queryParamString}`,
'Authorization': `Bearer ${HOT_AUTH_TOKEN}` {
}) headers: new Headers({
}) Authorization: `Bearer ${HOT_AUTH_TOKEN}`,
.then(res => res.json()) }),
}
)
.then((res) => res.json())
.then( .then(
(result) => { (result) => {
const processInstancesFromApi = result.results; const processInstancesFromApi = result.results;
@ -81,14 +108,25 @@ export default function ProcessInstanceList() {
(error) => { (error) => {
console.log(error); console.log(error);
} }
) );
} }
}, [searchParams, params, oneMonthInSeconds, oneHourInSeconds, parametersToAlwaysFilterBy]);
const handleFilter = ((event) => { getProcessInstances();
}, [
searchParams,
params,
oneMonthInSeconds,
oneHourInSeconds,
parametersToAlwaysFilterBy,
]);
const handleFilter = (event) => {
event.preventDefault(); event.preventDefault();
const page = searchParams.get('page') || DEFAULT_PAGE; const page = searchParams.get('page') || DEFAULT_PAGE;
const perPage = parseInt(searchParams.get('per_page') || DEFAULT_PER_PAGE); const perPage = parseInt(
searchParams.get('per_page') || DEFAULT_PER_PAGE,
10
);
let queryParamString = `per_page=${perPage}&page=${page}`; let queryParamString = `per_page=${perPage}&page=${page}`;
if (startFrom) { if (startFrom) {
@ -103,14 +141,16 @@ export default function ProcessInstanceList() {
if (endTill) { if (endTill) {
queryParamString += `&end_till=${endTill}`; queryParamString += `&end_till=${endTill}`;
} }
if (processStatus && processStatus !== "all") { if (processStatus && processStatus !== 'all') {
queryParamString += `&process_status=${processStatus}`; queryParamString += `&process_status=${processStatus}`;
} }
navigate(`/process-models/${params.process_group_id}/${params.process_model_id}/process-instances?${queryParamString}`) navigate(
}); `/process-models/${params.process_group_id}/${params.process_model_id}/process-instances?${queryParamString}`
);
};
const dateComponent = ((labelString, name, initialDate, onChangeFunction) => { const dateComponent = (labelString, name, initialDate, onChangeFunction) => {
return ( return (
<Stack className="ms-auto" direction="horizontal" gap={3}> <Stack className="ms-auto" direction="horizontal" gap={3}>
<label className="text-nowrap">{labelString}</label> <label className="text-nowrap">{labelString}</label>
@ -122,27 +162,37 @@ export default function ProcessInstanceList() {
dateFormat={DATE_FORMAT} dateFormat={DATE_FORMAT}
/> />
</Stack> </Stack>
) );
}); };
const getSearchParamsAsQueryString = (() => { const getSearchParamsAsQueryString = () => {
let queryParamString = ""; let queryParamString = '';
for (const paramName in parametersToAlwaysFilterBy) { Object.keys(parametersToAlwaysFilterBy).forEach((paramName) => {
let searchParamValue = searchParams.get(paramName); const searchParamValue = searchParams.get(paramName);
if (searchParamValue) { if (searchParamValue) {
queryParamString += `&${paramName}=${searchParamValue}`; queryParamString += `&${paramName}=${searchParamValue}`;
} }
} });
return queryParamString; return queryParamString;
}); };
const filterOptions = (() => { const filterOptions = () => {
const processStatusesRows = PROCESS_STATUSES.map(processStatusOption => { const processStatusesRows = PROCESS_STATUSES.map((processStatusOption) => {
if (processStatusOption === processStatus) { if (processStatusOption === processStatus) {
return <Dropdown.Item key={processStatusOption} active>{processStatusOption}</Dropdown.Item> return (
} else { <Dropdown.Item key={processStatusOption} active>
return <Dropdown.Item key={processStatusOption} onClick={(e) => setProcessStatus(processStatusOption)}>{processStatusOption}</Dropdown.Item> {processStatusOption}
</Dropdown.Item>
);
} }
return (
<Dropdown.Item
key={processStatusOption}
onClick={() => setProcessStatus(processStatusOption)}
>
{processStatusOption}
</Dropdown.Item>
);
}); });
return ( return (
@ -151,17 +201,25 @@ export default function ProcessInstanceList() {
<div className="col"> <div className="col">
<form onSubmit={handleFilter}> <form onSubmit={handleFilter}>
<Stack direction="horizontal" gap={3}> <Stack direction="horizontal" gap={3}>
{dateComponent("Start Range: ", "start-from", startFrom, setStartFrom)} {dateComponent(
{dateComponent("-", "start-till", startTill, setStartTill)} 'Start Range: ',
'start-from',
startFrom,
setStartFrom
)}
{dateComponent('-', 'start-till', startTill, setStartTill)}
</Stack> </Stack>
<br /> <br />
<Stack direction="horizontal" gap={3}> <Stack direction="horizontal" gap={3}>
{dateComponent("End Range: ", "end-from", endFrom, setEndFrom)} {dateComponent('End Range: ', 'end-from', endFrom, setEndFrom)}
{dateComponent("-", "end-till", endTill, setEndTill)} {dateComponent('-', 'end-till', endTill, setEndTill)}
</Stack> </Stack>
<br /> <br />
<Stack direction="horizontal" gap={3}> <Stack direction="horizontal" gap={3}>
<Dropdown data-qa="process-status-dropdown" id="process-status-dropdown"> <Dropdown
data-qa="process-status-dropdown"
id="process-status-dropdown"
>
<Dropdown.Toggle id="process-status" variant="light border"> <Dropdown.Toggle id="process-status" variant="light border">
Process Statuses: {processStatus} Process Statuses: {processStatus}
</Dropdown.Toggle> </Dropdown.Toggle>
@ -170,42 +228,48 @@ export default function ProcessInstanceList() {
{processStatusesRows} {processStatusesRows}
</Dropdown.Menu> </Dropdown.Menu>
</Dropdown> </Dropdown>
<Button className="ms-auto" variant="secondary" type="submit">Filter</Button> <Button className="ms-auto" variant="secondary" type="submit">
Filter
</Button>
</Stack> </Stack>
</form> </form>
</div> </div>
<div className="col"> <div className="col" />
</div>
</div> </div>
</div> </div>
) );
}); };
const buildTable = (() => { const buildTable = () => {
const rows = processInstances.map((row,i) => { const rows = processInstances.map((row) => {
let start_date = 'N/A' let startDate = 'N/A';
if (row.start_in_seconds) { if (row.start_in_seconds) {
start_date = new Date(row.start_in_seconds * 1000); startDate = new Date(row.start_in_seconds * 1000);
} }
let end_date = 'N/A' let endDate = 'N/A';
if (row.end_in_seconds) { if (row.end_in_seconds) {
end_date = new Date(row.end_in_seconds * 1000); endDate = new Date(row.end_in_seconds * 1000);
} }
return ( return (
<tr key={i}> <tr key={row.id}>
<td> <td>
<Link data-qa="process-instance-show-link" to={`/process-models/${params.process_group_id}/${params.process_model_id}/process-instances/${row.id}`}>{row.id}</Link> <Link
data-qa="process-instance-show-link"
to={`/process-models/${params.process_group_id}/${params.process_model_id}/process-instances/${row.id}`}
>
{row.id}
</Link>
</td> </td>
<td>{row.process_model_identifier}</td> <td>{row.process_model_identifier}</td>
<td>{row.process_group_id}</td> <td>{row.process_group_id}</td>
<td>{format(start_date, DATE_FORMAT)}</td> <td>{format(startDate, DATE_FORMAT)}</td>
<td>{format(end_date, DATE_FORMAT)}</td> <td>{format(endDate, DATE_FORMAT)}</td>
<td data-qa="process-instance-status">{row.status}</td> <td data-qa="process-instance-status">{row.status}</td>
</tr> </tr>
) );
}) });
return( return (
<Table striped bordered > <Table striped bordered>
<thead> <thead>
<tr> <tr>
<th>Process Instance Id</th> <th>Process Instance Id</th>
@ -216,36 +280,35 @@ export default function ProcessInstanceList() {
<th>Status</th> <th>Status</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>{rows}</tbody>
{rows}
</tbody>
</Table> </Table>
) );
}); };
if (pagination) { if (pagination) {
const perPage = parseInt(searchParams.get('per_page') || DEFAULT_PER_PAGE); const perPage = parseInt(
const page = parseInt(searchParams.get('page') || DEFAULT_PAGE); searchParams.get('per_page') || DEFAULT_PER_PAGE,
return( 10
);
const page = parseInt(searchParams.get('page') || DEFAULT_PAGE, 10);
return (
<main> <main>
<ProcessBreadcrumb <ProcessBreadcrumb
processModelId={params.process_model_id} processModelId={params.process_model_id}
processGroupId={params.process_group_id} processGroupId={params.process_group_id}
linkProcessModel="true" linkProcessModel="true"
/> />
<h2>Process Instances for {params.process_model_id}</h2> <h2>Process Instances for {params.process_model_id}</h2>
{filterOptions()} {filterOptions()}
<PaginationForTable <PaginationForTable
page={page} page={page}
perPage={perPage} perPage={perPage}
pagination={pagination} pagination={pagination}
tableToDisplay={buildTable()} tableToDisplay={buildTable()}
queryParamString={getSearchParamsAsQueryString()} queryParamString={getSearchParamsAsQueryString()}
path={`/process-models/${params.process_group_id}/${params.process_model_id}/process-instances`} path={`/process-models/${params.process_group_id}/${params.process_model_id}/process-instances`}
/> />
</main> </main>
) );
} else {
return(<></>)
} }
} }

View File

@ -1,64 +1,72 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from 'react';
import { useParams, useSearchParams } from "react-router-dom"; import { useParams, useSearchParams } from 'react-router-dom';
import { BACKEND_BASE_URL } from '../config'; import { Table } from 'react-bootstrap';
import { HOT_AUTH_TOKEN } from '../config'; import { BACKEND_BASE_URL, HOT_AUTH_TOKEN } from '../config';
import ProcessBreadcrumb from '../components/ProcessBreadcrumb' import ProcessBreadcrumb from '../components/ProcessBreadcrumb';
import { Table } from 'react-bootstrap'
import PaginationForTable, { DEFAULT_PER_PAGE, DEFAULT_PAGE } from '../components/PaginationForTable' import PaginationForTable, {
DEFAULT_PER_PAGE,
DEFAULT_PAGE,
} from '../components/PaginationForTable';
export default function ProcessInstanceReport() { export default function ProcessInstanceReport() {
let params = useParams(); const params = useParams();
let [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const [processInstances, setProcessInstances] = useState([]); const [processInstances, setProcessInstances] = useState([]);
const [pagination, setPagination] = useState(null); const [pagination, setPagination] = useState(null);
const [processGroupId, setProcessGroupId] = useState(null); const [processGroupId, setProcessGroupId] = useState(null);
useEffect(() => { useEffect(() => {
getProcessInstances();
function getProcessInstances() { function getProcessInstances() {
const page = searchParams.get('page') || DEFAULT_PAGE; const page = searchParams.get('page') || DEFAULT_PAGE;
const perPage = parseInt(searchParams.get('per_page') || DEFAULT_PER_PAGE); const perPage = parseInt(
fetch(`${BACKEND_BASE_URL}/process-models/${params.process_group_id}/${params.process_model_id}/process-instances/report?per_page=${perPage}&page=${page}`, { searchParams.get('per_page') || DEFAULT_PER_PAGE,
headers: new Headers({ 10
'Authorization': `Bearer ${HOT_AUTH_TOKEN}` );
}) fetch(
}) `${BACKEND_BASE_URL}/process-models/${params.process_group_id}/${params.process_model_id}/process-instances/report?per_page=${perPage}&page=${page}`,
.then(res => res.json()) {
headers: new Headers({
Authorization: `Bearer ${HOT_AUTH_TOKEN}`,
}),
}
)
.then((res) => res.json())
.then( .then(
(result) => { (result) => {
const processInstancesFromApi = result.results; const processInstancesFromApi = result.results;
setProcessInstances(processInstancesFromApi); setProcessInstances(processInstancesFromApi);
setPagination(result.pagination); setPagination(result.pagination);
if (processInstancesFromApi[0]) { if (processInstancesFromApi[0]) {
setProcessGroupId(processInstancesFromApi[0].process_group_id) setProcessGroupId(processInstancesFromApi[0].process_group_id);
} }
}, },
(error) => { (error) => {
console.log(error); console.log(error);
} }
) );
} }
getProcessInstances();
}, [searchParams, params]); }, [searchParams, params]);
const buildTable = (() => { const buildTable = () => {
const rows = processInstances.map((row,i) => { const rows = processInstances.map((row) => {
return ( return (
<tr key={i}> <tr key={row.id}>
<td>{row.id}</td> <td>{row.id}</td>
<td>{row.data.month}</td> <td>{row.data.month}</td>
<td>{row.data.milestone}</td> <td>{row.data.milestone}</td>
<td>{row.data.req_id}</td> <td>{row.data.req_id}</td>
<td>{row.data.feature}</td> <td>{row.data.feature}</td>
<td>{row.data.priority}</td> <td>{row.data.priority}</td>
</tr> </tr>
) );
}) });
return( return (
<Table striped bordered > <Table striped bordered>
<thead> <thead>
<tr> <tr>
<th>db id</th> <th>db id</th>
@ -69,34 +77,33 @@ export default function ProcessInstanceReport() {
<th>priority</th> <th>priority</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>{rows}</tbody>
{rows}
</tbody>
</Table> </Table>
) );
}); };
if (pagination) { if (pagination) {
const perPage = parseInt(searchParams.get('per_page') || DEFAULT_PER_PAGE); const perPage = parseInt(
const page = parseInt(searchParams.get('page') || DEFAULT_PAGE); searchParams.get('per_page') || DEFAULT_PER_PAGE,
return( 10
);
const page = parseInt(searchParams.get('page') || DEFAULT_PAGE, 10);
return (
<main> <main>
<ProcessBreadcrumb <ProcessBreadcrumb
processModelId={params.process_model_id} processModelId={params.process_model_id}
processGroupId={processGroupId} processGroupId={processGroupId}
linkProcessModel="true" linkProcessModel="true"
/> />
<h2>Process Instances for {params.process_model_id}</h2> <h2>Process Instances for {params.process_model_id}</h2>
<PaginationForTable <PaginationForTable
page={page} page={page}
perPage={perPage} perPage={perPage}
pagination={pagination} pagination={pagination}
tableToDisplay={buildTable()} tableToDisplay={buildTable()}
path={`/process-models/${params.process_group_id}/${params.process_model_id}/process-instances/report`} path={`/process-models/${params.process_group_id}/${params.process_model_id}/process-instances/report`}
/> />
</main> </main>
) );
} else {
return(<></>)
} }
} }

View File

@ -1,42 +1,46 @@
import React, { useEffect } from "react"; import React, { useEffect } from 'react';
import { useParams, useNavigate } from "react-router-dom"; import { useParams, useNavigate } from 'react-router-dom';
import { Button } from 'react-bootstrap';
import { BACKEND_BASE_URL, HOT_AUTH_TOKEN } from '../config'; import { BACKEND_BASE_URL, HOT_AUTH_TOKEN } from '../config';
import { Button } from 'react-bootstrap' import ProcessBreadcrumb from '../components/ProcessBreadcrumb';
import ProcessBreadcrumb from '../components/ProcessBreadcrumb'
export default function ProcessInstanceShow(props) { export default function ProcessInstanceShow() {
const navigate = useNavigate(); const navigate = useNavigate();
const params = useParams(); const params = useParams();
useEffect(() => { useEffect(() => {}, []);
}, []);
const deleteProcessInstance = (() => { const deleteProcessInstance = () => {
fetch(`${BACKEND_BASE_URL}/process-models/${params.process_group_id}/${params.process_model_id}/process-instances/${params.process_instance_id}`, { fetch(
headers: new Headers({ `${BACKEND_BASE_URL}/process-models/${params.process_group_id}/${params.process_model_id}/process-instances/${params.process_instance_id}`,
'Authorization': `Bearer ${HOT_AUTH_TOKEN}` {
}), headers: new Headers({
method: 'DELETE', Authorization: `Bearer ${HOT_AUTH_TOKEN}`,
}) }),
.then(res => res.json()) method: 'DELETE',
.then( }
(result) => { ).then(
navigate(`/process-models/${params.process_group_id}/${params.process_model_id}/process-instances`); () => {
}, navigate(
(error) => { `/process-models/${params.process_group_id}/${params.process_model_id}/process-instances`
console.log(error); );
} },
) (error) => {
}); console.log(error);
}
);
};
return ( return (
<main style={{ padding: "1rem 0" }}> <main style={{ padding: '1rem 0' }}>
<ProcessBreadcrumb <ProcessBreadcrumb
processModelId={params.process_model_id} processModelId={params.process_model_id}
processGroupId={params.process_group_id} processGroupId={params.process_group_id}
linkProcessModel="true" linkProcessModel="true"
/> />
<Button onClick={deleteProcessInstance} variant="danger">Delete process instance</Button> <Button onClick={deleteProcessInstance} variant="danger">
Delete process instance
</Button>
</main> </main>
) );
} }

View File

@ -1,21 +1,20 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from 'react';
import { useParams, useNavigate } from "react-router-dom"; import { useParams, useNavigate } from 'react-router-dom';
import { BACKEND_BASE_URL } from '../config'; import { Button, Stack } from 'react-bootstrap';
import { HOT_AUTH_TOKEN, STANDARD_HEADERS } from '../config'; import { BACKEND_BASE_URL, HOT_AUTH_TOKEN, STANDARD_HEADERS } from '../config';
import ProcessBreadcrumb from '../components/ProcessBreadcrumb' import ProcessBreadcrumb from '../components/ProcessBreadcrumb';
import { Button, Stack } from 'react-bootstrap'
export default function ProcessModelEdit() { export default function ProcessModelEdit() {
const [displayName, setDisplayName] = useState(""); const [displayName, setDisplayName] = useState('');
const params = useParams(); const params = useParams();
const navigate = useNavigate(); const navigate = useNavigate();
const [processModel, setProcessModel] = useState(null); const [processModel, setProcessModel] = useState(null);
const processModelPath = `process-models/${params.process_group_id}/${params.process_model_id}` const processModelPath = `process-models/${params.process_group_id}/${params.process_model_id}`;
useEffect(() => { useEffect(() => {
fetch(`${BACKEND_BASE_URL}/${processModelPath}`, STANDARD_HEADERS) fetch(`${BACKEND_BASE_URL}/${processModelPath}`, STANDARD_HEADERS)
.then(res => res.json()) .then((res) => res.json())
.then( .then(
(result) => { (result) => {
setProcessModel(result); setProcessModel(result);
@ -24,16 +23,16 @@ export default function ProcessModelEdit() {
(error) => { (error) => {
console.log(error); console.log(error);
} }
) );
}, [processModelPath]); }, [processModelPath]);
const updateProcessModel = ((event) => { const updateProcessModel = (event) => {
event.preventDefault() event.preventDefault();
fetch(`${BACKEND_BASE_URL}/${processModelPath}`, { fetch(`${BACKEND_BASE_URL}/${processModelPath}`, {
headers: new Headers({ headers: new Headers({
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Authorization': `Bearer ${HOT_AUTH_TOKEN}` Authorization: `Bearer ${HOT_AUTH_TOKEN}`,
}), }),
method: 'PUT', method: 'PUT',
body: JSON.stringify({ body: JSON.stringify({
@ -45,69 +44,68 @@ export default function ProcessModelEdit() {
standalone: processModel.standalone, standalone: processModel.standalone,
library: processModel.library, library: processModel.library,
}), }),
}) }).then(
.then(res => res.json()) () => {
.then( navigate(`/${processModelPath}`);
(result) => { },
navigate(`/${processModelPath}`) // Note: it's important to handle errors here
}, // instead of a catch() block so that we don't swallow
// Note: it's important to handle errors here // exceptions from actual bugs in components.
// instead of a catch() block so that we don't swallow (newError) => {
// exceptions from actual bugs in components. console.log(newError);
(newError) => { }
console.log(newError); );
} };
)
}); const deleteProcessModel = () => {
fetch(
`${BACKEND_BASE_URL}/process-models/${processModel.process_group_id}/${processModel.id}`,
{
headers: new Headers({
Authorization: `Bearer ${HOT_AUTH_TOKEN}`,
}),
method: 'DELETE',
}
).then(
() => {
navigate(`/process-groups/${params.process_group_id}`);
},
(error) => {
console.log(error);
}
);
};
const deleteProcessModel = (() => { const onDisplayNameChanged = (newDisplayName) => {
fetch(`${BACKEND_BASE_URL}/process-models/${processModel.process_group_id}/${processModel.id}`, {
headers: new Headers({
'Authorization': `Bearer ${HOT_AUTH_TOKEN}`
}),
method: 'DELETE',
})
.then(res => res.json())
.then(
(result) => {
navigate(`/process-groups/${params.process_group_id}`);
},
(error) => {
console.log(error);
}
)
});
const onDisplayNameChanged = ((newDisplayName) => {
setDisplayName(newDisplayName); setDisplayName(newDisplayName);
}); };
if (processModel) { if (processModel) {
return ( return (
<main style={{ padding: "1rem 0" }}> <main style={{ padding: '1rem 0' }}>
<ProcessBreadcrumb processGroupId={processModel.id} /> <ProcessBreadcrumb processGroupId={processModel.id} />
<h2>Edit Process Group: {processModel.id}</h2> <h2>Edit Process Group: {processModel.id}</h2>
<form onSubmit={updateProcessModel}> <form onSubmit={updateProcessModel}>
<label>Display Name:</label> <label>Display Name:</label>
<input <input
name='display_name' name="display_name"
type='text' type="text"
value={displayName} value={displayName}
onChange={e => onDisplayNameChanged(e.target.value)} onChange={(e) => onDisplayNameChanged(e.target.value)}
/> />
<br /> <br />
<br /> <br />
<Stack direction="horizontal" gap={3}> <Stack direction="horizontal" gap={3}>
<Button type="submit">Submit</Button> <Button type="submit">Submit</Button>
<Button variant="secondary" href={`/${processModelPath}`}>Cancel</Button> <Button variant="secondary" href={`/${processModelPath}`}>
<Button onClick={deleteProcessModel} variant="danger">Delete process model</Button> Cancel
</Button>
<Button onClick={deleteProcessModel} variant="danger">
Delete process model
</Button>
</Stack> </Stack>
</form> </form>
</main> </main>
); );
} else {
return (<></>)
} }
} }

View File

@ -1,20 +1,17 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from 'react';
import { BACKEND_BASE_URL } from '../config'; import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
import { HOT_AUTH_TOKEN } from '../config';
import { useNavigate, useParams, useSearchParams } from "react-router-dom";
import ReactDiagramEditor from "../react_diagram_editor"
import ProcessBreadcrumb from '../components/ProcessBreadcrumb'
import { Button, Modal } from 'react-bootstrap'; import { Button, Modal } from 'react-bootstrap';
import Editor from '@monaco-editor/react';
import { BACKEND_BASE_URL, HOT_AUTH_TOKEN } from '../config';
import Editor from "@monaco-editor/react"; import ReactDiagramEditor from '../react_diagram_editor';
import ProcessBreadcrumb from '../components/ProcessBreadcrumb';
export default function ProcessModelEditDiagram() { export default function ProcessModelEditDiagram() {
const [showFileNameEditor, setShowFileNameEditor] = useState(false); const [showFileNameEditor, setShowFileNameEditor] = useState(false);
const handleShowFileNameEditor = () => setShowFileNameEditor(true); const handleShowFileNameEditor = () => setShowFileNameEditor(true);
const [scriptText, setScriptText] = useState(""); const [scriptText, setScriptText] = useState('');
const [scriptModeling, setScriptModeling] = useState(null); const [scriptModeling, setScriptModeling] = useState(null);
const [scriptElement, setScriptElement] = useState(null); const [scriptElement, setScriptElement] = useState(null);
const [showScriptEditor, setShowScriptEditor] = useState(false); const [showScriptEditor, setShowScriptEditor] = useState(false);
@ -25,17 +22,21 @@ export default function ProcessModelEditDiagram() {
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const [processModelFile, setProcessModelFile] = useState(null); const [processModelFile, setProcessModelFile] = useState(null);
const [newFileName, setNewFileName] = useState(""); const [newFileName, setNewFileName] = useState('');
const [bpmnXmlForDiagramRendering, setBpmnXmlForDiagramRendering] = useState(null); const [bpmnXmlForDiagramRendering, setBpmnXmlForDiagramRendering] =
useState(null);
useEffect(() => { useEffect(() => {
if (params.file_name) { if (params.file_name) {
fetch(`${BACKEND_BASE_URL}/process-models/${params.process_group_id}/${params.process_model_id}/file/${params.file_name}`, { fetch(
headers: new Headers({ `${BACKEND_BASE_URL}/process-models/${params.process_group_id}/${params.process_model_id}/file/${params.file_name}`,
'Authorization': `Bearer ${HOT_AUTH_TOKEN}` {
}) headers: new Headers({
}) Authorization: `Bearer ${HOT_AUTH_TOKEN}`,
.then(res => res.json()) }),
}
)
.then((res) => res.json())
.then( .then(
(result) => { (result) => {
setProcessModelFile(result); setProcessModelFile(result);
@ -44,26 +45,16 @@ export default function ProcessModelEditDiagram() {
(error) => { (error) => {
console.log(error); console.log(error);
} }
) );
} }
}, [params]); }, [params]);
function onError(err) { const handleFileNameCancel = () => {
console.log('ERROR:', err);
}
const handleFileNameCancel = (() => {
setShowFileNameEditor(false); setShowFileNameEditor(false);
setNewFileName(""); setNewFileName('');
}); };
const handleFileNameSave = ((event) => { const saveDiagram = (bpmnXML, fileName = params.file_name) => {
event.preventDefault();
setShowFileNameEditor(false);
saveDiagram(bpmnXmlForDiagramRendering);
});
const saveDiagram = ((bpmnXML, fileName = params.file_name) => {
setBpmnXmlForDiagramRendering(bpmnXML); setBpmnXmlForDiagramRendering(bpmnXML);
let url = `${BACKEND_BASE_URL}/process-models/${params.process_group_id}/${params.process_model_id}/file`; let url = `${BACKEND_BASE_URL}/process-models/${params.process_group_id}/${params.process_model_id}/file`;
@ -72,7 +63,7 @@ export default function ProcessModelEditDiagram() {
if (newFileName) { if (newFileName) {
fileNameWithExtension = `${newFileName}.${searchParams.get('file_type')}`; fileNameWithExtension = `${newFileName}.${searchParams.get('file_type')}`;
httpMethod = 'POST' httpMethod = 'POST';
} else { } else {
url += `/${fileNameWithExtension}`; url += `/${fileNameWithExtension}`;
} }
@ -81,32 +72,38 @@ export default function ProcessModelEditDiagram() {
return; return;
} }
let bpmnFile = new File([bpmnXML], fileNameWithExtension); const bpmnFile = new File([bpmnXML], fileNameWithExtension);
const formData = new FormData(); const formData = new FormData();
formData.append('file', bpmnFile); formData.append('file', bpmnFile);
formData.append('fileName', bpmnFile.name); formData.append('fileName', bpmnFile.name);
fetch(url, { fetch(url, {
headers: new Headers({ headers: new Headers({
'Authorization': `Bearer ${HOT_AUTH_TOKEN}`, Authorization: `Bearer ${HOT_AUTH_TOKEN}`,
}), }),
method: httpMethod, method: httpMethod,
body: formData, body: formData,
}) }).then(
.then(res => res.json()) () => {
.then( if (!params.file_name) {
(result) => { navigate(
if (!params.file_name) { `/process-models/${params.process_group_id}/${params.process_model_id}/file/${fileNameWithExtension}`
navigate(`/process-models/${params.process_group_id}/${params.process_model_id}/file/${fileNameWithExtension}`); );
}
},
(error) => {
console.log(error);
} }
) },
}); (error) => {
console.log(error);
}
);
};
const newFileNameBox = (() => { const handleFileNameSave = (event) => {
let fileExtension = `.${searchParams.get('file_type')}`; event.preventDefault();
setShowFileNameEditor(false);
saveDiagram(bpmnXmlForDiagramRendering);
};
const newFileNameBox = () => {
const fileExtension = `.${searchParams.get('file_type')}`;
return ( return (
<Modal show={showFileNameEditor} onHide={handleFileNameCancel}> <Modal show={showFileNameEditor} onHide={handleFileNameCancel}>
<Modal.Header closeButton> <Modal.Header closeButton>
@ -116,11 +113,11 @@ export default function ProcessModelEditDiagram() {
<label>File Name:</label> <label>File Name:</label>
<span> <span>
<input <input
name='file_name' name="file_name"
type='text' type="text"
value={newFileName} value={newFileName}
onChange={e => setNewFileName(e.target.value)} onChange={(e) => setNewFileName(e.target.value)}
autoFocus={true} autoFocus
/> />
{fileExtension} {fileExtension}
</span> </span>
@ -134,29 +131,29 @@ export default function ProcessModelEditDiagram() {
</Modal.Footer> </Modal.Footer>
</form> </form>
</Modal> </Modal>
) );
}); };
const onLaunchScriptEditor = ((element, modeling) => { const onLaunchScriptEditor = (element, modeling) => {
setScriptText((element.businessObject.script || '')); setScriptText(element.businessObject.script || '');
setScriptModeling(modeling); setScriptModeling(modeling);
setScriptElement(element); setScriptElement(element);
handleShowScriptEditor(); handleShowScriptEditor();
}); };
const handleScriptEditorClose = (() => { const handleScriptEditorClose = () => {
setShowScriptEditor(false); setShowScriptEditor(false);
}); };
const handleEditorChange = ((value, event) => { const handleEditorChange = (value) => {
setScriptText(value); setScriptText(value);
scriptModeling.updateProperties(scriptElement, { scriptModeling.updateProperties(scriptElement, {
scriptFormat: "python", scriptFormat: 'python',
script: value script: value,
}); });
}); };
const scriptEditor = (() => { const scriptEditor = () => {
let scriptName = ""; let scriptName = '';
if (scriptElement) { if (scriptElement) {
scriptName = scriptElement.di.bpmnElement.name scriptName = scriptElement.di.bpmnElement.name;
} }
return ( return (
<Modal size="xl" show={showScriptEditor} onHide={handleScriptEditorClose}> <Modal size="xl" show={showScriptEditor} onHide={handleScriptEditorClose}>
@ -177,62 +174,61 @@ export default function ProcessModelEditDiagram() {
</Modal.Footer> </Modal.Footer>
</Modal> </Modal>
); );
}); };
const isDmn = (() => { const isDmn = () => {
const file_name = params.file_name || ""; const fileName = params.file_name || '';
if (searchParams.get('file_type') === "dmn" || file_name.endsWith(".dmn") ) { if (searchParams.get('file_type') === 'dmn' || fileName.endsWith('.dmn')) {
return true; return true;
} }
return false; return false;
}); };
const appropriateEditor = (() => { const appropriateEditor = () => {
if (isDmn()) { if (isDmn()) {
return ( return (
<ReactDiagramEditor <ReactDiagramEditor
process_model_id={params.process_model_id} process_model_id={params.process_model_id}
process_group_id={params.process_group_id} process_group_id={params.process_group_id}
onError={ onError } saveDiagram={saveDiagram}
saveDiagram={ saveDiagram }
diagramXML={bpmnXmlForDiagramRendering} diagramXML={bpmnXmlForDiagramRendering}
fileName={processModelFile ? processModelFile.name : null} fileName={processModelFile ? processModelFile.name : null}
diagramType='dmn' diagramType="dmn"
/> />
) );
} }
return ( return (
<ReactDiagramEditor <ReactDiagramEditor
process_model_id={params.process_model_id} process_model_id={params.process_model_id}
process_group_id={params.process_group_id} process_group_id={params.process_group_id}
onError={ onError } saveDiagram={saveDiagram}
saveDiagram={ saveDiagram }
diagramXML={bpmnXmlForDiagramRendering} diagramXML={bpmnXmlForDiagramRendering}
fileName={processModelFile ? processModelFile.name : null} fileName={processModelFile ? processModelFile.name : null}
diagramType='bpmn' diagramType="bpmn"
onLaunchScriptEditor={onLaunchScriptEditor} onLaunchScriptEditor={onLaunchScriptEditor}
/> />
) );
}); };
// if a file name is not given then this is a new model and the ReactDiagramEditor component will handle it // if a file name is not given then this is a new model and the ReactDiagramEditor component will handle it
if (bpmnXmlForDiagramRendering || !params.file_name) { if (bpmnXmlForDiagramRendering || !params.file_name) {
return ( return (
<main style={{ padding: "1rem 0" }}> <main style={{ padding: '1rem 0' }}>
<ProcessBreadcrumb <ProcessBreadcrumb
processGroupId={params.process_group_id} processGroupId={params.process_group_id}
processModelId={params.process_model_id} processModelId={params.process_model_id}
linkProcessModel="true" linkProcessModel="true"
/> />
<h2>Process Model File{processModelFile ? `: ${processModelFile.name}` : ""}</h2> <h2>
Process Model File
{processModelFile ? `: ${processModelFile.name}` : ''}
</h2>
{appropriateEditor()} {appropriateEditor()}
{newFileNameBox()} {newFileNameBox()}
{scriptEditor()} {scriptEditor()}
<div id="diagram-container"></div> <div id="diagram-container" />
</main> </main>
); );
} else {
return (<></>)
} }
} }

View File

@ -1,25 +1,24 @@
import React, { useState } from "react"; import React, { useState } from 'react';
import { useNavigate, useParams } from "react-router-dom"; import { useNavigate, useParams } from 'react-router-dom';
import { BACKEND_BASE_URL } from '../config'; import { BACKEND_BASE_URL, HOT_AUTH_TOKEN } from '../config';
import { HOT_AUTH_TOKEN } from '../config'; import ProcessBreadcrumb from '../components/ProcessBreadcrumb';
import ProcessBreadcrumb from '../components/ProcessBreadcrumb' import { slugifyString } from '../helpers';
import { slugifyString } from '../helpers'
export default function ProcessModelNew(props) { export default function ProcessModelNew() {
let params = useParams(); const params = useParams();
const [identifier, setIdentifier] = useState(""); const [identifier, setIdentifier] = useState('');
const [idHasBeenUpdatedByUser, setIdHasBeenUpdatedByUser] = useState(false); const [idHasBeenUpdatedByUser, setIdHasBeenUpdatedByUser] = useState(false);
const [displayName, setDisplayName] = useState(""); const [displayName, setDisplayName] = useState('');
const navigate = useNavigate(); const navigate = useNavigate();
const addProcessModel = ((event) => { const addProcessModel = (event) => {
event.preventDefault() event.preventDefault();
fetch(`${BACKEND_BASE_URL}/process-models`, { fetch(`${BACKEND_BASE_URL}/process-models`, {
headers: new Headers({ headers: new Headers({
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Authorization': `Bearer ${HOT_AUTH_TOKEN}` Authorization: `Bearer ${HOT_AUTH_TOKEN}`,
}), }),
method: 'POST', method: 'POST',
body: JSON.stringify({ body: JSON.stringify({
@ -31,49 +30,55 @@ export default function ProcessModelNew(props) {
standalone: false, standalone: false,
library: false, library: false,
}), }),
}) }).then(
.then(res => res.json()) () => {
.then( navigate(`/process-models/${params.process_group_id}/${identifier}`);
(result) => { },
navigate(`/process-models/${params.process_group_id}/${identifier}`); // Note: it's important to handle errors here
}, // instead of a catch() block so that we don't swallow
// Note: it's important to handle errors here // exceptions from actual bugs in components.
// instead of a catch() block so that we don't swallow (newError) => {
// exceptions from actual bugs in components. console.log(newError);
(newError) => { }
console.log(newError); );
} };
)
}); const onDisplayNameChanged = (newDisplayName) => {
const onDisplayNameChanged = ((newDisplayName) => {
setDisplayName(newDisplayName); setDisplayName(newDisplayName);
if (!idHasBeenUpdatedByUser) { if (!idHasBeenUpdatedByUser) {
setIdentifier(slugifyString(newDisplayName)); setIdentifier(slugifyString(newDisplayName));
} }
}); };
return ( return (
<main style={{ padding: "1rem 0" }}> <main style={{ padding: '1rem 0' }}>
<ProcessBreadcrumb /> <ProcessBreadcrumb />
<h2>Add Process Model</h2> <h2>Add Process Model</h2>
<form onSubmit={addProcessModel}> <form onSubmit={addProcessModel}>
<label>Display Name:</label> <label htmlFor="display_name">
<input Display Name:
name='display_name' <input
type='text' name="display_name"
value={displayName} id="display_name"
onChange={e => onDisplayNameChanged(e.target.value)} type="text"
/> value={displayName}
onChange={(e) => onDisplayNameChanged(e.target.value)}
/>
</label>
<br /> <br />
<label>ID:</label> <label htmlFor="id">
<input ID:
name='id' <input
type='text' name="id"
value={identifier} id="id"
onChange={e => { setIdentifier(e.target.value); setIdHasBeenUpdatedByUser(true)} } type="text"
/> value={identifier}
onChange={(e) => {
setIdentifier(e.target.value);
setIdHasBeenUpdatedByUser(true);
}}
/>
</label>
<br /> <br />
<button type="submit">Submit</button> <button type="submit">Submit</button>
</form> </form>

View File

@ -1,25 +1,26 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from 'react';
import { Link } from "react-router-dom"; import { Link, useParams } from 'react-router-dom';
import { BACKEND_BASE_URL } from '../config'; import { Button, Stack } from 'react-bootstrap';
import { HOT_AUTH_TOKEN } from '../config'; import { BACKEND_BASE_URL, HOT_AUTH_TOKEN } from '../config';
import { useParams } from "react-router-dom"; import ProcessBreadcrumb from '../components/ProcessBreadcrumb';
import ProcessBreadcrumb from '../components/ProcessBreadcrumb' import FileInput from '../components/FileInput';
import FileInput from '../components/FileInput'
import { Button, Stack } from 'react-bootstrap'
export default function ProcessModelShow() { export default function ProcessModelShow() {
let params = useParams(); const params = useParams();
const [processModel, setProcessModel] = useState(null); const [processModel, setProcessModel] = useState(null);
const [processInstanceResult, setProcessInstanceResult] = useState(null); const [processInstanceResult, setProcessInstanceResult] = useState(null);
useEffect(() => { useEffect(() => {
fetch(`${BACKEND_BASE_URL}/process-models/${params.process_group_id}/${params.process_model_id}`, { fetch(
headers: new Headers({ `${BACKEND_BASE_URL}/process-models/${params.process_group_id}/${params.process_model_id}`,
'Authorization': `Bearer ${HOT_AUTH_TOKEN}` {
}) headers: new Headers({
}) Authorization: `Bearer ${HOT_AUTH_TOKEN}`,
.then(res => res.json()) }),
}
)
.then((res) => res.json())
.then( .then(
(result) => { (result) => {
setProcessModel(result); setProcessModel(result);
@ -27,35 +28,20 @@ export default function ProcessModelShow() {
(error) => { (error) => {
console.log(error); console.log(error);
} }
) );
}, [params]); }, [params]);
const processInstanceCreateAndRun = ((event) => { const processModelRun = (processInstance) => {
fetch(`${BACKEND_BASE_URL}/process-models/${processModel.process_group_id}/${processModel.id}`, { fetch(
headers: new Headers({ `${BACKEND_BASE_URL}/process-models/${processModel.process_group_id}/${processModel.id}/process-instances/${processInstance.id}/run`,
'Authorization': `Bearer ${HOT_AUTH_TOKEN}` {
}), headers: new Headers({
method: 'POST', Authorization: `Bearer ${HOT_AUTH_TOKEN}`,
}) }),
.then(res => res.json()) method: 'POST',
.then( }
(result) => { )
processModelRun(result); .then((res) => res.json())
},
(error) => {
console.log(error);
}
)
});
const processModelRun = ((processInstance) => {
fetch(`${BACKEND_BASE_URL}/process-models/${processModel.process_group_id}/${processModel.id}/process-instances/${processInstance.id}/run`, {
headers: new Headers({
'Authorization': `Bearer ${HOT_AUTH_TOKEN}`
}),
method: 'POST',
})
.then(res => res.json())
.then( .then(
(result) => { (result) => {
setProcessInstanceResult(result); setProcessInstanceResult(result);
@ -63,59 +49,114 @@ export default function ProcessModelShow() {
(error) => { (error) => {
console.log(error); console.log(error);
} }
) );
}); };
let processInstanceResultTag = "" const processInstanceCreateAndRun = () => {
fetch(
`${BACKEND_BASE_URL}/process-models/${processModel.process_group_id}/${processModel.id}`,
{
headers: new Headers({
Authorization: `Bearer ${HOT_AUTH_TOKEN}`,
}),
method: 'POST',
}
)
.then((res) => res.json())
.then(
(result) => {
processModelRun(result);
},
(error) => {
console.log(error);
}
);
};
let processInstanceResultTag = '';
if (processInstanceResult) { if (processInstanceResult) {
processInstanceResultTag = <pre>{processInstanceResult.status}: {JSON.stringify(processInstanceResult.data)}</pre> processInstanceResultTag = (
<pre>
{processInstanceResult.status}:{' '}
{JSON.stringify(processInstanceResult.data)}
</pre>
);
} }
if (processModel) { if (processModel) {
let processModelFilesTag = ""; let processModelFilesTag = '';
processModelFilesTag = processModel.files.map(file_bpmn => { processModelFilesTag = processModel.files.map((fileBpmn) => {
if (file_bpmn.name === processModel.primary_file_name) { if (fileBpmn.name === processModel.primary_file_name) {
return ( return (
<li key={file_bpmn.name}> <li key={fileBpmn.name}>
<Link to={`/process-models/${processModel.process_group_id}/${processModel.id}/file/${file_bpmn.name}`}>{file_bpmn.name}</Link> - Primary File <Link
to={`/process-models/${processModel.process_group_id}/${processModel.id}/file/${fileBpmn.name}`}
>
{fileBpmn.name}
</Link>{' '}
- Primary File
</li> </li>
) );
} else {
return (
<li key={file_bpmn.name}>
<Link to={`/process-models/${processModel.process_group_id}/${processModel.id}/file/${file_bpmn.name}`}>{file_bpmn.name}</Link>
</li>
)
} }
}) return (
<li key={fileBpmn.name}>
<Link
to={`/process-models/${processModel.process_group_id}/${processModel.id}/file/${fileBpmn.name}`}
>
{fileBpmn.name}
</Link>
</li>
);
});
return ( return (
<main style={{ padding: "1rem 0" }}> <main style={{ padding: '1rem 0' }}>
<ProcessBreadcrumb <ProcessBreadcrumb
processGroupId={processModel.process_group_id} processGroupId={processModel.process_group_id}
processModelId={processModel.id} processModelId={processModel.id}
/> />
<h2>Process Model: {processModel.id}</h2> <h2>Process Model: {processModel.id}</h2>
{processInstanceResultTag} {processInstanceResultTag}
<FileInput processModel={processModel} /> <FileInput
<br /> processModelId={processModel.id}
<Stack direction="horizontal" gap={3}> processGroupId={processModel.process_group_id}
<Button onClick={processInstanceCreateAndRun} variant="primary">Run</Button> />
<Button href={`/process-models/${processModel.process_group_id}/${processModel.id}/edit`} variant="secondary">Edit process model</Button> <br />
<Button href={`/process-models/${processModel.process_group_id}/${processModel.id}/file?file_type=bpmn`} variant="warning">Add New BPMN File</Button> <Stack direction="horizontal" gap={3}>
<Button href={`/process-models/${processModel.process_group_id}/${processModel.id}/file?file_type=dmn`} variant="success">Add New DMN File</Button> <Button onClick={processInstanceCreateAndRun} variant="primary">
</Stack> Run
<br /> </Button>
<br /> <Button
<Link to={`/process-models/${processModel.process_group_id}/${processModel.id}/process-instances`}>Process Instances</Link> href={`/process-models/${processModel.process_group_id}/${processModel.id}/edit`}
<br /> variant="secondary"
<br /> >
<h3>Files</h3> Edit process model
<ul>{processModelFilesTag}</ul> </Button>
<Button
href={`/process-models/${processModel.process_group_id}/${processModel.id}/file?file_type=bpmn`}
variant="warning"
>
Add New BPMN File
</Button>
<Button
href={`/process-models/${processModel.process_group_id}/${processModel.id}/file?file_type=dmn`}
variant="success"
>
Add New DMN File
</Button>
</Stack>
<br />
<br />
<Link
to={`/process-models/${processModel.process_group_id}/${processModel.id}/process-instances`}
>
Process Instances
</Link>
<br />
<br />
<h3>Files</h3>
<ul>{processModelFilesTag}</ul>
</main> </main>
); );
} else {
return (<></>)
} }
} }

View File

@ -1,6 +1,6 @@
export default function Expenses() { export default function Expenses() {
return ( return (
<main style={{ padding: "1rem 0" }}> <main style={{ padding: '1rem 0' }}>
<h2>Expenses</h2> <h2>Expenses</h2>
</main> </main>
); );