Feature/pi show page diagram (#816)

* get most recent tasks based on last_state_change instead of task_model.id

* added api to get task instances of a task

* some changes to support displaying task instances

* forgot to commit the controller

* updated frontend to display info for other instances of a task w/ burnettk

* some formatting to the selected task instance w/ burnettk

* do not get task instances when selecting different instance w/ burnettk

* added tests for task-instances w/ burnettk

* some ui tweaks for task instance view w/ burnettk

* updates based on coderabbit

---------

Co-authored-by: jasquat <jasquat@users.noreply.github.com>
This commit is contained in:
jasquat 2023-12-18 14:23:51 -05:00 committed by GitHub
parent 4cb2fa66aa
commit 15d0d788e5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 347 additions and 100 deletions

View File

@ -2253,6 +2253,35 @@ paths:
schema: schema:
$ref: "#/components/schemas/Task" $ref: "#/components/schemas/Task"
/tasks/{process_instance_id}/{task_guid}/task-instances:
parameters:
- name: task_guid
in: path
required: true
description: The unique id of an existing process group.
schema:
type: string
- name: process_instance_id
in: path
required: true
description: The unique id of an existing process instance.
schema:
type: integer
get:
tags:
- Tasks
operationId: spiffworkflow_backend.routes.tasks_controller.task_instance_list
summary: Gets all tasks that have the same bpmn identifier and are in the same bpmn process as the given task guid.
responses:
"200":
description: All relevant tasks
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/Task"
/tasks/{process_instance_id}/{task_guid}: /tasks/{process_instance_id}/{task_guid}:
parameters: parameters:
- name: task_guid - name: task_guid

View File

@ -334,7 +334,7 @@ def process_instance_task_list_without_task_data_for_me(
to_task_guid: str | None = None, to_task_guid: str | None = None,
) -> flask.wrappers.Response: ) -> flask.wrappers.Response:
process_instance = _find_process_instance_for_me_or_raise(process_instance_id) process_instance = _find_process_instance_for_me_or_raise(process_instance_id)
return process_instance_task_list( return _process_instance_task_list(
_modified_process_model_identifier=modified_process_model_identifier, _modified_process_model_identifier=modified_process_model_identifier,
process_instance=process_instance, process_instance=process_instance,
most_recent_tasks_only=most_recent_tasks_only, most_recent_tasks_only=most_recent_tasks_only,
@ -351,7 +351,7 @@ def process_instance_task_list_without_task_data(
to_task_guid: str | None = None, to_task_guid: str | None = None,
) -> flask.wrappers.Response: ) -> flask.wrappers.Response:
process_instance = _find_process_instance_by_id_or_raise(process_instance_id) process_instance = _find_process_instance_by_id_or_raise(process_instance_id)
return process_instance_task_list( return _process_instance_task_list(
_modified_process_model_identifier=modified_process_model_identifier, _modified_process_model_identifier=modified_process_model_identifier,
process_instance=process_instance, process_instance=process_instance,
most_recent_tasks_only=most_recent_tasks_only, most_recent_tasks_only=most_recent_tasks_only,
@ -360,7 +360,7 @@ def process_instance_task_list_without_task_data(
) )
def process_instance_task_list( def _process_instance_task_list(
_modified_process_model_identifier: str, _modified_process_model_identifier: str,
process_instance: ProcessInstanceModel, process_instance: ProcessInstanceModel,
bpmn_process_guid: str | None = None, bpmn_process_guid: str | None = None,
@ -448,11 +448,6 @@ def process_instance_task_list(
BpmnProcessDefinitionModel.bpmn_identifier.label("bpmn_process_definition_identifier"), # type: ignore BpmnProcessDefinitionModel.bpmn_identifier.label("bpmn_process_definition_identifier"), # type: ignore
BpmnProcessDefinitionModel.bpmn_name.label("bpmn_process_definition_name"), # type: ignore BpmnProcessDefinitionModel.bpmn_name.label("bpmn_process_definition_name"), # type: ignore
bpmn_process_alias.guid.label("bpmn_process_guid"), bpmn_process_alias.guid.label("bpmn_process_guid"),
# not sure why we needed these
# direct_parent_bpmn_process_alias.guid.label("bpmn_process_direct_parent_guid"),
# direct_parent_bpmn_process_definition_alias.bpmn_identifier.label(
# "bpmn_process_direct_parent_bpmn_identifier"
# ),
TaskDefinitionModel.bpmn_identifier, TaskDefinitionModel.bpmn_identifier,
TaskDefinitionModel.bpmn_name, TaskDefinitionModel.bpmn_name,
TaskDefinitionModel.typename, TaskDefinitionModel.typename,
@ -462,6 +457,7 @@ def process_instance_task_list(
TaskModel.end_in_seconds, TaskModel.end_in_seconds,
TaskModel.start_in_seconds, TaskModel.start_in_seconds,
TaskModel.runtime_info, TaskModel.runtime_info,
TaskModel.properties_json,
) )
) )
@ -470,7 +466,7 @@ def process_instance_task_list(
task_models = task_model_query.all() task_models = task_model_query.all()
if most_recent_tasks_only: if most_recent_tasks_only:
most_recent_tasks = {} most_recent_tasks: dict[str, TaskModel] = {}
additional_tasks = [] additional_tasks = []
# if you have a loop and there is a subprocess, and you are going around for the second time, # if you have a loop and there is a subprocess, and you are going around for the second time,
@ -487,8 +483,15 @@ def process_instance_task_list(
full_bpmn_process_path = bpmn_process_cache[task_model.bpmn_process_guid] full_bpmn_process_path = bpmn_process_cache[task_model.bpmn_process_guid]
row_key = f"{':::'.join(full_bpmn_process_path)}:::{task_model.bpmn_identifier}" row_key = f"{':::'.join(full_bpmn_process_path)}:::{task_model.bpmn_identifier}"
if row_key not in most_recent_tasks: if (
row_key not in most_recent_tasks
or most_recent_tasks[row_key].properties_json["last_state_change"]
< task_model.properties_json["last_state_change"]
):
most_recent_tasks[row_key] = task_model most_recent_tasks[row_key] = task_model
# we may need to remove guids for tasks that are no longer considered most recent but that may not matter
# since any task like would no longer be in the list anyway and therefore will not be returned
if task_model.typename in ["SubWorkflowTask", "CallActivity"]: if task_model.typename in ["SubWorkflowTask", "CallActivity"]:
relevant_subprocess_guids.add(task_model.guid) relevant_subprocess_guids.add(task_model.guid)
elif task_model.runtime_info and ("instance" in task_model.runtime_info or "iteration" in task_model.runtime_info): elif task_model.runtime_info and ("instance" in task_model.runtime_info or "iteration" in task_model.runtime_info):

View File

@ -52,6 +52,7 @@ from spiffworkflow_backend.models.process_instance_event import ProcessInstanceE
from spiffworkflow_backend.models.process_model import ProcessModelInfo from spiffworkflow_backend.models.process_model import ProcessModelInfo
from spiffworkflow_backend.models.task import Task from spiffworkflow_backend.models.task import Task
from spiffworkflow_backend.models.task import TaskModel from spiffworkflow_backend.models.task import TaskModel
from spiffworkflow_backend.models.task_definition import TaskDefinitionModel
from spiffworkflow_backend.models.task_draft_data import TaskDraftDataDict from spiffworkflow_backend.models.task_draft_data import TaskDraftDataDict
from spiffworkflow_backend.models.task_draft_data import TaskDraftDataModel from spiffworkflow_backend.models.task_draft_data import TaskDraftDataModel
from spiffworkflow_backend.models.task_instructions_for_end_user import TaskInstructionsForEndUserModel from spiffworkflow_backend.models.task_instructions_for_end_user import TaskInstructionsForEndUserModel
@ -312,6 +313,31 @@ def task_data_update(
) )
def task_instance_list(
process_instance_id: int,
task_guid: str,
) -> Response:
task_model = _get_task_model_from_guid_or_raise(task_guid, process_instance_id)
task_model_instances = (
TaskModel.query.filter_by(task_definition_id=task_model.task_definition.id, bpmn_process_id=task_model.bpmn_process_id)
.order_by(TaskModel.id.desc()) # type: ignore
.join(TaskDefinitionModel, TaskDefinitionModel.id == TaskModel.task_definition_id)
.add_columns(
TaskDefinitionModel.bpmn_identifier,
TaskDefinitionModel.bpmn_name,
TaskDefinitionModel.typename,
TaskDefinitionModel.properties_json.label("task_definition_properties_json"), # type: ignore
TaskModel.guid,
TaskModel.state,
TaskModel.end_in_seconds,
TaskModel.start_in_seconds,
TaskModel.runtime_info,
TaskModel.properties_json,
)
).all()
return make_response(jsonify(task_model_instances), 200)
def manual_complete_task( def manual_complete_task(
modified_process_model_identifier: str, modified_process_model_identifier: str,
process_instance_id: int, process_instance_id: int,

View File

@ -1,4 +1,5 @@
import json import json
from uuid import UUID
from flask.app import Flask from flask.app import Flask
from flask.testing import FlaskClient from flask.testing import FlaskClient
@ -14,8 +15,10 @@ from spiffworkflow_backend.models.user import UserModel
from spiffworkflow_backend.routes.tasks_controller import _dequeued_interstitial_stream from spiffworkflow_backend.routes.tasks_controller import _dequeued_interstitial_stream
from spiffworkflow_backend.services.authorization_service import AuthorizationService from spiffworkflow_backend.services.authorization_service import AuthorizationService
from spiffworkflow_backend.services.process_instance_processor import ProcessInstanceProcessor from spiffworkflow_backend.services.process_instance_processor import ProcessInstanceProcessor
from spiffworkflow_backend.services.process_instance_service import ProcessInstanceService
from tests.spiffworkflow_backend.helpers.base_test import BaseTest from tests.spiffworkflow_backend.helpers.base_test import BaseTest
from tests.spiffworkflow_backend.helpers.test_data import load_test_spec
class TestTasksController(BaseTest): class TestTasksController(BaseTest):
@ -529,3 +532,76 @@ class TestTasksController(BaseTest):
cookie = headers_dict["Set-Cookie"] cookie = headers_dict["Set-Cookie"]
access_token = cookie.split(";")[0].split("=")[1] access_token = cookie.split(";")[0].split("=")[1]
assert access_token == "" assert access_token == ""
def test_task_instance_list(
self,
app: Flask,
client: FlaskClient,
with_db_and_bpmn_file_cleanup: None,
with_super_admin_user: UserModel,
) -> None:
process_model = load_test_spec(
process_model_id="test_group/loopback_to_manual_task",
process_model_source_directory="loopback_to_manual_task",
)
process_instance = self.create_process_instance_from_process_model(process_model=process_model)
processor = ProcessInstanceProcessor(process_instance)
processor.do_engine_steps(save=True, execution_strategy_name="greedy")
human_task_one = process_instance.active_human_tasks[0]
spiff_manual_task = processor.bpmn_process_instance.get_task_from_id(UUID(human_task_one.task_id))
ProcessInstanceService.complete_form_task(
processor, spiff_manual_task, {}, process_instance.process_initiator, human_task_one
)
human_task_one = process_instance.active_human_tasks[0]
spiff_manual_task = processor.bpmn_process_instance.get_task_from_id(UUID(human_task_one.task_id))
ProcessInstanceService.complete_form_task(
processor, spiff_manual_task, {}, process_instance.process_initiator, human_task_one
)
assert process_instance.status == ProcessInstanceStatus.user_input_required.value
response = client.get(
f"/v1.0/tasks/{process_instance.id}/{human_task_one.task_id}/task-instances",
headers=self.logged_in_headers(with_super_admin_user),
)
assert response.status_code == 200
assert response.content_type == "application/json"
assert isinstance(response.json, list)
expected_states = sorted(["COMPLETED", "COMPLETED", "MAYBE", "READY"])
actual_states = sorted([t["state"] for t in response.json])
assert actual_states == expected_states
def test_task_instance_list_returns_only_for_same_bpmn_process(
self,
app: Flask,
client: FlaskClient,
with_db_and_bpmn_file_cleanup: None,
with_super_admin_user: UserModel,
) -> None:
process_model = load_test_spec(
process_model_id="test_group/loopback_to_subprocess",
process_model_source_directory="loopback_to_subprocess",
)
process_instance = self.create_process_instance_from_process_model(process_model=process_model)
processor = ProcessInstanceProcessor(process_instance)
processor.do_engine_steps(save=True, execution_strategy_name="greedy")
human_task_one = process_instance.active_human_tasks[0]
spiff_manual_task = processor.bpmn_process_instance.get_task_from_id(UUID(human_task_one.task_id))
ProcessInstanceService.complete_form_task(
processor, spiff_manual_task, {}, process_instance.process_initiator, human_task_one
)
human_task_one = process_instance.active_human_tasks[0]
spiff_manual_task = processor.bpmn_process_instance.get_task_from_id(UUID(human_task_one.task_id))
ProcessInstanceService.complete_form_task(
processor, spiff_manual_task, {}, process_instance.process_initiator, human_task_one
)
assert process_instance.status == ProcessInstanceStatus.user_input_required.value
response = client.get(
f"/v1.0/tasks/{process_instance.id}/{human_task_one.task_id}/task-instances",
headers=self.logged_in_headers(with_super_admin_user),
)
assert response.status_code == 200
assert response.content_type == "application/json"
assert isinstance(response.json, list)
assert len(response.json) == 1

View File

@ -36,7 +36,7 @@ module.exports = {
'jsx-a11y/label-has-associated-control': 'off', 'jsx-a11y/label-has-associated-control': 'off',
'no-console': 'off', 'no-console': 'off',
'react/jsx-filename-extension': [ 'react/jsx-filename-extension': [
1, 'warn',
{ extensions: ['.js', '.jsx', '.tsx', '.ts'] }, { extensions: ['.js', '.jsx', '.tsx', '.ts'] },
], ],
'react/react-in-jsx-scope': 'off', 'react/react-in-jsx-scope': 'off',
@ -71,5 +71,6 @@ module.exports = {
tsx: 'never', tsx: 'never',
}, },
], ],
curly: ['error', 'all'],
}, },
}; };

View File

@ -176,7 +176,9 @@ export default function ReactDiagramEditor({
// @ts-ignore // @ts-ignore
diagramModelerToUse.on('import.parse.complete', event => { // eslint-disable-line diagramModelerToUse.on('import.parse.complete', event => { // eslint-disable-line
// @ts-ignore // @ts-ignore
if (!event.references) return; if (!event.references) {
return;
}
const refs = event.references.filter( const refs = event.references.filter(
(r: any) => (r: any) =>
r.property === 'bpmn:loopDataInputRef' || r.property === 'bpmn:loopDataInputRef' ||
@ -676,7 +678,9 @@ export default function ReactDiagramEditor({
const getReferencesButton = () => { const getReferencesButton = () => {
if (callers && callers.length > 0) { if (callers && callers.length > 0) {
let buttonText = `View ${callers.length} Reference`; let buttonText = `View ${callers.length} Reference`;
if (callers.length > 1) buttonText += 's'; if (callers.length > 1) {
buttonText += 's';
}
return ( return (
<Button onClick={() => setShowingReferences(true)}>{buttonText}</Button> <Button onClick={() => setShowingReferences(true)}>{buttonText}</Button>
); );

View File

@ -10,7 +10,7 @@ export const doNothing = () => {
}; };
// https://www.30secondsofcode.org/js/s/slugify // https://www.30secondsofcode.org/js/s/slugify
export const slugifyString = (str: any) => { export const slugifyString = (str: string) => {
return str return str
.toLowerCase() .toLowerCase()
.trim() .trim()
@ -28,6 +28,13 @@ export const HUMAN_TASK_TYPES = [
'Task', 'Task',
]; ];
export const MULTI_INSTANCE_TASK_TYPES = [
'ParallelMultiInstanceTask',
'SequentialMultiInstanceTask',
];
export const LOOP_TASK_TYPES = ['StandardLoopTask'];
export const underscorizeString = (inputString: string) => { export const underscorizeString = (inputString: string) => {
return slugifyString(inputString).replace(/-/g, '_'); return slugifyString(inputString).replace(/-/g, '_');
}; };

View File

@ -19,7 +19,9 @@ export function useBlocker(blocker: any, when: any = true) {
const { navigator } = useContext(NavigationContext); const { navigator } = useContext(NavigationContext);
useEffect(() => { useEffect(() => {
if (!when) return null; if (!when) {
return undefined;
}
const unblock = (navigator as any).block((tx: any) => { const unblock = (navigator as any).block((tx: any) => {
const autoUnblockingTx = { const autoUnblockingTx = {
@ -49,7 +51,9 @@ export function usePrompt(message: any, when: any = true) {
const blocker = useCallback( const blocker = useCallback(
(tx: any) => { (tx: any) => {
// eslint-disable-next-line no-alert // eslint-disable-next-line no-alert
if (window.confirm(message)) tx.retry(); if (window.confirm(message)) {
tx.retry();
}
}, },
[message] [message]
); );

View File

@ -5,11 +5,11 @@ import { KeyboardShortcuts } from '../interfaces';
export const overrideSystemHandling = (e: KeyboardEvent) => { export const overrideSystemHandling = (e: KeyboardEvent) => {
if (e) { if (e) {
if (e.preventDefault) e.preventDefault(); if (e.preventDefault) {
e.preventDefault();
}
if (e.stopPropagation) { if (e.stopPropagation) {
e.stopPropagation(); e.stopPropagation();
} else if (window.event) {
window.event.cancelBubble = true;
} }
} }
}; };

View File

@ -41,6 +41,11 @@ a.cds--header__menu-item {
font-style: italic; font-style: italic;
} }
.selected-task-instance {
color: #b0b0b0;
font-style: italic;
}
h1 { h1 {
font-weight: 400; font-weight: 400;
font-size: 28px; font-size: 28px;
@ -922,3 +927,11 @@ div.onboarding {
float: right; /* Floats the keys to the right */ float: right; /* Floats the keys to the right */
text-align: right; /* Aligns text to the right within the container */ text-align: right; /* Aligns text to the right within the container */
} }
.task-info-modal-accordion .cds--accordion__content {
padding-right: 1rem;
}
.task-instance-modal-row-item {
height: 48px;
line-height: 48px;
}

View File

@ -1,4 +1,4 @@
import { useCallback, useEffect, useState } from 'react'; import { ReactElement, useCallback, useEffect, useState } from 'react';
import Editor from '@monaco-editor/react'; import Editor from '@monaco-editor/react';
import { import {
useParams, useParams,
@ -22,6 +22,7 @@ import {
TrashCan, TrashCan,
Warning, Warning,
Link as LinkIcon, Link as LinkIcon,
View,
} from '@carbon/icons-react'; } from '@carbon/icons-react';
import { import {
Accordion, Accordion,
@ -29,7 +30,6 @@ import {
Grid, Grid,
Column, Column,
Button, Button,
ButtonSet,
Tag, Tag,
Modal, Modal,
Dropdown, Dropdown,
@ -51,6 +51,9 @@ import {
truncateString, truncateString,
unModifyProcessIdentifierForPathParam, unModifyProcessIdentifierForPathParam,
setPageTitle, setPageTitle,
MULTI_INSTANCE_TASK_TYPES,
LOOP_TASK_TYPES,
titleizeString,
} from '../helpers'; } from '../helpers';
import ButtonWithConfirmation from '../components/ButtonWithConfirmation'; import ButtonWithConfirmation from '../components/ButtonWithConfirmation';
import { useUriListForPermissions } from '../hooks/UriListForPermissions'; import { useUriListForPermissions } from '../hooks/UriListForPermissions';
@ -101,6 +104,9 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
null null
); );
const [taskDataToDisplay, setTaskDataToDisplay] = useState<string>(''); const [taskDataToDisplay, setTaskDataToDisplay] = useState<string>('');
const [taskInstancesToDisplay, setTaskInstancesToDisplay] = useState<Task[]>(
[]
);
const [showTaskDataLoading, setShowTaskDataLoading] = const [showTaskDataLoading, setShowTaskDataLoading] =
useState<boolean>(false); useState<boolean>(false);
@ -620,6 +626,22 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
return <div />; return <div />;
}; };
const initializeTaskInstancesToDisplay = (task: Task | null) => {
if (!task) {
return;
}
HttpService.makeCallToBackend({
path: `/tasks/${params.process_instance_id}/${task.guid}/task-instances`,
httpMethod: 'GET',
// reverse operates on self as well as return the new ordered array so reverse it right away
successCallback: (results: Task[]) =>
setTaskInstancesToDisplay(results.reverse()),
failureCallback: (error: any) => {
setTaskDataToDisplay(`ERROR: ${error.message}`);
},
});
};
const processTaskResult = (result: Task) => { const processTaskResult = (result: Task) => {
if (result == null) { if (result == null) {
setTaskDataToDisplay(''); setTaskDataToDisplay('');
@ -758,6 +780,7 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
if (matchingTask) { if (matchingTask) {
setTaskToDisplay(matchingTask); setTaskToDisplay(matchingTask);
initializeTaskDataToDisplay(matchingTask); initializeTaskDataToDisplay(matchingTask);
initializeTaskInstancesToDisplay(matchingTask);
} }
} }
}; };
@ -767,6 +790,7 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
setSelectingEvent(false); setSelectingEvent(false);
setAddingPotentialOwners(false); setAddingPotentialOwners(false);
initializeTaskDataToDisplay(taskToDisplay); initializeTaskDataToDisplay(taskToDisplay);
initializeTaskInstancesToDisplay(taskToDisplay);
setEventPayload('{}'); setEventPayload('{}');
setAdditionalPotentialOwners(null); setAdditionalPotentialOwners(null);
removeError(); removeError();
@ -775,6 +799,7 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
const handleTaskDataDisplayClose = () => { const handleTaskDataDisplayClose = () => {
setTaskToDisplay(null); setTaskToDisplay(null);
initializeTaskDataToDisplay(null); initializeTaskDataToDisplay(null);
initializeTaskInstancesToDisplay(null);
if (editingTaskData || selectingEvent || addingPotentialOwners) { if (editingTaskData || selectingEvent || addingPotentialOwners) {
resetTaskActionDetails(); resetTaskActionDetails();
} }
@ -896,11 +921,14 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
}; };
const eventDefinition = const eventDefinition =
task.task_definition_properties_json.event_definition; task.task_definition_properties_json.event_definition;
if (eventDefinition && eventDefinition.event_definitions) if (eventDefinition && eventDefinition.event_definitions) {
return eventDefinition.event_definitions.map((e: EventDefinition) => return eventDefinition.event_definitions.map((e: EventDefinition) =>
handleMessage(e) handleMessage(e)
); );
if (eventDefinition) return [handleMessage(eventDefinition)]; }
if (eventDefinition) {
return [handleMessage(eventDefinition)];
}
return []; return [];
}; };
@ -962,8 +990,9 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
}; };
const sendEvent = () => { const sendEvent = () => {
if ('payload' in eventToSend) if ('payload' in eventToSend) {
eventToSend.payload = JSON.parse(eventPayload); eventToSend.payload = JSON.parse(eventPayload);
}
HttpService.makeCallToBackend({ HttpService.makeCallToBackend({
path: targetUris.processInstanceSendEventPath, path: targetUris.processInstanceSendEventPath,
httpMethod: 'POST', httpMethod: 'POST',
@ -1235,6 +1264,13 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
); );
}; };
const taskIsInstanceOfMultiInstanceTask = (task: Task) => {
// this is the same check made in the backend in the _process_instance_task_list method to determine
// if the given task is an instance of a multi-instance or loop task.
// we need to avoid resetting the task instance list since the list may not be the same as we need
return 'instance' in task.runtime_info || 'iteration' in task.runtime_info;
};
const taskActionDetails = () => { const taskActionDetails = () => {
if (!taskToDisplay) { if (!taskToDisplay) {
return null; return null;
@ -1249,95 +1285,138 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
return dataArea; return dataArea;
}; };
const switchToTask = (taskId: string) => { const switchToTask = (taskGuid: string, taskListToUse: Task[] | null) => {
if (tasks) { if (taskListToUse && taskToDisplay) {
const task = tasks.find((t: Task) => t.guid === taskId); // set to null right away to hopefully avoid using the incorrect task later
setTaskToDisplay(null);
const task = taskListToUse.find((t: Task) => t.guid === taskGuid);
if (task) { if (task) {
setTaskToDisplay(task); setTaskToDisplay(task);
initializeTaskDataToDisplay(task); initializeTaskDataToDisplay(task);
} }
} }
}; };
const createButtonSetForTaskInstances = () => {
if (taskInstancesToDisplay.length === 0 || !taskToDisplay) {
return null;
}
return (
<>
{taskInstancesToDisplay.map((task: Task, index: number) => {
const buttonClass =
task.guid === taskToDisplay.guid ? 'selected-task-instance' : null;
return (
<Grid condensed fullWidth className={buttonClass}>
<Column md={1} lg={2} sm={1}>
<Button
kind="ghost"
renderIcon={View}
iconDescription="View"
tooltipPosition="right"
hasIconOnly
onClick={() =>
switchToTask(task.guid, taskInstancesToDisplay)
}
>
View
</Button>
</Column>
<Column md={7} lg={14} sm={3}>
<div className="task-instance-modal-row-item">
{index + 1} {': '}
{DateAndTimeService.convertSecondsToFormattedDateTime(
task.properties_json.last_state_change
)}{' '}
{' - '} {task.state}
</div>
</Column>
</Grid>
);
})}
</>
);
};
const multiInstanceSelector = () => { const createButtonsForMultiTasks = (instances: number[]) => {
if (!taskToDisplay || !taskToDisplay.runtime_info) { if (!tasks || !taskToDisplay) {
return [];
}
return instances.map((v: any) => {
return (
<Button
kind="ghost"
onClick={() =>
switchToTask(taskToDisplay.runtime_info.instance_map[v], tasks)
}
>
{v + 1}
</Button>
);
});
};
const taskInstanceSelector = () => {
if (!taskToDisplay) {
return null; return null;
} }
const clickAction = (item: any) => { const accordionItems = [];
return () => {
switchToTask(taskToDisplay.runtime_info.instance_map[item]);
};
};
const createButtonSet = (instances: string[]) => {
return (
<ButtonSet stacked>
{instances.map((v: any) => (
<Button kind="ghost" onClick={clickAction(v)}>
{v}
</Button>
))}
</ButtonSet>
);
};
if ( if (
taskToDisplay.typename === 'ParallelMultiInstanceTask' || !taskIsInstanceOfMultiInstanceTask(taskToDisplay) &&
taskToDisplay.typename === 'SequentialMultiInstanceTask' taskInstancesToDisplay.length > 0
) { ) {
let completedInstances = null; accordionItems.push(
if (taskToDisplay.runtime_info.completed.length > 0) { <AccordionItem
completedInstances = createButtonSet( title={`Task instances (${taskInstancesToDisplay.length})`}
taskToDisplay.runtime_info.completed className="task-info-modal-accordion"
>
{createButtonSetForTaskInstances()}
</AccordionItem>
); );
} }
let runningInstances = null;
if (taskToDisplay.runtime_info.running.length > 0) {
runningInstances = createButtonSet(taskToDisplay.runtime_info.running);
}
let futureInstances = null;
if (taskToDisplay.runtime_info.future.length > 0) {
futureInstances = createButtonSet(taskToDisplay.runtime_info.future);
}
return ( if (MULTI_INSTANCE_TASK_TYPES.includes(taskToDisplay.typename)) {
<Accordion> ['completed', 'running', 'future'].forEach((infoType: string) => {
<AccordionItem title="Completed instances"> let taskInstances: ReactElement[] = [];
{completedInstances} const infoArray = taskToDisplay.runtime_info[infoType];
if (taskToDisplay.runtime_info.completed.length > 0) {
taskInstances = createButtonsForMultiTasks(infoArray);
accordionItems.push(
<AccordionItem
title={`${titleizeString(infoType)} instances for MI task (${
taskInstances.length
})`}
>
{taskInstances}
</AccordionItem> </AccordionItem>
<AccordionItem title="Running instances">
{runningInstances}
</AccordionItem>
<AccordionItem title="Future instances">
{futureInstances}
</AccordionItem>
</Accordion>
); );
} }
if (taskToDisplay.typename === 'StandardLoopTask') { });
const buttons = []; }
for ( if (LOOP_TASK_TYPES.includes(taskToDisplay.typename)) {
let i = 0; const loopTaskInstanceIndexes = [
i < taskToDisplay.runtime_info.iterations_completed; ...Array(taskToDisplay.runtime_info.iterations_completed).keys(),
i += 1 ];
) const buttons = createButtonsForMultiTasks(loopTaskInstanceIndexes);
buttons.push( let text = '';
<Button kind="ghost" onClick={clickAction(i)}>
{i}
</Button>
);
let text = 'Loop iterations';
if ( if (
typeof taskToDisplay.runtime_info.iterations_remaining !== 'undefined' typeof taskToDisplay.runtime_info.iterations_remaining !==
) 'undefined' &&
text += ` (${taskToDisplay.runtime_info.iterations_remaining} remaining)`; taskToDisplay.state !== 'COMPLETED'
return ( ) {
<div> text += `${taskToDisplay.runtime_info.iterations_remaining} remaining`;
}
accordionItems.push(
<AccordionItem title={`Loop iterations (${buttons.length})`}>
<div>{text}</div> <div>{text}</div>
<div>{buttons}</div> <div>{buttons}</div>
</div> </AccordionItem>
); );
} }
if (accordionItems.length > 0) {
return <Accordion size="lg">{accordionItems}</Accordion>;
}
return null; return null;
}; };
@ -1375,12 +1454,18 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
if (typeof taskToUse.runtime_info.instance !== 'undefined') { if (typeof taskToUse.runtime_info.instance !== 'undefined') {
secondaryButtonText = 'Return to MultiInstance Task'; secondaryButtonText = 'Return to MultiInstance Task';
onSecondarySubmit = () => { onSecondarySubmit = () => {
switchToTask(taskToUse.properties_json.parent); switchToTask(taskToUse.properties_json.parent, [
...(tasks || []),
...taskInstancesToDisplay,
]);
}; };
} else if (typeof taskToUse.runtime_info.iteration !== 'undefined') { } else if (typeof taskToUse.runtime_info.iteration !== 'undefined') {
secondaryButtonText = 'Return to Loop Task'; secondaryButtonText = 'Return to Loop Task';
onSecondarySubmit = () => { onSecondarySubmit = () => {
switchToTask(taskToUse.properties_json.parent); switchToTask(taskToUse.properties_json.parent, [
...(tasks || []),
...taskInstancesToDisplay,
]);
}; };
} }
} }
@ -1394,8 +1479,7 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
onRequestClose={handleTaskDataDisplayClose} onRequestClose={handleTaskDataDisplayClose}
onSecondarySubmit={onSecondarySubmit} onSecondarySubmit={onSecondarySubmit}
onRequestSubmit={onRequestSubmit} onRequestSubmit={onRequestSubmit}
modalHeading={`${taskToUse.bpmn_identifier} (${taskToUse.typename} modalHeading={`${taskToUse.bpmn_identifier} (${taskToUse.typename}): ${taskToUse.state}`}
): ${taskToUse.state}`}
> >
<div className="indented-content explanatory-message"> <div className="indented-content explanatory-message">
{taskToUse.bpmn_name ? ( {taskToUse.bpmn_name ? (
@ -1414,7 +1498,7 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
</div> </div>
{taskDisplayButtons(taskToUse)} {taskDisplayButtons(taskToUse)}
{taskToUse.state === 'COMPLETED' ? ( {taskToUse.state === 'COMPLETED' ? (
<div> <div className="indented-content">
<Stack orientation="horizontal" gap={2}> <Stack orientation="horizontal" gap={2}>
{completionViewLink( {completionViewLink(
'View process instance at the time when this task was active.', 'View process instance at the time when this task was active.',
@ -1422,11 +1506,11 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
)} )}
</Stack> </Stack>
<br /> <br />
<br />
</div> </div>
) : null} ) : null}
<br />
{taskActionDetails()} {taskActionDetails()}
{multiInstanceSelector()} {taskInstanceSelector()}
</Modal> </Modal>
); );
}; };