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:
jasquat 2023-07-12 10:14:01 -04:00 committed by GitHub
parent 93b8c09e90
commit 841f3ccc8c
14 changed files with 645 additions and 245 deletions

View File

@ -1618,6 +1618,44 @@ paths:
schema: schema:
$ref: "#/components/schemas/Workflow" $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}: /process-data/{modified_process_model_identifier}/{process_instance_id}/{process_data_identifier}:
parameters: parameters:
- name: modified_process_model_identifier - name: modified_process_model_identifier
@ -1724,7 +1762,7 @@ paths:
required: true required: true
description: The unique id of the process instance description: The unique id of the process instance
schema: schema:
type: string type: integer
- name: task_guid - name: task_guid
in: path in: path
required: true required: true

View File

@ -47,6 +47,23 @@ class SpiffworkflowBaseDBModel(db.Model): # type: ignore
return m_type.value 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( def update_created_modified_on_create_listener(
mapper: Mapper, _connection: Connection, target: SpiffworkflowBaseDBModel mapper: Mapper, _connection: Connection, target: SpiffworkflowBaseDBModel

View File

@ -29,6 +29,7 @@ from sqlalchemy.orm import aliased
from sqlalchemy.orm.util import AliasedClass from sqlalchemy.orm.util import AliasedClass
from spiffworkflow_backend.exceptions.api_error import ApiError 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.db import db
from spiffworkflow_backend.models.group import GroupModel from spiffworkflow_backend.models.group import GroupModel
from spiffworkflow_backend.models.human_task import HumanTaskModel from spiffworkflow_backend.models.human_task import HumanTaskModel
@ -173,13 +174,12 @@ def task_data_show(
def task_data_update( def task_data_update(
process_instance_id: str, process_instance_id: int,
modified_process_model_identifier: str, modified_process_model_identifier: str,
task_guid: str, task_guid: str,
body: dict, body: dict,
) -> Response: ) -> Response:
"""Update task data.""" process_instance = ProcessInstanceModel.query.filter(ProcessInstanceModel.id == process_instance_id).first()
process_instance = ProcessInstanceModel.query.filter(ProcessInstanceModel.id == int(process_instance_id)).first()
if process_instance: if process_instance:
if process_instance.status != "suspended": if process_instance.status != "suspended":
raise ProcessInstanceTaskDataCannotBeUpdatedError( raise ProcessInstanceTaskDataCannotBeUpdatedError(
@ -227,13 +227,13 @@ def task_data_update(
def manual_complete_task( def manual_complete_task(
modified_process_model_identifier: str, modified_process_model_identifier: str,
process_instance_id: str, process_instance_id: int,
task_guid: str, task_guid: str,
body: dict, body: dict,
) -> Response: ) -> Response:
"""Mark a task complete without executing it.""" """Mark a task complete without executing it."""
execute = body.get("execute", True) 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: if process_instance:
processor = ProcessInstanceProcessor(process_instance) processor = ProcessInstanceProcessor(process_instance)
processor.manual_complete_task(task_guid, execute, g.user) 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: 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) 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) 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: 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.""" """Assure any instructions for end user are processed for jinja syntax."""
if extensions is None: if extensions is None:
@ -424,7 +484,7 @@ def _interstitial_stream(
return return
# path used by the interstitial page while executing tasks - ie the background processor is not executing them # 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: if execute_tasks and ready_engine_task_count == 0:
break break
@ -467,7 +527,7 @@ def _interstitial_stream(
yield _render_data("task", task) 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]) 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( def _get_tasks(
processes_started_by_user: bool = True, processes_started_by_user: bool = True,
has_lane_assignment_id: bool = True, has_lane_assignment_id: bool = True,

View File

@ -86,6 +86,7 @@ PATH_SEGMENTS_FOR_PERMISSION_ALL = [
{"path": "/process-model-natural-language", "relevant_permissions": ["create"]}, {"path": "/process-model-natural-language", "relevant_permissions": ["create"]},
{"path": "/process-model-publish", "relevant_permissions": ["create"]}, {"path": "/process-model-publish", "relevant_permissions": ["create"]},
{"path": "/process-model-tests", "relevant_permissions": ["create"]}, {"path": "/process-model-tests", "relevant_permissions": ["create"]},
{"path": "/task-assign", "relevant_permissions": ["create"]},
{"path": "/task-data", "relevant_permissions": ["read", "update"]}, {"path": "/task-data", "relevant_permissions": ["read", "update"]},
] ]
@ -546,6 +547,7 @@ class AuthorizationService:
permissions_to_assign.append(PermissionToAssign(permission="create", target_uri="/task-complete/*")) permissions_to_assign.append(PermissionToAssign(permission="create", target_uri="/task-complete/*"))
# read comes from PG and PM ALL permissions as well # 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="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="/event-error-details/*"))
permissions_to_assign.append(PermissionToAssign(permission="read", target_uri="/logs/*")) permissions_to_assign.append(PermissionToAssign(permission="read", target_uri="/logs/*"))

View File

@ -2493,6 +2493,7 @@ class TestProcessApi(BaseTest):
content_type="application/json", content_type="application/json",
data=json.dumps({"execute": False}), data=json.dumps({"execute": False}),
) )
assert response.json["status"] == "suspended" assert response.json["status"] == "suspended"
task_model = TaskModel.query.filter_by(guid=human_task["guid"]).first() task_model = TaskModel.query.filter_by(guid=human_task["guid"]).first()
assert task_model is not None assert task_model is not None

View File

@ -138,6 +138,7 @@ class TestAuthorizationService(BaseTest):
("/process-models/some-process-group:some-process-model:*", "delete"), ("/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:*", "read"),
("/process-models/some-process-group:some-process-model:*", "update"), ("/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:*", "read"),
("/task-data/some-process-group:some-process-model:*", "update"), ("/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/*", "delete"),
("/process-models/some-process-group:some-process-model/*", "read"), ("/process-models/some-process-group:some-process-model/*", "read"),
("/process-models/some-process-group:some-process-model/*", "update"), ("/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/*", "read"),
("/task-data/some-process-group:some-process-model/*", "update"), ("/task-data/some-process-group:some-process-model/*", "update"),
] ]
@ -333,6 +335,7 @@ class TestAuthorizationService(BaseTest):
("/secrets/*", "read"), ("/secrets/*", "read"),
("/secrets/*", "update"), ("/secrets/*", "update"),
("/send-event/*", "create"), ("/send-event/*", "create"),
("/task-assign/*", "create"),
("/task-complete/*", "create"), ("/task-complete/*", "create"),
("/task-data/*", "update"), ("/task-data/*", "update"),
("/task-data/*", "read"), ("/task-data/*", "read"),

View File

@ -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 { useNavigate } from 'react-router-dom';
import { fetchEventSource } from '@microsoft/fetch-event-source'; import { fetchEventSource } from '@microsoft/fetch-event-source';
// @ts-ignore // @ts-ignore
@ -10,6 +10,7 @@ import { getBasicHeaders } from '../services/HttpService';
import InstructionsForEndUser from './InstructionsForEndUser'; import InstructionsForEndUser from './InstructionsForEndUser';
import { ProcessInstance, ProcessInstanceTask } from '../interfaces'; import { ProcessInstance, ProcessInstanceTask } from '../interfaces';
import useAPIError from '../hooks/UseApiError'; import useAPIError from '../hooks/UseApiError';
import { HUMAN_TASK_TYPES } from '../helpers';
type OwnProps = { type OwnProps = {
processInstanceId: number; processInstanceId: number;
@ -35,9 +36,6 @@ export default function ProcessInterstitial({
useState<ProcessInstance | null>(null); useState<ProcessInstance | null>(null);
const navigate = useNavigate(); const navigate = useNavigate();
const userTasks = useMemo(() => {
return ['User Task', 'Manual Task'];
}, []);
const { addError } = useAPIError(); const { addError } = useAPIError();
useEffect(() => { useEffect(() => {
@ -77,10 +75,10 @@ export default function ProcessInterstitial({
!processInstance && !processInstance &&
myTask && myTask &&
myTask.can_complete && myTask.can_complete &&
userTasks.includes(myTask.type) HUMAN_TASK_TYPES.includes(myTask.type)
); );
}, },
[allowRedirect, processInstance, userTasks] [allowRedirect, processInstance]
); );
const shouldRedirectToProcessInstance = useCallback((): boolean => { const shouldRedirectToProcessInstance = useCallback((): boolean => {
@ -105,7 +103,6 @@ export default function ProcessInterstitial({
}, [ }, [
lastTask, lastTask,
navigate, navigate,
userTasks,
shouldRedirectToTask, shouldRedirectToTask,
processInstanceId, processInstanceId,
processInstanceShowPageUrl, processInstanceShowPageUrl,
@ -181,7 +178,7 @@ export default function ProcessInterstitial({
return userMessageForProcessInstance(processInstance, myTask); return userMessageForProcessInstance(processInstance, myTask);
} }
if (!myTask.can_complete && userTasks.includes(myTask.type)) { if (!myTask.can_complete && HUMAN_TASK_TYPES.includes(myTask.type)) {
return inlineMessage( return inlineMessage(
'', '',
`This next task is assigned to a different person or team. There is no action for you to take at this time.` `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)) { if (shouldRedirectToTask(myTask)) {
return inlineMessage('', `Redirecting ...`); 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( return inlineMessage(
'', '',
`The task "${myTask.title}" is ready for you to complete.` `The task "${myTask.title}" is ready for you to complete.`

View File

@ -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}
/>
);
}

View File

@ -26,6 +26,13 @@ export const slugifyString = (str: any) => {
.replace(/-+$/g, ''); .replace(/-+$/g, '');
}; };
export const HUMAN_TASK_TYPES = [
'User Task',
'Manual Task',
'UserTask',
'ManualTask',
];
export const underscorizeString = (inputString: string) => { export const underscorizeString = (inputString: string) => {
return slugifyString(inputString).replace(/-/g, '_'); return slugifyString(inputString).replace(/-/g, '_');
}; };

View File

@ -21,6 +21,7 @@ export const useUriListForPermissions = () => {
processInstanceResumePath: `/v1.0/process-instance-resume/${params.process_model_id}/${params.process_instance_id}`, 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}`, 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}`, 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}`, 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`, 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`, processInstanceTaskListPath: `/v1.0/process-instances/${params.process_model_id}/${params.process_instance_id}/task-info`,

View File

@ -44,7 +44,7 @@ h1 {
font-size: 28px; font-size: 28px;
line-height: 36px; line-height: 36px;
color: #161616; color: #161616;
margin-bottom: 1em margin-bottom: 1rem
} }
h2 { h2 {
@ -54,6 +54,13 @@ h2 {
color: #161616; color: #161616;
} }
h3 {
font-weight: 400;
font-size: 18px;
line-height: 20px;
color: #161616;
}
.span-tag { .span-tag {
color: black; color: black;
} }
@ -137,12 +144,12 @@ code {
.app-logo { .app-logo {
height: 37px; height: 37px;
width: 152px; width: 152px;
margin-top: 1em; margin-top: 1rem;
margin-bottom: 1em; margin-bottom: 1rem;
} }
.spiffworkflow-header-container { .spiffworkflow-header-container {
margin-bottom: 2em; margin-bottom: 2rem;
} }
.active-task-highlight:not(.djs-connection) .djs-visual > :nth-child(1) { .active-task-highlight:not(.djs-connection) .djs-visual > :nth-child(1) {
@ -168,11 +175,11 @@ code {
} }
.cds--breadcrumb { .cds--breadcrumb {
margin-bottom: 1em; margin-bottom: 2rem;
} }
.process-description { .process-description {
margin-bottom: 2em; margin-bottom: 2rem;
} }
h1.with-icons { h1.with-icons {
@ -211,7 +218,7 @@ dl dd {
} }
.with-bottom-margin { .with-bottom-margin {
margin-bottom: 1em; margin-bottom: 1rem;
} }
.user-profile-toggletip-content { .user-profile-toggletip-content {
@ -253,15 +260,15 @@ dl dd {
} }
.with-top-margin { .with-top-margin {
margin-top: 1em; margin-top: 1rem;
} }
.with-extra-top-margin { .with-extra-top-margin {
margin-top: 1.3em; margin-top: 1.3rem;
} }
.with-top-margin-for-label-next-to-text-input { .with-top-margin-for-label-next-to-text-input {
margin-top: 2.3em; margin-top: 2.3rem;
} }
.with-tiny-top-margin { .with-tiny-top-margin {
@ -273,13 +280,17 @@ dl dd {
} }
.with-large-bottom-margin { .with-large-bottom-margin {
margin-bottom: 3em; margin-bottom: 3rem;
} }
.with-tiny-bottom-margin { .with-tiny-bottom-margin {
margin-bottom: 4px; margin-bottom: 4px;
} }
.with-half-rem-bottom-margin {
margin-bottom: .5rem;
}
.diagram-viewer-canvas { .diagram-viewer-canvas {
border:1px solid #000000; border:1px solid #000000;
height:70vh; height:70vh;
@ -288,7 +299,7 @@ dl dd {
} }
.breadcrumb { .breadcrumb {
font-size: 1.5em; font-size: 1.5rem;
} }
.breadcrumb-item.active { .breadcrumb-item.active {
@ -296,7 +307,7 @@ dl dd {
} }
.container .nav-tabs { .container .nav-tabs {
margin-top: 1em; margin-top: 1rem;
} }
@ -314,7 +325,7 @@ dl dd {
} }
.markdown table th, .markdown table th,
.markdown table td { .markdown table td {
padding: .5em; padding: .5rem;
border: 1px solid lightgrey; border: 1px solid lightgrey;
} }
/* Zebra Table Style */ /* Zebra Table Style */
@ -323,7 +334,7 @@ dl dd {
} }
.form-instructions { .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 /* 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 { .tile-process-group-content-container {
width: 320px; width: 320px;
height: 264px; height: 264px;
padding: 1em; padding: 1rem;
position: relative; position: relative;
} }
.tile-process-group-display-name { .tile-process-group-display-name {
margin-top: 2em; margin-top: 2rem;
margin-bottom: 1em; margin-bottom: 1rem;
font-size: 20px; font-size: 20px;
line-height: 28px; line-height: 28px;
color: #161616; 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 { .tile-title-top {
margin-bottom: 2em; margin-bottom: 2rem;
font-size: 20px; font-size: 20px;
line-height: 28px; line-height: 28px;
color: #161616; 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 { .tile-pin-bottom {
position: absolute; position: absolute;
bottom: 1em; bottom: 1rem;
} }
.cds--tabs .cds--tabs__nav-link { .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 { td.actions-cell {
width: 1em; width: 1rem;
} }
.process-instance-list-table { .process-instance-list-table {
@ -411,12 +422,12 @@ td.actions-cell {
} }
.process-instance-table-header { .process-instance-table-header {
margin-bottom: 1em; margin-bottom: 1rem;
} }
.no-results-message { .no-results-message {
font-style: italic; font-style: italic;
margin-left: 2em; margin-left: 2rem;
font-size: 14px; font-size: 14px;
} }
@ -425,12 +436,12 @@ td.actions-cell {
line-height: 18px; line-height: 18px;
letter-spacing: 0.16px; letter-spacing: 0.16px;
color: #525252; 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 */ /* top and bottom margin since this is sort of the middle of three sections on the process model show page */
.process-model-files-section { .process-model-files-section {
margin: 2em 0; margin: 2rem 0;
} }
.filterIcon { .filterIcon {
@ -525,7 +536,7 @@ svg.notification-icon {
} }
.please-press-filter-button { .please-press-filter-button {
margin-bottom: 1em; margin-bottom: 1rem;
font-weight: bold; font-weight: bold;
} }
@ -537,28 +548,28 @@ svg.notification-icon {
.user_instructions_0 { .user_instructions_0 {
filter: opacity(1); filter: opacity(1);
font-size: 1.2em; font-size: 1.2rem;
margin-bottom: 30px; margin-bottom: 30px;
} }
.user_instructions_1 { .user_instructions_1 {
filter: opacity(60%); filter: opacity(60%);
font-size: 1.1em; font-size: 1.1rem;
} }
.user_instructions_2 { .user_instructions_2 {
filter: opacity(40%); filter: opacity(40%);
font-size: 1em; font-size: 1rem;
} }
.user_instructions_3 { .user_instructions_3 {
filter: opacity(20%); filter: opacity(20%);
font-size: 9em; font-size: 9rem;
} }
.user_instructions_4 { .user_instructions_4 {
filter: opacity(10%); filter: opacity(10%);
font-size: 8em; font-size: 8rem;
} }
.float-right { .float-right {
@ -649,7 +660,25 @@ hr {
font-style: italic; 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 { #hidden-form-for-autosave {
display: none; display: none;
} }

View File

@ -26,10 +26,6 @@ export interface TaskPropertiesJson {
last_state_change: number; last_state_change: number;
} }
export interface TaskDefinitionPropertiesJson {
spec: string;
}
export interface EventDefinition { export interface EventDefinition {
typename: string; typename: string;
payload: any; payload: any;
@ -38,6 +34,11 @@ export interface EventDefinition {
message_var?: string; message_var?: string;
} }
export interface TaskDefinitionPropertiesJson {
spec: string;
event_definition: EventDefinition;
}
export interface SignalButton { export interface SignalButton {
label: string; label: string;
event: EventDefinition; event: EventDefinition;
@ -286,7 +287,7 @@ export interface PaginationObject {
} }
export interface CarbonComboBoxSelection { export interface CarbonComboBoxSelection {
selectedItem: ProcessModel; selectedItem: any;
} }
export interface CarbonComboBoxProcessSelection { export interface CarbonComboBoxProcessSelection {

View File

@ -7,12 +7,20 @@ import {
useSearchParams, useSearchParams,
} from 'react-router-dom'; } from 'react-router-dom';
import { import {
TrashCan, Send,
StopOutline, ButtonSet,
PauseOutline,
PlayOutline,
InProgress,
Checkmark, Checkmark,
Edit,
InProgress,
PauseOutline,
UserFollow,
Play,
PlayOutline,
Reset,
RuleDraft,
SkipForward,
StopOutline,
TrashCan,
Warning, Warning,
// @ts-ignore // @ts-ignore
} from '@carbon/icons-react'; } from '@carbon/icons-react';
@ -37,6 +45,7 @@ import HttpService from '../services/HttpService';
import ReactDiagramEditor from '../components/ReactDiagramEditor'; import ReactDiagramEditor from '../components/ReactDiagramEditor';
import { import {
convertSecondsToFormattedDateTime, convertSecondsToFormattedDateTime,
HUMAN_TASK_TYPES,
modifyProcessIdentifierForPathParam, modifyProcessIdentifierForPathParam,
unModifyProcessIdentifierForPathParam, unModifyProcessIdentifierForPathParam,
} from '../helpers'; } from '../helpers';
@ -50,12 +59,14 @@ import {
ProcessInstance, ProcessInstance,
Task, Task,
TaskDefinitionPropertiesJson, TaskDefinitionPropertiesJson,
User,
} from '../interfaces'; } from '../interfaces';
import { usePermissionFetcher } from '../hooks/PermissionService'; import { usePermissionFetcher } from '../hooks/PermissionService';
import ProcessInstanceClass from '../classes/ProcessInstanceClass'; import ProcessInstanceClass from '../classes/ProcessInstanceClass';
import TaskListTable from '../components/TaskListTable'; import TaskListTable from '../components/TaskListTable';
import useAPIError from '../hooks/UseApiError'; import useAPIError from '../hooks/UseApiError';
import ProcessInterstitial from '../components/ProcessInterstitial'; import ProcessInterstitial from '../components/ProcessInterstitial';
import UserSearch from '../components/UserSearch';
import ProcessInstanceLogList from '../components/ProcessInstanceLogList'; import ProcessInstanceLogList from '../components/ProcessInstanceLogList';
import MessageInstanceList from '../components/MessageInstanceList'; import MessageInstanceList from '../components/MessageInstanceList';
@ -68,6 +79,8 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
const params = useParams(); const params = useParams();
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const eventsThatNeedPayload = ['MessageEventDefinition'];
const [processInstance, setProcessInstance] = const [processInstance, setProcessInstance] =
useState<ProcessInstance | null>(null); useState<ProcessInstance | null>(null);
const [tasks, setTasks] = useState<Task[] | null>(null); const [tasks, setTasks] = useState<Task[] | null>(null);
@ -89,6 +102,12 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
const [eventTextEditorEnabled, setEventTextEditorEnabled] = const [eventTextEditorEnabled, setEventTextEditorEnabled] =
useState<boolean>(false); useState<boolean>(false);
const [addingPotentialOwners, setAddingPotentialOwners] =
useState<boolean>(false);
const [additionalPotentialOwners, setAdditionalPotentialOwners] = useState<
User[] | null
>(null);
const { addError, removeError } = useAPIError(); const { addError, removeError } = useAPIError();
const unModifiedProcessModelId = unModifyProcessIdentifierForPathParam( const unModifiedProcessModelId = unModifyProcessIdentifierForPathParam(
`${params.process_model_id}` `${params.process_model_id}`
@ -109,6 +128,7 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
[targetUris.messageInstanceListPath]: ['GET'], [targetUris.messageInstanceListPath]: ['GET'],
[targetUris.processInstanceActionPath]: ['DELETE', 'GET'], [targetUris.processInstanceActionPath]: ['DELETE', 'GET'],
[targetUris.processInstanceLogListPath]: ['GET'], [targetUris.processInstanceLogListPath]: ['GET'],
[targetUris.processInstanceTaskAssignPath]: ['POST'],
[targetUris.processInstanceTaskDataPath]: ['GET', 'PUT'], [targetUris.processInstanceTaskDataPath]: ['GET', 'PUT'],
[targetUris.processInstanceSendEventPath]: ['POST'], [targetUris.processInstanceSendEventPath]: ['POST'],
[targetUris.processInstanceCompleteTaskPath]: ['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 = () => { const handleTaskDataDisplayClose = () => {
setTaskToDisplay(null); setTaskToDisplay(null);
initializeTaskDataToDisplay(null); initializeTaskDataToDisplay(null);
if (editingTaskData || selectingEvent || addingPotentialOwners) {
resetTaskActionDetails();
}
}; };
const getTaskById = (taskId: string) => { const getTaskById = (taskId: string) => {
@ -602,8 +635,9 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
const canSendEvent = (task: Task) => { const canSendEvent = (task: Task) => {
// We actually could allow this for any waiting events // We actually could allow this for any waiting events
const taskTypes = ['Event Based Gateway']; const taskTypes = ['EventBasedGateway'];
return ( return (
!selectingEvent &&
processInstance && processInstance &&
processInstance.status === 'waiting' && processInstance.status === 'waiting' &&
ability.can('POST', targetUris.processInstanceSendEventPath) && 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) => { const canResetProcess = (task: Task) => {
return ( return (
ability.can('POST', targetUris.processInstanceResetPath) && ability.can('POST', targetUris.processInstanceResetPath) &&
@ -635,7 +680,7 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
const getEvents = (task: Task) => { const getEvents = (task: Task) => {
const handleMessage = (eventDefinition: EventDefinition) => { const handleMessage = (eventDefinition: EventDefinition) => {
if (eventDefinition.typename === 'MessageEventDefinition') { if (eventsThatNeedPayload.includes(eventDefinition.typename)) {
const newEvent = eventDefinition; const newEvent = eventDefinition;
delete newEvent.message_var; delete newEvent.message_var;
newEvent.payload = {}; newEvent.payload = {};
@ -643,22 +688,16 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
} }
return eventDefinition; return eventDefinition;
}; };
if (task.event_definition && task.event_definition.event_definitions) const eventDefinition =
return task.event_definition.event_definitions.map((e: EventDefinition) => task.task_definition_properties_json.event_definition;
if (eventDefinition && eventDefinition.event_definitions)
return eventDefinition.event_definitions.map((e: EventDefinition) =>
handleMessage(e) handleMessage(e)
); );
if (task.event_definition) return [handleMessage(task.event_definition)]; if (eventDefinition) return [handleMessage(eventDefinition)];
return []; return [];
}; };
const cancelUpdatingTask = () => {
setEditingTaskData(false);
setSelectingEvent(false);
initializeTaskDataToDisplay(taskToDisplay);
setEventPayload('{}');
removeError();
};
const taskDataStringToObject = (dataString: string) => { const taskDataStringToObject = (dataString: string) => {
return JSON.parse(dataString); return JSON.parse(dataString);
}; };
@ -673,7 +712,6 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
}; // spread operator }; // spread operator
setTaskToDisplay(taskToDisplayCopy); setTaskToDisplay(taskToDisplayCopy);
} }
refreshPage();
}; };
const saveTaskData = () => { 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 = () => { 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',
successCallback: saveTaskDataResult, successCallback: refreshPage,
failureCallback: addError, failureCallback: addError,
postBody: eventToSend, postBody: eventToSend,
}); });
@ -720,18 +780,24 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
const taskDisplayButtons = (task: Task) => { const taskDisplayButtons = (task: Task) => {
const buttons = []; const buttons = [];
if (editingTaskData || addingPotentialOwners || selectingEvent) {
return null;
}
if ( if (
task.typename === 'Script Task' && task.typename === 'ScriptTask' &&
ability.can('PUT', targetUris.processModelShowPath) ability.can('PUT', targetUris.processModelShowPath)
) { ) {
buttons.push( buttons.push(
<Button <Button
kind="ghost"
align="top-left"
renderIcon={RuleDraft}
iconDescription="Create Script Unit Test"
hasIconOnly
data-qa="create-script-unit-test-button" data-qa="create-script-unit-test-button"
onClick={createScriptUnitTest} onClick={createScriptUnitTest}
> />
Create Script Unit Test
</Button>
); );
} }
@ -749,90 +815,94 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
); );
} }
if (editingTaskData) { if (canEditTaskData(task)) {
buttons.push(
<Button data-qa="save-task-data-button" onClick={saveTaskData}>
Save
</Button>
);
buttons.push( buttons.push(
<Button <Button
data-qa="cancel-task-data-edit-button" kind="ghost"
onClick={cancelUpdatingTask} renderIcon={Edit}
> align="top-left"
Cancel iconDescription="Edit Task Data"
</Button> 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; return buttons;
}; };
@ -841,104 +911,211 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
if (taskDataToDisplay.startsWith('ERROR:')) { if (taskDataToDisplay.startsWith('ERROR:')) {
taskDataClassName = 'failure-string'; taskDataClassName = 'failure-string';
} }
return editingTaskData ? ( const numberOfLines = taskDataToDisplay.split('\n').length;
<Editor let heightInEm = numberOfLines + 5;
height={600} let scrollEnabled = false;
width="auto" let minimapEnabled = false;
defaultLanguage="json" if (heightInEm > 30) {
defaultValue={taskDataToDisplay} heightInEm = 30;
onChange={(value) => setTaskDataToDisplay(value || '')} 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 ? ( {showTaskDataLoading ? (
<Loading className="some-class" withOverlay={false} small /> <Loading className="some-class" withOverlay={false} small />
) : null} ) : 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 potentialOwnerSelector = () => {
const editor = ( return (
<Editor
height={300}
width="auto"
defaultLanguage="json"
defaultValue={eventPayload}
onChange={(value: any) => setEventPayload(value || '{}')}
options={{ readOnly: !eventTextEditorEnabled }}
/>
);
return selectingEvent ? (
<Stack orientation="vertical"> <Stack orientation="vertical">
<Dropdown <h3 className="task-data-details-header">Update task ownership</h3>
id="process-instance-select-event" <div className="indented-content">
titleText="Event" <p className="explanatory-message with-tiny-bottom-margin">
label="Select Event" Select a user who should be allowed to complete this task
items={candidateEvents} </p>
itemToString={(item: any) => item.name || item.label || item.typename} <UserSearch
onChange={(value: any) => { className="modal-dropdown"
setEventToSend(value.selectedItem); onSelectedUser={(user: User) => {
setEventTextEditorEnabled( setAdditionalPotentialOwners([user]);
value.selectedItem.typename === 'MessageEventDefinition' }}
); />
}} </div>
/>
{editor}
</Stack> </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 = () => { const taskUpdateDisplayArea = () => {
if (!taskToDisplay) { if (!taskToDisplay) {
return null; return null;
} }
const taskToUse: Task = { ...taskToDisplay, data: taskDataToDisplay }; const taskToUse: Task = { ...taskToDisplay, data: taskDataToDisplay };
const candidateEvents: any = getEvents(taskToUse);
if (taskToDisplay) { let primaryButtonText = 'Close';
let taskTitleText = taskToUse.guid; let secondaryButtonText = null;
if (taskToUse.bpmn_name) { let onRequestSubmit = handleTaskDataDisplayClose;
taskTitleText += ` (${taskToUse.bpmn_name})`; let onSecondarySubmit = handleTaskDataDisplayClose;
} let dangerous = false;
return ( if (editingTaskData) {
<Modal primaryButtonText = 'Save';
open={!!taskToUse} secondaryButtonText = 'Cancel';
passiveModal onSecondarySubmit = resetTaskActionDetails;
onRequestClose={handleTaskDataDisplayClose} onRequestSubmit = saveTaskData;
> dangerous = true;
<Stack orientation="horizontal" gap={2}> } else if (selectingEvent) {
<span title={taskTitleText}>{taskToUse.bpmn_identifier}</span> ( primaryButtonText = 'Send';
{taskToUse.typename} secondaryButtonText = 'Cancel';
): {taskToUse.state} onSecondarySubmit = resetTaskActionDetails;
{taskDisplayButtons(taskToUse)} onRequestSubmit = sendEvent;
</Stack> 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> <div>
<Stack orientation="horizontal" gap={2}> <Stack orientation="horizontal" gap={2}>
Guid: {taskToUse.guid} Guid: {taskToUse.guid}
</Stack> </Stack>
</div> </div>
{taskToUse.state === 'COMPLETED' ? ( </div>
<div> <ButtonSet>{taskDisplayButtons(taskToUse)}</ButtonSet>
<Stack orientation="horizontal" gap={2}> {taskToUse.state === 'COMPLETED' ? (
{completionViewLink( <div>
'View process instance at the time when this task was active.', <Stack orientation="horizontal" gap={2}>
taskToUse.guid {completionViewLink(
)} 'View process instance at the time when this task was active.',
</Stack> taskToUse.guid
<br /> )}
<br /> </Stack>
</div> <br />
) : null} <br />
{selectingEvent </div>
? eventSelector(candidateEvents) ) : null}
: taskDataContainer()} {taskActionDetails()}
</Modal> </Modal>
); );
}
return null;
}; };
const buttonIcons = () => { const buttonIcons = () => {

View File

@ -71,9 +71,6 @@ export default function SecretNew() {
}} }}
/> />
<ButtonSet> <ButtonSet>
<Button kind="primary" type="submit">
Submit
</Button>
<Button <Button
kind="" kind=""
className="button-white-background" className="button-white-background"
@ -81,6 +78,9 @@ export default function SecretNew() {
> >
Cancel Cancel
</Button> </Button>
<Button kind="primary" type="submit">
Submit
</Button>
</ButtonSet> </ButtonSet>
</Stack> </Stack>
</Form> </Form>