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:
|
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
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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'],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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, '_');
|
||||||
};
|
};
|
||||||
|
|
|
@ -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]
|
||||||
);
|
);
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
|
@ -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()}
|
||||||
let runningInstances = null;
|
</AccordionItem>
|
||||||
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}
|
|
||||||
</AccordionItem>
|
|
||||||
<AccordionItem title="Running instances">
|
|
||||||
{runningInstances}
|
|
||||||
</AccordionItem>
|
|
||||||
<AccordionItem title="Future instances">
|
|
||||||
{futureInstances}
|
|
||||||
</AccordionItem>
|
|
||||||
</Accordion>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (taskToDisplay.typename === 'StandardLoopTask') {
|
|
||||||
const buttons = [];
|
if (MULTI_INSTANCE_TASK_TYPES.includes(taskToDisplay.typename)) {
|
||||||
for (
|
['completed', 'running', 'future'].forEach((infoType: string) => {
|
||||||
let i = 0;
|
let taskInstances: ReactElement[] = [];
|
||||||
i < taskToDisplay.runtime_info.iterations_completed;
|
const infoArray = taskToDisplay.runtime_info[infoType];
|
||||||
i += 1
|
if (taskToDisplay.runtime_info.completed.length > 0) {
|
||||||
)
|
taskInstances = createButtonsForMultiTasks(infoArray);
|
||||||
buttons.push(
|
accordionItems.push(
|
||||||
<Button kind="ghost" onClick={clickAction(i)}>
|
<AccordionItem
|
||||||
{i}
|
title={`${titleizeString(infoType)} instances for MI task (${
|
||||||
</Button>
|
taskInstances.length
|
||||||
);
|
})`}
|
||||||
let text = 'Loop iterations';
|
>
|
||||||
|
{taskInstances}
|
||||||
|
</AccordionItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (LOOP_TASK_TYPES.includes(taskToDisplay.typename)) {
|
||||||
|
const loopTaskInstanceIndexes = [
|
||||||
|
...Array(taskToDisplay.runtime_info.iterations_completed).keys(),
|
||||||
|
];
|
||||||
|
const buttons = createButtonsForMultiTasks(loopTaskInstanceIndexes);
|
||||||
|
let text = '';
|
||||||
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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in New Issue