Merge branch 'main' into feature/waku-fault-message

This commit is contained in:
mike cullerton 2023-01-12 07:19:06 -05:00
commit 36ee3b9551
26 changed files with 669 additions and 413 deletions

View File

@ -11,8 +11,7 @@ set -o errtrace -o errexit -o nounset -o pipefail
# so we can see what resources that user has access to
# originally from https://medium.com/keycloak/keycloak-jwt-token-using-curl-post-72c9e791ba8c
# btw, meta config endpoint: http://localhost:7002/realms/spiffworkflow/.well-known/openid-configuration
# token exchange described at https://github.com/keycloak/keycloak-documentation/blob/main/securing_apps/topics/token-exchange/token-exchange.adoc
# btw, meta config endpoint: http://localhost:7002/realms/spiffworkflow/.well-known/openid-configuration token exchange described at https://github.com/keycloak/keycloak-documentation/blob/main/securing_apps/topics/token-exchange/token-exchange.adoc
# some UMA stuff at https://github.com/keycloak/keycloak-documentation/blob/main/authorization_services/topics/service-authorization-obtaining-permission.adoc,
# though resource_set docs are elsewhere.
@ -21,11 +20,13 @@ set -o errtrace -o errexit -o nounset -o pipefail
# ./bin/get_token repeat_form_user_1 repeat_form_user_1 # actually has permissions to the resource in this script
# ./bin/get_token ciadmin1 ciadmin1 '%2Fprocess-models'
HOSTNAME=localhost:7002
# KEYCLOAK_BASE_URL=http://localhost:7002
KEYCLOAK_BASE_URL=https://keycloak.dev.spiffworkflow.org
# BACKEND_BASE_URL=http://localhost:7000
BACKEND_BASE_URL=https://api.dev.spiffworkflow.org
REALM_NAME=spiffworkflow
USERNAME=${1-ciuser1}
PASSWORD=${2-ciuser1}
URI_TO_TEST_AGAINST=${3-'%2Fprocess-models%2Fcategory_number_one%2Fprocess-model-with-repeating-form'}
USERNAME=${1-fin}
PASSWORD=${2-fin}
FRONTEND_CLIENT_ID=spiffworkflow-frontend
BACKEND_CLIENT_ID=spiffworkflow-backend
@ -33,7 +34,7 @@ BACKEND_CLIENT_SECRET="JXeQExm0JhQPLumgHtIIqf52bDalHz0q" # noqa: S105
SECURE=false
BACKEND_BASIC_AUTH=$(echo -n "${BACKEND_CLIENT_ID}:${BACKEND_CLIENT_SECRET}" | base64)
KEYCLOAK_URL=http://$HOSTNAME/realms/$REALM_NAME/protocol/openid-connect/token
KEYCLOAK_URL=$KEYCLOAK_BASE_URL/realms/$REALM_NAME/protocol/openid-connect/token
echo "Using Keycloak: $KEYCLOAK_URL"
echo "realm: $REALM_NAME"
@ -49,55 +50,72 @@ else
INSECURE=--insecure
fi
### Basic auth test with backend
result=$(curl -s -X POST "$KEYCLOAK_URL" "$INSECURE" \
-H "Content-Type: application/x-www-form-urlencoded" \
-H "Authorization: Basic $BACKEND_BASIC_AUTH" \
-d "username=$USERNAME" \
-d "password=$PASSWORD" \
-d 'grant_type=password' \
-d "client_id=$FRONTEND_CLIENT_ID" \
)
frontend_token=$(jq -r '.access_token' <<< "$result")
result=$(curl -s -X POST "$KEYCLOAK_URL" "$INSECURE" \
-H "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode 'grant_type=urn:ietf:params:oauth:grant-type:token-exchange' \
-d "client_id=$BACKEND_CLIENT_ID" \
-d "subject_token=${frontend_token}" \
-H "Authorization: Basic $BACKEND_BASIC_AUTH" \
-d "audience=${BACKEND_CLIENT_ID}" \
)
backend_token=$(jq -r '.access_token' <<< "$result")
curl --fail -v "${BACKEND_BASE_URL}/v1.0/process-groups?per_page=1" -H "Authorization: Bearer $backend_token"
if [[ "$backend_token" != 'null' ]]; then
echo "backend_token: $backend_token"
echo "Getting resource set"
# everything_resource_id='446bdcf4-a3bd-41c7-a0f8-67a225ba6b57'
resource_result=$(curl -s "http://${HOSTNAME}/realms/spiffworkflow/authz/protection/resource_set?matchingUri=true&deep=true&max=-1&exactName=false&uri=${URI_TO_TEST_AGAINST}" -H "Authorization: Bearer $backend_token")
# resource_result=$(curl -s "http://${HOSTNAME}/realms/spiffworkflow/authz/protection/resource_set?matchingUri=false&deep=true&max=-1&exactName=false&type=admin" -H "Authorization: Bearer $backend_token")
### Get with frontend and exchange with backend - not configured to work in keycloak atm
# result=$(curl -s -X POST "$KEYCLOAK_URL" "$INSECURE" \
# -H "Content-Type: application/x-www-form-urlencoded" \
# -d "username=$USERNAME" \
# -d "password=$PASSWORD" \
# -d 'grant_type=password' \
# -d "client_id=$FRONTEND_CLIENT_ID" \
# )
# frontend_token=$(jq -r '.access_token' <<< "$result")
#
# result=$(curl -s -X POST "$KEYCLOAK_URL" "$INSECURE" \
# -H "Content-Type: application/x-www-form-urlencoded" \
# --data-urlencode 'grant_type=urn:ietf:params:oauth:grant-type:token-exchange' \
# -d "client_id=$BACKEND_CLIENT_ID" \
# -d "subject_token=${frontend_token}" \
# -H "Authorization: Basic $BACKEND_BASIC_AUTH" \
# -d "audience=${BACKEND_CLIENT_ID}" \
# )
# backend_token=$(jq -r '.access_token' <<< "$result")
resource_id_name_pairs=$(jq -r '.[] | "\(._id):\(.name)"' <<<"$resource_result" || echo '')
if [[ -z "$resource_id_name_pairs" || "$resource_id_name_pairs" == "null" ]]; then
>&2 echo "ERROR: Could not find the resource id from the result: ${resource_result}"
exit 1
fi
echo $resource_id_name_pairs
echo "Getting permissions"
for resource_id_name_pair in $resource_id_name_pairs ; do
resource_id=$(awk -F ':' '{print $1}' <<<"$resource_id_name_pair")
resource_name=$(awk -F ':' '{print $2}' <<<"$resource_id_name_pair")
echo "Checking $resource_name"
curl -s -X POST "$KEYCLOAK_URL" "$INSECURE" \
-H "Content-Type: application/x-www-form-urlencoded" \
-H "Authorization: Basic $BACKEND_BASIC_AUTH" \
-d "audience=${BACKEND_CLIENT_ID}" \
--data-urlencode "grant_type=urn:ietf:params:oauth:grant-type:uma-ticket" \
-d "permission=${resource_id}" \
-d "subject_token=${backend_token}" \
| jq .
done
else
echo "Failed auth result: $result"
fi
### Check fine grain permissions - does not work currently
# URI_TO_TEST_AGAINST=${3-'%2Fprocess-models%2Fcategory_number_one%2Fprocess-model-with-repeating-form'}
# if [[ "$backend_token" != 'null' ]]; then
# echo "backend_token: $backend_token"
#
# echo "Getting resource set"
# # everything_resource_id='446bdcf4-a3bd-41c7-a0f8-67a225ba6b57'
# resource_result=$(curl -s "${BASE_URL}/realms/spiffworkflow/authz/protection/resource_set?matchingUri=true&deep=true&max=-1&exactName=false&uri=${URI_TO_TEST_AGAINST}" -H "Authorization: Bearer $backend_token")
# # resource_result=$(curl -s "${BASE_URL}/realms/spiffworkflow/authz/protection/resource_set?matchingUri=false&deep=true&max=-1&exactName=false&type=admin" -H "Authorization: Bearer $backend_token")
#
# resource_id_name_pairs=$(jq -r '.[] | "\(._id):\(.name)"' <<<"$resource_result" || echo '')
# if [[ -z "$resource_id_name_pairs" || "$resource_id_name_pairs" == "null" ]]; then
# >&2 echo "ERROR: Could not find the resource id from the result: ${resource_result}"
# exit 1
# fi
# echo $resource_id_name_pairs
#
# echo "Getting permissions"
# for resource_id_name_pair in $resource_id_name_pairs ; do
# resource_id=$(awk -F ':' '{print $1}' <<<"$resource_id_name_pair")
# resource_name=$(awk -F ':' '{print $2}' <<<"$resource_id_name_pair")
#
# echo "Checking $resource_name"
# curl -s -X POST "$KEYCLOAK_URL" "$INSECURE" \
# -H "Content-Type: application/x-www-form-urlencoded" \
# -H "Authorization: Basic $BACKEND_BASIC_AUTH" \
# -d "audience=${BACKEND_CLIENT_ID}" \
# --data-urlencode "grant_type=urn:ietf:params:oauth:grant-type:uma-ticket" \
# -d "permission=${resource_id}" \
# -d "subject_token=${backend_token}" \
# | jq .
# done
# else
# echo "Failed auth result: $result"
# fi

View File

@ -397,6 +397,12 @@ paths:
description: the modified process model id
schema:
type: string
- name: include_file_references
in: query
required: false
description: include all file references in the return
schema:
type: boolean
get:
operationId: spiffworkflow_backend.routes.process_models_controller.process_model_show
summary: Returns a single process model

View File

@ -15,6 +15,7 @@ from flask import jsonify
from flask import make_response
from flask.wrappers import Response
from flask_bpmn.api.api_error import ApiError
from werkzeug.datastructures import FileStorage
from spiffworkflow_backend.interfaces import IdToProcessGroupMapping
from spiffworkflow_backend.models.file import FileSchema
@ -38,6 +39,9 @@ from spiffworkflow_backend.services.process_instance_report_service import (
ProcessInstanceReportService,
)
from spiffworkflow_backend.services.process_model_service import ProcessModelService
from spiffworkflow_backend.services.spec_file_service import (
ProcessModelFileInvalidError,
)
from spiffworkflow_backend.services.spec_file_service import SpecFileService
@ -123,7 +127,9 @@ def process_model_update(
return ProcessModelInfoSchema().dump(process_model)
def process_model_show(modified_process_model_identifier: str) -> Any:
def process_model_show(
modified_process_model_identifier: str, include_file_references: bool = False
) -> Any:
"""Process_model_show."""
process_model_identifier = modified_process_model_identifier.replace(":", "/")
process_model = _get_process_model(process_model_identifier)
@ -132,8 +138,12 @@ def process_model_show(modified_process_model_identifier: str) -> Any:
key=lambda f: "" if f.name == process_model.primary_file_name else f.sort_index,
)
process_model.files = files
if include_file_references:
for file in process_model.files:
file.references = SpecFileService.get_references_for_file(file, process_model)
file.references = SpecFileService.get_references_for_file(
file, process_model
)
process_model.parent_groups = ProcessModelService.get_parent_group_array(
process_model.id
@ -226,26 +236,11 @@ def process_model_file_update(
modified_process_model_identifier: str, file_name: str
) -> flask.wrappers.Response:
"""Process_model_file_update."""
process_model_identifier = modified_process_model_identifier.replace(":", "/")
process_model = _get_process_model(process_model_identifier)
request_file = _get_file_from_request()
request_file_contents = request_file.stream.read()
if not request_file_contents:
raise ApiError(
error_code="file_contents_empty",
message="Given request file does not have any content",
status_code=400,
message = f"User: {g.user.username} clicked save for"
return _create_or_update_process_model_file(
modified_process_model_identifier, message, 200
)
SpecFileService.update_file(process_model, file_name, request_file_contents)
_commit_and_push_to_git(
f"User: {g.user.username} clicked save for"
f" {process_model_identifier}/{file_name}"
)
return Response(json.dumps({"ok": True}), status=200, mimetype="application/json")
def process_model_file_delete(
modified_process_model_identifier: str, file_name: str
@ -275,28 +270,9 @@ def process_model_file_create(
modified_process_model_identifier: str,
) -> flask.wrappers.Response:
"""Process_model_file_create."""
process_model_identifier = modified_process_model_identifier.replace(":", "/")
process_model = _get_process_model(process_model_identifier)
request_file = _get_file_from_request()
if not request_file.filename:
raise ApiError(
error_code="could_not_get_filename",
message="Could not get filename from request",
status_code=400,
)
file = SpecFileService.add_file(
process_model, request_file.filename, request_file.stream.read()
)
file_contents = SpecFileService.get_data(process_model, file.name)
file.file_contents = file_contents
file.process_model_id = process_model.id
_commit_and_push_to_git(
f"User: {g.user.username} added process model file"
f" {process_model_identifier}/{file.name}"
)
return Response(
json.dumps(FileSchema().dump(file)), status=201, mimetype="application/json"
message = f"User: {g.user.username} added process model file"
return _create_or_update_process_model_file(
modified_process_model_identifier, message, 201
)
@ -466,9 +442,9 @@ def process_model_create_with_natural_language(
)
def _get_file_from_request() -> Any:
def _get_file_from_request() -> FileStorage:
"""Get_file_from_request."""
request_file = connexion.request.files.get("file")
request_file: FileStorage = connexion.request.files.get("file")
if not request_file:
raise ApiError(
error_code="no_file_given",
@ -506,3 +482,58 @@ def _get_process_group_from_modified_identifier(
status_code=400,
)
return process_group
def _create_or_update_process_model_file(
modified_process_model_identifier: str,
message_for_git_commit: str,
http_status_to_return: int,
) -> flask.wrappers.Response:
"""_create_or_update_process_model_file."""
process_model_identifier = modified_process_model_identifier.replace(":", "/")
process_model = _get_process_model(process_model_identifier)
request_file = _get_file_from_request()
# for mypy
request_file_contents = request_file.stream.read()
if not request_file_contents:
raise ApiError(
error_code="file_contents_empty",
message="Given request file does not have any content",
status_code=400,
)
if not request_file.filename:
raise ApiError(
error_code="could_not_get_filename",
message="Could not get filename from request",
status_code=400,
)
file = None
try:
file = SpecFileService.update_file(
process_model, request_file.filename, request_file_contents
)
except ProcessModelFileInvalidError as exception:
raise (
ApiError(
error_code="process_model_file_invalid",
message=(
f"Invalid Process model file cannot be save: {request_file.name}."
f" Received error: {str(exception)}"
),
status_code=400,
)
) from exception
file_contents = SpecFileService.get_data(process_model, file.name)
file.file_contents = file_contents
file.process_model_id = process_model.id
_commit_and_push_to_git(
f"{message_for_git_commit} {process_model_identifier}/{file.name}"
)
return Response(
json.dumps(FileSchema().dump(file)),
status=http_status_to_return,
mimetype="application/json",
)

View File

@ -34,6 +34,8 @@ from SpiffWorkflow.bpmn.serializer.workflow import BpmnWorkflowSerializer # typ
from SpiffWorkflow.bpmn.specs.BpmnProcessSpec import BpmnProcessSpec # type: ignore
from SpiffWorkflow.bpmn.specs.events.EndEvent import EndEvent # type: ignore
from SpiffWorkflow.bpmn.specs.events.event_definitions import CancelEventDefinition # type: ignore
from SpiffWorkflow.bpmn.specs.events.StartEvent import StartEvent # type: ignore
from SpiffWorkflow.bpmn.specs.SubWorkflowTask import SubWorkflowTask # type: ignore
from SpiffWorkflow.bpmn.workflow import BpmnWorkflow # type: ignore
from SpiffWorkflow.dmn.parser.BpmnDmnParser import BpmnDmnParser # type: ignore
from SpiffWorkflow.dmn.serializer.task_spec_converters import BusinessRuleTaskConverter # type: ignore
@ -787,6 +789,15 @@ class ProcessInstanceProcessor:
f"Manually executing Task {spiff_task.task_spec.name} of process"
f" instance {self.process_instance_model.id}"
)
# Executing a subworkflow manually will restart its subprocess and allow stepping through it
if isinstance(spiff_task.task_spec, SubWorkflowTask):
subprocess = self.bpmn_process_instance.get_subprocess(spiff_task)
# We have to get to the actual start event
for task in self.bpmn_process_instance.get_tasks(workflow=subprocess):
task.complete()
if isinstance(task.task_spec, StartEvent):
break
else:
spiff_task.complete()
else:
spiff_logger = logging.getLogger("spiff")
@ -796,7 +807,20 @@ class ProcessInstanceProcessor:
spiff_task._set_state(TaskState.COMPLETED)
for child in spiff_task.children:
child.task_spec._update(child)
self.bpmn_process_instance.last_task = spiff_task
spiff_task.workflow.last_task = spiff_task
if isinstance(spiff_task.task_spec, EndEvent):
for task in self.bpmn_process_instance.get_tasks(
TaskState.DEFINITE_MASK, workflow=spiff_task.workflow
):
task.complete()
# A subworkflow task will become ready when its workflow is complete. Engine steps would normally
# then complete it, but we have to do it ourselves here.
for task in self.bpmn_process_instance.get_tasks(TaskState.READY):
if isinstance(task.task_spec, SubWorkflowTask):
task.complete()
self.increment_spiff_step()
self.add_step()
self.save()

View File

@ -6,7 +6,8 @@ from typing import List
from typing import Optional
from flask_bpmn.models.db import db
from SpiffWorkflow.bpmn.parser.ValidationException import ValidationException # type: ignore
from lxml import etree # type: ignore
from SpiffWorkflow.bpmn.parser.BpmnParser import BpmnValidator # type: ignore
from spiffworkflow_backend.models.file import File
from spiffworkflow_backend.models.file import FileType
@ -29,6 +30,10 @@ class ProcessModelFileNotFoundError(Exception):
"""ProcessModelFileNotFoundError."""
class ProcessModelFileInvalidError(Exception):
"""ProcessModelFileInvalidError."""
class SpecFileService(FileSystemService):
"""SpecFileService."""
@ -44,7 +49,6 @@ class SpecFileService(FileSystemService):
extension_filter: str = "",
) -> List[File]:
"""Return all files associated with a workflow specification."""
# path = SpecFileService.workflow_path(process_model_info)
path = os.path.join(
FileSystemService.root_path(), process_model_info.id_for_file_path()
)
@ -76,9 +80,22 @@ class SpecFileService(FileSystemService):
)
return references
@staticmethod
@classmethod
def get_references_for_file(
file: File, process_model_info: ProcessModelInfo
cls, file: File, process_model_info: ProcessModelInfo
) -> list[SpecReference]:
"""Get_references_for_file."""
full_file_path = SpecFileService.full_file_path(process_model_info, file.name)
file_contents: bytes = b""
with open(full_file_path) as f:
file_contents = f.read().encode()
return cls.get_references_for_file_contents(
process_model_info, file.name, file_contents
)
@classmethod
def get_references_for_file_contents(
cls, process_model_info: ProcessModelInfo, file_name: str, binary_data: bytes
) -> list[SpecReference]:
"""Uses spiffworkflow to parse BPMN and DMN files to determine how they can be externally referenced.
@ -89,8 +106,8 @@ class SpecFileService(FileSystemService):
type = {str} 'process' / 'decision'
"""
references: list[SpecReference] = []
full_file_path = SpecFileService.full_file_path(process_model_info, file.name)
file_path = os.path.join(process_model_info.id_for_file_path(), file.name)
file_path = os.path.join(process_model_info.id_for_file_path(), file_name)
file_type = FileSystemService.file_type(file_name)
parser = MyCustomParser()
parser_type = None
sub_parser = None
@ -100,14 +117,14 @@ class SpecFileService(FileSystemService):
messages = {}
correlations = {}
start_messages = []
if file.type == FileType.bpmn.value:
parser.add_bpmn_file(full_file_path)
if file_type.value == FileType.bpmn.value:
parser.add_bpmn_xml(etree.fromstring(binary_data))
parser_type = "process"
sub_parsers = list(parser.process_parsers.values())
messages = parser.messages
correlations = parser.correlations
elif file.type == FileType.dmn.value:
parser.add_dmn_file(full_file_path)
elif file_type.value == FileType.dmn.value:
parser.add_dmn_xml(etree.fromstring(binary_data))
sub_parsers = list(parser.dmn_parsers.values())
parser_type = "decision"
else:
@ -127,7 +144,7 @@ class SpecFileService(FileSystemService):
display_name=sub_parser.get_name(),
process_model_id=process_model_info.id,
type=parser_type,
file_name=file.name,
file_name=file_name,
relative_path=file_path,
has_lanes=has_lanes,
is_executable=is_executable,
@ -147,23 +164,36 @@ class SpecFileService(FileSystemService):
# Same as update
return SpecFileService.update_file(process_model_info, file_name, binary_data)
@staticmethod
@classmethod
def validate_bpmn_xml(cls, file_name: str, binary_data: bytes) -> None:
"""Validate_bpmn_xml."""
file_type = FileSystemService.file_type(file_name)
if file_type.value == FileType.bpmn.value:
validator = BpmnValidator()
parser = MyCustomParser(validator=validator)
try:
parser.add_bpmn_xml(etree.fromstring(binary_data), filename=file_name)
except etree.XMLSyntaxError as exception:
raise ProcessModelFileInvalidError(
f"Received error trying to parse bpmn xml: {str(exception)}"
) from exception
@classmethod
def update_file(
process_model_info: ProcessModelInfo, file_name: str, binary_data: bytes
cls, process_model_info: ProcessModelInfo, file_name: str, binary_data: bytes
) -> File:
"""Update_file."""
SpecFileService.assert_valid_file_name(file_name)
full_file_path = SpecFileService.full_file_path(process_model_info, file_name)
SpecFileService.write_file_data_to_system(full_file_path, binary_data)
file = SpecFileService.to_file_object(file_name, full_file_path)
cls.validate_bpmn_xml(file_name, binary_data)
references = SpecFileService.get_references_for_file(file, process_model_info)
references = cls.get_references_for_file_contents(
process_model_info, file_name, binary_data
)
primary_process_ref = next(
(ref for ref in references if ref.is_primary and ref.is_executable), None
)
SpecFileService.clear_caches_for_file(file_name, process_model_info)
for ref in references:
# If no valid primary process is defined, default to the first process in the
# updated file.
@ -184,7 +214,11 @@ class SpecFileService(FileSystemService):
update_hash,
)
SpecFileService.update_caches(ref)
return file
# make sure we save the file as the last thing we do to ensure validations have run
full_file_path = SpecFileService.full_file_path(process_model_info, file_name)
SpecFileService.write_file_data_to_system(full_file_path, binary_data)
return SpecFileService.to_file_object(file_name, full_file_path)
@staticmethod
def get_data(process_model_info: ProcessModelInfo, file_name: str) -> bytes:
@ -282,7 +316,7 @@ class SpecFileService(FileSystemService):
# if the old relative bpmn file no longer exists, then assume things were moved around
# on the file system. Otherwise, assume it is a duplicate process id and error.
if os.path.isfile(full_bpmn_file_path):
raise ValidationException(
raise ProcessModelFileInvalidError(
f"Process id ({ref.identifier}) has already been used for "
f"{process_id_lookup.relative_path}. It cannot be reused."
)
@ -314,7 +348,7 @@ class SpecFileService(FileSystemService):
identifier=message_model_identifier
).first()
if message_model is None:
raise ValidationException(
raise ProcessModelFileInvalidError(
"Could not find message model with identifier"
f" '{message_model_identifier}'Required by a Start Event in :"
f" {ref.file_name}"
@ -336,7 +370,7 @@ class SpecFileService(FileSystemService):
message_triggerable_process_model.process_model_identifier
!= ref.process_model_id
):
raise ValidationException(
raise ProcessModelFileInvalidError(
"Message model is already used to start process model"
f" {ref.process_model_id}"
)
@ -355,7 +389,7 @@ class SpecFileService(FileSystemService):
identifier=message_model_identifier
).first()
if message_model is None:
raise ValidationException(
raise ProcessModelFileInvalidError(
"Could not find message model with identifier"
f" '{message_model_identifier}'specified by correlation"
f" property: {cpre}"

View File

@ -2,10 +2,10 @@
<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="3.0.0-dev">
<bpmn:correlationProperty id="message_correlation_property" name="Message Correlation Property">
<bpmn:correlationPropertyRetrievalExpression messageRef="message_send">
<bpmn:formalExpression>to</bpmn:formalExpression>
<bpmn:messagePath>to</bpmn:messagePath>
</bpmn:correlationPropertyRetrievalExpression>
<bpmn:correlationPropertyRetrievalExpression messageRef="message_response">
<bpmn:formalExpression>from.name</bpmn:formalExpression>
<bpmn:messagePath>from.name</bpmn:messagePath>
</bpmn:correlationPropertyRetrievalExpression>
</bpmn:correlationProperty>
<bpmn:message id="message_send" name="Message Send">
@ -20,7 +20,7 @@
</bpmn:message>
<bpmn:correlationProperty id="correlation_property_one" name="Correlation Property One">
<bpmn:correlationPropertyRetrievalExpression messageRef="message_send">
<bpmn:formalExpression>new</bpmn:formalExpression>
<bpmn:messagePath>new</bpmn:messagePath>
</bpmn:correlationPropertyRetrievalExpression>
</bpmn:correlationProperty>
<bpmn:process id="test_dot_notation" name="Test Dot Notation" isExecutable="true">

View File

@ -12,18 +12,18 @@
</bpmn:collaboration>
<bpmn:correlationProperty id="message_correlation_property_topica" name="Message Correlation Property TopicA">
<bpmn:correlationPropertyRetrievalExpression messageRef="message_send">
<bpmn:formalExpression>topica</bpmn:formalExpression>
<bpmn:messagePath>topica</bpmn:messagePath>
</bpmn:correlationPropertyRetrievalExpression>
<bpmn:correlationPropertyRetrievalExpression messageRef="message_response">
<bpmn:formalExpression>the_payload.topica</bpmn:formalExpression>
<bpmn:messagePath>the_payload.topica</bpmn:messagePath>
</bpmn:correlationPropertyRetrievalExpression>
</bpmn:correlationProperty>
<bpmn:correlationProperty id="message_correlation_property_topicb" name="Message Correlation Property TopicB">
<bpmn:correlationPropertyRetrievalExpression messageRef="message_send">
<bpmn:formalExpression>topicb</bpmn:formalExpression>
<bpmn:messagePath>topicb</bpmn:messagePath>
</bpmn:correlationPropertyRetrievalExpression>
<bpmn:correlationPropertyRetrievalExpression messageRef="message_response">
<bpmn:formalExpression>the_payload.topicb</bpmn:formalExpression>
<bpmn:messagePath>the_payload.topicb</bpmn:messagePath>
</bpmn:correlationPropertyRetrievalExpression>
</bpmn:correlationProperty>
<bpmn:message id="message_send" name="Message Send">

View File

@ -12,18 +12,18 @@
</bpmn:collaboration>
<bpmn:correlationProperty id="message_correlation_property_topica" name="Message Correlation Property TopicA">
<bpmn:correlationPropertyRetrievalExpression messageRef="message_send">
<bpmn:formalExpression>topica</bpmn:formalExpression>
<bpmn:messagePath>topica</bpmn:messagePath>
</bpmn:correlationPropertyRetrievalExpression>
<bpmn:correlationPropertyRetrievalExpression messageRef="message_response">
<bpmn:formalExpression>the_payload.topica</bpmn:formalExpression>
<bpmn:messagePath>the_payload.topica</bpmn:messagePath>
</bpmn:correlationPropertyRetrievalExpression>
</bpmn:correlationProperty>
<bpmn:correlationProperty id="message_correlation_property_topicb" name="Message Correlation Property TopicB">
<bpmn:correlationPropertyRetrievalExpression messageRef="message_send">
<bpmn:formalExpression>topicb</bpmn:formalExpression>
<bpmn:messagePath>topicb</bpmn:messagePath>
</bpmn:correlationPropertyRetrievalExpression>
<bpmn:correlationPropertyRetrievalExpression messageRef="message_response">
<bpmn:formalExpression>the_payload.topicb</bpmn:formalExpression>
<bpmn:messagePath>the_payload.topicb</bpmn:messagePath>
</bpmn:correlationPropertyRetrievalExpression>
</bpmn:correlationProperty>
<bpmn:process id="message_send_process" name="Message Send Process" isExecutable="true">

View File

@ -12,18 +12,18 @@
</bpmn:collaboration>
<bpmn:correlationProperty id="mcp_topica_one" name="MCP TopicA One">
<bpmn:correlationPropertyRetrievalExpression messageRef="message_send_one">
<bpmn:formalExpression>topica_one</bpmn:formalExpression>
<bpmn:messagePath>topica_one</bpmn:messagePath>
</bpmn:correlationPropertyRetrievalExpression>
<bpmn:correlationPropertyRetrievalExpression messageRef="message_response_one">
<bpmn:formalExpression>topica_one</bpmn:formalExpression>
<bpmn:messagePath>topica_one</bpmn:messagePath>
</bpmn:correlationPropertyRetrievalExpression>
</bpmn:correlationProperty>
<bpmn:correlationProperty id="mcp_topicb_one" name="MCP TopicB_one">
<bpmn:correlationPropertyRetrievalExpression messageRef="message_send_one">
<bpmn:formalExpression>topicb_one</bpmn:formalExpression>
<bpmn:messagePath>topicb_one</bpmn:messagePath>
</bpmn:correlationPropertyRetrievalExpression>
<bpmn:correlationPropertyRetrievalExpression messageRef="message_response_one">
<bpmn:formalExpression>topicb_one</bpmn:formalExpression>
<bpmn:messagePath>topicb_one</bpmn:messagePath>
</bpmn:correlationPropertyRetrievalExpression>
</bpmn:correlationProperty>
<bpmn:message id="message_send_one" name="Message Send One">

View File

@ -12,18 +12,18 @@
</bpmn:collaboration>
<bpmn:correlationProperty id="mcp_topica_two" name="MCP TopicA Two">
<bpmn:correlationPropertyRetrievalExpression messageRef="message_send_two">
<bpmn:formalExpression>topica_two</bpmn:formalExpression>
<bpmn:messagePath>topica_two</bpmn:messagePath>
</bpmn:correlationPropertyRetrievalExpression>
<bpmn:correlationPropertyRetrievalExpression messageRef="message_response_two">
<bpmn:formalExpression>topica_two</bpmn:formalExpression>
<bpmn:messagePath>topica_two</bpmn:messagePath>
</bpmn:correlationPropertyRetrievalExpression>
</bpmn:correlationProperty>
<bpmn:correlationProperty id="mcp_topicb_two" name="MCP TopicB_two">
<bpmn:correlationPropertyRetrievalExpression messageRef="message_send_two">
<bpmn:formalExpression>topicb_two</bpmn:formalExpression>
<bpmn:messagePath>topicb_two</bpmn:messagePath>
</bpmn:correlationPropertyRetrievalExpression>
<bpmn:correlationPropertyRetrievalExpression messageRef="message_response_two">
<bpmn:formalExpression>topicb_two</bpmn:formalExpression>
<bpmn:messagePath>topicb_two</bpmn:messagePath>
</bpmn:correlationPropertyRetrievalExpression>
</bpmn:correlationProperty>
<bpmn:message id="message_send_two" name="Message Send Two">

View File

@ -1,6 +1,6 @@
<?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="3.0.0-dev">
<bpmn:collaboration id="Collaboration_0oye1os" messages="[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object]">
<bpmn:collaboration id="Collaboration_0oye1os">
<bpmn:participant id="message_initiator" name="Message Initiator" processRef="message_send_process" />
<bpmn:participant id="message-receiver-one" name="Message Receiver One" />
<bpmn:participant id="message-receiver-two" name="Message Receiver Two" />
@ -19,18 +19,18 @@
</bpmn:collaboration>
<bpmn:correlationProperty id="mcp_topica_one" name="MCP TopicA One">
<bpmn:correlationPropertyRetrievalExpression messageRef="message_send_one">
<bpmn:formalExpression>topica_one</bpmn:formalExpression>
<bpmn:messagePath>topica_one</bpmn:messagePath>
</bpmn:correlationPropertyRetrievalExpression>
<bpmn:correlationPropertyRetrievalExpression messageRef="message_response_one">
<bpmn:formalExpression>payload_var_one.topica_one</bpmn:formalExpression>
<bpmn:messagePath>payload_var_one.topica_one</bpmn:messagePath>
</bpmn:correlationPropertyRetrievalExpression>
</bpmn:correlationProperty>
<bpmn:correlationProperty id="mcp_topicb_one" name="MCP TopicB_one">
<bpmn:correlationPropertyRetrievalExpression messageRef="message_send_one">
<bpmn:formalExpression>topicb_one</bpmn:formalExpression>
<bpmn:messagePath>topicb_one</bpmn:messagePath>
</bpmn:correlationPropertyRetrievalExpression>
<bpmn:correlationPropertyRetrievalExpression messageRef="message_response_one">
<bpmn:formalExpression>payload_var_one.topicb</bpmn:formalExpression>
<bpmn:messagePath>payload_var_one.topicb</bpmn:messagePath>
</bpmn:correlationPropertyRetrievalExpression>
</bpmn:correlationProperty>
<bpmn:process id="message_send_process" name="Message Send Process" isExecutable="true">
@ -117,18 +117,18 @@ del time</bpmn:script>
</bpmn:message>
<bpmn:correlationProperty id="mcp_topica_two" name="MCP Topica Two">
<bpmn:correlationPropertyRetrievalExpression messageRef="message_send_two">
<bpmn:formalExpression>topica_two</bpmn:formalExpression>
<bpmn:messagePath>topica_two</bpmn:messagePath>
</bpmn:correlationPropertyRetrievalExpression>
<bpmn:correlationPropertyRetrievalExpression messageRef="message_response_two">
<bpmn:formalExpression>topica_two</bpmn:formalExpression>
<bpmn:messagePath>topica_two</bpmn:messagePath>
</bpmn:correlationPropertyRetrievalExpression>
</bpmn:correlationProperty>
<bpmn:correlationProperty id="mcp_topicb_two" name="MCP Topicb Two">
<bpmn:correlationPropertyRetrievalExpression messageRef="message_send_two">
<bpmn:formalExpression>topicb_two</bpmn:formalExpression>
<bpmn:messagePath>topicb_two</bpmn:messagePath>
</bpmn:correlationPropertyRetrievalExpression>
<bpmn:correlationPropertyRetrievalExpression messageRef="message_response_two">
<bpmn:formalExpression>topicb_two</bpmn:formalExpression>
<bpmn:messagePath>topicb_two</bpmn:messagePath>
</bpmn:correlationPropertyRetrievalExpression>
</bpmn:correlationProperty>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">

View File

@ -860,7 +860,7 @@ class TestProcessApi(BaseTest):
assert response.status_code == 200
assert response.json is not None
assert response.json["ok"]
assert response.json["file_contents"] is not None
response = client.get(
f"/v1.0/process-models/{modified_process_model_id}/files/random_fact.svg",

View File

@ -5,13 +5,15 @@ import pytest
from flask import Flask
from flask.testing import FlaskClient
from flask_bpmn.models.db import db
from SpiffWorkflow.bpmn.parser.ValidationException import ValidationException # type: ignore
from tests.spiffworkflow_backend.helpers.base_test import BaseTest
from tests.spiffworkflow_backend.helpers.test_data import load_test_spec
from spiffworkflow_backend.models.spec_reference import SpecReferenceCache
from spiffworkflow_backend.models.user import UserModel
from spiffworkflow_backend.services.process_model_service import ProcessModelService
from spiffworkflow_backend.services.spec_file_service import (
ProcessModelFileInvalidError,
)
from spiffworkflow_backend.services.spec_file_service import SpecFileService
@ -74,7 +76,7 @@ class TestSpecFileService(BaseTest):
bpmn_process_id_lookups[0].relative_path
== self.call_activity_nested_relative_file_path
)
with pytest.raises(ValidationException) as exception:
with pytest.raises(ProcessModelFileInvalidError) as exception:
load_test_spec(
"call_activity_nested_duplicate",
process_model_source_directory="call_activity_duplicate",
@ -85,6 +87,14 @@ class TestSpecFileService(BaseTest):
in str(exception.value)
)
process_model = ProcessModelService.get_process_model(
"call_activity_nested_duplicate"
)
full_file_path = SpecFileService.full_file_path(
process_model, "call_activity_nested_duplicate.bpmn"
)
assert not os.path.isfile(full_file_path)
def test_updates_relative_file_path_when_appropriate(
self,
app: Flask,
@ -206,3 +216,23 @@ class TestSpecFileService(BaseTest):
assert dmn1[0].display_name == "Decision 1"
assert dmn1[0].identifier == "Decision_0vrtcmk"
assert dmn1[0].type == "decision"
def test_validate_bpmn_xml_with_invalid_xml(
self,
app: Flask,
client: FlaskClient,
with_db_and_bpmn_file_cleanup: None,
) -> None:
"""Test_validate_bpmn_xml_with_invalid_xml."""
process_model = load_test_spec(
process_model_id="group/invalid_xml",
bpmn_file_name="script_error_with_task_data.bpmn",
process_model_source_directory="error",
)
with pytest.raises(ProcessModelFileInvalidError):
SpecFileService.update_file(
process_model, "bad_xml.bpmn", b"THIS_IS_NOT_VALID_XML"
)
full_file_path = SpecFileService.full_file_path(process_model, "bad_xml.bpmn")
assert not os.path.isfile(full_file_path)

View File

@ -23,7 +23,7 @@
"@rjsf/core": "*",
"@rjsf/mui": "^5.0.0-beta.13",
"@rjsf/utils": "^5.0.0-beta.13",
"@rjsf/validator-ajv6": "^5.0.0-beta.13",
"@rjsf/validator-ajv8": "^5.0.0-beta.16",
"@tanstack/react-table": "^8.2.2",
"@testing-library/jest-dom": "^5.16.4",
"@testing-library/react": "^13.3.0",
@ -4863,9 +4863,9 @@
}
},
"node_modules/@rjsf/core": {
"version": "5.0.0-beta.13",
"resolved": "https://registry.npmjs.org/@rjsf/core/-/core-5.0.0-beta.13.tgz",
"integrity": "sha512-uQ3A9aJhMJsz9ct5tV3ogZkSFEkKUxrM9SJ9Hc8ijxmuaW7Jv8tNv5jiWZZsLvNXlIONX83s6JqkiOJf6IOAvg==",
"version": "5.0.0-beta.16",
"resolved": "https://registry.npmjs.org/@rjsf/core/-/core-5.0.0-beta.16.tgz",
"integrity": "sha512-TqOd3CKptWAswX9PU8pLSoAe5zI03J6Kk/aWAFbMj+xW/6hR5PXHbs5X5kxwpQx7IVXiJZZZpP5n1oDsu4GwNg==",
"dependencies": {
"lodash": "^4.17.15",
"lodash-es": "^4.17.15",
@ -4881,9 +4881,9 @@
}
},
"node_modules/@rjsf/mui": {
"version": "5.0.0-beta.13",
"resolved": "https://registry.npmjs.org/@rjsf/mui/-/mui-5.0.0-beta.13.tgz",
"integrity": "sha512-hwCtADpjNssq/CsT3Wj1FDVJfdCN3gptKedGjbusLUEwQqXoVzkzl25e/IRfN8y/JxYu4lMXDU89bN9nJSKWLA==",
"version": "5.0.0-beta.16",
"resolved": "https://registry.npmjs.org/@rjsf/mui/-/mui-5.0.0-beta.16.tgz",
"integrity": "sha512-QskaSc2Zcwqz+nKoACstvn5LhrAx4EmicYc/6kNoj3jKH6MlfVCA7FYumu5g6TIqMrDEvZuZqBKtjL64Tv52PQ==",
"engines": {
"node": ">=14"
},
@ -4898,9 +4898,9 @@
}
},
"node_modules/@rjsf/utils": {
"version": "5.0.0-beta.13",
"resolved": "https://registry.npmjs.org/@rjsf/utils/-/utils-5.0.0-beta.13.tgz",
"integrity": "sha512-hWWWFD2ifjSOhqWueML4OHrZe2HW5pE2nfKGhCObFbwtggHoQlj64xDBsJ1qfUG8DGvCHztJQ/sKIaOvXnpt7w==",
"version": "5.0.0-beta.16",
"resolved": "https://registry.npmjs.org/@rjsf/utils/-/utils-5.0.0-beta.16.tgz",
"integrity": "sha512-dNQ620Q6a9cB28sjjRgJkxIuD9TFd03sNMlcZVdZOuZC6wjfGc4rKG0Lc7+xgLFvSPFKwXJprzfKSM3yuy9jXg==",
"dependencies": {
"json-schema-merge-allof": "^0.8.1",
"jsonpointer": "^5.0.1",
@ -4933,12 +4933,13 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz",
"integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w=="
},
"node_modules/@rjsf/validator-ajv6": {
"version": "5.0.0-beta.13",
"resolved": "https://registry.npmjs.org/@rjsf/validator-ajv6/-/validator-ajv6-5.0.0-beta.13.tgz",
"integrity": "sha512-X9N3/HJYV23MjUN/VJHIdBhUdBuMTUsh4HAZm50eUvUAhWK95wIqjjhAs24rzeLajrjFeH7kFr89zAqDgIFhVQ==",
"node_modules/@rjsf/validator-ajv8": {
"version": "5.0.0-beta.16",
"resolved": "https://registry.npmjs.org/@rjsf/validator-ajv8/-/validator-ajv8-5.0.0-beta.16.tgz",
"integrity": "sha512-VrQzR9HEH/1BF2TW/lRJuV+kILzR4geS+iW5Th1OlPeNp1NNWZuSO1kCU9O0JA17t2WHOEl/SFZXZBnN1/zwzQ==",
"dependencies": {
"ajv": "^6.7.0",
"ajv-formats": "^2.1.1",
"ajv8": "npm:ajv@^8.11.0",
"lodash": "^4.17.15",
"lodash-es": "^4.17.15"
},
@ -4946,7 +4947,7 @@
"node": ">=14"
},
"peerDependencies": {
"@rjsf/utils": "^5.0.0-beta.1"
"@rjsf/utils": "^5.0.0-beta.12"
}
},
"node_modules/@rollup/plugin-babel": {
@ -6822,6 +6823,27 @@
"ajv": "^6.9.1"
}
},
"node_modules/ajv8": {
"name": "ajv",
"version": "8.12.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz",
"integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==",
"dependencies": {
"fast-deep-equal": "^3.1.1",
"json-schema-traverse": "^1.0.0",
"require-from-string": "^2.0.2",
"uri-js": "^4.2.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/ajv8/node_modules/json-schema-traverse": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="
},
"node_modules/ansi-align": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz",
@ -34767,9 +34789,9 @@
}
},
"@rjsf/core": {
"version": "5.0.0-beta.13",
"resolved": "https://registry.npmjs.org/@rjsf/core/-/core-5.0.0-beta.13.tgz",
"integrity": "sha512-uQ3A9aJhMJsz9ct5tV3ogZkSFEkKUxrM9SJ9Hc8ijxmuaW7Jv8tNv5jiWZZsLvNXlIONX83s6JqkiOJf6IOAvg==",
"version": "5.0.0-beta.16",
"resolved": "https://registry.npmjs.org/@rjsf/core/-/core-5.0.0-beta.16.tgz",
"integrity": "sha512-TqOd3CKptWAswX9PU8pLSoAe5zI03J6Kk/aWAFbMj+xW/6hR5PXHbs5X5kxwpQx7IVXiJZZZpP5n1oDsu4GwNg==",
"requires": {
"lodash": "^4.17.15",
"lodash-es": "^4.17.15",
@ -34778,15 +34800,15 @@
}
},
"@rjsf/mui": {
"version": "5.0.0-beta.13",
"resolved": "https://registry.npmjs.org/@rjsf/mui/-/mui-5.0.0-beta.13.tgz",
"integrity": "sha512-hwCtADpjNssq/CsT3Wj1FDVJfdCN3gptKedGjbusLUEwQqXoVzkzl25e/IRfN8y/JxYu4lMXDU89bN9nJSKWLA==",
"version": "5.0.0-beta.16",
"resolved": "https://registry.npmjs.org/@rjsf/mui/-/mui-5.0.0-beta.16.tgz",
"integrity": "sha512-QskaSc2Zcwqz+nKoACstvn5LhrAx4EmicYc/6kNoj3jKH6MlfVCA7FYumu5g6TIqMrDEvZuZqBKtjL64Tv52PQ==",
"requires": {}
},
"@rjsf/utils": {
"version": "5.0.0-beta.13",
"resolved": "https://registry.npmjs.org/@rjsf/utils/-/utils-5.0.0-beta.13.tgz",
"integrity": "sha512-hWWWFD2ifjSOhqWueML4OHrZe2HW5pE2nfKGhCObFbwtggHoQlj64xDBsJ1qfUG8DGvCHztJQ/sKIaOvXnpt7w==",
"version": "5.0.0-beta.16",
"resolved": "https://registry.npmjs.org/@rjsf/utils/-/utils-5.0.0-beta.16.tgz",
"integrity": "sha512-dNQ620Q6a9cB28sjjRgJkxIuD9TFd03sNMlcZVdZOuZC6wjfGc4rKG0Lc7+xgLFvSPFKwXJprzfKSM3yuy9jXg==",
"requires": {
"json-schema-merge-allof": "^0.8.1",
"jsonpointer": "^5.0.1",
@ -34812,12 +34834,13 @@
}
}
},
"@rjsf/validator-ajv6": {
"version": "5.0.0-beta.13",
"resolved": "https://registry.npmjs.org/@rjsf/validator-ajv6/-/validator-ajv6-5.0.0-beta.13.tgz",
"integrity": "sha512-X9N3/HJYV23MjUN/VJHIdBhUdBuMTUsh4HAZm50eUvUAhWK95wIqjjhAs24rzeLajrjFeH7kFr89zAqDgIFhVQ==",
"@rjsf/validator-ajv8": {
"version": "5.0.0-beta.16",
"resolved": "https://registry.npmjs.org/@rjsf/validator-ajv8/-/validator-ajv8-5.0.0-beta.16.tgz",
"integrity": "sha512-VrQzR9HEH/1BF2TW/lRJuV+kILzR4geS+iW5Th1OlPeNp1NNWZuSO1kCU9O0JA17t2WHOEl/SFZXZBnN1/zwzQ==",
"requires": {
"ajv": "^6.7.0",
"ajv-formats": "^2.1.1",
"ajv8": "npm:ajv@^8.11.0",
"lodash": "^4.17.15",
"lodash-es": "^4.17.15"
}
@ -36295,6 +36318,24 @@
"integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==",
"requires": {}
},
"ajv8": {
"version": "npm:ajv@8.12.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz",
"integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==",
"requires": {
"fast-deep-equal": "^3.1.1",
"json-schema-traverse": "^1.0.0",
"require-from-string": "^2.0.2",
"uri-js": "^4.2.2"
},
"dependencies": {
"json-schema-traverse": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="
}
}
},
"ansi-align": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz",

View File

@ -18,7 +18,7 @@
"@rjsf/core": "*",
"@rjsf/mui": "^5.0.0-beta.13",
"@rjsf/utils": "^5.0.0-beta.13",
"@rjsf/validator-ajv6": "^5.0.0-beta.13",
"@rjsf/validator-ajv8": "^5.0.0-beta.16",
"@tanstack/react-table": "^8.2.2",
"@testing-library/jest-dom": "^5.16.4",
"@testing-library/react": "^13.3.0",

View File

@ -37,7 +37,9 @@ export default function ProcessModelSearch({
const shouldFilterProcessModel = (options: any) => {
const processModel: ProcessModel = options.item;
const { inputValue } = options;
return getFullProcessModelLabel(processModel).includes(inputValue);
return getFullProcessModelLabel(processModel)
.toLowerCase()
.includes((inputValue || '').toLowerCase());
};
return (
<ComboBox

View File

@ -528,11 +528,24 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
}
};
const isCurrentTask = (task: any) => {
const subprocessTypes = [
'Subprocess',
'Call Activity',
'Transactional Subprocess',
];
return (
(task.state === 'WAITING' &&
subprocessTypes.filter((t) => t === task.type).length > 0) ||
task.state === 'READY'
);
};
const canEditTaskData = (task: any) => {
return (
processInstance &&
ability.can('PUT', targetUris.processInstanceTaskListDataPath) &&
task.state === 'READY' &&
isCurrentTask(task) &&
processInstance.status === 'suspended' &&
showingLastSpiffStep()
);
@ -556,7 +569,7 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
processInstance &&
processInstance.status === 'suspended' &&
ability.can('POST', targetUris.processInstanceCompleteTaskPath) &&
task.state === 'READY' &&
isCurrentTask(task) &&
showingLastSpiffStep()
);
};

View File

@ -134,7 +134,7 @@ export default function ProcessModelEditDiagram() {
setProcessModel(result);
};
HttpService.makeCallToBackend({
path: `/${processModelPath}`,
path: `/${processModelPath}?include_file_references=true`,
successCallback: processResult,
});
}, [processModelPath]);
@ -966,7 +966,6 @@ export default function ProcessModelEditDiagram() {
{scriptEditorAndTests()}
{markdownEditor()}
{processModelSelector()}
{`Processes length: ${processes.length}`}
<div id="diagram-container" />
</>
);

View File

@ -374,6 +374,7 @@ export default function ProcessModelShow() {
const doFileUpload = (event: any) => {
event.preventDefault();
setErrorObject(null);
const url = `/process-models/${modifiedProcessModelId}/files`;
const formData = new FormData();
formData.append('file', filesToUpload[0]);
@ -383,6 +384,7 @@ export default function ProcessModelShow() {
successCallback: onUploadedCallback,
httpMethod: 'POST',
postBody: formData,
failureCallback: setErrorObject,
});
setFilesToUpload(null);
};

View File

@ -1,4 +1,4 @@
import { useEffect, useState } from 'react';
import { useContext, useEffect, useState } from 'react';
import Editor from '@monaco-editor/react';
import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
// @ts-ignore
@ -8,18 +8,25 @@ 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';
// 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 [showFileNameEditor, setShowFileNameEditor] = useState(false);
const [newFileName, setNewFileName] = useState('');
const searchParams = useSearchParams()[0];
const handleShowFileNameEditor = () => setShowFileNameEditor(true);
const navigate = useNavigate();
const [displaySaveFileMessage, setDisplaySaveFileMessage] =
useState<boolean>(false);
const [processModelFile, setProcessModelFile] = useState<ProcessFile | null>(
null
);
@ -70,6 +77,7 @@ export default function ReactFormEditor() {
}, [params, modifiedProcessModelId]);
const navigateToProcessModelFile = (_result: any) => {
setDisplaySaveFileMessage(true);
if (!params.file_name) {
const fileNameWithExtension = `${newFileName}.${fileExtension}`;
navigate(
@ -79,6 +87,9 @@ export default function ReactFormEditor() {
};
const saveFile = () => {
setErrorObject(null);
setDisplaySaveFileMessage(false);
let url = `/process-models/${modifiedProcessModelId}/files`;
let httpMethod = 'PUT';
let fileNameWithExtension = params.file_name;
@ -105,6 +116,7 @@ export default function ReactFormEditor() {
HttpService.makeCallToBackend({
path: url,
successCallback: navigateToProcessModelFile,
failureCallback: setErrorObject,
httpMethod,
postBody: formData,
});
@ -162,6 +174,20 @@ export default function ReactFormEditor() {
);
};
const saveFileMessage = () => {
if (displaySaveFileMessage) {
return (
<Notification
title="File Saved: "
onClose={() => setDisplaySaveFileMessage(false)}
>
Changes to the file were saved.
</Notification>
);
}
return null;
};
if (processModelFile || !params.file_name) {
const processModelFileName = processModelFile ? processModelFile.name : '';
return (
@ -182,6 +208,7 @@ export default function ReactFormEditor() {
{processModelFileName}
</h1>
{newFileNameBox()}
{saveFileMessage()}
<Button onClick={saveFile} variant="danger" data-qa="file-save-button">
Save
</Button>

View File

@ -1,13 +1,6 @@
import { useContext, useEffect, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
// FIXME: npm install @rjsf/validator-ajv8 and use it as soon as
// rawErrors is fixed.
// https://react-jsonschema-form.readthedocs.io/en/latest/usage/validation/
// https://github.com/rjsf-team/react-jsonschema-form/issues/2309 links to a codesandbox that might be useful to fork
// if we wanted to file a defect against rjsf to show the difference between validator-ajv6 and validator-ajv8.
// https://github.com/rjsf-team/react-jsonschema-form/blob/main/docs/api-reference/uiSchema.md talks about rawErrors
import validator from '@rjsf/validator-ajv6';
import validator from '@rjsf/validator-ajv8';
import {
TabList,

View File

@ -1,12 +1,23 @@
import React, { CSSProperties } from 'react';
import Box from '@mui/material/Box';
import Grid from '@mui/material/Grid';
import Paper from '@mui/material/Paper';
import { ArrayFieldTemplateItemType } from '@rjsf/utils';
import {
ArrayFieldTemplateItemType,
FormContextType,
RJSFSchema,
StrictRJSFSchema,
} from '@rjsf/utils';
function ArrayFieldItemTemplate(props: ArrayFieldTemplateItemType) {
/** The `ArrayFieldItemTemplate` component is the template used to render an items of an array.
*
* @param props - The `ArrayFieldTemplateItemType` props for the component
*/
export default function ArrayFieldItemTemplate<
T = any,
S extends StrictRJSFSchema = RJSFSchema,
F extends FormContextType = any
>(props: ArrayFieldTemplateItemType<T, S, F>) {
const {
children,
className,
disabled,
hasToolbar,
hasMoveDown,
@ -16,8 +27,8 @@ function ArrayFieldItemTemplate(props: ArrayFieldTemplateItemType) {
onDropIndexClick,
onReorderClick,
readonly,
uiSchema,
registry,
uiSchema,
} = props;
const { MoveDownButton, MoveUpButton, RemoveButton } =
registry.templates.ButtonTemplates;
@ -26,25 +37,26 @@ function ArrayFieldItemTemplate(props: ArrayFieldTemplateItemType) {
paddingLeft: 6,
paddingRight: 6,
fontWeight: 'bold',
minWidth: 0,
};
return (
<Grid container alignItems="center">
<Grid item xs style={{ overflow: 'auto' }}>
<Box mb={2}>
<Paper elevation={2}>
<Box p={2}>{children}</Box>
</Paper>
</Box>
</Grid>
<div className={className}>
<div className={hasToolbar ? 'col-xs-9' : 'col-xs-12'}>{children}</div>
{hasToolbar && (
<Grid item>
<div className="col-xs-3 array-item-toolbox">
<div
className="btn-group"
style={{
display: 'flex',
justifyContent: 'space-around',
}}
>
{(hasMoveUp || hasMoveDown) && (
<MoveUpButton
style={btnStyle}
disabled={disabled || readonly || !hasMoveUp}
onClick={onReorderClick(index, index - 1)}
uiSchema={uiSchema}
registry={registry}
/>
)}
{(hasMoveUp || hasMoveDown) && (
@ -53,6 +65,7 @@ function ArrayFieldItemTemplate(props: ArrayFieldTemplateItemType) {
disabled={disabled || readonly || !hasMoveDown}
onClick={onReorderClick(index, index + 1)}
uiSchema={uiSchema}
registry={registry}
/>
)}
{hasRemove && (
@ -61,12 +74,12 @@ function ArrayFieldItemTemplate(props: ArrayFieldTemplateItemType) {
disabled={disabled || readonly}
onClick={onDropIndexClick(index)}
uiSchema={uiSchema}
registry={registry}
/>
)}
</Grid>
</div>
</div>
)}
</Grid>
</div>
);
}
export default ArrayFieldItemTemplate;

View File

@ -1,17 +1,26 @@
import React from 'react';
import Box from '@mui/material/Box';
import Grid from '@mui/material/Grid';
import Paper from '@mui/material/Paper';
import {
ArrayFieldTemplateItemType,
ArrayFieldTemplateProps,
getTemplate,
getUiOptions,
ArrayFieldTemplateProps,
ArrayFieldTemplateItemType,
FormContextType,
RJSFSchema,
StrictRJSFSchema,
} from '@rjsf/utils';
function ArrayFieldTemplate(props: ArrayFieldTemplateProps) {
/** The `ArrayFieldTemplate` component is the template used to render all items in an array.
*
* @param props - The `ArrayFieldTemplateItemType` props for the component
*/
export default function ArrayFieldTemplate<
T = any,
S extends StrictRJSFSchema = RJSFSchema,
F extends FormContextType = any
>(props: ArrayFieldTemplateProps<T, S, F>) {
const {
canAdd,
className,
disabled,
idSchema,
uiSchema,
@ -23,36 +32,36 @@ function ArrayFieldTemplate(props: ArrayFieldTemplateProps) {
schema,
title,
} = props;
const uiOptions = getUiOptions(uiSchema);
const ArrayFieldDescriptionTemplate =
getTemplate<'ArrayFieldDescriptionTemplate'>(
const uiOptions = getUiOptions<T, S, F>(uiSchema);
const ArrayFieldDescriptionTemplate = getTemplate<
'ArrayFieldDescriptionTemplate',
registry,
uiOptions
);
const ArrayFieldItemTemplate = getTemplate<'ArrayFieldItemTemplate'>(
T,
S,
F
>('ArrayFieldDescriptionTemplate', registry, uiOptions);
const ArrayFieldItemTemplate = getTemplate<'ArrayFieldItemTemplate', T, S, F>(
'ArrayFieldItemTemplate',
registry,
uiOptions
);
const ArrayFieldTitleTemplate = getTemplate<'ArrayFieldTitleTemplate'>(
const ArrayFieldTitleTemplate = getTemplate<
'ArrayFieldTitleTemplate',
registry,
uiOptions
);
T,
S,
F
>('ArrayFieldTitleTemplate', registry, uiOptions);
// Button templates are not overridden in the uiSchema
const {
ButtonTemplates: { AddButton },
} = registry.templates;
return (
<Paper elevation={2}>
<Box p={2}>
<fieldset className={className} id={idSchema.$id}>
<ArrayFieldTitleTemplate
idSchema={idSchema}
title={uiOptions.title || title}
required={required}
schema={schema}
uiSchema={uiSchema}
required={required}
registry={registry}
/>
<ArrayFieldDescriptionTemplate
@ -62,29 +71,23 @@ function ArrayFieldTemplate(props: ArrayFieldTemplateProps) {
uiSchema={uiSchema}
registry={registry}
/>
<Grid container key={`array-item-list-${idSchema.$id}`}>
<div className="row array-item-list">
{items &&
items.map(({ key, ...itemProps }: ArrayFieldTemplateItemType) => (
items.map(
({ key, ...itemProps }: ArrayFieldTemplateItemType<T, S, F>) => (
<ArrayFieldItemTemplate key={key} {...itemProps} />
))}
)
)}
</div>
{canAdd && (
<Grid container justifyContent="flex-end">
<Grid item>
<Box mt={2}>
<AddButton
className="array-item-add"
onClick={onAddClick}
disabled={disabled || readonly}
uiSchema={uiSchema}
registry={registry}
/>
</Box>
</Grid>
</Grid>
)}
</Grid>
</Box>
</Paper>
</fieldset>
);
}
export default ArrayFieldTemplate;

View File

@ -0,0 +1,29 @@
import React from "react";
const REQUIRED_FIELD_SYMBOL = "*";
export type LabelProps = {
/** The label for the field */
label?: string;
/** A boolean value stating if the field is required */
required?: boolean;
/** The id of the input field being labeled */
id?: string;
};
/** Renders a label for a field
*
* @param props - The `LabelProps` for this component
*/
export default function Label(props: LabelProps) {
const { label, required, id } = props;
if (!label) {
return null;
}
return (
<label className="control-label" htmlFor={id}>
{label}
{required && <span className="required">{REQUIRED_FIELD_SYMBOL}</span>}
</label>
);
}

View File

@ -1,89 +1,87 @@
import React from 'react';
import Grid from '@mui/material/Grid';
import {
FormContextType,
ObjectFieldTemplatePropertyType,
ObjectFieldTemplateProps,
RJSFSchema,
StrictRJSFSchema,
canExpand,
getTemplate,
getUiOptions,
} from '@rjsf/utils';
function ObjectFieldTemplate({
/** The `ObjectFieldTemplate` is the template to use to render all the inner properties of an object along with the
* title and description if available. If the object is expandable, then an `AddButton` is also rendered after all
* the properties.
*
* @param props - The `ObjectFieldTemplateProps` for this component
*/
export default function ObjectFieldTemplate<
T = any,
S extends StrictRJSFSchema = RJSFSchema,
F extends FormContextType = any
>(props: ObjectFieldTemplateProps<T, S, F>) {
const {
description,
title,
properties,
required,
disabled,
readonly,
uiSchema,
idSchema,
schema,
formData,
idSchema,
onAddClick,
properties,
readonly,
registry,
}: ObjectFieldTemplateProps) {
const uiOptions = getUiOptions(uiSchema);
const TitleFieldTemplate = getTemplate<'TitleFieldTemplate'>(
required,
schema,
title,
uiSchema,
} = props;
const options = getUiOptions<T, S, F>(uiSchema);
const TitleFieldTemplate = getTemplate<'TitleFieldTemplate', T, S, F>(
'TitleFieldTemplate',
registry,
uiOptions
options
);
const DescriptionFieldTemplate = getTemplate<'DescriptionFieldTemplate'>(
const DescriptionFieldTemplate = getTemplate<
'DescriptionFieldTemplate',
registry,
uiOptions
);
T,
S,
F
>('DescriptionFieldTemplate', registry, options);
// Button templates are not overridden in the uiSchema
const {
ButtonTemplates: { AddButton },
} = registry.templates;
return (
<>
{(uiOptions.title || title) && (
<fieldset id={idSchema.$id}>
{(options.title || title) && (
<TitleFieldTemplate
id={`${idSchema.$id}-title`}
title={title}
id={`${idSchema.$id}__title`}
title={options.title || title}
required={required}
schema={schema}
uiSchema={uiSchema}
registry={registry}
/>
)}
{(uiOptions.description || description) && (
{(options.description || description) && (
<DescriptionFieldTemplate
id={`${idSchema.$id}-description`}
description={uiOptions.description || description!}
id={`${idSchema.$id}__description`}
description={options.description || description!}
schema={schema}
uiSchema={uiSchema}
registry={registry}
/>
)}
<Grid container spacing={2} style={{ marginTop: '10px' }}>
{properties.map((element, index) =>
// Remove the <Grid> if the inner element is hidden as the <Grid>
// itself would otherwise still take up space.
element.hidden ? (
element.content
) : (
<Grid item xs={12} key={index} style={{ marginBottom: '10px' }}>
{element.content}
</Grid>
)
)}
{canExpand(schema, uiSchema, formData) && (
<Grid container justifyContent="flex-end">
<Grid item>
{properties.map((prop: ObjectFieldTemplatePropertyType) => prop.content)}
{canExpand<T, S, F>(schema, uiSchema, formData) && (
<AddButton
className="object-property-expand"
onClick={onAddClick(schema)}
disabled={disabled || readonly}
uiSchema={uiSchema}
registry={registry}
/>
</Grid>
</Grid>
)}
</Grid>
</>
</fieldset>
);
}
export default ObjectFieldTemplate;

View File

@ -1,80 +1,73 @@
import React, { CSSProperties } from 'react';
import FormControl from '@mui/material/FormControl';
import Grid from '@mui/material/Grid';
import InputLabel from '@mui/material/InputLabel';
import Input from '@mui/material/OutlinedInput';
import {
ADDITIONAL_PROPERTY_FLAG,
FormContextType,
RJSFSchema,
StrictRJSFSchema,
WrapIfAdditionalTemplateProps,
} from '@rjsf/utils';
function WrapIfAdditionalTemplate({
children,
import Label from '../FieldTemplate/Label';
/** The `WrapIfAdditional` component is used by the `FieldTemplate` to rename, or remove properties that are
* part of an `additionalProperties` part of a schema.
*
* @param props - The `WrapIfAdditionalProps` for this component
*/
export default function WrapIfAdditionalTemplate<
T = any,
S extends StrictRJSFSchema = RJSFSchema,
F extends FormContextType = any
>(props: WrapIfAdditionalTemplateProps<T, S, F>) {
const {
id,
classNames,
disabled,
id,
label,
onDropPropertyClick,
onKeyChange,
onDropPropertyClick,
readonly,
required,
schema,
children,
uiSchema,
registry,
}: WrapIfAdditionalTemplateProps) {
} = props;
// Button templates are not overridden in the uiSchema
const { RemoveButton } = registry.templates.ButtonTemplates;
const keyLabel = `${label} Key`; // i18n ?
const additional = ADDITIONAL_PROPERTY_FLAG in schema;
const btnStyle: CSSProperties = {
flex: 1,
paddingLeft: 6,
paddingRight: 6,
fontWeight: 'bold',
};
if (!additional) {
return <div className={classNames}>{children}</div>;
}
const handleBlur = ({ target }: React.FocusEvent<HTMLInputElement>) =>
onKeyChange(target.value);
return (
<Grid
container
key={`${id}-key`}
alignItems="center"
spacing={2}
className={classNames}
>
<Grid item xs>
<FormControl fullWidth required={required}>
<InputLabel>{keyLabel}</InputLabel>
<Input
defaultValue={label}
disabled={disabled || readonly}
id={`${id}-key`}
name={`${id}-key`}
onBlur={!readonly ? handleBlur : undefined}
<div className={classNames}>
<div className="row">
<div className="col-xs-5 form-additional">
<div className="form-group">
<Label label={keyLabel} required={required} id={`${id}-key`} />
<input
className="form-control"
type="text"
id={`${id}-key`}
onBlur={(event) => onKeyChange(event.target.value)}
defaultValue={label}
/>
</FormControl>
</Grid>
<Grid item xs>
{children}
</Grid>
<Grid item>
</div>
</div>
<div className="form-additional form-group col-xs-5">{children}</div>
<div className="col-xs-2">
<RemoveButton
iconType="default"
style={btnStyle}
className="array-item-remove btn-block"
style={{ border: '0' }}
disabled={disabled || readonly}
onClick={onDropPropertyClick(label)}
uiSchema={uiSchema}
registry={registry}
/>
</Grid>
</Grid>
</div>
</div>
</div>
);
}
export default WrapIfAdditionalTemplate;