Merge pull request #278 from sartography/feature/interstitial_process_instance_show

Feature/interstitial process instance show
This commit is contained in:
Dan Funk 2023-05-24 17:49:08 -04:00 committed by GitHub
commit aba88d6c7c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 146 additions and 121 deletions

View File

@ -10,7 +10,6 @@ import HomePageRoutes from './routes/HomePageRoutes';
import About from './routes/About'; import About from './routes/About';
import ErrorBoundary from './components/ErrorBoundary'; import ErrorBoundary from './components/ErrorBoundary';
import AdminRoutes from './routes/AdminRoutes'; import AdminRoutes from './routes/AdminRoutes';
import ProcessRoutes from './routes/ProcessRoutes';
import { AbilityContext } from './contexts/Can'; import { AbilityContext } from './contexts/Can';
import UserService from './services/UserService'; import UserService from './services/UserService';
@ -41,7 +40,6 @@ export default function App() {
<Route path="/*" element={<HomePageRoutes />} /> <Route path="/*" element={<HomePageRoutes />} />
<Route path="/about" element={<About />} /> <Route path="/about" element={<About />} />
<Route path="/tasks/*" element={<HomePageRoutes />} /> <Route path="/tasks/*" element={<HomePageRoutes />} />
<Route path="/process/*" element={<ProcessRoutes />} />
<Route path="/admin/*" element={<AdminRoutes />} /> <Route path="/admin/*" element={<AdminRoutes />} />
</Routes> </Routes>
</ErrorBoundary> </ErrorBoundary>

View File

@ -782,7 +782,7 @@ export default function ProcessInstanceListTable({
undefined, undefined,
paginationQueryParamPrefix paginationQueryParamPrefix
); );
page = 1; // Reset page back to 0 page = 1;
const newReportMetadata = getNewReportMetadataBasedOnPageWidgets(); const newReportMetadata = getNewReportMetadataBasedOnPageWidgets();
setReportMetadata(newReportMetadata); setReportMetadata(newReportMetadata);
@ -1590,9 +1590,7 @@ export default function ProcessInstanceListTable({
}); });
if (showActionsColumn) { if (showActionsColumn) {
let buttonElement = null; let buttonElement = null;
const interstitialUrl = `/process/${modifyProcessIdentifierForPathParam( const taskShowUrl = `/tasks/${processInstance.id}/${processInstance.task_id}`;
processInstance.process_model_identifier
)}/${processInstance.id}/interstitial`;
const regex = new RegExp(`\\b(${preferredUsername}|${userEmail})\\b`); const regex = new RegExp(`\\b(${preferredUsername}|${userEmail})\\b`);
let hasAccessToCompleteTask = false; let hasAccessToCompleteTask = false;
if ( if (
@ -1601,20 +1599,18 @@ export default function ProcessInstanceListTable({
) { ) {
hasAccessToCompleteTask = true; hasAccessToCompleteTask = true;
} }
let buttonText = 'View'; buttonElement = null;
if (hasAccessToCompleteTask && processInstance.task_id) { if (hasAccessToCompleteTask && processInstance.task_id) {
buttonText = 'Go';
}
buttonElement = ( buttonElement = (
<Button <Button
kind="secondary" kind="secondary"
href={interstitialUrl} href={taskShowUrl}
style={{ width: '60px' }} style={{ width: '60px' }}
> >
{buttonText} Go
</Button> </Button>
); );
}
if ( if (
processInstance.status === 'not_started' || processInstance.status === 'not_started' ||

View File

@ -96,7 +96,7 @@ export default function ProcessInstanceRun({
const onProcessInstanceRun = (processInstance: any) => { const onProcessInstanceRun = (processInstance: any) => {
const processInstanceId = (processInstance as any).id; const processInstanceId = (processInstance as any).id;
navigate( navigate(
`/process/${modifyProcessIdentifierForPathParam( `/admin/process-instances/${modifyProcessIdentifierForPathParam(
processModel.id processModel.id
)}/${processInstanceId}/interstitial` )}/${processInstanceId}/interstitial`
); );

View File

@ -1,36 +1,41 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react'; import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { fetchEventSource } from '@microsoft/fetch-event-source'; import { fetchEventSource } from '@microsoft/fetch-event-source';
// @ts-ignore // @ts-ignore
import { Loading, Button } from '@carbon/react'; import { Loading } from '@carbon/react';
import { BACKEND_BASE_URL } from '../config'; import { BACKEND_BASE_URL } from '../config';
import { getBasicHeaders } from '../services/HttpService'; import { getBasicHeaders } from '../services/HttpService';
// @ts-ignore // @ts-ignore
import InstructionsForEndUser from '../components/InstructionsForEndUser'; import InstructionsForEndUser from './InstructionsForEndUser';
import ProcessBreadcrumb from '../components/ProcessBreadcrumb';
import { ProcessInstance, ProcessInstanceTask } from '../interfaces'; import { ProcessInstance, ProcessInstanceTask } from '../interfaces';
import useAPIError from '../hooks/UseApiError'; import useAPIError from '../hooks/UseApiError';
export default function ProcessInterstitial() { type OwnProps = {
processInstanceId: number;
processInstanceShowPageUrl: string;
allowRedirect: boolean;
};
export default function ProcessInterstitial({
processInstanceId,
allowRedirect,
processInstanceShowPageUrl,
}: OwnProps) {
const [data, setData] = useState<any[]>([]); const [data, setData] = useState<any[]>([]);
const [lastTask, setLastTask] = useState<any>(null); const [lastTask, setLastTask] = useState<any>(null);
const [state, setState] = useState<string>('RUNNING');
const [processInstance, setProcessInstance] = const [processInstance, setProcessInstance] =
useState<ProcessInstance | null>(null); useState<ProcessInstance | null>(null);
const [state, setState] = useState<string>('RUNNING');
const params = useParams();
const navigate = useNavigate(); const navigate = useNavigate();
const userTasks = useMemo(() => { const userTasks = useMemo(() => {
return ['User Task', 'Manual Task']; return ['User Task', 'Manual Task'];
}, []); }, []);
const { addError } = useAPIError(); const { addError } = useAPIError();
const processInstanceShowPageBaseUrl = `/admin/process-instances/for-me/${params.modified_process_model_identifier}`;
useEffect(() => { useEffect(() => {
fetchEventSource( fetchEventSource(`${BACKEND_BASE_URL}/tasks/${processInstanceId}`, {
`${BACKEND_BASE_URL}/tasks/${params.process_instance_id}`,
{
headers: getBasicHeaders(), headers: getBasicHeaders(),
onmessage(ev) { onmessage(ev) {
const retValue = JSON.parse(ev.data); const retValue = JSON.parse(ev.data);
@ -44,37 +49,55 @@ export default function ProcessInterstitial() {
} }
}, },
onclose() { onclose() {
console.log('The state is closed.');
setState('CLOSED'); setState('CLOSED');
}, },
} });
);
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []); // it is critical to only run this once. }, []); // it is critical to only run this once.
const shouldRedirect = useCallback( const shouldRedirectToTask = useCallback(
(myTask: ProcessInstanceTask): boolean => { (myTask: ProcessInstanceTask): boolean => {
return ( return (
allowRedirect &&
!processInstance && !processInstance &&
myTask && myTask &&
myTask.can_complete && myTask.can_complete &&
userTasks.includes(myTask.type) userTasks.includes(myTask.type)
); );
}, },
[userTasks, processInstance] [allowRedirect, processInstance, userTasks]
); );
const shouldRedirectToProcessInstance = useCallback((): boolean => {
return allowRedirect && state === 'CLOSED';
}, [allowRedirect, state]);
useEffect(() => { useEffect(() => {
// Added this seperate use effect so that the timer interval will be cleared if // Added this seperate use effect so that the timer interval will be cleared if
// we end up redirecting back to the TaskShow page. // we end up redirecting back to the TaskShow page.
if (shouldRedirect(lastTask)) { if (shouldRedirectToTask(lastTask)) {
lastTask.properties.instructionsForEndUser = ''; lastTask.properties.instructionsForEndUser = '';
const timerId = setInterval(() => { const timerId = setInterval(() => {
navigate(`/tasks/${lastTask.process_instance_id}/${lastTask.id}`); navigate(`/tasks/${lastTask.process_instance_id}/${lastTask.id}`);
}, 2000); }, 2000);
return () => clearInterval(timerId); return () => clearInterval(timerId);
} }
if (shouldRedirectToProcessInstance()) {
// Navigate without pause as we will be showing the same information.
navigate(processInstanceShowPageUrl);
}
return undefined; return undefined;
}, [lastTask, navigate, userTasks, shouldRedirect]); }, [
lastTask,
navigate,
userTasks,
shouldRedirectToTask,
processInstanceId,
processInstanceShowPageUrl,
state,
shouldRedirectToProcessInstance,
]);
const getStatus = (): string => { const getStatus = (): string => {
if (processInstance) { if (processInstance) {
@ -95,35 +118,13 @@ export default function ProcessInterstitial() {
<Loading <Loading
description="Active loading indicator" description="Active loading indicator"
withOverlay={false} withOverlay={false}
style={{ margin: 'auto' }} style={{ margin: '50px 0 50px 50px' }}
/> />
); );
} }
return null; return null;
}; };
const getReturnHomeButton = (index: number) => {
if (
index === 0 &&
!shouldRedirect(lastTask) &&
['WAITING', 'ERROR', 'LOCKED', 'COMPLETED', 'READY'].includes(getStatus())
) {
return (
<div style={{ padding: '10px 0 0 0' }}>
<Button
kind="secondary"
data-qa="return-to-home-button"
onClick={() => navigate(`/tasks`)}
style={{ marginBottom: 30 }}
>
Return to Home
</Button>
</div>
);
}
return '';
};
const userMessage = (myTask: ProcessInstanceTask) => { const userMessage = (myTask: ProcessInstanceTask) => {
if (!processInstance || processInstance.status === 'completed') { if (!processInstance || processInstance.status === 'completed') {
if (!myTask.can_complete && userTasks.includes(myTask.type)) { if (!myTask.can_complete && userTasks.includes(myTask.type)) {
@ -134,9 +135,12 @@ export default function ProcessInterstitial() {
</p> </p>
); );
} }
if (shouldRedirect(myTask)) { if (shouldRedirectToTask(myTask)) {
return <div>Redirecting you to the next task now ...</div>; return <div>Redirecting you to the next task now ...</div>;
} }
if (myTask && myTask.can_complete && userTasks.includes(myTask.type)) {
return `The task ${myTask.title} is ready for you to complete.`;
}
if (myTask.error_message) { if (myTask.error_message) {
return <div>{myTask.error_message}</div>; return <div>{myTask.error_message}</div>;
} }
@ -161,40 +165,24 @@ export default function ProcessInterstitial() {
navigate(`/tasks`); navigate(`/tasks`);
} }
let displayableData = data;
if (state === 'CLOSED') {
displayableData = [data[0]];
}
if (lastTask) { if (lastTask) {
return ( return (
<> <>
<ProcessBreadcrumb
hotCrumbs={[
['Process Groups', '/admin'],
{
entityToExplode: lastTask.process_model_identifier,
entityType: 'process-model-id',
linkLastItem: true,
},
[
`Process Instance: ${params.process_instance_id}`,
`${processInstanceShowPageBaseUrl}/${params.process_instance_id}`,
],
]}
/>
{getLoadingIcon()} {getLoadingIcon()}
<div style={{ maxWidth: 800, margin: 'auto', padding: 50 }}> {displayableData.map((d, index) => (
{data.map((d, index) => (
<>
<div <div
className={ className={
index < 4 index < 4 ? `user_instructions_${index}` : `user_instructions_4`
? `user_instructions_${index}`
: `user_instructions_4`
} }
> >
{userMessage(d)} {userMessage(d)}
</div> </div>
{getReturnHomeButton(index)}
</>
))} ))}
</div>
</> </>
); );
} }

View File

@ -23,6 +23,7 @@ export interface RecentProcessModel {
export interface TaskPropertiesJson { export interface TaskPropertiesJson {
parent: string; parent: string;
last_state_change: number;
} }
export interface TaskDefinitionPropertiesJson { export interface TaskDefinitionPropertiesJson {

View File

@ -22,6 +22,7 @@ import Configuration from './Configuration';
import JsonSchemaFormBuilder from './JsonSchemaFormBuilder'; import JsonSchemaFormBuilder from './JsonSchemaFormBuilder';
import ProcessModelNewExperimental from './ProcessModelNewExperimental'; import ProcessModelNewExperimental from './ProcessModelNewExperimental';
import ProcessInstanceFindById from './ProcessInstanceFindById'; import ProcessInstanceFindById from './ProcessInstanceFindById';
import ProcessInterstitialPage from './ProcessInterstitialPage';
export default function AdminRoutes() { export default function AdminRoutes() {
const location = useLocation(); const location = useLocation();
@ -75,6 +76,14 @@ export default function AdminRoutes() {
path="process-instances/for-me/:process_model_id/:process_instance_id/:to_task_guid" path="process-instances/for-me/:process_model_id/:process_instance_id/:to_task_guid"
element={<ProcessInstanceShow variant="for-me" />} element={<ProcessInstanceShow variant="for-me" />}
/> />
<Route
path="process-instances/for-me/:process_model_id/:process_instance_id/interstitial"
element={<ProcessInterstitialPage variant="for-me" />}
/>
<Route
path="process-instances/:process_model_id/:process_instance_id/interstitial"
element={<ProcessInterstitialPage variant="all" />}
/>
<Route <Route
path="process-instances/:process_model_id/:process_instance_id" path="process-instances/:process_model_id/:process_instance_id"
element={<ProcessInstanceShow variant="all" />} element={<ProcessInstanceShow variant="all" />}

View File

@ -53,6 +53,7 @@ import { usePermissionFetcher } from '../hooks/PermissionService';
import ProcessInstanceClass from '../classes/ProcessInstanceClass'; import ProcessInstanceClass from '../classes/ProcessInstanceClass';
import TaskListTable from '../components/TaskListTable'; import TaskListTable from '../components/TaskListTable';
import useAPIError from '../hooks/UseApiError'; import useAPIError from '../hooks/UseApiError';
import ProcessInterstitial from '../components/ProcessInterstitial';
type OwnProps = { type OwnProps = {
variant: string; variant: string;
@ -1109,6 +1110,11 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
</h1> </h1>
{buttonIcons()} {buttonIcons()}
</Stack> </Stack>
<ProcessInterstitial
processInstanceId={processInstance.id}
processInstanceShowPageUrl={processInstanceShowPageBaseUrl}
allowRedirect={false}
/>
<br /> <br />
<br /> <br />
<Grid condensed fullWidth> <Grid condensed fullWidth>

View File

@ -0,0 +1,41 @@
import React from 'react';
import { useParams } from 'react-router-dom';
// @ts-ignore
import ProcessInterstitial from '../components/ProcessInterstitial';
import ProcessBreadcrumb from '../components/ProcessBreadcrumb';
type OwnProps = {
variant: string;
};
export default function ProcessInterstitialPage({ variant }: OwnProps) {
const params = useParams();
let processInstanceShowPageUrl = `/admin/process-instances/for-me/${params.process_model_id}/${params.process_instance_id}`;
if (variant === 'all') {
processInstanceShowPageUrl = `/admin/process-instances/${params.process_model_id}/${params.process_instance_id}`;
}
return (
<>
<ProcessBreadcrumb
hotCrumbs={[
['Process Groups', '/admin'],
{
entityToExplode: String(params.process_model_id),
entityType: 'process-model-id',
linkLastItem: true,
},
[
`Process Instance: ${params.process_instance_id}`,
`${processInstanceShowPageUrl}`,
],
]}
/>
<ProcessInterstitial
processInstanceId={Number(params.process_instance_id)}
processInstanceShowPageUrl={processInstanceShowPageUrl}
allowRedirect
/>
</>
);
}

View File

@ -1,14 +0,0 @@
import { Route, Routes } from 'react-router-dom';
// @ts-ignore
import ProcessInterstitial from './ProcessInterstitial';
export default function ProcessRoutes() {
return (
<Routes>
<Route
path=":modified_process_model_identifier/:process_instance_id/interstitial"
element={<ProcessInterstitial />}
/>
</Routes>
);
}

View File

@ -102,7 +102,7 @@ export default function TaskShow() {
const navigateToInterstitial = (myTask: Task) => { const navigateToInterstitial = (myTask: Task) => {
navigate( navigate(
`/process/${modifyProcessIdentifierForPathParam( `/admin/process-instances/${modifyProcessIdentifierForPathParam(
myTask.process_model_identifier myTask.process_model_identifier
)}/${myTask.process_instance_id}/interstitial` )}/${myTask.process_instance_id}/interstitial`
); );