Feature/task assignment (#352)
* added an api to assign a list of users to a task w/ burnettk * use the modal submit and close buttons when saving task data on the instance show page w/ burnettk * switch save and cancel buttons on secrets new page w/ burnettk * add some icons, tho still missing event stuff * finished adding imporoved icons and fixing up task modal w/ burnettk * added some user search options to assig tasks to w/ burnettk * cleaned up task details modal and added call to backend to add potential users w/ burnettk * fixed broken tests w/ burnettk * removed some merge comments w/ burnettk * process instance id is an int not a str w/ burnettk --------- Co-authored-by: jasquat <jasquat@users.noreply.github.com> Co-authored-by: burnettk <burnettk@users.noreply.github.com>
This commit is contained in:
parent
93b8c09e90
commit
841f3ccc8c
|
@ -1618,6 +1618,44 @@ paths:
|
|||
schema:
|
||||
$ref: "#/components/schemas/Workflow"
|
||||
|
||||
/task-assign/{modified_process_model_identifier}/{process_instance_id}/{task_guid}:
|
||||
parameters:
|
||||
- name: modified_process_model_identifier
|
||||
in: path
|
||||
required: true
|
||||
description: The modified id of an existing process model
|
||||
schema:
|
||||
type: string
|
||||
- name: process_instance_id
|
||||
in: path
|
||||
required: true
|
||||
description: The unique id of an existing process instance.
|
||||
schema:
|
||||
type: integer
|
||||
- name: task_guid
|
||||
in: path
|
||||
required: true
|
||||
description: The unique id of the task.
|
||||
schema:
|
||||
type: string
|
||||
post:
|
||||
operationId: spiffworkflow_backend.routes.tasks_controller.task_assign
|
||||
summary: Assign a given task to a list of additional users
|
||||
tags:
|
||||
- Process Instances
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/User"
|
||||
responses:
|
||||
"200":
|
||||
description: "ok: true"
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/OkTrue"
|
||||
|
||||
/process-data/{modified_process_model_identifier}/{process_instance_id}/{process_data_identifier}:
|
||||
parameters:
|
||||
- name: modified_process_model_identifier
|
||||
|
@ -1724,7 +1762,7 @@ paths:
|
|||
required: true
|
||||
description: The unique id of the process instance
|
||||
schema:
|
||||
type: string
|
||||
type: integer
|
||||
- name: task_guid
|
||||
in: path
|
||||
required: true
|
||||
|
|
|
@ -47,6 +47,23 @@ class SpiffworkflowBaseDBModel(db.Model): # type: ignore
|
|||
|
||||
return m_type.value
|
||||
|
||||
@classmethod
|
||||
def commit_with_rollback_on_exception(cls) -> None:
|
||||
"""Attempts to commit the session and rolls back if it fails.
|
||||
|
||||
We may need to add other error handling here as we go. But sqlalchemy insists that we
|
||||
"frame" our commits.
|
||||
|
||||
https://docs.sqlalchemy.org/en/20/errors.html#error-7s2a
|
||||
https://docs.sqlalchemy.org/en/20/faq/sessions.html#faq-session-rollback
|
||||
https://docs.sqlalchemy.org/en/20/orm/session_basics.html#session-faq-whentocreate
|
||||
"""
|
||||
try:
|
||||
db.session.commit()
|
||||
except Exception:
|
||||
db.session.rollback()
|
||||
raise
|
||||
|
||||
|
||||
def update_created_modified_on_create_listener(
|
||||
mapper: Mapper, _connection: Connection, target: SpiffworkflowBaseDBModel
|
||||
|
|
|
@ -29,6 +29,7 @@ from sqlalchemy.orm import aliased
|
|||
from sqlalchemy.orm.util import AliasedClass
|
||||
|
||||
from spiffworkflow_backend.exceptions.api_error import ApiError
|
||||
from spiffworkflow_backend.models.db import SpiffworkflowBaseDBModel
|
||||
from spiffworkflow_backend.models.db import db
|
||||
from spiffworkflow_backend.models.group import GroupModel
|
||||
from spiffworkflow_backend.models.human_task import HumanTaskModel
|
||||
|
@ -173,13 +174,12 @@ def task_data_show(
|
|||
|
||||
|
||||
def task_data_update(
|
||||
process_instance_id: str,
|
||||
process_instance_id: int,
|
||||
modified_process_model_identifier: str,
|
||||
task_guid: str,
|
||||
body: dict,
|
||||
) -> Response:
|
||||
"""Update task data."""
|
||||
process_instance = ProcessInstanceModel.query.filter(ProcessInstanceModel.id == int(process_instance_id)).first()
|
||||
process_instance = ProcessInstanceModel.query.filter(ProcessInstanceModel.id == process_instance_id).first()
|
||||
if process_instance:
|
||||
if process_instance.status != "suspended":
|
||||
raise ProcessInstanceTaskDataCannotBeUpdatedError(
|
||||
|
@ -227,13 +227,13 @@ def task_data_update(
|
|||
|
||||
def manual_complete_task(
|
||||
modified_process_model_identifier: str,
|
||||
process_instance_id: str,
|
||||
process_instance_id: int,
|
||||
task_guid: str,
|
||||
body: dict,
|
||||
) -> Response:
|
||||
"""Mark a task complete without executing it."""
|
||||
execute = body.get("execute", True)
|
||||
process_instance = ProcessInstanceModel.query.filter(ProcessInstanceModel.id == int(process_instance_id)).first()
|
||||
process_instance = ProcessInstanceModel.query.filter(ProcessInstanceModel.id == process_instance_id).first()
|
||||
if process_instance:
|
||||
processor = ProcessInstanceProcessor(process_instance)
|
||||
processor.manual_complete_task(task_guid, execute, g.user)
|
||||
|
@ -249,6 +249,57 @@ def manual_complete_task(
|
|||
)
|
||||
|
||||
|
||||
def task_assign(
|
||||
modified_process_model_identifier: str,
|
||||
process_instance_id: int,
|
||||
task_guid: str,
|
||||
body: dict,
|
||||
) -> Response:
|
||||
process_instance = _find_process_instance_by_id_or_raise(process_instance_id)
|
||||
|
||||
if process_instance.status != ProcessInstanceStatus.suspended.value:
|
||||
raise ApiError(
|
||||
error_code="error_not_suspended",
|
||||
message="The process instance must be suspended to perform this operation",
|
||||
status_code=400,
|
||||
)
|
||||
|
||||
if "user_ids" not in body:
|
||||
raise ApiError(
|
||||
error_code="malformed_request",
|
||||
message="user_ids as an array must be given in the body of the request.",
|
||||
status_code=400,
|
||||
)
|
||||
|
||||
_get_process_model(
|
||||
process_instance.process_model_identifier,
|
||||
)
|
||||
|
||||
task_model = _get_task_model_from_guid_or_raise(task_guid, process_instance_id)
|
||||
human_tasks = HumanTaskModel.query.filter_by(
|
||||
process_instance_id=process_instance.id, task_id=task_model.guid
|
||||
).all()
|
||||
|
||||
if len(human_tasks) > 1:
|
||||
raise ApiError(
|
||||
error_code="multiple_tasks_found",
|
||||
message="More than one ready tasks were found. This should never happen.",
|
||||
status_code=400,
|
||||
)
|
||||
|
||||
human_task = human_tasks[0]
|
||||
|
||||
for user_id in body["user_ids"]:
|
||||
human_task_user = HumanTaskUserModel.query.filter_by(user_id=user_id, human_task=human_task).first()
|
||||
if human_task_user is None:
|
||||
human_task_user = HumanTaskUserModel(user_id=user_id, human_task=human_task)
|
||||
db.session.add(human_task_user)
|
||||
|
||||
SpiffworkflowBaseDBModel.commit_with_rollback_on_exception()
|
||||
|
||||
return make_response(jsonify({"ok": True}), 200)
|
||||
|
||||
|
||||
def task_show(process_instance_id: int, task_guid: str = "next") -> flask.wrappers.Response:
|
||||
process_instance = _find_process_instance_by_id_or_raise(process_instance_id)
|
||||
|
||||
|
@ -353,6 +404,15 @@ def task_show(process_instance_id: int, task_guid: str = "next") -> flask.wrappe
|
|||
return make_response(jsonify(task_model), 200)
|
||||
|
||||
|
||||
def task_submit(
|
||||
process_instance_id: int,
|
||||
task_guid: str,
|
||||
body: dict[str, Any],
|
||||
) -> flask.wrappers.Response:
|
||||
with sentry_sdk.start_span(op="controller_action", description="tasks_controller.task_submit"):
|
||||
return _task_submit_shared(process_instance_id, task_guid, body)
|
||||
|
||||
|
||||
def _render_instructions_for_end_user(task_model: TaskModel, extensions: dict | None = None) -> str:
|
||||
"""Assure any instructions for end user are processed for jinja syntax."""
|
||||
if extensions is None:
|
||||
|
@ -424,7 +484,7 @@ def _interstitial_stream(
|
|||
return
|
||||
|
||||
# path used by the interstitial page while executing tasks - ie the background processor is not executing them
|
||||
ready_engine_task_count = get_ready_engine_step_count(processor.bpmn_process_instance)
|
||||
ready_engine_task_count = _get_ready_engine_step_count(processor.bpmn_process_instance)
|
||||
if execute_tasks and ready_engine_task_count == 0:
|
||||
break
|
||||
|
||||
|
@ -467,7 +527,7 @@ def _interstitial_stream(
|
|||
yield _render_data("task", task)
|
||||
|
||||
|
||||
def get_ready_engine_step_count(bpmn_process_instance: BpmnWorkflow) -> int:
|
||||
def _get_ready_engine_step_count(bpmn_process_instance: BpmnWorkflow) -> int:
|
||||
return len([t for t in bpmn_process_instance.get_tasks(TaskState.READY) if not t.task_spec.manual])
|
||||
|
||||
|
||||
|
@ -656,15 +716,6 @@ def _task_submit_shared(
|
|||
)
|
||||
|
||||
|
||||
def task_submit(
|
||||
process_instance_id: int,
|
||||
task_guid: str,
|
||||
body: dict[str, Any],
|
||||
) -> flask.wrappers.Response:
|
||||
with sentry_sdk.start_span(op="controller_action", description="tasks_controller.task_submit"):
|
||||
return _task_submit_shared(process_instance_id, task_guid, body)
|
||||
|
||||
|
||||
def _get_tasks(
|
||||
processes_started_by_user: bool = True,
|
||||
has_lane_assignment_id: bool = True,
|
||||
|
|
|
@ -86,6 +86,7 @@ PATH_SEGMENTS_FOR_PERMISSION_ALL = [
|
|||
{"path": "/process-model-natural-language", "relevant_permissions": ["create"]},
|
||||
{"path": "/process-model-publish", "relevant_permissions": ["create"]},
|
||||
{"path": "/process-model-tests", "relevant_permissions": ["create"]},
|
||||
{"path": "/task-assign", "relevant_permissions": ["create"]},
|
||||
{"path": "/task-data", "relevant_permissions": ["read", "update"]},
|
||||
]
|
||||
|
||||
|
@ -546,6 +547,7 @@ class AuthorizationService:
|
|||
permissions_to_assign.append(PermissionToAssign(permission="create", target_uri="/task-complete/*"))
|
||||
|
||||
# read comes from PG and PM ALL permissions as well
|
||||
permissions_to_assign.append(PermissionToAssign(permission="create", target_uri="/task-assign/*"))
|
||||
permissions_to_assign.append(PermissionToAssign(permission="update", target_uri="/task-data/*"))
|
||||
permissions_to_assign.append(PermissionToAssign(permission="read", target_uri="/event-error-details/*"))
|
||||
permissions_to_assign.append(PermissionToAssign(permission="read", target_uri="/logs/*"))
|
||||
|
|
|
@ -2493,6 +2493,7 @@ class TestProcessApi(BaseTest):
|
|||
content_type="application/json",
|
||||
data=json.dumps({"execute": False}),
|
||||
)
|
||||
|
||||
assert response.json["status"] == "suspended"
|
||||
task_model = TaskModel.query.filter_by(guid=human_task["guid"]).first()
|
||||
assert task_model is not None
|
||||
|
|
|
@ -138,6 +138,7 @@ class TestAuthorizationService(BaseTest):
|
|||
("/process-models/some-process-group:some-process-model:*", "delete"),
|
||||
("/process-models/some-process-group:some-process-model:*", "read"),
|
||||
("/process-models/some-process-group:some-process-model:*", "update"),
|
||||
("/task-assign/some-process-group:some-process-model:*", "create"),
|
||||
("/task-data/some-process-group:some-process-model:*", "read"),
|
||||
("/task-data/some-process-group:some-process-model:*", "update"),
|
||||
]
|
||||
|
@ -223,6 +224,7 @@ class TestAuthorizationService(BaseTest):
|
|||
("/process-models/some-process-group:some-process-model/*", "delete"),
|
||||
("/process-models/some-process-group:some-process-model/*", "read"),
|
||||
("/process-models/some-process-group:some-process-model/*", "update"),
|
||||
("/task-assign/some-process-group:some-process-model/*", "create"),
|
||||
("/task-data/some-process-group:some-process-model/*", "read"),
|
||||
("/task-data/some-process-group:some-process-model/*", "update"),
|
||||
]
|
||||
|
@ -333,6 +335,7 @@ class TestAuthorizationService(BaseTest):
|
|||
("/secrets/*", "read"),
|
||||
("/secrets/*", "update"),
|
||||
("/send-event/*", "create"),
|
||||
("/task-assign/*", "create"),
|
||||
("/task-complete/*", "create"),
|
||||
("/task-data/*", "update"),
|
||||
("/task-data/*", "read"),
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { fetchEventSource } from '@microsoft/fetch-event-source';
|
||||
// @ts-ignore
|
||||
|
@ -10,6 +10,7 @@ import { getBasicHeaders } from '../services/HttpService';
|
|||
import InstructionsForEndUser from './InstructionsForEndUser';
|
||||
import { ProcessInstance, ProcessInstanceTask } from '../interfaces';
|
||||
import useAPIError from '../hooks/UseApiError';
|
||||
import { HUMAN_TASK_TYPES } from '../helpers';
|
||||
|
||||
type OwnProps = {
|
||||
processInstanceId: number;
|
||||
|
@ -35,9 +36,6 @@ export default function ProcessInterstitial({
|
|||
useState<ProcessInstance | null>(null);
|
||||
|
||||
const navigate = useNavigate();
|
||||
const userTasks = useMemo(() => {
|
||||
return ['User Task', 'Manual Task'];
|
||||
}, []);
|
||||
const { addError } = useAPIError();
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -77,10 +75,10 @@ export default function ProcessInterstitial({
|
|||
!processInstance &&
|
||||
myTask &&
|
||||
myTask.can_complete &&
|
||||
userTasks.includes(myTask.type)
|
||||
HUMAN_TASK_TYPES.includes(myTask.type)
|
||||
);
|
||||
},
|
||||
[allowRedirect, processInstance, userTasks]
|
||||
[allowRedirect, processInstance]
|
||||
);
|
||||
|
||||
const shouldRedirectToProcessInstance = useCallback((): boolean => {
|
||||
|
@ -105,7 +103,6 @@ export default function ProcessInterstitial({
|
|||
}, [
|
||||
lastTask,
|
||||
navigate,
|
||||
userTasks,
|
||||
shouldRedirectToTask,
|
||||
processInstanceId,
|
||||
processInstanceShowPageUrl,
|
||||
|
@ -181,7 +178,7 @@ export default function ProcessInterstitial({
|
|||
return userMessageForProcessInstance(processInstance, myTask);
|
||||
}
|
||||
|
||||
if (!myTask.can_complete && userTasks.includes(myTask.type)) {
|
||||
if (!myTask.can_complete && HUMAN_TASK_TYPES.includes(myTask.type)) {
|
||||
return inlineMessage(
|
||||
'',
|
||||
`This next task is assigned to a different person or team. There is no action for you to take at this time.`
|
||||
|
@ -190,7 +187,11 @@ export default function ProcessInterstitial({
|
|||
if (shouldRedirectToTask(myTask)) {
|
||||
return inlineMessage('', `Redirecting ...`);
|
||||
}
|
||||
if (myTask && myTask.can_complete && userTasks.includes(myTask.type)) {
|
||||
if (
|
||||
myTask &&
|
||||
myTask.can_complete &&
|
||||
HUMAN_TASK_TYPES.includes(myTask.type)
|
||||
) {
|
||||
return inlineMessage(
|
||||
'',
|
||||
`The task "${myTask.title}" is ready for you to complete.`
|
||||
|
|
|
@ -0,0 +1,72 @@
|
|||
import { ComboBox } from '@carbon/react';
|
||||
import { useRef, useState } from 'react';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
import { CarbonComboBoxSelection, User } from '../interfaces';
|
||||
import HttpService from '../services/HttpService';
|
||||
|
||||
type OwnProps = {
|
||||
onSelectedUser: Function;
|
||||
label?: string;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export default function UserSearch({
|
||||
onSelectedUser,
|
||||
className,
|
||||
label = 'User',
|
||||
}: OwnProps) {
|
||||
const lastRequestedInitatorSearchTerm = useRef<string>();
|
||||
const [selectedUser, setSelectedUser] = useState<User | null>(null);
|
||||
const [userList, setUserList] = useState<User[]>([]);
|
||||
|
||||
const handleUserSearchResult = (result: any, inputText: string) => {
|
||||
if (lastRequestedInitatorSearchTerm.current === result.username_prefix) {
|
||||
setUserList(result.users);
|
||||
result.users.forEach((user: User) => {
|
||||
if (user.username === inputText) {
|
||||
setSelectedUser(user);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const searchForUser = (inputText: string) => {
|
||||
if (inputText) {
|
||||
lastRequestedInitatorSearchTerm.current = inputText;
|
||||
HttpService.makeCallToBackend({
|
||||
path: `/users/search?username_prefix=${inputText}`,
|
||||
successCallback: (result: any) =>
|
||||
handleUserSearchResult(result, inputText),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const addDebouncedSearchUser = useDebouncedCallback(
|
||||
(value: string) => {
|
||||
searchForUser(value);
|
||||
},
|
||||
// delay in ms
|
||||
250
|
||||
);
|
||||
return (
|
||||
<ComboBox
|
||||
onInputChange={addDebouncedSearchUser}
|
||||
className={className}
|
||||
onChange={(selection: CarbonComboBoxSelection) => {
|
||||
onSelectedUser(selection.selectedItem);
|
||||
}}
|
||||
id="user-search"
|
||||
data-qa="user-search"
|
||||
items={userList}
|
||||
itemToString={(processInstanceInitatorOption: User) => {
|
||||
if (processInstanceInitatorOption) {
|
||||
return processInstanceInitatorOption.username;
|
||||
}
|
||||
return null;
|
||||
}}
|
||||
placeholder="Start typing username"
|
||||
titleText={label}
|
||||
selectedItem={selectedUser}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -26,6 +26,13 @@ export const slugifyString = (str: any) => {
|
|||
.replace(/-+$/g, '');
|
||||
};
|
||||
|
||||
export const HUMAN_TASK_TYPES = [
|
||||
'User Task',
|
||||
'Manual Task',
|
||||
'UserTask',
|
||||
'ManualTask',
|
||||
];
|
||||
|
||||
export const underscorizeString = (inputString: string) => {
|
||||
return slugifyString(inputString).replace(/-/g, '_');
|
||||
};
|
||||
|
|
|
@ -21,6 +21,7 @@ export const useUriListForPermissions = () => {
|
|||
processInstanceResumePath: `/v1.0/process-instance-resume/${params.process_model_id}/${params.process_instance_id}`,
|
||||
processInstanceSendEventPath: `/v1.0/send-event/${params.process_model_id}/${params.process_instance_id}`,
|
||||
processInstanceSuspendPath: `/v1.0/process-instance-suspend/${params.process_model_id}/${params.process_instance_id}`,
|
||||
processInstanceTaskAssignPath: `/v1.0/task-assign/${params.process_model_id}/${params.process_instance_id}`,
|
||||
processInstanceTaskDataPath: `/v1.0/task-data/${params.process_model_id}/${params.process_instance_id}`,
|
||||
processInstanceTaskListForMePath: `/v1.0/process-instances/for-me/${params.process_model_id}/${params.process_instance_id}/task-info`,
|
||||
processInstanceTaskListPath: `/v1.0/process-instances/${params.process_model_id}/${params.process_instance_id}/task-info`,
|
||||
|
|
|
@ -44,7 +44,7 @@ h1 {
|
|||
font-size: 28px;
|
||||
line-height: 36px;
|
||||
color: #161616;
|
||||
margin-bottom: 1em
|
||||
margin-bottom: 1rem
|
||||
}
|
||||
|
||||
h2 {
|
||||
|
@ -54,6 +54,13 @@ h2 {
|
|||
color: #161616;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-weight: 400;
|
||||
font-size: 18px;
|
||||
line-height: 20px;
|
||||
color: #161616;
|
||||
}
|
||||
|
||||
.span-tag {
|
||||
color: black;
|
||||
}
|
||||
|
@ -137,12 +144,12 @@ code {
|
|||
.app-logo {
|
||||
height: 37px;
|
||||
width: 152px;
|
||||
margin-top: 1em;
|
||||
margin-bottom: 1em;
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.spiffworkflow-header-container {
|
||||
margin-bottom: 2em;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.active-task-highlight:not(.djs-connection) .djs-visual > :nth-child(1) {
|
||||
|
@ -168,11 +175,11 @@ code {
|
|||
}
|
||||
|
||||
.cds--breadcrumb {
|
||||
margin-bottom: 1em;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.process-description {
|
||||
margin-bottom: 2em;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
h1.with-icons {
|
||||
|
@ -211,7 +218,7 @@ dl dd {
|
|||
}
|
||||
|
||||
.with-bottom-margin {
|
||||
margin-bottom: 1em;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.user-profile-toggletip-content {
|
||||
|
@ -253,15 +260,15 @@ dl dd {
|
|||
}
|
||||
|
||||
.with-top-margin {
|
||||
margin-top: 1em;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.with-extra-top-margin {
|
||||
margin-top: 1.3em;
|
||||
margin-top: 1.3rem;
|
||||
}
|
||||
|
||||
.with-top-margin-for-label-next-to-text-input {
|
||||
margin-top: 2.3em;
|
||||
margin-top: 2.3rem;
|
||||
}
|
||||
|
||||
.with-tiny-top-margin {
|
||||
|
@ -273,13 +280,17 @@ dl dd {
|
|||
}
|
||||
|
||||
.with-large-bottom-margin {
|
||||
margin-bottom: 3em;
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
.with-tiny-bottom-margin {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.with-half-rem-bottom-margin {
|
||||
margin-bottom: .5rem;
|
||||
}
|
||||
|
||||
.diagram-viewer-canvas {
|
||||
border:1px solid #000000;
|
||||
height:70vh;
|
||||
|
@ -288,7 +299,7 @@ dl dd {
|
|||
}
|
||||
|
||||
.breadcrumb {
|
||||
font-size: 1.5em;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.breadcrumb-item.active {
|
||||
|
@ -296,7 +307,7 @@ dl dd {
|
|||
}
|
||||
|
||||
.container .nav-tabs {
|
||||
margin-top: 1em;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
|
||||
|
@ -314,7 +325,7 @@ dl dd {
|
|||
}
|
||||
.markdown table th,
|
||||
.markdown table td {
|
||||
padding: .5em;
|
||||
padding: .5rem;
|
||||
border: 1px solid lightgrey;
|
||||
}
|
||||
/* Zebra Table Style */
|
||||
|
@ -323,7 +334,7 @@ dl dd {
|
|||
}
|
||||
|
||||
.form-instructions {
|
||||
margin-bottom: 10em;
|
||||
margin-bottom: 10rem;
|
||||
}
|
||||
|
||||
/* Json Web Form CSS Fix - Bootstrap now requries that each li have a "list-inline-item." Also have a PR
|
||||
|
@ -347,13 +358,13 @@ in on this with the react-jsonschema-form repo. This is just a patch fix to allo
|
|||
.tile-process-group-content-container {
|
||||
width: 320px;
|
||||
height: 264px;
|
||||
padding: 1em;
|
||||
padding: 1rem;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.tile-process-group-display-name {
|
||||
margin-top: 2em;
|
||||
margin-bottom: 1em;
|
||||
margin-top: 2rem;
|
||||
margin-bottom: 1rem;
|
||||
font-size: 20px;
|
||||
line-height: 28px;
|
||||
color: #161616;
|
||||
|
@ -361,7 +372,7 @@ in on this with the react-jsonschema-form repo. This is just a patch fix to allo
|
|||
}
|
||||
|
||||
.tile-title-top {
|
||||
margin-bottom: 2em;
|
||||
margin-bottom: 2rem;
|
||||
font-size: 20px;
|
||||
line-height: 28px;
|
||||
color: #161616;
|
||||
|
@ -386,7 +397,7 @@ in on this with the react-jsonschema-form repo. This is just a patch fix to allo
|
|||
|
||||
.tile-pin-bottom {
|
||||
position: absolute;
|
||||
bottom: 1em;
|
||||
bottom: 1rem;
|
||||
}
|
||||
|
||||
.cds--tabs .cds--tabs__nav-link {
|
||||
|
@ -398,7 +409,7 @@ in on this with the react-jsonschema-form repo. This is just a patch fix to allo
|
|||
}
|
||||
|
||||
td.actions-cell {
|
||||
width: 1em;
|
||||
width: 1rem;
|
||||
}
|
||||
|
||||
.process-instance-list-table {
|
||||
|
@ -411,12 +422,12 @@ td.actions-cell {
|
|||
}
|
||||
|
||||
.process-instance-table-header {
|
||||
margin-bottom: 1em;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.no-results-message {
|
||||
font-style: italic;
|
||||
margin-left: 2em;
|
||||
margin-left: 2rem;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
|
@ -425,12 +436,12 @@ td.actions-cell {
|
|||
line-height: 18px;
|
||||
letter-spacing: 0.16px;
|
||||
color: #525252;
|
||||
margin-bottom: 1em;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
/* top and bottom margin since this is sort of the middle of three sections on the process model show page */
|
||||
.process-model-files-section {
|
||||
margin: 2em 0;
|
||||
margin: 2rem 0;
|
||||
}
|
||||
|
||||
.filterIcon {
|
||||
|
@ -525,7 +536,7 @@ svg.notification-icon {
|
|||
}
|
||||
|
||||
.please-press-filter-button {
|
||||
margin-bottom: 1em;
|
||||
margin-bottom: 1rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
|
@ -537,28 +548,28 @@ svg.notification-icon {
|
|||
|
||||
.user_instructions_0 {
|
||||
filter: opacity(1);
|
||||
font-size: 1.2em;
|
||||
font-size: 1.2rem;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.user_instructions_1 {
|
||||
filter: opacity(60%);
|
||||
font-size: 1.1em;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.user_instructions_2 {
|
||||
filter: opacity(40%);
|
||||
font-size: 1em;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.user_instructions_3 {
|
||||
filter: opacity(20%);
|
||||
font-size: 9em;
|
||||
font-size: 9rem;
|
||||
}
|
||||
|
||||
.user_instructions_4 {
|
||||
filter: opacity(10%);
|
||||
font-size: 8em;
|
||||
font-size: 8rem;
|
||||
}
|
||||
|
||||
.float-right {
|
||||
|
@ -649,7 +660,25 @@ hr {
|
|||
font-style: italic;
|
||||
}
|
||||
|
||||
.modal-dropdown {
|
||||
height: 20rem;
|
||||
width: "auto";
|
||||
}
|
||||
|
||||
.task-data-details-header {
|
||||
margin-top: 1.5rem;
|
||||
margin-bottom: .5rem;
|
||||
}
|
||||
|
||||
.explanatory-message {
|
||||
font-style: italic;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.indented-content {
|
||||
margin-left: 1rem;
|
||||
}
|
||||
|
||||
#hidden-form-for-autosave {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
|
|
@ -26,10 +26,6 @@ export interface TaskPropertiesJson {
|
|||
last_state_change: number;
|
||||
}
|
||||
|
||||
export interface TaskDefinitionPropertiesJson {
|
||||
spec: string;
|
||||
}
|
||||
|
||||
export interface EventDefinition {
|
||||
typename: string;
|
||||
payload: any;
|
||||
|
@ -38,6 +34,11 @@ export interface EventDefinition {
|
|||
message_var?: string;
|
||||
}
|
||||
|
||||
export interface TaskDefinitionPropertiesJson {
|
||||
spec: string;
|
||||
event_definition: EventDefinition;
|
||||
}
|
||||
|
||||
export interface SignalButton {
|
||||
label: string;
|
||||
event: EventDefinition;
|
||||
|
@ -286,7 +287,7 @@ export interface PaginationObject {
|
|||
}
|
||||
|
||||
export interface CarbonComboBoxSelection {
|
||||
selectedItem: ProcessModel;
|
||||
selectedItem: any;
|
||||
}
|
||||
|
||||
export interface CarbonComboBoxProcessSelection {
|
||||
|
|
|
@ -7,12 +7,20 @@ import {
|
|||
useSearchParams,
|
||||
} from 'react-router-dom';
|
||||
import {
|
||||
TrashCan,
|
||||
StopOutline,
|
||||
PauseOutline,
|
||||
PlayOutline,
|
||||
InProgress,
|
||||
Send,
|
||||
ButtonSet,
|
||||
Checkmark,
|
||||
Edit,
|
||||
InProgress,
|
||||
PauseOutline,
|
||||
UserFollow,
|
||||
Play,
|
||||
PlayOutline,
|
||||
Reset,
|
||||
RuleDraft,
|
||||
SkipForward,
|
||||
StopOutline,
|
||||
TrashCan,
|
||||
Warning,
|
||||
// @ts-ignore
|
||||
} from '@carbon/icons-react';
|
||||
|
@ -37,6 +45,7 @@ import HttpService from '../services/HttpService';
|
|||
import ReactDiagramEditor from '../components/ReactDiagramEditor';
|
||||
import {
|
||||
convertSecondsToFormattedDateTime,
|
||||
HUMAN_TASK_TYPES,
|
||||
modifyProcessIdentifierForPathParam,
|
||||
unModifyProcessIdentifierForPathParam,
|
||||
} from '../helpers';
|
||||
|
@ -50,12 +59,14 @@ import {
|
|||
ProcessInstance,
|
||||
Task,
|
||||
TaskDefinitionPropertiesJson,
|
||||
User,
|
||||
} from '../interfaces';
|
||||
import { usePermissionFetcher } from '../hooks/PermissionService';
|
||||
import ProcessInstanceClass from '../classes/ProcessInstanceClass';
|
||||
import TaskListTable from '../components/TaskListTable';
|
||||
import useAPIError from '../hooks/UseApiError';
|
||||
import ProcessInterstitial from '../components/ProcessInterstitial';
|
||||
import UserSearch from '../components/UserSearch';
|
||||
import ProcessInstanceLogList from '../components/ProcessInstanceLogList';
|
||||
import MessageInstanceList from '../components/MessageInstanceList';
|
||||
|
||||
|
@ -68,6 +79,8 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
|
|||
const params = useParams();
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
const eventsThatNeedPayload = ['MessageEventDefinition'];
|
||||
|
||||
const [processInstance, setProcessInstance] =
|
||||
useState<ProcessInstance | null>(null);
|
||||
const [tasks, setTasks] = useState<Task[] | null>(null);
|
||||
|
@ -89,6 +102,12 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
|
|||
const [eventTextEditorEnabled, setEventTextEditorEnabled] =
|
||||
useState<boolean>(false);
|
||||
|
||||
const [addingPotentialOwners, setAddingPotentialOwners] =
|
||||
useState<boolean>(false);
|
||||
const [additionalPotentialOwners, setAdditionalPotentialOwners] = useState<
|
||||
User[] | null
|
||||
>(null);
|
||||
|
||||
const { addError, removeError } = useAPIError();
|
||||
const unModifiedProcessModelId = unModifyProcessIdentifierForPathParam(
|
||||
`${params.process_model_id}`
|
||||
|
@ -109,6 +128,7 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
|
|||
[targetUris.messageInstanceListPath]: ['GET'],
|
||||
[targetUris.processInstanceActionPath]: ['DELETE', 'GET'],
|
||||
[targetUris.processInstanceLogListPath]: ['GET'],
|
||||
[targetUris.processInstanceTaskAssignPath]: ['POST'],
|
||||
[targetUris.processInstanceTaskDataPath]: ['GET', 'PUT'],
|
||||
[targetUris.processInstanceSendEventPath]: ['POST'],
|
||||
[targetUris.processInstanceCompleteTaskPath]: ['POST'],
|
||||
|
@ -539,9 +559,22 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
|
|||
}
|
||||
};
|
||||
|
||||
const resetTaskActionDetails = () => {
|
||||
setEditingTaskData(false);
|
||||
setSelectingEvent(false);
|
||||
setAddingPotentialOwners(false);
|
||||
initializeTaskDataToDisplay(taskToDisplay);
|
||||
setEventPayload('{}');
|
||||
setAdditionalPotentialOwners(null);
|
||||
removeError();
|
||||
};
|
||||
|
||||
const handleTaskDataDisplayClose = () => {
|
||||
setTaskToDisplay(null);
|
||||
initializeTaskDataToDisplay(null);
|
||||
if (editingTaskData || selectingEvent || addingPotentialOwners) {
|
||||
resetTaskActionDetails();
|
||||
}
|
||||
};
|
||||
|
||||
const getTaskById = (taskId: string) => {
|
||||
|
@ -602,8 +635,9 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
|
|||
|
||||
const canSendEvent = (task: Task) => {
|
||||
// We actually could allow this for any waiting events
|
||||
const taskTypes = ['Event Based Gateway'];
|
||||
const taskTypes = ['EventBasedGateway'];
|
||||
return (
|
||||
!selectingEvent &&
|
||||
processInstance &&
|
||||
processInstance.status === 'waiting' &&
|
||||
ability.can('POST', targetUris.processInstanceSendEventPath) &&
|
||||
|
@ -623,6 +657,17 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
|
|||
);
|
||||
};
|
||||
|
||||
const canAddPotentialOwners = (task: Task) => {
|
||||
return (
|
||||
HUMAN_TASK_TYPES.includes(task.typename) &&
|
||||
processInstance &&
|
||||
processInstance.status === 'suspended' &&
|
||||
ability.can('POST', targetUris.processInstanceTaskAssignPath) &&
|
||||
isActiveTask(task) &&
|
||||
showingActiveTask()
|
||||
);
|
||||
};
|
||||
|
||||
const canResetProcess = (task: Task) => {
|
||||
return (
|
||||
ability.can('POST', targetUris.processInstanceResetPath) &&
|
||||
|
@ -635,7 +680,7 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
|
|||
|
||||
const getEvents = (task: Task) => {
|
||||
const handleMessage = (eventDefinition: EventDefinition) => {
|
||||
if (eventDefinition.typename === 'MessageEventDefinition') {
|
||||
if (eventsThatNeedPayload.includes(eventDefinition.typename)) {
|
||||
const newEvent = eventDefinition;
|
||||
delete newEvent.message_var;
|
||||
newEvent.payload = {};
|
||||
|
@ -643,22 +688,16 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
|
|||
}
|
||||
return eventDefinition;
|
||||
};
|
||||
if (task.event_definition && task.event_definition.event_definitions)
|
||||
return task.event_definition.event_definitions.map((e: EventDefinition) =>
|
||||
const eventDefinition =
|
||||
task.task_definition_properties_json.event_definition;
|
||||
if (eventDefinition && eventDefinition.event_definitions)
|
||||
return eventDefinition.event_definitions.map((e: EventDefinition) =>
|
||||
handleMessage(e)
|
||||
);
|
||||
if (task.event_definition) return [handleMessage(task.event_definition)];
|
||||
if (eventDefinition) return [handleMessage(eventDefinition)];
|
||||
return [];
|
||||
};
|
||||
|
||||
const cancelUpdatingTask = () => {
|
||||
setEditingTaskData(false);
|
||||
setSelectingEvent(false);
|
||||
initializeTaskDataToDisplay(taskToDisplay);
|
||||
setEventPayload('{}');
|
||||
removeError();
|
||||
};
|
||||
|
||||
const taskDataStringToObject = (dataString: string) => {
|
||||
return JSON.parse(dataString);
|
||||
};
|
||||
|
@ -673,7 +712,6 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
|
|||
}; // spread operator
|
||||
setTaskToDisplay(taskToDisplayCopy);
|
||||
}
|
||||
refreshPage();
|
||||
};
|
||||
|
||||
const saveTaskData = () => {
|
||||
|
@ -695,13 +733,35 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
|
|||
});
|
||||
};
|
||||
|
||||
const addPotentialOwners = () => {
|
||||
if (!additionalPotentialOwners) {
|
||||
return;
|
||||
}
|
||||
if (!taskToDisplay) {
|
||||
return;
|
||||
}
|
||||
removeError();
|
||||
|
||||
const userIds = additionalPotentialOwners.map((user: User) => user.id);
|
||||
|
||||
HttpService.makeCallToBackend({
|
||||
path: `${targetUris.processInstanceTaskAssignPath}/${taskToDisplay.guid}`,
|
||||
httpMethod: 'POST',
|
||||
successCallback: resetTaskActionDetails,
|
||||
failureCallback: addError,
|
||||
postBody: {
|
||||
user_ids: userIds,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const sendEvent = () => {
|
||||
if ('payload' in eventToSend)
|
||||
eventToSend.payload = JSON.parse(eventPayload);
|
||||
HttpService.makeCallToBackend({
|
||||
path: targetUris.processInstanceSendEventPath,
|
||||
httpMethod: 'POST',
|
||||
successCallback: saveTaskDataResult,
|
||||
successCallback: refreshPage,
|
||||
failureCallback: addError,
|
||||
postBody: eventToSend,
|
||||
});
|
||||
|
@ -720,18 +780,24 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
|
|||
|
||||
const taskDisplayButtons = (task: Task) => {
|
||||
const buttons = [];
|
||||
if (editingTaskData || addingPotentialOwners || selectingEvent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (
|
||||
task.typename === 'Script Task' &&
|
||||
task.typename === 'ScriptTask' &&
|
||||
ability.can('PUT', targetUris.processModelShowPath)
|
||||
) {
|
||||
buttons.push(
|
||||
<Button
|
||||
kind="ghost"
|
||||
align="top-left"
|
||||
renderIcon={RuleDraft}
|
||||
iconDescription="Create Script Unit Test"
|
||||
hasIconOnly
|
||||
data-qa="create-script-unit-test-button"
|
||||
onClick={createScriptUnitTest}
|
||||
>
|
||||
Create Script Unit Test
|
||||
</Button>
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -749,90 +815,94 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
|
|||
);
|
||||
}
|
||||
|
||||
if (editingTaskData) {
|
||||
buttons.push(
|
||||
<Button data-qa="save-task-data-button" onClick={saveTaskData}>
|
||||
Save
|
||||
</Button>
|
||||
);
|
||||
if (canEditTaskData(task)) {
|
||||
buttons.push(
|
||||
<Button
|
||||
data-qa="cancel-task-data-edit-button"
|
||||
onClick={cancelUpdatingTask}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
kind="ghost"
|
||||
renderIcon={Edit}
|
||||
align="top-left"
|
||||
iconDescription="Edit Task Data"
|
||||
hasIconOnly
|
||||
data-qa="edit-task-data-button"
|
||||
onClick={() => setEditingTaskData(true)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (canAddPotentialOwners(task)) {
|
||||
buttons.push(
|
||||
<Button
|
||||
kind="ghost"
|
||||
renderIcon={UserFollow}
|
||||
align="top-left"
|
||||
iconDescription="Assign user"
|
||||
title="Allow an additional user to complete this task"
|
||||
hasIconOnly
|
||||
data-qa="add-potential-owners-button"
|
||||
onClick={() => setAddingPotentialOwners(true)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (canCompleteTask(task)) {
|
||||
buttons.push(
|
||||
<Button
|
||||
kind="ghost"
|
||||
renderIcon={Play}
|
||||
align="top-left"
|
||||
iconDescription="Execute Task"
|
||||
hasIconOnly
|
||||
data-qa="execute-task-complete-button"
|
||||
onClick={() => completeTask(true)}
|
||||
>
|
||||
Execute Task
|
||||
</Button>
|
||||
);
|
||||
buttons.push(
|
||||
<Button
|
||||
kind="ghost"
|
||||
renderIcon={SkipForward}
|
||||
align="top-left"
|
||||
iconDescription="Skip Task"
|
||||
hasIconOnly
|
||||
data-qa="mark-task-complete-button"
|
||||
onClick={() => completeTask(false)}
|
||||
>
|
||||
Skip Task
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
if (canSendEvent(task)) {
|
||||
buttons.push(
|
||||
<Button
|
||||
kind="ghost"
|
||||
renderIcon={Send}
|
||||
align="top-left"
|
||||
iconDescription="Send Event"
|
||||
hasIconOnly
|
||||
data-qa="select-event-button"
|
||||
onClick={() => setSelectingEvent(true)}
|
||||
>
|
||||
Send Event
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
if (canResetProcess(task)) {
|
||||
let titleText =
|
||||
'This will reset (rewind) the process to put it into a state as if the execution of the process never went past this task. ';
|
||||
titleText += 'Yes, we invented a time machine. ';
|
||||
titleText +=
|
||||
'And no, you cannot change your mind after using this feature.';
|
||||
buttons.push(
|
||||
<Button
|
||||
kind="ghost"
|
||||
renderIcon={Reset}
|
||||
hasIconOnly
|
||||
iconDescription="Reset Process Here"
|
||||
title={titleText}
|
||||
data-qa="reset-process-button"
|
||||
onClick={() => resetProcessInstance()}
|
||||
/>
|
||||
);
|
||||
} else if (selectingEvent) {
|
||||
buttons.push(
|
||||
<Button data-qa="send-event-button" onClick={sendEvent}>
|
||||
Send
|
||||
</Button>
|
||||
);
|
||||
buttons.push(
|
||||
<Button
|
||||
data-qa="cancel-task-data-edit-button"
|
||||
onClick={cancelUpdatingTask}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
);
|
||||
} else {
|
||||
if (canEditTaskData(task)) {
|
||||
buttons.push(
|
||||
<Button
|
||||
data-qa="edit-task-data-button"
|
||||
onClick={() => setEditingTaskData(true)}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
if (canCompleteTask(task)) {
|
||||
buttons.push(
|
||||
<Button
|
||||
data-qa="mark-task-complete-button"
|
||||
onClick={() => completeTask(false)}
|
||||
>
|
||||
Skip Task
|
||||
</Button>
|
||||
);
|
||||
buttons.push(
|
||||
<Button
|
||||
data-qa="execute-task-complete-button"
|
||||
onClick={() => completeTask(true)}
|
||||
>
|
||||
Execute Task
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
if (canSendEvent(task)) {
|
||||
buttons.push(
|
||||
<Button
|
||||
data-qa="select-event-button"
|
||||
onClick={() => setSelectingEvent(true)}
|
||||
>
|
||||
Send Event
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
if (canResetProcess(task)) {
|
||||
let titleText =
|
||||
'This will reset (rewind) the process to put it into a state as if the execution of the process never went past this task. ';
|
||||
titleText += 'Yes, we invented a time machine. ';
|
||||
titleText += 'And no, you cannot go back after using this feature.';
|
||||
buttons.push(
|
||||
<Button
|
||||
title={titleText}
|
||||
data-qa="reset-process-button"
|
||||
onClick={() => resetProcessInstance()}
|
||||
>
|
||||
Reset Process Here
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return buttons;
|
||||
};
|
||||
|
||||
|
@ -841,104 +911,211 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
|
|||
if (taskDataToDisplay.startsWith('ERROR:')) {
|
||||
taskDataClassName = 'failure-string';
|
||||
}
|
||||
return editingTaskData ? (
|
||||
<Editor
|
||||
height={600}
|
||||
width="auto"
|
||||
defaultLanguage="json"
|
||||
defaultValue={taskDataToDisplay}
|
||||
onChange={(value) => setTaskDataToDisplay(value || '')}
|
||||
/>
|
||||
) : (
|
||||
const numberOfLines = taskDataToDisplay.split('\n').length;
|
||||
let heightInEm = numberOfLines + 5;
|
||||
let scrollEnabled = false;
|
||||
let minimapEnabled = false;
|
||||
if (heightInEm > 30) {
|
||||
heightInEm = 30;
|
||||
scrollEnabled = true;
|
||||
minimapEnabled = true;
|
||||
}
|
||||
let taskDataHeader = 'Task data';
|
||||
let editorReadOnly = true;
|
||||
let taskDataHeaderClassName = 'with-half-rem-bottom-margin';
|
||||
|
||||
if (editingTaskData) {
|
||||
editorReadOnly = false;
|
||||
taskDataHeader = 'Edit task data';
|
||||
taskDataHeaderClassName = 'task-data-details-header';
|
||||
}
|
||||
|
||||
if (!taskDataToDisplay) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{showTaskDataLoading ? (
|
||||
<Loading className="some-class" withOverlay={false} small />
|
||||
) : null}
|
||||
<pre className={taskDataClassName}>{taskDataToDisplay}</pre>
|
||||
{taskDataClassName !== '' ? (
|
||||
<pre className={taskDataClassName}>{taskDataToDisplay}</pre>
|
||||
) : (
|
||||
<>
|
||||
<h3 className={taskDataHeaderClassName}>{taskDataHeader}</h3>
|
||||
<Editor
|
||||
height={`${heightInEm}rem`}
|
||||
width="auto"
|
||||
defaultLanguage="json"
|
||||
defaultValue={taskDataToDisplay}
|
||||
onChange={(value) => {
|
||||
setTaskDataToDisplay(value || '');
|
||||
}}
|
||||
options={{
|
||||
readOnly: editorReadOnly,
|
||||
scrollBeyondLastLine: scrollEnabled,
|
||||
minimap: { enabled: minimapEnabled },
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const eventSelector = (candidateEvents: any) => {
|
||||
const editor = (
|
||||
<Editor
|
||||
height={300}
|
||||
width="auto"
|
||||
defaultLanguage="json"
|
||||
defaultValue={eventPayload}
|
||||
onChange={(value: any) => setEventPayload(value || '{}')}
|
||||
options={{ readOnly: !eventTextEditorEnabled }}
|
||||
/>
|
||||
);
|
||||
return selectingEvent ? (
|
||||
const potentialOwnerSelector = () => {
|
||||
return (
|
||||
<Stack orientation="vertical">
|
||||
<Dropdown
|
||||
id="process-instance-select-event"
|
||||
titleText="Event"
|
||||
label="Select Event"
|
||||
items={candidateEvents}
|
||||
itemToString={(item: any) => item.name || item.label || item.typename}
|
||||
onChange={(value: any) => {
|
||||
setEventToSend(value.selectedItem);
|
||||
setEventTextEditorEnabled(
|
||||
value.selectedItem.typename === 'MessageEventDefinition'
|
||||
);
|
||||
}}
|
||||
/>
|
||||
{editor}
|
||||
<h3 className="task-data-details-header">Update task ownership</h3>
|
||||
<div className="indented-content">
|
||||
<p className="explanatory-message with-tiny-bottom-margin">
|
||||
Select a user who should be allowed to complete this task
|
||||
</p>
|
||||
<UserSearch
|
||||
className="modal-dropdown"
|
||||
onSelectedUser={(user: User) => {
|
||||
setAdditionalPotentialOwners([user]);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Stack>
|
||||
) : (
|
||||
taskDataContainer()
|
||||
);
|
||||
};
|
||||
|
||||
const eventSelector = (candidateEvents: any) => {
|
||||
let editor = null;
|
||||
let className = 'modal-dropdown';
|
||||
if (eventTextEditorEnabled) {
|
||||
className = '';
|
||||
editor = (
|
||||
<Editor
|
||||
height={300}
|
||||
width="auto"
|
||||
defaultLanguage="json"
|
||||
defaultValue={eventPayload}
|
||||
onChange={(value: any) => setEventPayload(value || '{}')}
|
||||
options={{ readOnly: !eventTextEditorEnabled }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Stack orientation="vertical">
|
||||
<h3 className="task-data-details-header">Choose event to send</h3>
|
||||
<div className="indented-content">
|
||||
<p className="explanatory-message with-tiny-bottom-margin">
|
||||
Select an event to send. A message event will require a body as
|
||||
well.
|
||||
</p>
|
||||
<Dropdown
|
||||
id="process-instance-select-event"
|
||||
className={className}
|
||||
label="Select Event"
|
||||
items={candidateEvents}
|
||||
itemToString={(item: any) =>
|
||||
item.name || item.label || item.typename
|
||||
}
|
||||
onChange={(value: any) => {
|
||||
setEventToSend(value.selectedItem);
|
||||
setEventTextEditorEnabled(
|
||||
eventsThatNeedPayload.includes(value.selectedItem.typename)
|
||||
);
|
||||
}}
|
||||
/>
|
||||
{editor}
|
||||
</div>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
const taskActionDetails = () => {
|
||||
if (!taskToDisplay) {
|
||||
return null;
|
||||
}
|
||||
let dataArea = taskDataContainer();
|
||||
if (selectingEvent) {
|
||||
const candidateEvents: any = getEvents(taskToDisplay);
|
||||
dataArea = eventSelector(candidateEvents);
|
||||
} else if (addingPotentialOwners) {
|
||||
dataArea = potentialOwnerSelector();
|
||||
}
|
||||
return dataArea;
|
||||
};
|
||||
|
||||
const taskUpdateDisplayArea = () => {
|
||||
if (!taskToDisplay) {
|
||||
return null;
|
||||
}
|
||||
const taskToUse: Task = { ...taskToDisplay, data: taskDataToDisplay };
|
||||
const candidateEvents: any = getEvents(taskToUse);
|
||||
if (taskToDisplay) {
|
||||
let taskTitleText = taskToUse.guid;
|
||||
if (taskToUse.bpmn_name) {
|
||||
taskTitleText += ` (${taskToUse.bpmn_name})`;
|
||||
}
|
||||
return (
|
||||
<Modal
|
||||
open={!!taskToUse}
|
||||
passiveModal
|
||||
onRequestClose={handleTaskDataDisplayClose}
|
||||
>
|
||||
<Stack orientation="horizontal" gap={2}>
|
||||
<span title={taskTitleText}>{taskToUse.bpmn_identifier}</span> (
|
||||
{taskToUse.typename}
|
||||
): {taskToUse.state}
|
||||
{taskDisplayButtons(taskToUse)}
|
||||
</Stack>
|
||||
|
||||
let primaryButtonText = 'Close';
|
||||
let secondaryButtonText = null;
|
||||
let onRequestSubmit = handleTaskDataDisplayClose;
|
||||
let onSecondarySubmit = handleTaskDataDisplayClose;
|
||||
let dangerous = false;
|
||||
if (editingTaskData) {
|
||||
primaryButtonText = 'Save';
|
||||
secondaryButtonText = 'Cancel';
|
||||
onSecondarySubmit = resetTaskActionDetails;
|
||||
onRequestSubmit = saveTaskData;
|
||||
dangerous = true;
|
||||
} else if (selectingEvent) {
|
||||
primaryButtonText = 'Send';
|
||||
secondaryButtonText = 'Cancel';
|
||||
onSecondarySubmit = resetTaskActionDetails;
|
||||
onRequestSubmit = sendEvent;
|
||||
dangerous = true;
|
||||
} else if (addingPotentialOwners) {
|
||||
primaryButtonText = 'Add';
|
||||
secondaryButtonText = 'Cancel';
|
||||
onSecondarySubmit = resetTaskActionDetails;
|
||||
onRequestSubmit = addPotentialOwners;
|
||||
dangerous = true;
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={!!taskToUse}
|
||||
danger={dangerous}
|
||||
primaryButtonText={primaryButtonText}
|
||||
secondaryButtonText={secondaryButtonText}
|
||||
onRequestClose={handleTaskDataDisplayClose}
|
||||
onSecondarySubmit={onSecondarySubmit}
|
||||
onRequestSubmit={onRequestSubmit}
|
||||
modalHeading={`${taskToUse.bpmn_identifier} (${taskToUse.typename}
|
||||
): ${taskToUse.state}`}
|
||||
>
|
||||
<div className="indented-content explanatory-message">
|
||||
{taskToUse.bpmn_name ? (
|
||||
<div>
|
||||
<Stack orientation="horizontal" gap={2}>
|
||||
Name: {taskToUse.bpmn_name}
|
||||
</Stack>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div>
|
||||
<Stack orientation="horizontal" gap={2}>
|
||||
Guid: {taskToUse.guid}
|
||||
</Stack>
|
||||
</div>
|
||||
{taskToUse.state === 'COMPLETED' ? (
|
||||
<div>
|
||||
<Stack orientation="horizontal" gap={2}>
|
||||
{completionViewLink(
|
||||
'View process instance at the time when this task was active.',
|
||||
taskToUse.guid
|
||||
)}
|
||||
</Stack>
|
||||
<br />
|
||||
<br />
|
||||
</div>
|
||||
) : null}
|
||||
{selectingEvent
|
||||
? eventSelector(candidateEvents)
|
||||
: taskDataContainer()}
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
</div>
|
||||
<ButtonSet>{taskDisplayButtons(taskToUse)}</ButtonSet>
|
||||
{taskToUse.state === 'COMPLETED' ? (
|
||||
<div>
|
||||
<Stack orientation="horizontal" gap={2}>
|
||||
{completionViewLink(
|
||||
'View process instance at the time when this task was active.',
|
||||
taskToUse.guid
|
||||
)}
|
||||
</Stack>
|
||||
<br />
|
||||
<br />
|
||||
</div>
|
||||
) : null}
|
||||
{taskActionDetails()}
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
const buttonIcons = () => {
|
||||
|
|
|
@ -71,9 +71,6 @@ export default function SecretNew() {
|
|||
}}
|
||||
/>
|
||||
<ButtonSet>
|
||||
<Button kind="primary" type="submit">
|
||||
Submit
|
||||
</Button>
|
||||
<Button
|
||||
kind=""
|
||||
className="button-white-background"
|
||||
|
@ -81,6 +78,9 @@ export default function SecretNew() {
|
|||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button kind="primary" type="submit">
|
||||
Submit
|
||||
</Button>
|
||||
</ButtonSet>
|
||||
</Stack>
|
||||
</Form>
|
||||
|
|
Loading…
Reference in New Issue