diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/api.yml b/spiffworkflow-backend/src/spiffworkflow_backend/api.yml index 8c17ab4e5..f40d31eba 100755 --- a/spiffworkflow-backend/src/spiffworkflow_backend/api.yml +++ b/spiffworkflow-backend/src/spiffworkflow_backend/api.yml @@ -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 diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_instances_controller.py b/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_instances_controller.py index a7fee495a..bb95b720c 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_instances_controller.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_instances_controller.py @@ -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): diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/routes/tasks_controller.py b/spiffworkflow-backend/src/spiffworkflow_backend/routes/tasks_controller.py index ba359678c..063078429 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/routes/tasks_controller.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/routes/tasks_controller.py @@ -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, diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_tasks_controller.py b/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_tasks_controller.py index 013530440..a2518bbf8 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_tasks_controller.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_tasks_controller.py @@ -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 diff --git a/spiffworkflow-frontend/.eslintrc.js b/spiffworkflow-frontend/.eslintrc.js index a7a64af3a..1c66bd3c9 100644 --- a/spiffworkflow-frontend/.eslintrc.js +++ b/spiffworkflow-frontend/.eslintrc.js @@ -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'], }, }; diff --git a/spiffworkflow-frontend/src/components/ReactDiagramEditor.tsx b/spiffworkflow-frontend/src/components/ReactDiagramEditor.tsx index 35e6f1cd2..af0c4dbbe 100644 --- a/spiffworkflow-frontend/src/components/ReactDiagramEditor.tsx +++ b/spiffworkflow-frontend/src/components/ReactDiagramEditor.tsx @@ -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 ( ); diff --git a/spiffworkflow-frontend/src/helpers.tsx b/spiffworkflow-frontend/src/helpers.tsx index 5190c2265..112577d7c 100644 --- a/spiffworkflow-frontend/src/helpers.tsx +++ b/spiffworkflow-frontend/src/helpers.tsx @@ -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, '_'); }; diff --git a/spiffworkflow-frontend/src/hooks/UsePrompt.tsx b/spiffworkflow-frontend/src/hooks/UsePrompt.tsx index 63b873616..f2a8a05e2 100644 --- a/spiffworkflow-frontend/src/hooks/UsePrompt.tsx +++ b/spiffworkflow-frontend/src/hooks/UsePrompt.tsx @@ -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] ); diff --git a/spiffworkflow-frontend/src/hooks/useKeyboardShortcut.tsx b/spiffworkflow-frontend/src/hooks/useKeyboardShortcut.tsx index bd62b5b95..40018e6d5 100644 --- a/spiffworkflow-frontend/src/hooks/useKeyboardShortcut.tsx +++ b/spiffworkflow-frontend/src/hooks/useKeyboardShortcut.tsx @@ -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; } } }; diff --git a/spiffworkflow-frontend/src/index.css b/spiffworkflow-frontend/src/index.css index b23447232..c026019c1 100644 --- a/spiffworkflow-frontend/src/index.css +++ b/spiffworkflow-frontend/src/index.css @@ -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; +} diff --git a/spiffworkflow-frontend/src/routes/ProcessInstanceShow.tsx b/spiffworkflow-frontend/src/routes/ProcessInstanceShow.tsx index fe47ff08c..1f9223622 100644 --- a/spiffworkflow-frontend/src/routes/ProcessInstanceShow.tsx +++ b/spiffworkflow-frontend/src/routes/ProcessInstanceShow.tsx @@ -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(''); + const [taskInstancesToDisplay, setTaskInstancesToDisplay] = useState( + [] + ); const [showTaskDataLoading, setShowTaskDataLoading] = useState(false); @@ -620,6 +626,22 @@ export default function ProcessInstanceShow({ variant }: OwnProps) { return
; }; + 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 ( + + + + + +
+ {index + 1} {': '} + {DateAndTimeService.convertSecondsToFormattedDateTime( + task.properties_json.last_state_change + )}{' '} + {' - '} {task.state} +
+
+
+ ); + })} + + ); + }; - const multiInstanceSelector = () => { - if (!taskToDisplay || !taskToDisplay.runtime_info) { + const createButtonsForMultiTasks = (instances: number[]) => { + if (!tasks || !taskToDisplay) { + return []; + } + return instances.map((v: any) => { + return ( + + ); + }); + }; + + const taskInstanceSelector = () => { + if (!taskToDisplay) { return null; } - const clickAction = (item: any) => { - return () => { - switchToTask(taskToDisplay.runtime_info.instance_map[item]); - }; - }; - const createButtonSet = (instances: string[]) => { - return ( - - {instances.map((v: any) => ( - - ))} - - ); - }; + 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 - ); - } - 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 ( - - - {completedInstances} - - - {runningInstances} - - - {futureInstances} - - + accordionItems.push( + + {createButtonSetForTaskInstances()} + ); } - if (taskToDisplay.typename === 'StandardLoopTask') { - const buttons = []; - for ( - let i = 0; - i < taskToDisplay.runtime_info.iterations_completed; - i += 1 - ) - buttons.push( - - ); - let text = 'Loop iterations'; + + 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( + + {taskInstances} + + ); + } + }); + } + 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 ( -
+ typeof taskToDisplay.runtime_info.iterations_remaining !== + 'undefined' && + taskToDisplay.state !== 'COMPLETED' + ) { + text += `${taskToDisplay.runtime_info.iterations_remaining} remaining`; + } + accordionItems.push( +
{text}
{buttons}
-
+ ); } + if (accordionItems.length > 0) { + return {accordionItems}; + } 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}`} >
{taskToUse.bpmn_name ? ( @@ -1414,7 +1498,7 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
{taskDisplayButtons(taskToUse)} {taskToUse.state === 'COMPLETED' ? ( -
+
{completionViewLink( 'View process instance at the time when this task was active.', @@ -1422,11 +1506,11 @@ export default function ProcessInstanceShow({ variant }: OwnProps) { )}
-
) : null} +
{taskActionDetails()} - {multiInstanceSelector()} + {taskInstanceSelector()} ); };