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:
$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}:
parameters:
- 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,
) -> flask.wrappers.Response:
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,
process_instance=process_instance,
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,
) -> flask.wrappers.Response:
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,
process_instance=process_instance,
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,
process_instance: ProcessInstanceModel,
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_name.label("bpmn_process_definition_name"), # type: ignore
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_name,
TaskDefinitionModel.typename,
@ -462,6 +457,7 @@ def process_instance_task_list(
TaskModel.end_in_seconds,
TaskModel.start_in_seconds,
TaskModel.runtime_info,
TaskModel.properties_json,
)
)
@ -470,7 +466,7 @@ def process_instance_task_list(
task_models = task_model_query.all()
if most_recent_tasks_only:
most_recent_tasks = {}
most_recent_tasks: dict[str, TaskModel] = {}
additional_tasks = []
# 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]
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
# 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"]:
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):

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.task import Task
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 TaskDraftDataModel
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(
modified_process_model_identifier: str,
process_instance_id: int,

View File

@ -1,4 +1,5 @@
import json
from uuid import UUID
from flask.app import Flask
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.services.authorization_service import AuthorizationService
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.test_data import load_test_spec
class TestTasksController(BaseTest):
@ -529,3 +532,76 @@ class TestTasksController(BaseTest):
cookie = headers_dict["Set-Cookie"]
access_token = cookie.split(";")[0].split("=")[1]
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',
'no-console': 'off',
'react/jsx-filename-extension': [
1,
'warn',
{ extensions: ['.js', '.jsx', '.tsx', '.ts'] },
],
'react/react-in-jsx-scope': 'off',
@ -71,5 +71,6 @@ module.exports = {
tsx: 'never',
},
],
curly: ['error', 'all'],
},
};

View File

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

View File

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

View File

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

View File

@ -5,11 +5,11 @@ import { KeyboardShortcuts } from '../interfaces';
export const overrideSystemHandling = (e: KeyboardEvent) => {
if (e) {
if (e.preventDefault) e.preventDefault();
if (e.preventDefault) {
e.preventDefault();
}
if (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;
}
.selected-task-instance {
color: #b0b0b0;
font-style: italic;
}
h1 {
font-weight: 400;
font-size: 28px;
@ -922,3 +927,11 @@ div.onboarding {
float: right; /* Floats the keys to the right */
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 {
useParams,
@ -22,6 +22,7 @@ import {
TrashCan,
Warning,
Link as LinkIcon,
View,
} from '@carbon/icons-react';
import {
Accordion,
@ -29,7 +30,6 @@ import {
Grid,
Column,
Button,
ButtonSet,
Tag,
Modal,
Dropdown,
@ -51,6 +51,9 @@ import {
truncateString,
unModifyProcessIdentifierForPathParam,
setPageTitle,
MULTI_INSTANCE_TASK_TYPES,
LOOP_TASK_TYPES,
titleizeString,
} from '../helpers';
import ButtonWithConfirmation from '../components/ButtonWithConfirmation';
import { useUriListForPermissions } from '../hooks/UriListForPermissions';
@ -101,6 +104,9 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
null
);
const [taskDataToDisplay, setTaskDataToDisplay] = useState<string>('');
const [taskInstancesToDisplay, setTaskInstancesToDisplay] = useState<Task[]>(
[]
);
const [showTaskDataLoading, setShowTaskDataLoading] =
useState<boolean>(false);
@ -620,6 +626,22 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
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) => {
if (result == null) {
setTaskDataToDisplay('');
@ -758,6 +780,7 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
if (matchingTask) {
setTaskToDisplay(matchingTask);
initializeTaskDataToDisplay(matchingTask);
initializeTaskInstancesToDisplay(matchingTask);
}
}
};
@ -767,6 +790,7 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
setSelectingEvent(false);
setAddingPotentialOwners(false);
initializeTaskDataToDisplay(taskToDisplay);
initializeTaskInstancesToDisplay(taskToDisplay);
setEventPayload('{}');
setAdditionalPotentialOwners(null);
removeError();
@ -775,6 +799,7 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
const handleTaskDataDisplayClose = () => {
setTaskToDisplay(null);
initializeTaskDataToDisplay(null);
initializeTaskInstancesToDisplay(null);
if (editingTaskData || selectingEvent || addingPotentialOwners) {
resetTaskActionDetails();
}
@ -896,11 +921,14 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
};
const eventDefinition =
task.task_definition_properties_json.event_definition;
if (eventDefinition && eventDefinition.event_definitions)
if (eventDefinition && eventDefinition.event_definitions) {
return eventDefinition.event_definitions.map((e: EventDefinition) =>
handleMessage(e)
);
if (eventDefinition) return [handleMessage(eventDefinition)];
}
if (eventDefinition) {
return [handleMessage(eventDefinition)];
}
return [];
};
@ -962,8 +990,9 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
};
const sendEvent = () => {
if ('payload' in eventToSend)
if ('payload' in eventToSend) {
eventToSend.payload = JSON.parse(eventPayload);
}
HttpService.makeCallToBackend({
path: targetUris.processInstanceSendEventPath,
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 = () => {
if (!taskToDisplay) {
return null;
@ -1249,95 +1285,138 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
return dataArea;
};
const switchToTask = (taskId: string) => {
if (tasks) {
const task = tasks.find((t: Task) => t.guid === taskId);
const switchToTask = (taskGuid: string, taskListToUse: Task[] | null) => {
if (taskListToUse && taskToDisplay) {
// 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) {
setTaskToDisplay(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 = () => {
if (!taskToDisplay || !taskToDisplay.runtime_info) {
const createButtonsForMultiTasks = (instances: number[]) => {
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;
}
const clickAction = (item: any) => {
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>
);
};
const accordionItems = [];
if (
taskToDisplay.typename === 'ParallelMultiInstanceTask' ||
taskToDisplay.typename === 'SequentialMultiInstanceTask'
!taskIsInstanceOfMultiInstanceTask(taskToDisplay) &&
taskInstancesToDisplay.length > 0
) {
let completedInstances = null;
if (taskToDisplay.runtime_info.completed.length > 0) {
completedInstances = createButtonSet(
taskToDisplay.runtime_info.completed
accordionItems.push(
<AccordionItem
title={`Task instances (${taskInstancesToDisplay.length})`}
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 (
<Accordion>
<AccordionItem title="Completed instances">
{completedInstances}
if (MULTI_INSTANCE_TASK_TYPES.includes(taskToDisplay.typename)) {
['completed', 'running', 'future'].forEach((infoType: string) => {
let taskInstances: ReactElement[] = [];
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 title="Running instances">
{runningInstances}
</AccordionItem>
<AccordionItem title="Future instances">
{futureInstances}
</AccordionItem>
</Accordion>
);
}
if (taskToDisplay.typename === 'StandardLoopTask') {
const buttons = [];
for (
let i = 0;
i < taskToDisplay.runtime_info.iterations_completed;
i += 1
)
buttons.push(
<Button kind="ghost" onClick={clickAction(i)}>
{i}
</Button>
);
let text = 'Loop iterations';
});
}
if (LOOP_TASK_TYPES.includes(taskToDisplay.typename)) {
const loopTaskInstanceIndexes = [
...Array(taskToDisplay.runtime_info.iterations_completed).keys(),
];
const buttons = createButtonsForMultiTasks(loopTaskInstanceIndexes);
let text = '';
if (
typeof taskToDisplay.runtime_info.iterations_remaining !== 'undefined'
)
text += ` (${taskToDisplay.runtime_info.iterations_remaining} remaining)`;
return (
<div>
typeof taskToDisplay.runtime_info.iterations_remaining !==
'undefined' &&
taskToDisplay.state !== 'COMPLETED'
) {
text += `${taskToDisplay.runtime_info.iterations_remaining} remaining`;
}
accordionItems.push(
<AccordionItem title={`Loop iterations (${buttons.length})`}>
<div>{text}</div>
<div>{buttons}</div>
</div>
</AccordionItem>
);
}
if (accordionItems.length > 0) {
return <Accordion size="lg">{accordionItems}</Accordion>;
}
return null;
};
@ -1375,12 +1454,18 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
if (typeof taskToUse.runtime_info.instance !== 'undefined') {
secondaryButtonText = 'Return to MultiInstance Task';
onSecondarySubmit = () => {
switchToTask(taskToUse.properties_json.parent);
switchToTask(taskToUse.properties_json.parent, [
...(tasks || []),
...taskInstancesToDisplay,
]);
};
} else if (typeof taskToUse.runtime_info.iteration !== 'undefined') {
secondaryButtonText = 'Return to Loop Task';
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}
onSecondarySubmit={onSecondarySubmit}
onRequestSubmit={onRequestSubmit}
modalHeading={`${taskToUse.bpmn_identifier} (${taskToUse.typename}
): ${taskToUse.state}`}
modalHeading={`${taskToUse.bpmn_identifier} (${taskToUse.typename}): ${taskToUse.state}`}
>
<div className="indented-content explanatory-message">
{taskToUse.bpmn_name ? (
@ -1414,7 +1498,7 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
</div>
{taskDisplayButtons(taskToUse)}
{taskToUse.state === 'COMPLETED' ? (
<div>
<div className="indented-content">
<Stack orientation="horizontal" gap={2}>
{completionViewLink(
'View process instance at the time when this task was active.',
@ -1422,11 +1506,11 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
)}
</Stack>
<br />
<br />
</div>
) : null}
<br />
{taskActionDetails()}
{multiInstanceSelector()}
{taskInstanceSelector()}
</Modal>
);
};