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:
parent
4cb2fa66aa
commit
15d0d788e5
|
@ -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
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'],
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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, '_');
|
||||
};
|
||||
|
|
|
@ -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]
|
||||
);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue