Merge pull request #110 from sartography/feature/jinja_errors
Feature/jinja errors
This commit is contained in:
commit
74191e3b45
|
@ -13,8 +13,10 @@ services:
|
|||
- "${SPIFF_FRONTEND_PORT:-8001}:${SPIFF_FRONTEND_PORT:-8001}/tcp"
|
||||
|
||||
spiffworkflow-backend:
|
||||
container_name: spiffworkflow-backend
|
||||
image: ghcr.io/sartography/spiffworkflow-backend:latest
|
||||
# container_name: spiffworkflow-backend
|
||||
build: ./spiffworkflow-backend/.
|
||||
# dockerfile: Dockerfile
|
||||
# image: ghcr.io/sartography/spiffworkflow-backend:latest
|
||||
depends_on:
|
||||
spiffworkflow-db:
|
||||
condition: service_healthy
|
||||
|
|
|
@ -47,7 +47,10 @@ if [[ "${SPIFFWORKFLOW_BACKEND_RUN_DATA_SETUP:-}" != "false" ]]; then
|
|||
SPIFFWORKFLOW_BACKEND_FAIL_ON_INVALID_PROCESS_MODELS=false poetry run python bin/save_all_bpmn.py
|
||||
fi
|
||||
|
||||
export IS_GUNICORN="true"
|
||||
# Assure that the the Process Models Directory is initialized as a git repo
|
||||
git init "${BPMN_SPEC_ABSOLUTE_DIR}"
|
||||
git config --global --add safe.directory "${BPMN_SPEC_ABSOLUTE_DIR}"
|
||||
|
||||
export IS_GUNICORN="true"
|
||||
# THIS MUST BE THE LAST COMMAND!
|
||||
exec poetry run gunicorn ${additional_args} --bind "0.0.0.0:$port" --workers="$workers" --limit-request-line 8192 --timeout 90 --capture-output --access-logfile '-' --log-level debug wsgi:app
|
||||
|
|
|
@ -72,7 +72,7 @@ zookeeper = ["kazoo"]
|
|||
|
||||
[[package]]
|
||||
name = "astroid"
|
||||
version = "2.12.12"
|
||||
version = "2.13.3"
|
||||
description = "An abstract syntax tree for Python with inference support."
|
||||
category = "main"
|
||||
optional = false
|
||||
|
@ -80,7 +80,7 @@ python-versions = ">=3.7.2"
|
|||
|
||||
[package.dependencies]
|
||||
lazy-object-proxy = ">=1.4.0"
|
||||
typing-extensions = {version = ">=3.10", markers = "python_version < \"3.10\""}
|
||||
typing-extensions = {version = ">=4.0.0", markers = "python_version < \"3.11\""}
|
||||
wrapt = [
|
||||
{version = ">=1.11,<2", markers = "python_version < \"3.11\""},
|
||||
{version = ">=1.14,<2", markers = "python_version >= \"3.11\""},
|
||||
|
@ -430,6 +430,17 @@ calendars = ["convertdate", "convertdate", "hijri-converter"]
|
|||
fasttext = ["fasttext"]
|
||||
langdetect = ["langdetect"]
|
||||
|
||||
[[package]]
|
||||
name = "dill"
|
||||
version = "0.3.6"
|
||||
description = "serialize all of python"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[package.extras]
|
||||
graph = ["objgraph (>=1.7.2)"]
|
||||
|
||||
[[package]]
|
||||
name = "distlib"
|
||||
version = "0.3.6"
|
||||
|
@ -854,6 +865,20 @@ category = "main"
|
|||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "isort"
|
||||
version = "5.11.4"
|
||||
description = "A Python utility / library to sort Python imports."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7.0"
|
||||
|
||||
[package.extras]
|
||||
colors = ["colorama (>=0.4.3,<0.5.0)"]
|
||||
pipfile-deprecated-finder = ["pipreqs", "requirementslib"]
|
||||
plugins = ["setuptools"]
|
||||
requirements-deprecated-finder = ["pip-api", "pipreqs"]
|
||||
|
||||
[[package]]
|
||||
name = "itsdangerous"
|
||||
version = "2.1.2"
|
||||
|
@ -1029,7 +1054,7 @@ tests = ["pytest", "pytest-lazy-fixture (>=0.6.2)"]
|
|||
name = "mccabe"
|
||||
version = "0.7.0"
|
||||
description = "McCabe checker, plugin for flake8"
|
||||
category = "dev"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
|
||||
|
@ -1136,7 +1161,7 @@ flake8 = ">=3.9.1"
|
|||
name = "platformdirs"
|
||||
version = "2.5.2"
|
||||
description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
|
||||
category = "dev"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
|
||||
|
@ -1266,6 +1291,32 @@ dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.4.0)", "pre-commit", "pyte
|
|||
docs = ["sphinx (>=4.5.0,<5.0.0)", "sphinx-rtd-theme", "zope.interface"]
|
||||
tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "pylint"
|
||||
version = "2.15.10"
|
||||
description = "python code static checker"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7.2"
|
||||
|
||||
[package.dependencies]
|
||||
astroid = ">=2.12.13,<=2.14.0-dev0"
|
||||
colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""}
|
||||
dill = [
|
||||
{version = ">=0.2", markers = "python_version < \"3.11\""},
|
||||
{version = ">=0.3.6", markers = "python_version >= \"3.11\""},
|
||||
]
|
||||
isort = ">=4.2.5,<6"
|
||||
mccabe = ">=0.6,<0.8"
|
||||
platformdirs = ">=2.2.0"
|
||||
tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
|
||||
tomlkit = ">=0.10.1"
|
||||
typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\""}
|
||||
|
||||
[package.extras]
|
||||
spelling = ["pyenchant (>=3.2,<4.0)"]
|
||||
testutils = ["gitpython (>3)"]
|
||||
|
||||
[[package]]
|
||||
name = "pyparsing"
|
||||
version = "3.0.9"
|
||||
|
@ -1873,6 +1924,14 @@ category = "main"
|
|||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[[package]]
|
||||
name = "tomlkit"
|
||||
version = "0.11.6"
|
||||
description = "Style preserving TOML library"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
|
||||
[[package]]
|
||||
name = "tornado"
|
||||
version = "6.2"
|
||||
|
@ -2145,7 +2204,7 @@ testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools"
|
|||
[metadata]
|
||||
lock-version = "1.1"
|
||||
python-versions = ">=3.9,<3.12"
|
||||
content-hash = "701115e291a4014376871a0004a8d27e14c4a9092bd8c07e4ca190dd374b221a"
|
||||
content-hash = "95c08ed2de5b5d047474666c9e9a5ff3e7e94e6184649c2aa6d3a961711f14b0"
|
||||
|
||||
[metadata.files]
|
||||
alabaster = [
|
||||
|
@ -2169,8 +2228,8 @@ apscheduler = [
|
|||
{file = "APScheduler-3.9.1.post1.tar.gz", hash = "sha256:b2bea0309569da53a7261bfa0ce19c67ddbfe151bda776a6a907579fdbd3eb2a"},
|
||||
]
|
||||
astroid = [
|
||||
{file = "astroid-2.12.12-py3-none-any.whl", hash = "sha256:72702205200b2a638358369d90c222d74ebc376787af8fb2f7f2a86f7b5cc85f"},
|
||||
{file = "astroid-2.12.12.tar.gz", hash = "sha256:1c00a14f5a3ed0339d38d2e2e5b74ea2591df5861c0936bb292b84ccf3a78d83"},
|
||||
{file = "astroid-2.13.3-py3-none-any.whl", hash = "sha256:14c1603c41cc61aae731cad1884a073c4645e26f126d13ac8346113c95577f3b"},
|
||||
{file = "astroid-2.13.3.tar.gz", hash = "sha256:6afc22718a48a689ca24a97981ad377ba7fb78c133f40335dfd16772f29bcfb1"},
|
||||
]
|
||||
attrs = [
|
||||
{file = "attrs-22.1.0-py2.py3-none-any.whl", hash = "sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c"},
|
||||
|
@ -2354,6 +2413,10 @@ dateparser = [
|
|||
{file = "dateparser-1.1.2-py2.py3-none-any.whl", hash = "sha256:d31659dc806a7d88e2b510b2c74f68b525ae531f145c62a57a99bd616b7f90cf"},
|
||||
{file = "dateparser-1.1.2.tar.gz", hash = "sha256:3821bf191f95b2658c4abd91571c09821ce7a2bc179bf6cefd8b4515c3ccf9ef"},
|
||||
]
|
||||
dill = [
|
||||
{file = "dill-0.3.6-py3-none-any.whl", hash = "sha256:a07ffd2351b8c678dfc4a856a3005f8067aea51d6ba6c700796a4d9e280f39f0"},
|
||||
{file = "dill-0.3.6.tar.gz", hash = "sha256:e5db55f3687856d8fbdab002ed78544e1c4559a130302693d839dfe8f93f2373"},
|
||||
]
|
||||
distlib = [
|
||||
{file = "distlib-0.3.6-py2.py3-none-any.whl", hash = "sha256:f35c4b692542ca110de7ef0bea44d73981caeb34ca0b9b6b2e6d7790dda8f80e"},
|
||||
{file = "distlib-0.3.6.tar.gz", hash = "sha256:14bad2d9b04d3a36127ac97f30b12a19268f211063d8f8ee4f47108896e11b46"},
|
||||
|
@ -2532,6 +2595,10 @@ iniconfig = [
|
|||
{file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
|
||||
{file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"},
|
||||
]
|
||||
isort = [
|
||||
{file = "isort-5.11.4-py3-none-any.whl", hash = "sha256:c033fd0edb91000a7f09527fe5c75321878f98322a77ddcc81adbd83724afb7b"},
|
||||
{file = "isort-5.11.4.tar.gz", hash = "sha256:6db30c5ded9815d813932c04c2f85a360bcdd35fed496f4d8f35495ef0a261b6"},
|
||||
]
|
||||
itsdangerous = [
|
||||
{file = "itsdangerous-2.1.2-py3-none-any.whl", hash = "sha256:2c2349112351b88699d8d4b6b075022c0808887cb7ad10069318a8b0bc88db44"},
|
||||
{file = "itsdangerous-2.1.2.tar.gz", hash = "sha256:5dbbc68b317e5e42f327f9021763545dc3fc3bfe22e6deb96aaf1fc38874156a"},
|
||||
|
@ -2925,6 +2992,10 @@ pyjwt = [
|
|||
{file = "PyJWT-2.6.0-py3-none-any.whl", hash = "sha256:d83c3d892a77bbb74d3e1a2cfa90afaadb60945205d1095d9221f04466f64c14"},
|
||||
{file = "PyJWT-2.6.0.tar.gz", hash = "sha256:69285c7e31fc44f68a1feb309e948e0df53259d579295e6cfe2b1792329f05fd"},
|
||||
]
|
||||
pylint = [
|
||||
{file = "pylint-2.15.10-py3-none-any.whl", hash = "sha256:9df0d07e8948a1c3ffa3b6e2d7e6e63d9fb457c5da5b961ed63106594780cc7e"},
|
||||
{file = "pylint-2.15.10.tar.gz", hash = "sha256:b3dc5ef7d33858f297ac0d06cc73862f01e4f2e74025ec3eff347ce0bc60baf5"},
|
||||
]
|
||||
pyparsing = [
|
||||
{file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"},
|
||||
{file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"},
|
||||
|
@ -3356,6 +3427,10 @@ tomli = [
|
|||
{file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
|
||||
{file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
|
||||
]
|
||||
tomlkit = [
|
||||
{file = "tomlkit-0.11.6-py3-none-any.whl", hash = "sha256:07de26b0d8cfc18f871aec595fda24d95b08fef89d147caa861939f37230bf4b"},
|
||||
{file = "tomlkit-0.11.6.tar.gz", hash = "sha256:71b952e5721688937fb02cf9d354dbcf0785066149d2855e44531ebdd2b65d73"},
|
||||
]
|
||||
tornado = [
|
||||
{file = "tornado-6.2-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:20f638fd8cc85f3cbae3c732326e96addff0a15e22d80f049e00121651e82e72"},
|
||||
{file = "tornado-6.2-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:87dcafae3e884462f90c90ecc200defe5e580a7fbbb4365eda7c7c1eb809ebc9"},
|
||||
|
|
|
@ -74,6 +74,7 @@ pytz = "^2022.6"
|
|||
dateparser = "^1.1.2"
|
||||
types-dateparser = "^1.1.4.1"
|
||||
flask-jwt-extended = "^4.4.4"
|
||||
pylint = "^2.15.10"
|
||||
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
|
|
|
@ -28,17 +28,6 @@ groups:
|
|||
users:
|
||||
[
|
||||
admin@spiffworkflow.org,
|
||||
oskar@spiffworkflow.org
|
||||
]
|
||||
Education:
|
||||
users:
|
||||
[
|
||||
malala@spiffworkflow.org
|
||||
]
|
||||
President:
|
||||
users:
|
||||
[
|
||||
nelson@spiffworkflow.org
|
||||
]
|
||||
|
||||
permissions:
|
||||
|
@ -82,21 +71,6 @@ permissions:
|
|||
users: [ ]
|
||||
allowed_permissions: [ read ]
|
||||
uri: /processes
|
||||
# Members of the Education group can change the processes under "education".
|
||||
education-admin:
|
||||
groups: ["Education", "President"]
|
||||
users: []
|
||||
allowed_permissions: [create, read, update, delete]
|
||||
uri: /process-groups/education:*
|
||||
|
||||
# Anyone can start an education process.
|
||||
education-everybody:
|
||||
groups: [everybody]
|
||||
users: []
|
||||
allowed_permissions: [create, read]
|
||||
uri: /process-instances/misc:category_number_one:process-model-with-form/*
|
||||
|
||||
# Anyone can see their own user groups.
|
||||
groups-everybody:
|
||||
groups: [everybody]
|
||||
users: []
|
||||
|
|
|
@ -13,6 +13,8 @@
|
|||
<div class="error">{{error_message}}</div>
|
||||
<div class="login">
|
||||
<form id="login" method="post" action="{{ url_for('openid.form_submit') }}">
|
||||
<p><b>Important:</b> This login form is for demonstration purposes only. In production systems you should
|
||||
be using a real Open ID System.</p>
|
||||
<input type="text" class="cds--text-input" name="Uname" id="Uname" placeholder="Username">
|
||||
<br><br>
|
||||
<input type="Password" class="cds--text-input" name="Pass" id="Pass" placeholder="Password">
|
||||
|
|
|
@ -15,6 +15,8 @@ from flask import g
|
|||
from flask import jsonify
|
||||
from flask import make_response
|
||||
from flask.wrappers import Response
|
||||
from jinja2 import TemplateSyntaxError
|
||||
from SpiffWorkflow.exceptions import WorkflowTaskException # type: ignore
|
||||
from SpiffWorkflow.task import Task as SpiffTask # type: ignore
|
||||
from SpiffWorkflow.task import TaskState
|
||||
from sqlalchemy import and_
|
||||
|
@ -32,6 +34,7 @@ from spiffworkflow_backend.models.human_task_user import HumanTaskUserModel
|
|||
from spiffworkflow_backend.models.process_instance import ProcessInstanceModel
|
||||
from spiffworkflow_backend.models.process_instance import ProcessInstanceStatus
|
||||
from spiffworkflow_backend.models.process_model import ProcessModelInfo
|
||||
from spiffworkflow_backend.models.task import Task
|
||||
from spiffworkflow_backend.models.user import UserModel
|
||||
from spiffworkflow_backend.routes.process_api_blueprint import (
|
||||
_find_principal_or_raise,
|
||||
|
@ -251,7 +254,7 @@ def task_show(process_instance_id: int, task_id: str) -> flask.wrappers.Response
|
|||
|
||||
form_contents = _prepare_form_data(
|
||||
form_schema_file_name,
|
||||
task.data,
|
||||
spiff_task,
|
||||
process_model_with_form,
|
||||
)
|
||||
|
||||
|
@ -271,7 +274,7 @@ def task_show(process_instance_id: int, task_id: str) -> flask.wrappers.Response
|
|||
) from exception
|
||||
|
||||
if task.data:
|
||||
_update_form_schema_with_task_data_as_needed(form_dict, task.data)
|
||||
_update_form_schema_with_task_data_as_needed(form_dict, task)
|
||||
|
||||
if form_contents:
|
||||
task.form_schema = form_dict
|
||||
|
@ -279,7 +282,7 @@ def task_show(process_instance_id: int, task_id: str) -> flask.wrappers.Response
|
|||
if form_ui_schema_file_name:
|
||||
ui_form_contents = _prepare_form_data(
|
||||
form_ui_schema_file_name,
|
||||
task.data,
|
||||
task,
|
||||
process_model_with_form,
|
||||
)
|
||||
if ui_form_contents:
|
||||
|
@ -287,9 +290,15 @@ def task_show(process_instance_id: int, task_id: str) -> flask.wrappers.Response
|
|||
|
||||
if task.properties and task.data and "instructionsForEndUser" in task.properties:
|
||||
if task.properties["instructionsForEndUser"]:
|
||||
task.properties["instructionsForEndUser"] = _render_jinja_template(
|
||||
task.properties["instructionsForEndUser"], task.data
|
||||
)
|
||||
try:
|
||||
task.properties["instructionsForEndUser"] = _render_jinja_template(
|
||||
task.properties["instructionsForEndUser"], spiff_task
|
||||
)
|
||||
except WorkflowTaskException as wfe:
|
||||
wfe.add_note("Failed to render instructions for end user.")
|
||||
raise ApiError.from_workflow_exception(
|
||||
"instructions_error", str(wfe), exp=wfe
|
||||
) from wfe
|
||||
return make_response(jsonify(task), 200)
|
||||
|
||||
|
||||
|
@ -501,23 +510,45 @@ def _get_tasks(
|
|||
|
||||
|
||||
def _prepare_form_data(
|
||||
form_file: str, task_data: Union[dict, None], process_model: ProcessModelInfo
|
||||
form_file: str, spiff_task: SpiffTask, process_model: ProcessModelInfo
|
||||
) -> str:
|
||||
"""Prepare_form_data."""
|
||||
if task_data is None:
|
||||
if spiff_task.data is None:
|
||||
return ""
|
||||
|
||||
file_contents = SpecFileService.get_data(process_model, form_file).decode("utf-8")
|
||||
return _render_jinja_template(file_contents, task_data)
|
||||
try:
|
||||
return _render_jinja_template(file_contents, spiff_task)
|
||||
except WorkflowTaskException as wfe:
|
||||
wfe.add_note(f"Error in Json Form File '{form_file}'")
|
||||
api_error = ApiError.from_workflow_exception(
|
||||
"instructions_error", str(wfe), exp=wfe
|
||||
)
|
||||
api_error.file_name = form_file
|
||||
raise api_error
|
||||
|
||||
|
||||
def _render_jinja_template(unprocessed_template: str, data: dict[str, Any]) -> str:
|
||||
def _render_jinja_template(unprocessed_template: str, spiff_task: SpiffTask) -> str:
|
||||
"""Render_jinja_template."""
|
||||
jinja_environment = jinja2.Environment(
|
||||
autoescape=True, lstrip_blocks=True, trim_blocks=True
|
||||
)
|
||||
template = jinja_environment.from_string(unprocessed_template)
|
||||
return template.render(**data)
|
||||
try:
|
||||
template = jinja_environment.from_string(unprocessed_template)
|
||||
return template.render(**spiff_task.data)
|
||||
except jinja2.exceptions.TemplateError as template_error:
|
||||
wfe = WorkflowTaskException(
|
||||
str(template_error), task=spiff_task, exception=template_error
|
||||
)
|
||||
if isinstance(template_error, TemplateSyntaxError):
|
||||
wfe.line_number = template_error.lineno
|
||||
wfe.error_line = template_error.source.split("\n")[
|
||||
template_error.lineno - 1
|
||||
]
|
||||
wfe.add_note(
|
||||
"Jinja2 template errors can happen when trying to displaying task data"
|
||||
)
|
||||
raise wfe from template_error
|
||||
|
||||
|
||||
def _get_spiff_task_from_process_instance(
|
||||
|
@ -543,10 +574,11 @@ def _get_spiff_task_from_process_instance(
|
|||
|
||||
|
||||
# originally from: https://bitcoden.com/answers/python-nested-dictionary-update-value-where-any-nested-key-matches
|
||||
def _update_form_schema_with_task_data_as_needed(
|
||||
in_dict: dict, task_data: dict
|
||||
) -> None:
|
||||
def _update_form_schema_with_task_data_as_needed(in_dict: dict, task: Task) -> None:
|
||||
"""Update_nested."""
|
||||
if task.data is None:
|
||||
return None
|
||||
|
||||
for k, value in in_dict.items():
|
||||
if "anyOf" == k:
|
||||
# value will look like the array on the right of "anyOf": ["options_from_task_data_var:awesome_options"]
|
||||
|
@ -561,19 +593,25 @@ def _update_form_schema_with_task_data_as_needed(
|
|||
"options_from_task_data_var:", ""
|
||||
)
|
||||
|
||||
if task_data_var not in task_data:
|
||||
if task_data_var not in task.data:
|
||||
wte = WorkflowTaskException(
|
||||
(
|
||||
"Error building form. Attempting to create a"
|
||||
" selection list with options from variable"
|
||||
f" '{task_data_var}' but it doesn't exist in"
|
||||
" the Task Data."
|
||||
),
|
||||
task=task,
|
||||
)
|
||||
raise (
|
||||
ApiError(
|
||||
ApiError.from_workflow_exception(
|
||||
error_code="missing_task_data_var",
|
||||
message=(
|
||||
"Task data is missing variable:"
|
||||
f" {task_data_var}"
|
||||
),
|
||||
status_code=500,
|
||||
message=str(wte),
|
||||
exp=wte,
|
||||
)
|
||||
)
|
||||
|
||||
select_options_from_task_data = task_data.get(task_data_var)
|
||||
select_options_from_task_data = task.data.get(task_data_var)
|
||||
if isinstance(select_options_from_task_data, list):
|
||||
if all(
|
||||
"value" in d and "label" in d
|
||||
|
@ -596,11 +634,11 @@ def _update_form_schema_with_task_data_as_needed(
|
|||
|
||||
in_dict[k] = options_for_react_json_schema_form
|
||||
elif isinstance(value, dict):
|
||||
_update_form_schema_with_task_data_as_needed(value, task_data)
|
||||
_update_form_schema_with_task_data_as_needed(value, task)
|
||||
elif isinstance(value, list):
|
||||
for o in value:
|
||||
if isinstance(o, dict):
|
||||
_update_form_schema_with_task_data_as_needed(o, task_data)
|
||||
_update_form_schema_with_task_data_as_needed(o, task)
|
||||
|
||||
|
||||
def _get_potential_owner_usernames(assigned_user: AliasedClass) -> Any:
|
||||
|
|
|
@ -873,7 +873,10 @@ class ProcessInstanceProcessor:
|
|||
f"Event of type {event_definition.event_type} sent to process instance"
|
||||
f" {self.process_instance_model.id}"
|
||||
)
|
||||
self.bpmn_process_instance.catch(event_definition)
|
||||
try:
|
||||
self.bpmn_process_instance.catch(event_definition)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
self.do_engine_steps(save=True)
|
||||
|
||||
def add_step(self, step: Union[dict, None] = None) -> None:
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" xmlns:spiffworkflow="http://spiffworkflow.org/bpmn/schema/1.0/core" id="Definitions_96f6665" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="5.0.0">
|
||||
<bpmn:process id="Proccess_With_Bad_Form" name="Process With Form" isExecutable="true">
|
||||
<bpmn:startEvent id="StartEvent_1">
|
||||
<bpmn:outgoing>Flow_0smvjir</bpmn:outgoing>
|
||||
</bpmn:startEvent>
|
||||
<bpmn:sequenceFlow id="Flow_0smvjir" sourceRef="StartEvent_1" targetRef="Activity_1cscoeg" />
|
||||
<bpmn:endEvent id="Event_00xci7j">
|
||||
<bpmn:incoming>Flow_1boyhcj</bpmn:incoming>
|
||||
</bpmn:endEvent>
|
||||
<bpmn:sequenceFlow id="Flow_1boyhcj" sourceRef="Activity_1cscoeg" targetRef="Event_00xci7j" />
|
||||
<bpmn:manualTask id="Activity_1cscoeg" name="DisplayInfo">
|
||||
<bpmn:extensionElements>
|
||||
<spiffworkflow:instructionsForEndUser>Hello {{ name }}
|
||||
Department: {{ department }}
|
||||
{{ x +=- 1}}
|
||||
</spiffworkflow:instructionsForEndUser>
|
||||
</bpmn:extensionElements>
|
||||
<bpmn:incoming>Flow_0smvjir</bpmn:incoming>
|
||||
<bpmn:outgoing>Flow_1boyhcj</bpmn:outgoing>
|
||||
</bpmn:manualTask>
|
||||
</bpmn:process>
|
||||
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
|
||||
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Proccess_With_Bad_Form">
|
||||
<bpmndi:BPMNEdge id="Flow_1boyhcj_di" bpmnElement="Flow_1boyhcj">
|
||||
<di:waypoint x="340" y="117" />
|
||||
<di:waypoint x="382" y="117" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_0smvjir_di" bpmnElement="Flow_0smvjir">
|
||||
<di:waypoint x="215" y="117" />
|
||||
<di:waypoint x="240" y="117" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
|
||||
<dc:Bounds x="179" y="99" width="36" height="36" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Activity_00g930h_di" bpmnElement="Activity_1cscoeg">
|
||||
<dc:Bounds x="240" y="77" width="100" height="80" />
|
||||
<bpmndi:BPMNLabel />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Event_00xci7j_di" bpmnElement="Event_00xci7j">
|
||||
<dc:Bounds x="382" y="99" width="36" height="36" />
|
||||
</bpmndi:BPMNShape>
|
||||
</bpmndi:BPMNPlane>
|
||||
</bpmndi:BPMNDiagram>
|
||||
</bpmn:definitions>
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"title": "Simple form",
|
||||
"description": "A simple form example with some bad Jinja2 Syntax in it {{ x +=- 1}}",
|
||||
"type": "object",
|
||||
"required": ["name"],
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"title": "Name",
|
||||
"default": "World"
|
||||
},
|
||||
"department": {
|
||||
"type": "string",
|
||||
"title": "Department",
|
||||
"enum": ["Finance", "HR", "IT"]
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"name": {
|
||||
"ui:title": "Name",
|
||||
"ui:description": "(Your name)"
|
||||
},
|
||||
"department": {
|
||||
"ui:title": "Department",
|
||||
"ui:description": "(Your department)"
|
||||
},
|
||||
"ui:order": ["name", "department"]
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:spiffworkflow="http://spiffworkflow.org/bpmn/schema/1.0/core" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" id="Definitions_96f6665" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.0.0-dev">
|
||||
<bpmn:process id="Proccess_With_Bad_Form" name="Process With Form" isExecutable="true">
|
||||
<bpmn:startEvent id="StartEvent_1">
|
||||
<bpmn:outgoing>Flow_0smvjir</bpmn:outgoing>
|
||||
</bpmn:startEvent>
|
||||
<bpmn:sequenceFlow id="Flow_0smvjir" sourceRef="StartEvent_1" targetRef="Activity_SimpleForm" />
|
||||
<bpmn:endEvent id="Event_00xci7j">
|
||||
<bpmn:incoming>Flow_1boyhcj</bpmn:incoming>
|
||||
</bpmn:endEvent>
|
||||
<bpmn:sequenceFlow id="Flow_1boyhcj" sourceRef="Activity_1cscoeg" targetRef="Event_00xci7j" />
|
||||
<bpmn:manualTask id="Activity_1cscoeg" name="DisplayInfo">
|
||||
<bpmn:extensionElements>
|
||||
<spiffworkflow:instructionsForEndUser>Hello {{ name }}
|
||||
Department: {{ department }}
|
||||
</spiffworkflow:instructionsForEndUser>
|
||||
</bpmn:extensionElements>
|
||||
<bpmn:incoming>Flow_1ly1khd</bpmn:incoming>
|
||||
<bpmn:outgoing>Flow_1boyhcj</bpmn:outgoing>
|
||||
</bpmn:manualTask>
|
||||
<bpmn:sequenceFlow id="Flow_1ly1khd" sourceRef="Activity_SimpleForm" targetRef="Activity_1cscoeg" />
|
||||
<bpmn:userTask id="Activity_SimpleForm" name="Simple Form">
|
||||
<bpmn:extensionElements>
|
||||
<spiffworkflow:properties>
|
||||
<spiffworkflow:property name="formJsonSchemaFilename" value="simple_form.json" />
|
||||
<spiffworkflow:property name="formUiSchemaFilename" value="simple_form_ui.json" />
|
||||
</spiffworkflow:properties>
|
||||
</bpmn:extensionElements>
|
||||
<bpmn:incoming>Flow_0smvjir</bpmn:incoming>
|
||||
<bpmn:outgoing>Flow_1ly1khd</bpmn:outgoing>
|
||||
</bpmn:userTask>
|
||||
</bpmn:process>
|
||||
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
|
||||
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Proccess_WithForm">
|
||||
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
|
||||
<dc:Bounds x="179" y="159" width="36" height="36" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Event_00xci7j_di" bpmnElement="Event_00xci7j">
|
||||
<dc:Bounds x="592" y="159" width="36" height="36" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Activity_00g930h_di" bpmnElement="Activity_1cscoeg">
|
||||
<dc:Bounds x="430" y="137" width="100" height="80" />
|
||||
<bpmndi:BPMNLabel />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Activity_0x5k4l1_di" bpmnElement="Activity_SimpleForm">
|
||||
<dc:Bounds x="270" y="137" width="100" height="80" />
|
||||
<bpmndi:BPMNLabel />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNEdge id="Flow_0smvjir_di" bpmnElement="Flow_0smvjir">
|
||||
<di:waypoint x="215" y="177" />
|
||||
<di:waypoint x="270" y="177" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_1boyhcj_di" bpmnElement="Flow_1boyhcj">
|
||||
<di:waypoint x="530" y="177" />
|
||||
<di:waypoint x="592" y="177" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_1ly1khd_di" bpmnElement="Flow_1ly1khd">
|
||||
<di:waypoint x="370" y="177" />
|
||||
<di:waypoint x="430" y="177" />
|
||||
</bpmndi:BPMNEdge>
|
||||
</bpmndi:BPMNPlane>
|
||||
</bpmndi:BPMNDiagram>
|
||||
</bpmn:definitions>
|
|
@ -42,7 +42,7 @@ def load_test_spec(
|
|||
) -> ProcessModelInfo:
|
||||
"""Loads a bpmn file into the process model dir based on a directory in tests/data."""
|
||||
if process_model_source_directory is None:
|
||||
raise Exception("You must inclode a `process_model_source_directory`.")
|
||||
raise Exception("You must include a `process_model_source_directory`.")
|
||||
|
||||
spec = ExampleDataLoader.create_spec(
|
||||
process_model_id=process_model_id,
|
||||
|
|
|
@ -0,0 +1,103 @@
|
|||
"""Test_various_bpmn_constructs."""
|
||||
from typing import Any
|
||||
|
||||
from flask.app import Flask
|
||||
from flask.testing import FlaskClient
|
||||
from tests.spiffworkflow_backend.helpers.base_test import BaseTest
|
||||
from tests.spiffworkflow_backend.helpers.test_data import load_test_spec
|
||||
|
||||
from spiffworkflow_backend import db
|
||||
from spiffworkflow_backend.models.human_task import HumanTaskModel
|
||||
from spiffworkflow_backend.models.user import UserModel
|
||||
|
||||
|
||||
class TestForGoodErrors(BaseTest):
|
||||
"""Assure when certain errors happen when rendering a jinaj2 error that it makes some sense."""
|
||||
|
||||
def get_next_user_task(
|
||||
self,
|
||||
process_instance_id: int,
|
||||
client: FlaskClient,
|
||||
with_super_admin_user: UserModel,
|
||||
) -> Any:
|
||||
"""Returns the next available user task for a given process instance, if possible."""
|
||||
human_tasks = (
|
||||
db.session.query(HumanTaskModel)
|
||||
.filter(HumanTaskModel.process_instance_id == process_instance_id)
|
||||
.all()
|
||||
)
|
||||
assert len(human_tasks) > 0, "No human tasks found for process."
|
||||
human_task = human_tasks[0]
|
||||
response = client.get(
|
||||
f"/v1.0/tasks/{process_instance_id}/{human_task.task_id}",
|
||||
headers=self.logged_in_headers(with_super_admin_user),
|
||||
)
|
||||
return response
|
||||
|
||||
def test_invalid_form(
|
||||
self,
|
||||
app: Flask,
|
||||
client: FlaskClient,
|
||||
with_db_and_bpmn_file_cleanup: None,
|
||||
with_super_admin_user: UserModel,
|
||||
) -> None:
|
||||
"""React json form schema with bad jinja syntax provides good error."""
|
||||
process_model = load_test_spec(
|
||||
process_model_id="group/simple_form_with_error",
|
||||
process_model_source_directory="simple_form_with_error",
|
||||
)
|
||||
response = self.create_process_instance_from_process_model_id_with_api(
|
||||
client,
|
||||
# process_model.process_group_id,
|
||||
process_model.id,
|
||||
headers=self.logged_in_headers(with_super_admin_user),
|
||||
)
|
||||
assert response.json is not None
|
||||
process_instance_id = response.json["id"]
|
||||
response = client.post(
|
||||
f"/v1.0/process-instances/{self.modify_process_identifier_for_path_param(process_model.id)}/{process_instance_id}/run",
|
||||
headers=self.logged_in_headers(with_super_admin_user),
|
||||
)
|
||||
assert response.status_code == 200
|
||||
response = self.get_next_user_task(
|
||||
process_instance_id, client, with_super_admin_user
|
||||
)
|
||||
assert response.json is not None
|
||||
assert response.json["error_type"] == "TemplateSyntaxError"
|
||||
assert response.json["line_number"] == 3
|
||||
assert response.json["file_name"] == "simple_form.json"
|
||||
|
||||
def test_jinja2_error_message_for_end_user_instructions(
|
||||
self,
|
||||
app: Flask,
|
||||
client: FlaskClient,
|
||||
with_db_and_bpmn_file_cleanup: None,
|
||||
with_super_admin_user: UserModel,
|
||||
) -> None:
|
||||
"""Test_task_data_is_set_even_if_process_instance_errors."""
|
||||
process_model = load_test_spec(
|
||||
process_model_id="group/end_user_instructions_error",
|
||||
bpmn_file_name="instructions_error.bpmn",
|
||||
process_model_source_directory="error",
|
||||
)
|
||||
process_instance = self.create_process_instance_from_process_model(
|
||||
process_model=process_model, user=with_super_admin_user
|
||||
)
|
||||
|
||||
response = client.post(
|
||||
f"/v1.0/process-instances/{self.modify_process_identifier_for_path_param(process_model.id)}/{process_instance.id}/run",
|
||||
headers=self.logged_in_headers(with_super_admin_user),
|
||||
)
|
||||
response = self.get_next_user_task(
|
||||
process_instance.id, client, with_super_admin_user
|
||||
)
|
||||
|
||||
assert response.status_code == 400
|
||||
assert response.json is not None
|
||||
assert response.json["error_type"] == "TemplateSyntaxError"
|
||||
assert response.json["line_number"] == 3
|
||||
assert response.json["error_line"] == "{{ x +=- 1}}"
|
||||
assert response.json["file_name"] == "instructions_error.bpmn"
|
||||
assert "instructions for end user" in response.json["message"]
|
||||
assert "Jinja2" in response.json["message"]
|
||||
assert "unexpected '='" in response.json["message"]
|
|
@ -1680,6 +1680,7 @@ class TestProcessApi(BaseTest):
|
|||
f"/v1.0/tasks/{process_instance_id}/{human_task.task_id}",
|
||||
headers=self.logged_in_headers(with_super_admin_user),
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json is not None
|
||||
assert (
|
||||
response.json["form_schema"]["definitions"]["Color"]["anyOf"][1]["title"]
|
||||
|
@ -2809,7 +2810,7 @@ class TestProcessApi(BaseTest):
|
|||
)
|
||||
|
||||
data = {
|
||||
"dateTime": "timedelta(hours=1)",
|
||||
"dateTime": "PT1H",
|
||||
"external": True,
|
||||
"internal": True,
|
||||
"label": "Event_0e4owa3",
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -49,6 +49,7 @@
|
|||
"react-bootstrap": "^2.5.0",
|
||||
"react-bootstrap-typeahead": "^6.0.0",
|
||||
"react-datepicker": "^4.8.0",
|
||||
"react-devtools": "^4.27.1",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-icons": "^4.4.0",
|
||||
"react-jsonschema-form": "^1.8.1",
|
||||
|
|
|
@ -1,29 +1,20 @@
|
|||
import { useMemo, useState } from 'react';
|
||||
// @ts-ignore
|
||||
import { Content } from '@carbon/react';
|
||||
|
||||
import { BrowserRouter, Routes, Route } from 'react-router-dom';
|
||||
import { defineAbility } from '@casl/ability';
|
||||
import ErrorContext from './contexts/ErrorContext';
|
||||
import NavigationBar from './components/NavigationBar';
|
||||
|
||||
import HomePageRoutes from './routes/HomePageRoutes';
|
||||
import ErrorBoundary from './components/ErrorBoundary';
|
||||
import AdminRoutes from './routes/AdminRoutes';
|
||||
import { ErrorForDisplay } from './interfaces';
|
||||
|
||||
import { AbilityContext } from './contexts/Can';
|
||||
import UserService from './services/UserService';
|
||||
import ErrorDisplay from './components/ErrorDisplay';
|
||||
import APIErrorProvider from './contexts/APIErrorContext';
|
||||
|
||||
export default function App() {
|
||||
const [errorObject, setErrorObject] = useState<ErrorForDisplay | null>(null);
|
||||
|
||||
const errorContextValueArray = useMemo(
|
||||
() => [errorObject, setErrorObject],
|
||||
[errorObject]
|
||||
);
|
||||
|
||||
if (!UserService.isLoggedIn()) {
|
||||
UserService.doLogin();
|
||||
return null;
|
||||
|
@ -35,7 +26,7 @@ export default function App() {
|
|||
<div className="cds--white">
|
||||
{/* @ts-ignore */}
|
||||
<AbilityContext.Provider value={ability}>
|
||||
<ErrorContext.Provider value={errorContextValueArray}>
|
||||
<APIErrorProvider>
|
||||
<BrowserRouter>
|
||||
<NavigationBar />
|
||||
<Content>
|
||||
|
@ -49,7 +40,7 @@ export default function App() {
|
|||
</ErrorBoundary>
|
||||
</Content>
|
||||
</BrowserRouter>
|
||||
</ErrorContext.Provider>
|
||||
</APIErrorProvider>
|
||||
</AbilityContext.Provider>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { useContext } from 'react';
|
||||
import ErrorContext from '../contexts/ErrorContext';
|
||||
import { Notification } from './Notification';
|
||||
import useAPIError from '../hooks/UseApiError';
|
||||
|
||||
function errorDetailDisplay(
|
||||
errorObject: any,
|
||||
|
@ -20,8 +19,8 @@ function errorDetailDisplay(
|
|||
}
|
||||
|
||||
export default function ErrorDisplay() {
|
||||
const [errorObject, setErrorObject] = (useContext as any)(ErrorContext);
|
||||
|
||||
const errorObject = useAPIError().error;
|
||||
const { removeError } = useAPIError();
|
||||
let errorTag = null;
|
||||
if (errorObject) {
|
||||
let sentryLinkTag = null;
|
||||
|
@ -50,7 +49,7 @@ export default function ErrorDisplay() {
|
|||
);
|
||||
const errorLine = errorDetailDisplay(errorObject, 'error_line', 'Context');
|
||||
let taskTrace = null;
|
||||
if ('task_trace' in errorObject && errorObject.task_trace.length > 1) {
|
||||
if (errorObject.task_trace && errorObject.task_trace.length > 1) {
|
||||
taskTrace = (
|
||||
<div className="error_info">
|
||||
<span className="error_title">Call Activity Trace:</span>
|
||||
|
@ -60,11 +59,7 @@ export default function ErrorDisplay() {
|
|||
}
|
||||
|
||||
errorTag = (
|
||||
<Notification
|
||||
title={title}
|
||||
onClose={() => setErrorObject(null)}
|
||||
type="error"
|
||||
>
|
||||
<Notification title={title} onClose={() => removeError()} type="error">
|
||||
{message}
|
||||
<br />
|
||||
{sentryLinkTag}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { useContext, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import {
|
||||
Link,
|
||||
useNavigate,
|
||||
|
@ -40,13 +40,11 @@ import {
|
|||
getProcessModelFullIdentifierFromSearchParams,
|
||||
modifyProcessIdentifierForPathParam,
|
||||
refreshAtInterval,
|
||||
setErrorMessageSafely,
|
||||
} from '../helpers';
|
||||
|
||||
import PaginationForTable from './PaginationForTable';
|
||||
import 'react-datepicker/dist/react-datepicker.css';
|
||||
|
||||
import ErrorContext from '../contexts/ErrorContext';
|
||||
import HttpService from '../services/HttpService';
|
||||
|
||||
import 'react-bootstrap-typeahead/css/Typeahead.css';
|
||||
|
@ -61,6 +59,7 @@ import {
|
|||
ReportMetadata,
|
||||
ReportFilter,
|
||||
User,
|
||||
ErrorForDisplay,
|
||||
} from '../interfaces';
|
||||
import ProcessModelSearch from './ProcessModelSearch';
|
||||
import ProcessInstanceReportSearch from './ProcessInstanceReportSearch';
|
||||
|
@ -68,6 +67,7 @@ import ProcessInstanceListDeleteReport from './ProcessInstanceListDeleteReport';
|
|||
import ProcessInstanceListSaveAsReport from './ProcessInstanceListSaveAsReport';
|
||||
import { FormatProcessModelDisplayName } from './MiniComponents';
|
||||
import { Notification } from './Notification';
|
||||
import useAPIError from '../hooks/UseApiError';
|
||||
|
||||
const REFRESH_INTERVAL = 5;
|
||||
const REFRESH_TIMEOUT = 600;
|
||||
|
@ -110,6 +110,7 @@ export default function ProcessInstanceListTable({
|
|||
const params = useParams();
|
||||
const [searchParams] = useSearchParams();
|
||||
const navigate = useNavigate();
|
||||
const { addError, removeError } = useAPIError();
|
||||
|
||||
const [processInstances, setProcessInstances] = useState([]);
|
||||
const [reportMetadata, setReportMetadata] = useState<ReportMetadata | null>();
|
||||
|
@ -133,8 +134,6 @@ export default function ProcessInstanceListTable({
|
|||
const [endFromTimeInvalid, setEndFromTimeInvalid] = useState<boolean>(false);
|
||||
const [endToTimeInvalid, setEndToTimeInvalid] = useState<boolean>(false);
|
||||
|
||||
const [errorObject, setErrorObject] = (useContext as any)(ErrorContext);
|
||||
|
||||
const processInstanceListPathPrefix =
|
||||
variant === 'all'
|
||||
? '/admin/process-instances/all'
|
||||
|
@ -517,7 +516,7 @@ export default function ProcessInstanceListTable({
|
|||
}
|
||||
if (message !== '') {
|
||||
valid = false;
|
||||
setErrorMessageSafely(message, errorObject, setErrorObject);
|
||||
addError({ message } as ErrorForDisplay);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -579,7 +578,7 @@ export default function ProcessInstanceListTable({
|
|||
queryParamString += `&process_initiator_username=${processInitiatorSelection.username}`;
|
||||
}
|
||||
|
||||
setErrorObject(null);
|
||||
removeError();
|
||||
setProcessInstanceReportJustSaved(null);
|
||||
setProcessInstanceFilters({});
|
||||
navigate(`${processInstanceListPathPrefix}?${queryParamString}`);
|
||||
|
@ -679,7 +678,7 @@ export default function ProcessInstanceListTable({
|
|||
queryParamString = `?report_id=${selectedReport.id}`;
|
||||
}
|
||||
|
||||
setErrorObject(null);
|
||||
removeError();
|
||||
setProcessInstanceReportJustSaved(mode || null);
|
||||
navigate(`${processInstanceListPathPrefix}${queryParamString}`);
|
||||
};
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { useContext } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
Button,
|
||||
|
@ -11,9 +10,9 @@ import {
|
|||
RecentProcessModel,
|
||||
} from '../interfaces';
|
||||
import HttpService from '../services/HttpService';
|
||||
import ErrorContext from '../contexts/ErrorContext';
|
||||
import { modifyProcessIdentifierForPathParam } from '../helpers';
|
||||
import { usePermissionFetcher } from '../hooks/PermissionService';
|
||||
import useAPIError from '../hooks/UseApiError';
|
||||
|
||||
const storeRecentProcessModelInLocalStorage = (
|
||||
processModelForStorage: ProcessModel
|
||||
|
@ -78,7 +77,7 @@ export default function ProcessInstanceRun({
|
|||
checkPermissions = true,
|
||||
}: OwnProps) {
|
||||
const navigate = useNavigate();
|
||||
const setErrorObject = (useContext as any)(ErrorContext)[1];
|
||||
const { addError, removeError } = useAPIError();
|
||||
const modifiedProcessModelId = modifyProcessIdentifierForPathParam(
|
||||
processModel.id
|
||||
);
|
||||
|
@ -105,22 +104,22 @@ export default function ProcessInstanceRun({
|
|||
};
|
||||
|
||||
const processModelRun = (processInstance: any) => {
|
||||
setErrorObject(null);
|
||||
removeError();
|
||||
storeRecentProcessModelInLocalStorage(processModel);
|
||||
HttpService.makeCallToBackend({
|
||||
path: `/process-instances/${modifiedProcessModelId}/${processInstance.id}/run`,
|
||||
successCallback: onProcessInstanceRun,
|
||||
failureCallback: setErrorObject,
|
||||
failureCallback: addError,
|
||||
httpMethod: 'POST',
|
||||
});
|
||||
};
|
||||
|
||||
const processInstanceCreateAndRun = () => {
|
||||
setErrorObject(null);
|
||||
removeError();
|
||||
HttpService.makeCallToBackend({
|
||||
path: processInstanceCreatePath,
|
||||
successCallback: processModelRun,
|
||||
failureCallback: setErrorObject,
|
||||
failureCallback: addError,
|
||||
httpMethod: 'POST',
|
||||
});
|
||||
};
|
||||
|
|
|
@ -6,7 +6,7 @@ import BpmnViewer from 'bpmn-js/lib/Viewer';
|
|||
import {
|
||||
BpmnPropertiesPanelModule,
|
||||
BpmnPropertiesProviderModule,
|
||||
// @ts-expect-error TS(7016) FIXME: Could not find a declaration file for module 'bpmn... Remove this comment to see the full error message
|
||||
// @ts-expect-error TS(7016) FIXME: Could not find a declaration file for module 'bpmn... RemoFve this comment to see the full error message
|
||||
} from 'bpmn-js-properties-panel';
|
||||
|
||||
// @ts-expect-error TS(7016) FIXME: Could not find a declaration file for module 'dmn-... Remove this comment to see the full error message
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
import React, { createContext, useState } from 'react';
|
||||
import { ErrorForDisplay } from '../interfaces';
|
||||
|
||||
type ErrorContextType = {
|
||||
error: null | ErrorForDisplay;
|
||||
addError: Function;
|
||||
removeError: Function;
|
||||
};
|
||||
export const APIErrorContext = createContext<ErrorContextType>({
|
||||
error: null,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
addError: () => {},
|
||||
removeError: () => {},
|
||||
});
|
||||
|
||||
// @ts-ignore
|
||||
// eslint-disable-next-line react/prop-types
|
||||
export default function APIErrorProvider({ children }) {
|
||||
const [error, setError] = useState<ErrorForDisplay | null>(null);
|
||||
const addError = (errorForDisplay: ErrorForDisplay | null) => {
|
||||
setError(errorForDisplay);
|
||||
};
|
||||
const removeError = () => setError(null);
|
||||
|
||||
const contextValue = React.useMemo(
|
||||
() => ({
|
||||
error,
|
||||
addError: (newError: ErrorForDisplay | null) => {
|
||||
addError(newError);
|
||||
},
|
||||
removeError: () => {
|
||||
removeError();
|
||||
},
|
||||
}),
|
||||
[error]
|
||||
);
|
||||
|
||||
return (
|
||||
<APIErrorContext.Provider value={contextValue}>
|
||||
{children}
|
||||
</APIErrorContext.Provider>
|
||||
);
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
import { createContext } from 'react';
|
||||
|
||||
// @ts-expect-error TS(2554) FIXME: Expected 1 arguments, but got 0.
|
||||
const ErrorContext = createContext();
|
||||
export default ErrorContext;
|
|
@ -8,7 +8,6 @@ import {
|
|||
DEFAULT_PER_PAGE,
|
||||
DEFAULT_PAGE,
|
||||
} from './components/PaginationForTable';
|
||||
import { ErrorForDisplay } from './interfaces';
|
||||
|
||||
// https://www.30secondsofcode.org/js/s/slugify
|
||||
export const slugifyString = (str: any) => {
|
||||
|
@ -258,20 +257,6 @@ export const getBpmnProcessIdentifiers = (rootBpmnElement: any) => {
|
|||
return childProcesses;
|
||||
};
|
||||
|
||||
// Setting the error message state to the same string is still considered a change
|
||||
// and re-renders the page so check the message first to avoid that.
|
||||
export const setErrorMessageSafely = (
|
||||
newErrorMessageString: string,
|
||||
oldErrorMessage: ErrorForDisplay,
|
||||
errorMessageSetter: any
|
||||
) => {
|
||||
if (oldErrorMessage && oldErrorMessage.message === newErrorMessageString) {
|
||||
return null;
|
||||
}
|
||||
errorMessageSetter({ message: newErrorMessageString });
|
||||
return null;
|
||||
};
|
||||
|
||||
export const isInteger = (str: string | number) => {
|
||||
return /^\d+$/.test(str.toString());
|
||||
};
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
// src/common/hooks/useAPIError/index.js
|
||||
import { useContext } from 'react';
|
||||
import { APIErrorContext } from '../contexts/APIErrorContext';
|
||||
|
||||
function useAPIError() {
|
||||
const { error, addError, removeError } = useContext(APIErrorContext);
|
||||
return { error, addError, removeError };
|
||||
}
|
||||
|
||||
export default useAPIError;
|
|
@ -24,20 +24,28 @@ export interface RecentProcessModel {
|
|||
export interface ProcessInstanceTask {
|
||||
id: number;
|
||||
task_id: string;
|
||||
|
||||
calling_subprocess_task_id: string;
|
||||
created_at_in_seconds: number;
|
||||
current_user_is_potential_owner: number;
|
||||
data: any;
|
||||
form_schema: any;
|
||||
form_ui_schema: any;
|
||||
lane_assignment_id: string;
|
||||
name: string;
|
||||
process_identifier: string;
|
||||
process_initiator_username: string;
|
||||
process_instance_id: number;
|
||||
process_instance_status: string;
|
||||
process_model_display_name: string;
|
||||
process_model_identifier: string;
|
||||
task_title: string;
|
||||
lane_assignment_id: string;
|
||||
process_instance_status: string;
|
||||
properties: any;
|
||||
state: string;
|
||||
process_identifier: string;
|
||||
name: string;
|
||||
process_initiator_username: string;
|
||||
created_at_in_seconds: number;
|
||||
task_title: string;
|
||||
title: string;
|
||||
type: string;
|
||||
updated_at_in_seconds: number;
|
||||
current_user_is_potential_owner: number;
|
||||
calling_subprocess_task_id: string;
|
||||
|
||||
potential_owner_usernames?: string;
|
||||
assigned_user_group_identifier?: string;
|
||||
}
|
||||
|
@ -190,6 +198,7 @@ export interface ErrorForDisplay {
|
|||
task_id?: string;
|
||||
line_number?: number;
|
||||
file_name?: string;
|
||||
task_trace?: [string];
|
||||
}
|
||||
|
||||
export interface AuthenticationParam {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Routes, Route, useLocation } from 'react-router-dom';
|
||||
|
||||
import { useContext, useEffect } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import ProcessGroupList from './ProcessGroupList';
|
||||
import ProcessGroupShow from './ProcessGroupShow';
|
||||
import ProcessGroupNew from './ProcessGroupNew';
|
||||
|
@ -17,7 +17,6 @@ import ProcessInstanceReportList from './ProcessInstanceReportList';
|
|||
import ProcessInstanceReportNew from './ProcessInstanceReportNew';
|
||||
import ProcessInstanceReportEdit from './ProcessInstanceReportEdit';
|
||||
import ReactFormEditor from './ReactFormEditor';
|
||||
import ErrorContext from '../contexts/ErrorContext';
|
||||
import ProcessInstanceLogList from './ProcessInstanceLogList';
|
||||
import MessageInstanceList from './MessageInstanceList';
|
||||
import Configuration from './Configuration';
|
||||
|
@ -27,11 +26,8 @@ import ProcessInstanceFindById from './ProcessInstanceFindById';
|
|||
|
||||
export default function AdminRoutes() {
|
||||
const location = useLocation();
|
||||
const setErrorObject = (useContext as any)(ErrorContext)[1];
|
||||
|
||||
useEffect(() => {
|
||||
setErrorObject(null);
|
||||
}, [location, setErrorObject]);
|
||||
useEffect(() => {}, [location]);
|
||||
|
||||
if (UserService.hasRole(['admin'])) {
|
||||
return (
|
||||
|
|
|
@ -1,14 +1,13 @@
|
|||
import { useContext, useEffect, useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
// @ts-ignore
|
||||
import { Table } from '@carbon/react';
|
||||
import ErrorContext from '../contexts/ErrorContext';
|
||||
import useAPIError from '../hooks/UseApiError';
|
||||
import { AuthenticationItem } from '../interfaces';
|
||||
import HttpService from '../services/HttpService';
|
||||
import UserService from '../services/UserService';
|
||||
|
||||
export default function AuthenticationList() {
|
||||
const setErrorObject = (useContext as any)(ErrorContext)[1];
|
||||
|
||||
const { addError } = useAPIError();
|
||||
const [authenticationList, setAuthenticationList] = useState<
|
||||
AuthenticationItem[] | null
|
||||
>(null);
|
||||
|
@ -26,9 +25,9 @@ export default function AuthenticationList() {
|
|||
HttpService.makeCallToBackend({
|
||||
path: `/authentications`,
|
||||
successCallback: processResult,
|
||||
failureCallback: setErrorObject,
|
||||
failureCallback: addError,
|
||||
});
|
||||
}, [setErrorObject]);
|
||||
}, [addError]);
|
||||
|
||||
const buildTable = () => {
|
||||
if (authenticationList) {
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { useContext, useEffect, useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Route, Routes, useLocation, useNavigate } from 'react-router-dom';
|
||||
// @ts-ignore
|
||||
import { Tabs, TabList, Tab } from '@carbon/react';
|
||||
import { Can } from '@casl/react';
|
||||
import ErrorContext from '../contexts/ErrorContext';
|
||||
import useAPIError from '../hooks/UseApiError';
|
||||
import SecretList from './SecretList';
|
||||
import SecretNew from './SecretNew';
|
||||
import SecretShow from './SecretShow';
|
||||
|
@ -14,7 +14,7 @@ import { usePermissionFetcher } from '../hooks/PermissionService';
|
|||
|
||||
export default function Configuration() {
|
||||
const location = useLocation();
|
||||
const setErrorObject = (useContext as any)(ErrorContext)[1];
|
||||
const { removeError } = useAPIError();
|
||||
const [selectedTabIndex, setSelectedTabIndex] = useState<number>(0);
|
||||
const navigate = useNavigate();
|
||||
|
||||
|
@ -26,13 +26,14 @@ export default function Configuration() {
|
|||
const { ability } = usePermissionFetcher(permissionRequestData);
|
||||
|
||||
useEffect(() => {
|
||||
setErrorObject(null);
|
||||
console.log('Configuration remove error');
|
||||
removeError();
|
||||
let newSelectedTabIndex = 0;
|
||||
if (location.pathname.match(/^\/admin\/configuration\/authentications\b/)) {
|
||||
newSelectedTabIndex = 1;
|
||||
}
|
||||
setSelectedTabIndex(newSelectedTabIndex);
|
||||
}, [location, setErrorObject]);
|
||||
}, [location, removeError]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
import { useContext, useEffect, useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Route, Routes, useLocation, useNavigate } from 'react-router-dom';
|
||||
// @ts-ignore
|
||||
import { Tabs, TabList, Tab } from '@carbon/react';
|
||||
import TaskShow from './TaskShow';
|
||||
import ErrorContext from '../contexts/ErrorContext';
|
||||
import MyTasks from './MyTasks';
|
||||
import GroupedTasks from './GroupedTasks';
|
||||
import CompletedInstances from './CompletedInstances';
|
||||
|
@ -11,12 +10,11 @@ import CreateNewInstance from './CreateNewInstance';
|
|||
|
||||
export default function HomePageRoutes() {
|
||||
const location = useLocation();
|
||||
const setErrorObject = (useContext as any)(ErrorContext)[1];
|
||||
const [selectedTabIndex, setSelectedTabIndex] = useState<number>(0);
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
setErrorObject(null);
|
||||
// Do not remove errors here, or they always get removed.
|
||||
let newSelectedTabIndex = 0;
|
||||
if (location.pathname.match(/^\/tasks\/completed-instances\b/)) {
|
||||
newSelectedTabIndex = 1;
|
||||
|
@ -24,7 +22,7 @@ export default function HomePageRoutes() {
|
|||
newSelectedTabIndex = 2;
|
||||
}
|
||||
setSelectedTabIndex(newSelectedTabIndex);
|
||||
}, [location, setErrorObject]);
|
||||
}, [location]);
|
||||
|
||||
const renderTabs = () => {
|
||||
if (location.pathname.match(/^\/tasks\/\d+\/\b/)) {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { useContext, useEffect, useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import Editor from '@monaco-editor/react';
|
||||
import {
|
||||
useParams,
|
||||
|
@ -39,7 +39,6 @@ import {
|
|||
unModifyProcessIdentifierForPathParam,
|
||||
} from '../helpers';
|
||||
import ButtonWithConfirmation from '../components/ButtonWithConfirmation';
|
||||
import ErrorContext from '../contexts/ErrorContext';
|
||||
import { useUriListForPermissions } from '../hooks/UriListForPermissions';
|
||||
import {
|
||||
PermissionsToCheck,
|
||||
|
@ -51,6 +50,7 @@ import {
|
|||
import { usePermissionFetcher } from '../hooks/PermissionService';
|
||||
import ProcessInstanceClass from '../classes/ProcessInstanceClass';
|
||||
import TaskListTable from '../components/TaskListTable';
|
||||
import useAPIError from '../hooks/UseApiError';
|
||||
|
||||
type OwnProps = {
|
||||
variant: string;
|
||||
|
@ -79,8 +79,7 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
|
|||
const [showProcessInstanceMetadata, setShowProcessInstanceMetadata] =
|
||||
useState<boolean>(false);
|
||||
|
||||
const setErrorObject = (useContext as any)(ErrorContext)[1];
|
||||
|
||||
const { addError, removeError } = useAPIError();
|
||||
const unModifiedProcessModelId = unModifyProcessIdentifierForPathParam(
|
||||
`${params.process_model_id}`
|
||||
);
|
||||
|
@ -155,11 +154,11 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
|
|||
}
|
||||
}
|
||||
}, [
|
||||
targetUris,
|
||||
params,
|
||||
modifiedProcessModelId,
|
||||
permissionsLoaded,
|
||||
ability,
|
||||
targetUris,
|
||||
searchParams,
|
||||
taskListPath,
|
||||
variant,
|
||||
|
@ -718,7 +717,8 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
|
|||
setSelectingEvent(false);
|
||||
initializeTaskDataToDisplay(taskToDisplay);
|
||||
setEventPayload('{}');
|
||||
setErrorObject(null);
|
||||
console.log('cancel updating task');
|
||||
removeError();
|
||||
};
|
||||
|
||||
const taskDataStringToObject = (dataString: string) => {
|
||||
|
@ -733,16 +733,12 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
|
|||
refreshPage();
|
||||
};
|
||||
|
||||
const saveTaskDataFailure = (result: any) => {
|
||||
setErrorObject({ message: result.message });
|
||||
};
|
||||
|
||||
const saveTaskData = () => {
|
||||
if (!taskToDisplay) {
|
||||
return;
|
||||
}
|
||||
|
||||
setErrorObject(null);
|
||||
console.log('saveTaskData');
|
||||
removeError();
|
||||
|
||||
// taskToUse is copy of taskToDisplay, with taskDataToDisplay in data attribute
|
||||
const taskToUse: any = { ...taskToDisplay, data: taskDataToDisplay };
|
||||
|
@ -750,7 +746,7 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
|
|||
path: `${targetUris.processInstanceTaskListDataPath}/${taskToUse.id}`,
|
||||
httpMethod: 'PUT',
|
||||
successCallback: saveTaskDataResult,
|
||||
failureCallback: saveTaskDataFailure,
|
||||
failureCallback: addError,
|
||||
postBody: {
|
||||
new_task_data: taskToUse.data,
|
||||
},
|
||||
|
@ -764,7 +760,7 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
|
|||
path: `/send-event/${modifiedProcessModelId}/${params.process_instance_id}`,
|
||||
httpMethod: 'POST',
|
||||
successCallback: saveTaskDataResult,
|
||||
failureCallback: saveTaskDataFailure,
|
||||
failureCallback: addError,
|
||||
postBody: eventToSend,
|
||||
});
|
||||
};
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { useContext, useEffect, useRef, useState } from 'react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import {
|
||||
generatePath,
|
||||
useNavigate,
|
||||
|
@ -28,7 +28,7 @@ import MDEditor from '@uiw/react-md-editor';
|
|||
import ReactDiagramEditor from '../components/ReactDiagramEditor';
|
||||
import ProcessBreadcrumb from '../components/ProcessBreadcrumb';
|
||||
import HttpService from '../services/HttpService';
|
||||
import ErrorContext from '../contexts/ErrorContext';
|
||||
import useAPIError from '../hooks/UseApiError';
|
||||
import { makeid, modifyProcessIdentifierForPathParam } from '../helpers';
|
||||
import {
|
||||
CarbonComboBoxProcessSelection,
|
||||
|
@ -105,7 +105,7 @@ export default function ProcessModelEditDiagram() {
|
|||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
const setErrorObject = (useContext as any)(ErrorContext)[1];
|
||||
const { addError, removeError } = useAPIError();
|
||||
const [processModelFile, setProcessModelFile] = useState<ProcessFile | null>(
|
||||
null
|
||||
);
|
||||
|
@ -182,7 +182,7 @@ export default function ProcessModelEditDiagram() {
|
|||
|
||||
const saveDiagram = (bpmnXML: any, fileName = params.file_name) => {
|
||||
setDisplaySaveFileMessage(false);
|
||||
setErrorObject(null);
|
||||
removeError();
|
||||
setBpmnXmlForDiagramRendering(bpmnXML);
|
||||
|
||||
let url = `/process-models/${modifiedProcessModelId}/files`;
|
||||
|
@ -208,7 +208,7 @@ export default function ProcessModelEditDiagram() {
|
|||
HttpService.makeCallToBackend({
|
||||
path: url,
|
||||
successCallback: navigateToProcessModelFile,
|
||||
failureCallback: setErrorObject,
|
||||
failureCallback: addError,
|
||||
httpMethod,
|
||||
postBody: formData,
|
||||
});
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { useContext, useEffect, useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Link, useNavigate, useParams } from 'react-router-dom';
|
||||
import {
|
||||
Add,
|
||||
|
@ -32,7 +32,8 @@ import {
|
|||
import { Can } from '@casl/react';
|
||||
import ProcessBreadcrumb from '../components/ProcessBreadcrumb';
|
||||
import HttpService from '../services/HttpService';
|
||||
import ErrorContext from '../contexts/ErrorContext';
|
||||
import useAPIError from '../hooks/UseApiError';
|
||||
|
||||
import {
|
||||
getGroupFromModifiedModelId,
|
||||
modifyProcessIdentifierForPathParam,
|
||||
|
@ -52,7 +53,7 @@ import { Notification } from '../components/Notification';
|
|||
|
||||
export default function ProcessModelShow() {
|
||||
const params = useParams();
|
||||
const setErrorObject = (useContext as any)(ErrorContext)[1];
|
||||
const { addError, removeError } = useAPIError();
|
||||
|
||||
const [processModel, setProcessModel] = useState<ProcessModel | null>(null);
|
||||
const [processInstance, setProcessInstance] =
|
||||
|
@ -148,7 +149,7 @@ export default function ProcessModelShow() {
|
|||
!('file_contents' in processModelFile) ||
|
||||
processModelFile.file_contents === undefined
|
||||
) {
|
||||
setErrorObject({
|
||||
addError({
|
||||
message: `Could not file file contents for file: ${processModelFile.name}`,
|
||||
});
|
||||
return;
|
||||
|
@ -169,7 +170,7 @@ export default function ProcessModelShow() {
|
|||
};
|
||||
|
||||
const downloadFile = (fileName: string) => {
|
||||
setErrorObject(null);
|
||||
removeError();
|
||||
const processModelPath = `process-models/${modifiedProcessModelId}`;
|
||||
HttpService.makeCallToBackend({
|
||||
path: `/${processModelPath}/files/${fileName}`,
|
||||
|
@ -374,7 +375,7 @@ export default function ProcessModelShow() {
|
|||
|
||||
const doFileUpload = (event: any) => {
|
||||
event.preventDefault();
|
||||
setErrorObject(null);
|
||||
removeError();
|
||||
const url = `/process-models/${modifiedProcessModelId}/files`;
|
||||
const formData = new FormData();
|
||||
formData.append('file', filesToUpload[0]);
|
||||
|
@ -384,7 +385,7 @@ export default function ProcessModelShow() {
|
|||
successCallback: onUploadedCallback,
|
||||
httpMethod: 'POST',
|
||||
postBody: formData,
|
||||
failureCallback: setErrorObject,
|
||||
failureCallback: addError,
|
||||
});
|
||||
setFilesToUpload(null);
|
||||
};
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { useContext, useEffect, useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import Editor from '@monaco-editor/react';
|
||||
import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
|
||||
// @ts-ignore
|
||||
|
@ -8,16 +8,14 @@ import HttpService from '../services/HttpService';
|
|||
import ButtonWithConfirmation from '../components/ButtonWithConfirmation';
|
||||
import { modifyProcessIdentifierForPathParam } from '../helpers';
|
||||
import { ProcessFile } from '../interfaces';
|
||||
import ErrorContext from '../contexts/ErrorContext';
|
||||
import { Notification } from '../components/Notification';
|
||||
|
||||
import useAPIError from '../hooks/UseApiError';
|
||||
// NOTE: This is mostly the same as ProcessModelEditDiagram and if we go this route could
|
||||
// possibly be merged into it. I'm leaving as a separate file now in case it does
|
||||
// end up diverging greatly
|
||||
export default function ReactFormEditor() {
|
||||
const params = useParams();
|
||||
const setErrorObject = (useContext as any)(ErrorContext)[1];
|
||||
|
||||
const { addError, removeError } = useAPIError();
|
||||
const [showFileNameEditor, setShowFileNameEditor] = useState(false);
|
||||
const [newFileName, setNewFileName] = useState('');
|
||||
const searchParams = useSearchParams()[0];
|
||||
|
@ -87,7 +85,7 @@ export default function ReactFormEditor() {
|
|||
};
|
||||
|
||||
const saveFile = () => {
|
||||
setErrorObject(null);
|
||||
removeError();
|
||||
setDisplaySaveFileMessage(false);
|
||||
|
||||
let url = `/process-models/${modifiedProcessModelId}/files`;
|
||||
|
@ -116,7 +114,7 @@ export default function ReactFormEditor() {
|
|||
HttpService.makeCallToBackend({
|
||||
path: url,
|
||||
successCallback: navigateToProcessModelFile,
|
||||
failureCallback: setErrorObject,
|
||||
failureCallback: addError,
|
||||
httpMethod,
|
||||
postBody: formData,
|
||||
});
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { useContext, useEffect, useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import validator from '@rjsf/validator-ajv8';
|
||||
|
||||
|
@ -17,70 +17,64 @@ import remarkGfm from 'remark-gfm';
|
|||
// eslint-disable-next-line import/no-named-as-default
|
||||
import Form from '../themes/carbon';
|
||||
import HttpService from '../services/HttpService';
|
||||
import ErrorContext from '../contexts/ErrorContext';
|
||||
import useAPIError from '../hooks/UseApiError';
|
||||
import { modifyProcessIdentifierForPathParam } from '../helpers';
|
||||
import { useUriListForPermissions } from '../hooks/UriListForPermissions';
|
||||
import { PermissionsToCheck } from '../interfaces';
|
||||
import { usePermissionFetcher } from '../hooks/PermissionService';
|
||||
import { ProcessInstanceTask } from '../interfaces';
|
||||
|
||||
export default function TaskShow() {
|
||||
const [task, setTask] = useState(null);
|
||||
const [task, setTask] = useState<ProcessInstanceTask | null>(null);
|
||||
const [userTasks, setUserTasks] = useState(null);
|
||||
const params = useParams();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const setErrorObject = (useContext as any)(ErrorContext)[1];
|
||||
|
||||
const { targetUris } = useUriListForPermissions();
|
||||
const permissionRequestData: PermissionsToCheck = {
|
||||
[targetUris.processInstanceTaskListDataPath]: ['GET'],
|
||||
};
|
||||
const { ability, permissionsLoaded } = usePermissionFetcher(
|
||||
permissionRequestData
|
||||
);
|
||||
const { addError, removeError } = useAPIError();
|
||||
|
||||
useEffect(() => {
|
||||
if (permissionsLoaded) {
|
||||
const processResult = (result: any) => {
|
||||
setTask(result);
|
||||
if (ability.can('GET', targetUris.processInstanceTaskListDataPath)) {
|
||||
HttpService.makeCallToBackend({
|
||||
path: `/task-data/${modifyProcessIdentifierForPathParam(
|
||||
result.process_model_identifier
|
||||
)}/${params.process_instance_id}`,
|
||||
successCallback: setUserTasks,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const processResult = (result: ProcessInstanceTask) => {
|
||||
setTask(result);
|
||||
const url = `/task-data/${modifyProcessIdentifierForPathParam(
|
||||
result.process_model_identifier
|
||||
)}/${params.process_instance_id}`;
|
||||
// if user is unauthorized to get task-data then don't do anything
|
||||
// Checking like this so we can dynamically create the url with the correct process model
|
||||
// instead of passing the process model identifier in through the params
|
||||
HttpService.makeCallToBackend({
|
||||
path: `/tasks/${params.process_instance_id}/${params.task_id}`,
|
||||
successCallback: processResult,
|
||||
// This causes the page to continuously reload
|
||||
// failureCallback: setErrorObject,
|
||||
path: url,
|
||||
successCallback: setUserTasks,
|
||||
onUnauthorized: () => {},
|
||||
failureCallback: (error: any) => {
|
||||
addError(error);
|
||||
},
|
||||
});
|
||||
}
|
||||
}, [params, permissionsLoaded, ability, targetUris]);
|
||||
};
|
||||
HttpService.makeCallToBackend({
|
||||
path: `/tasks/${params.process_instance_id}/${params.task_id}`,
|
||||
successCallback: processResult,
|
||||
failureCallback: addError,
|
||||
});
|
||||
// FIXME: not sure what to do about addError. adding it to this array causes the page to endlessly reload
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [params]);
|
||||
|
||||
const processSubmitResult = (result: any) => {
|
||||
setErrorObject(null);
|
||||
removeError();
|
||||
if (result.ok) {
|
||||
navigate(`/tasks`);
|
||||
} else if (result.process_instance_id) {
|
||||
navigate(`/tasks/${result.process_instance_id}/${result.id}`);
|
||||
} else {
|
||||
setErrorObject(`Received unexpected error: ${result.message}`);
|
||||
addError(result);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFormSubmit = (event: any) => {
|
||||
setErrorObject(null);
|
||||
removeError();
|
||||
const dataToSubmit = event.formData;
|
||||
delete dataToSubmit.isManualTask;
|
||||
HttpService.makeCallToBackend({
|
||||
path: `/tasks/${params.process_instance_id}/${params.task_id}`,
|
||||
successCallback: processSubmitResult,
|
||||
failureCallback: setErrorObject,
|
||||
failureCallback: addError,
|
||||
httpMethod: 'PUT',
|
||||
postBody: dataToSubmit,
|
||||
});
|
||||
|
@ -166,12 +160,16 @@ export default function TaskShow() {
|
|||
return errors;
|
||||
};
|
||||
|
||||
const formElement = (taskToUse: any) => {
|
||||
const formElement = () => {
|
||||
if (!task) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let formUiSchema;
|
||||
let taskData = taskToUse.data;
|
||||
let jsonSchema = taskToUse.form_schema;
|
||||
let taskData = task.data;
|
||||
let jsonSchema = task.form_schema;
|
||||
let reactFragmentToHideSubmitButton = null;
|
||||
if (taskToUse.type === 'Manual Task') {
|
||||
if (task.type === 'Manual Task') {
|
||||
taskData = {};
|
||||
jsonSchema = {
|
||||
type: 'object',
|
||||
|
@ -189,10 +187,10 @@ export default function TaskShow() {
|
|||
'ui:widget': 'hidden',
|
||||
},
|
||||
};
|
||||
} else if (taskToUse.form_ui_schema) {
|
||||
formUiSchema = JSON.parse(taskToUse.form_ui_schema);
|
||||
} else if (task.form_ui_schema) {
|
||||
formUiSchema = JSON.parse(task.form_ui_schema);
|
||||
}
|
||||
if (taskToUse.state !== 'READY') {
|
||||
if (task.state !== 'READY') {
|
||||
formUiSchema = Object.assign(formUiSchema || {}, {
|
||||
'ui:readonly': true,
|
||||
});
|
||||
|
@ -204,7 +202,7 @@ export default function TaskShow() {
|
|||
reactFragmentToHideSubmitButton = <div />;
|
||||
}
|
||||
|
||||
if (taskToUse.type === 'Manual Task' && taskToUse.state === 'READY') {
|
||||
if (task.type === 'Manual Task' && task.state === 'READY') {
|
||||
reactFragmentToHideSubmitButton = (
|
||||
<div>
|
||||
<Button type="submit">Continue</Button>
|
||||
|
@ -234,10 +232,13 @@ export default function TaskShow() {
|
|||
);
|
||||
};
|
||||
|
||||
const instructionsElement = (taskToUse: any) => {
|
||||
const instructionsElement = () => {
|
||||
if (!task) {
|
||||
return null;
|
||||
}
|
||||
let instructions = '';
|
||||
if (taskToUse.properties.instructionsForEndUser) {
|
||||
instructions = taskToUse.properties.instructionsForEndUser;
|
||||
if (task.properties.instructionsForEndUser) {
|
||||
instructions = task.properties.instructionsForEndUser;
|
||||
}
|
||||
return (
|
||||
<div className="markdown">
|
||||
|
@ -249,21 +250,19 @@ export default function TaskShow() {
|
|||
};
|
||||
|
||||
if (task) {
|
||||
const taskToUse = task as any;
|
||||
let statusString = '';
|
||||
if (taskToUse.state !== 'READY') {
|
||||
statusString = ` ${taskToUse.state}`;
|
||||
if (task.state !== 'READY') {
|
||||
statusString = ` ${task.state}`;
|
||||
}
|
||||
|
||||
return (
|
||||
<main>
|
||||
<div>{buildTaskNavigation()}</div>
|
||||
<h3>
|
||||
Task: {taskToUse.title} ({taskToUse.process_model_display_name})
|
||||
{statusString}
|
||||
Task: {task.title} ({task.process_model_display_name}){statusString}
|
||||
</h3>
|
||||
{instructionsElement(taskToUse)}
|
||||
{formElement(taskToUse)}
|
||||
{instructionsElement()}
|
||||
{formElement()}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ type backendCallProps = {
|
|||
path: string;
|
||||
successCallback: Function;
|
||||
failureCallback?: Function;
|
||||
onUnauthorized?: Function;
|
||||
httpMethod?: string;
|
||||
extraHeaders?: object;
|
||||
postBody?: any;
|
||||
|
@ -37,6 +38,7 @@ const makeCallToBackend = ({
|
|||
path,
|
||||
successCallback,
|
||||
failureCallback,
|
||||
onUnauthorized,
|
||||
httpMethod = 'GET',
|
||||
extraHeaders = {},
|
||||
postBody = {},
|
||||
|
@ -88,9 +90,13 @@ backendCallProps) => {
|
|||
if (isSuccessful) {
|
||||
successCallback(result);
|
||||
} else if (is403) {
|
||||
// Hopefully we can make this service a hook and use the error message context directly
|
||||
// eslint-disable-next-line no-alert
|
||||
alert(result.message);
|
||||
if (onUnauthorized) {
|
||||
onUnauthorized(result);
|
||||
} else {
|
||||
// Hopefully we can make this service a hook and use the error message context directly
|
||||
// eslint-disable-next-line no-alert
|
||||
alert(result.message);
|
||||
}
|
||||
} else {
|
||||
let message = 'A server error occurred.';
|
||||
if (result.message) {
|
||||
|
|
Loading…
Reference in New Issue