Merge pull request #201 from sartography/feature/notion_261.1_save_as_draft
added ability to save a form as draft w/ burnettk
This commit is contained in:
commit
099973c701
|
@ -1832,10 +1832,10 @@ paths:
|
||||||
description: The unique id of an existing process instance.
|
description: The unique id of an existing process instance.
|
||||||
schema:
|
schema:
|
||||||
type: integer
|
type: integer
|
||||||
- name: terminate_loop
|
- name: save_as_draft
|
||||||
in: query
|
in: query
|
||||||
required: false
|
required: false
|
||||||
description: Terminate the loop on a looping task
|
description: Save the data to task but do not complete it.
|
||||||
schema:
|
schema:
|
||||||
type: boolean
|
type: boolean
|
||||||
get:
|
get:
|
||||||
|
|
|
@ -180,13 +180,7 @@ def task_data_show(
|
||||||
process_instance_id: int,
|
process_instance_id: int,
|
||||||
task_guid: str,
|
task_guid: str,
|
||||||
) -> flask.wrappers.Response:
|
) -> flask.wrappers.Response:
|
||||||
task_model = TaskModel.query.filter_by(guid=task_guid, process_instance_id=process_instance_id).first()
|
task_model = _get_task_model_from_guid_or_raise(task_guid, process_instance_id)
|
||||||
if task_model is None:
|
|
||||||
raise ApiError(
|
|
||||||
error_code="task_not_found",
|
|
||||||
message=f"Cannot find a task with guid '{task_guid}' for process instance '{process_instance_id}'",
|
|
||||||
status_code=400,
|
|
||||||
)
|
|
||||||
task_model.data = task_model.json_data()
|
task_model.data = task_model.json_data()
|
||||||
return make_response(jsonify(task_model), 200)
|
return make_response(jsonify(task_model), 200)
|
||||||
|
|
||||||
|
@ -216,13 +210,11 @@ def task_data_update(
|
||||||
if "new_task_data" in body:
|
if "new_task_data" in body:
|
||||||
new_task_data_str: str = body["new_task_data"]
|
new_task_data_str: str = body["new_task_data"]
|
||||||
new_task_data_dict = json.loads(new_task_data_str)
|
new_task_data_dict = json.loads(new_task_data_str)
|
||||||
json_data_dict = TaskService.update_task_data_on_task_model(
|
json_data_dict = TaskService.update_task_data_on_task_model_and_return_dict_if_updated(
|
||||||
task_model, new_task_data_dict, "json_data_hash"
|
task_model, new_task_data_dict, "json_data_hash"
|
||||||
)
|
)
|
||||||
if json_data_dict is not None:
|
if json_data_dict is not None:
|
||||||
TaskService.insert_or_update_json_data_records({json_data_dict["hash"]: json_data_dict})
|
TaskService.insert_or_update_json_data_records({json_data_dict["hash"]: json_data_dict})
|
||||||
# json_data = JsonDataModel(**json_data_dict)
|
|
||||||
# db.session.add(json_data)
|
|
||||||
ProcessInstanceProcessor.add_event_to_process_instance(
|
ProcessInstanceProcessor.add_event_to_process_instance(
|
||||||
process_instance, ProcessInstanceEventType.task_data_edited.value, task_guid=task_guid
|
process_instance, ProcessInstanceEventType.task_data_edited.value, task_guid=task_guid
|
||||||
)
|
)
|
||||||
|
@ -389,11 +381,11 @@ def process_data_show(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def task_submit_shared(
|
def _task_submit_shared(
|
||||||
process_instance_id: int,
|
process_instance_id: int,
|
||||||
task_guid: str,
|
task_guid: str,
|
||||||
body: Dict[str, Any],
|
body: Dict[str, Any],
|
||||||
terminate_loop: bool = False,
|
save_as_draft: bool = False,
|
||||||
) -> flask.wrappers.Response:
|
) -> flask.wrappers.Response:
|
||||||
principal = _find_principal_or_raise()
|
principal = _find_principal_or_raise()
|
||||||
process_instance = _find_process_instance_by_id_or_raise(process_instance_id)
|
process_instance = _find_process_instance_by_id_or_raise(process_instance_id)
|
||||||
|
@ -420,25 +412,10 @@ def task_submit_shared(
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
if terminate_loop and spiff_task.is_looping():
|
# multi-instance code from crconnect - we may need it or may not
|
||||||
spiff_task.terminate_loop()
|
# if terminate_loop and spiff_task.is_looping():
|
||||||
|
# spiff_task.terminate_loop()
|
||||||
human_task = _find_human_task_or_raise(
|
#
|
||||||
process_instance_id=process_instance_id,
|
|
||||||
task_guid=task_guid,
|
|
||||||
only_tasks_that_can_be_completed=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
with sentry_sdk.start_span(op="task", description="complete_form_task"):
|
|
||||||
with ProcessInstanceQueueService.dequeued(process_instance):
|
|
||||||
ProcessInstanceService.complete_form_task(
|
|
||||||
processor=processor,
|
|
||||||
spiff_task=spiff_task,
|
|
||||||
data=body,
|
|
||||||
user=g.user,
|
|
||||||
human_task=human_task,
|
|
||||||
)
|
|
||||||
|
|
||||||
# If we need to update all tasks, then get the next ready task and if it a multi-instance with the same
|
# If we need to update all tasks, then get the next ready task and if it a multi-instance with the same
|
||||||
# task spec, complete that form as well.
|
# task spec, complete that form as well.
|
||||||
# if update_all:
|
# if update_all:
|
||||||
|
@ -449,15 +426,41 @@ def task_submit_shared(
|
||||||
# last_index = next_task.task_info()["mi_index"]
|
# last_index = next_task.task_info()["mi_index"]
|
||||||
# next_task = processor.next_task()
|
# next_task = processor.next_task()
|
||||||
|
|
||||||
next_human_task_assigned_to_me = (
|
if save_as_draft:
|
||||||
HumanTaskModel.query.filter_by(process_instance_id=process_instance_id, completed=False)
|
task_model = _get_task_model_from_guid_or_raise(task_guid, process_instance_id)
|
||||||
.order_by(asc(HumanTaskModel.id)) # type: ignore
|
json_data_dict = TaskService.update_task_data_on_task_model_and_return_dict_if_updated(
|
||||||
.join(HumanTaskUserModel)
|
task_model, body, "json_data_hash"
|
||||||
.filter_by(user_id=principal.user_id)
|
)
|
||||||
.first()
|
if json_data_dict is not None:
|
||||||
)
|
TaskService.insert_or_update_json_data_dict(json_data_dict)
|
||||||
if next_human_task_assigned_to_me:
|
db.session.add(task_model)
|
||||||
return make_response(jsonify(HumanTaskModel.to_task(next_human_task_assigned_to_me)), 200)
|
db.session.commit()
|
||||||
|
else:
|
||||||
|
human_task = _find_human_task_or_raise(
|
||||||
|
process_instance_id=process_instance_id,
|
||||||
|
task_guid=task_guid,
|
||||||
|
only_tasks_that_can_be_completed=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
with sentry_sdk.start_span(op="task", description="complete_form_task"):
|
||||||
|
with ProcessInstanceQueueService.dequeued(process_instance):
|
||||||
|
ProcessInstanceService.complete_form_task(
|
||||||
|
processor=processor,
|
||||||
|
spiff_task=spiff_task,
|
||||||
|
data=body,
|
||||||
|
user=g.user,
|
||||||
|
human_task=human_task,
|
||||||
|
)
|
||||||
|
|
||||||
|
next_human_task_assigned_to_me = (
|
||||||
|
HumanTaskModel.query.filter_by(process_instance_id=process_instance_id, completed=False)
|
||||||
|
.order_by(asc(HumanTaskModel.id)) # type: ignore
|
||||||
|
.join(HumanTaskUserModel)
|
||||||
|
.filter_by(user_id=principal.user_id)
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
if next_human_task_assigned_to_me:
|
||||||
|
return make_response(jsonify(HumanTaskModel.to_task(next_human_task_assigned_to_me)), 200)
|
||||||
|
|
||||||
return Response(json.dumps({"ok": True}), status=202, mimetype="application/json")
|
return Response(json.dumps({"ok": True}), status=202, mimetype="application/json")
|
||||||
|
|
||||||
|
@ -466,11 +469,11 @@ def task_submit(
|
||||||
process_instance_id: int,
|
process_instance_id: int,
|
||||||
task_guid: str,
|
task_guid: str,
|
||||||
body: Dict[str, Any],
|
body: Dict[str, Any],
|
||||||
terminate_loop: bool = False,
|
save_as_draft: bool = False,
|
||||||
) -> flask.wrappers.Response:
|
) -> flask.wrappers.Response:
|
||||||
"""Task_submit_user_data."""
|
"""Task_submit_user_data."""
|
||||||
with sentry_sdk.start_span(op="controller_action", description="tasks_controller.task_submit"):
|
with sentry_sdk.start_span(op="controller_action", description="tasks_controller.task_submit"):
|
||||||
return task_submit_shared(process_instance_id, task_guid, body, terminate_loop)
|
return _task_submit_shared(process_instance_id, task_guid, body, save_as_draft)
|
||||||
|
|
||||||
|
|
||||||
def _get_tasks(
|
def _get_tasks(
|
||||||
|
@ -764,3 +767,16 @@ def _munge_form_ui_schema_based_on_hidden_fields_in_task_data(task: Task) -> Non
|
||||||
relevant_depth_of_ui_schema = relevant_depth_of_ui_schema[hidden_field_part]
|
relevant_depth_of_ui_schema = relevant_depth_of_ui_schema[hidden_field_part]
|
||||||
if len(hidden_field_parts) == ii + 1:
|
if len(hidden_field_parts) == ii + 1:
|
||||||
relevant_depth_of_ui_schema["ui:widget"] = "hidden"
|
relevant_depth_of_ui_schema["ui:widget"] = "hidden"
|
||||||
|
|
||||||
|
|
||||||
|
def _get_task_model_from_guid_or_raise(task_guid: str, process_instance_id: int) -> TaskModel:
|
||||||
|
task_model: Optional[TaskModel] = TaskModel.query.filter_by(
|
||||||
|
guid=task_guid, process_instance_id=process_instance_id
|
||||||
|
).first()
|
||||||
|
if task_model is None:
|
||||||
|
raise ApiError(
|
||||||
|
error_code="task_not_found",
|
||||||
|
message=f"Cannot find a task with guid '{task_guid}' for process instance '{process_instance_id}'",
|
||||||
|
status_code=400,
|
||||||
|
)
|
||||||
|
return task_model
|
||||||
|
|
|
@ -93,6 +93,7 @@ from spiffworkflow_backend.services.process_instance_queue_service import Proces
|
||||||
from spiffworkflow_backend.services.process_model_service import ProcessModelService
|
from spiffworkflow_backend.services.process_model_service import ProcessModelService
|
||||||
from spiffworkflow_backend.services.service_task_service import ServiceTaskDelegate
|
from spiffworkflow_backend.services.service_task_service import ServiceTaskDelegate
|
||||||
from spiffworkflow_backend.services.spec_file_service import SpecFileService
|
from spiffworkflow_backend.services.spec_file_service import SpecFileService
|
||||||
|
from spiffworkflow_backend.services.task_service import JsonDataDict
|
||||||
from spiffworkflow_backend.services.task_service import TaskService
|
from spiffworkflow_backend.services.task_service import TaskService
|
||||||
from spiffworkflow_backend.services.user_service import UserService
|
from spiffworkflow_backend.services.user_service import UserService
|
||||||
from spiffworkflow_backend.services.workflow_execution_service import (
|
from spiffworkflow_backend.services.workflow_execution_service import (
|
||||||
|
@ -1790,12 +1791,9 @@ class ProcessInstanceProcessor:
|
||||||
db.session.add(human_task)
|
db.session.add(human_task)
|
||||||
|
|
||||||
json_data_dict_list = TaskService.update_task_model(task_model, spiff_task, self._serializer)
|
json_data_dict_list = TaskService.update_task_model(task_model, spiff_task, self._serializer)
|
||||||
for json_data_dict in json_data_dict_list:
|
json_data_dict_mapping: dict[str, JsonDataDict] = {}
|
||||||
if json_data_dict is not None:
|
TaskService.update_json_data_dicts_using_list(json_data_dict_list, json_data_dict_mapping)
|
||||||
json_data = db.session.query(JsonDataModel.id).filter_by(hash=json_data_dict["hash"]).first()
|
TaskService.insert_or_update_json_data_records(json_data_dict_mapping)
|
||||||
if json_data is None:
|
|
||||||
json_data = JsonDataModel(**json_data_dict)
|
|
||||||
db.session.add(json_data)
|
|
||||||
|
|
||||||
self.add_event_to_process_instance(
|
self.add_event_to_process_instance(
|
||||||
self.process_instance_model,
|
self.process_instance_model,
|
||||||
|
|
|
@ -130,7 +130,7 @@ class TaskService:
|
||||||
self.task_models[task_model.guid] = task_model
|
self.task_models[task_model.guid] = task_model
|
||||||
if bpmn_process_json_data is not None:
|
if bpmn_process_json_data is not None:
|
||||||
json_data_dict_list.append(bpmn_process_json_data)
|
json_data_dict_list.append(bpmn_process_json_data)
|
||||||
self._update_json_data_dicts_using_list(json_data_dict_list, self.json_data_dicts)
|
self.update_json_data_dicts_using_list(json_data_dict_list, self.json_data_dicts)
|
||||||
|
|
||||||
if task_model.state == "COMPLETED" or task_failed:
|
if task_model.state == "COMPLETED" or task_failed:
|
||||||
event_type = ProcessInstanceEventType.task_completed.value
|
event_type = ProcessInstanceEventType.task_completed.value
|
||||||
|
@ -207,8 +207,12 @@ class TaskService:
|
||||||
python_env_data_dict = cls._get_python_env_data_dict_from_spiff_task(spiff_task, serializer)
|
python_env_data_dict = cls._get_python_env_data_dict_from_spiff_task(spiff_task, serializer)
|
||||||
task_model.properties_json = new_properties_json
|
task_model.properties_json = new_properties_json
|
||||||
task_model.state = TaskStateNames[new_properties_json["state"]]
|
task_model.state = TaskStateNames[new_properties_json["state"]]
|
||||||
json_data_dict = cls.update_task_data_on_task_model(task_model, spiff_task_data, "json_data_hash")
|
json_data_dict = cls.update_task_data_on_task_model_and_return_dict_if_updated(
|
||||||
python_env_dict = cls.update_task_data_on_task_model(task_model, python_env_data_dict, "python_env_data_hash")
|
task_model, spiff_task_data, "json_data_hash"
|
||||||
|
)
|
||||||
|
python_env_dict = cls.update_task_data_on_task_model_and_return_dict_if_updated(
|
||||||
|
task_model, python_env_data_dict, "python_env_data_hash"
|
||||||
|
)
|
||||||
return [json_data_dict, python_env_dict]
|
return [json_data_dict, python_env_dict]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@ -446,7 +450,11 @@ class TaskService:
|
||||||
return json_data_dict
|
return json_data_dict
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def update_task_data_on_task_model(
|
def insert_or_update_json_data_dict(cls, json_data_dict: JsonDataDict) -> None:
|
||||||
|
TaskService.insert_or_update_json_data_records({json_data_dict["hash"]: json_data_dict})
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def update_task_data_on_task_model_and_return_dict_if_updated(
|
||||||
cls, task_model: TaskModel, task_data_dict: dict, task_model_data_column: str
|
cls, task_model: TaskModel, task_data_dict: dict, task_model_data_column: str
|
||||||
) -> Optional[JsonDataDict]:
|
) -> Optional[JsonDataDict]:
|
||||||
task_data_json = json.dumps(task_data_dict, sort_keys=True)
|
task_data_json = json.dumps(task_data_dict, sort_keys=True)
|
||||||
|
@ -501,11 +509,11 @@ class TaskService:
|
||||||
python_env_data_hash: Optional[str] = None,
|
python_env_data_hash: Optional[str] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
if json_data_hash is None:
|
if json_data_hash is None:
|
||||||
cls.update_task_data_on_task_model(task_model, {}, "json_data_hash")
|
cls.update_task_data_on_task_model_and_return_dict_if_updated(task_model, {}, "json_data_hash")
|
||||||
else:
|
else:
|
||||||
task_model.json_data_hash = json_data_hash
|
task_model.json_data_hash = json_data_hash
|
||||||
if python_env_data_hash is None:
|
if python_env_data_hash is None:
|
||||||
cls.update_task_data_on_task_model(task_model, {}, "python_env_data")
|
cls.update_task_data_on_task_model_and_return_dict_if_updated(task_model, {}, "python_env_data")
|
||||||
else:
|
else:
|
||||||
task_model.python_env_data_hash = python_env_data_hash
|
task_model.python_env_data_hash = python_env_data_hash
|
||||||
|
|
||||||
|
@ -556,7 +564,7 @@ class TaskService:
|
||||||
return converted_data
|
return converted_data
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _update_json_data_dicts_using_list(
|
def update_json_data_dicts_using_list(
|
||||||
cls, json_data_dict_list: list[Optional[JsonDataDict]], json_data_dicts: dict[str, JsonDataDict]
|
cls, json_data_dict_list: list[Optional[JsonDataDict]], json_data_dicts: dict[str, JsonDataDict]
|
||||||
) -> None:
|
) -> None:
|
||||||
for json_data_dict in json_data_dict_list:
|
for json_data_dict in json_data_dict_list:
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { useNavigate, useParams } from 'react-router-dom';
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
import validator from '@rjsf/validator-ajv8';
|
import validator from '@rjsf/validator-ajv8';
|
||||||
|
|
||||||
|
@ -9,6 +9,7 @@ import {
|
||||||
Grid,
|
Grid,
|
||||||
Column,
|
Column,
|
||||||
Button,
|
Button,
|
||||||
|
ButtonSet,
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
} from '@carbon/react';
|
} from '@carbon/react';
|
||||||
|
|
||||||
|
@ -21,6 +22,13 @@ import { modifyProcessIdentifierForPathParam } from '../helpers';
|
||||||
import { ProcessInstanceTask } from '../interfaces';
|
import { ProcessInstanceTask } from '../interfaces';
|
||||||
import ProcessBreadcrumb from '../components/ProcessBreadcrumb';
|
import ProcessBreadcrumb from '../components/ProcessBreadcrumb';
|
||||||
|
|
||||||
|
class UnexpectedHumanTaskType extends Error {
|
||||||
|
constructor(message: string) {
|
||||||
|
super(message);
|
||||||
|
this.name = 'UnexpectedHumanTaskType';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default function TaskShow() {
|
export default function TaskShow() {
|
||||||
const [task, setTask] = useState<ProcessInstanceTask | null>(null);
|
const [task, setTask] = useState<ProcessInstanceTask | null>(null);
|
||||||
const [userTasks] = useState(null);
|
const [userTasks] = useState(null);
|
||||||
|
@ -30,6 +38,9 @@ export default function TaskShow() {
|
||||||
|
|
||||||
const { addError, removeError } = useAPIError();
|
const { addError, removeError } = useAPIError();
|
||||||
|
|
||||||
|
// eslint-disable-next-line sonarjs/no-duplicate-string
|
||||||
|
const supportedHumanTaskTypes = ['User Task', 'Manual Task'];
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const processResult = (result: ProcessInstanceTask) => {
|
const processResult = (result: ProcessInstanceTask) => {
|
||||||
setTask(result);
|
setTask(result);
|
||||||
|
@ -76,16 +87,22 @@ export default function TaskShow() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFormSubmit = (event: any) => {
|
const handleFormSubmit = (formObject: any, event: any) => {
|
||||||
if (disabled) {
|
if (disabled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const submitButtonId = event.nativeEvent.submitter.id;
|
||||||
|
let queryParams = '';
|
||||||
|
console.log('submitButtonId', submitButtonId);
|
||||||
|
if (submitButtonId === 'save-as-draft-button') {
|
||||||
|
queryParams = '?save_as_draft=true';
|
||||||
|
}
|
||||||
setDisabled(true);
|
setDisabled(true);
|
||||||
removeError();
|
removeError();
|
||||||
const dataToSubmit = event.formData;
|
const dataToSubmit = formObject.formData;
|
||||||
delete dataToSubmit.isManualTask;
|
delete dataToSubmit.isManualTask;
|
||||||
HttpService.makeCallToBackend({
|
HttpService.makeCallToBackend({
|
||||||
path: `/tasks/${params.process_instance_id}/${params.task_id}`,
|
path: `/tasks/${params.process_instance_id}/${params.task_id}${queryParams}`,
|
||||||
successCallback: processSubmitResult,
|
successCallback: processSubmitResult,
|
||||||
failureCallback: (error: any) => {
|
failureCallback: (error: any) => {
|
||||||
addError(error);
|
addError(error);
|
||||||
|
@ -226,16 +243,33 @@ export default function TaskShow() {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (task.state === 'READY') {
|
if (task.state === 'READY') {
|
||||||
let buttonText = 'Submit';
|
let submitButtonText = 'Submit';
|
||||||
|
let saveAsDraftButton = null;
|
||||||
if (task.type === 'Manual Task') {
|
if (task.type === 'Manual Task') {
|
||||||
buttonText = 'Continue';
|
submitButtonText = 'Continue';
|
||||||
|
} else if (task.type === 'User Task') {
|
||||||
|
saveAsDraftButton = (
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
id="save-as-draft-button"
|
||||||
|
disabled={disabled}
|
||||||
|
kind="secondary"
|
||||||
|
>
|
||||||
|
Save as draft
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
throw new UnexpectedHumanTaskType(
|
||||||
|
`Invalid task type given: ${task.type}. Only supported types: ${supportedHumanTaskTypes}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
reactFragmentToHideSubmitButton = (
|
reactFragmentToHideSubmitButton = (
|
||||||
<div>
|
<ButtonSet>
|
||||||
<Button type="submit" disabled={disabled}>
|
<Button type="submit" id="submit-button" disabled={disabled}>
|
||||||
{buttonText}
|
{submitButtonText}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
{saveAsDraftButton}
|
||||||
|
</ButtonSet>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue