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

This commit is contained in:
mike cullerton 2023-01-12 07:19:06 -05:00
commit 223793e79d
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 # so we can see what resources that user has access to
# originally from https://medium.com/keycloak/keycloak-jwt-token-using-curl-post-72c9e791ba8c # 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 # 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
# 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, # 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. # 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 repeat_form_user_1 repeat_form_user_1 # actually has permissions to the resource in this script
# ./bin/get_token ciadmin1 ciadmin1 '%2Fprocess-models' # ./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 REALM_NAME=spiffworkflow
USERNAME=${1-ciuser1} USERNAME=${1-fin}
PASSWORD=${2-ciuser1} PASSWORD=${2-fin}
URI_TO_TEST_AGAINST=${3-'%2Fprocess-models%2Fcategory_number_one%2Fprocess-model-with-repeating-form'}
FRONTEND_CLIENT_ID=spiffworkflow-frontend FRONTEND_CLIENT_ID=spiffworkflow-frontend
BACKEND_CLIENT_ID=spiffworkflow-backend BACKEND_CLIENT_ID=spiffworkflow-backend
@ -33,7 +34,7 @@ BACKEND_CLIENT_SECRET="JXeQExm0JhQPLumgHtIIqf52bDalHz0q" # noqa: S105
SECURE=false SECURE=false
BACKEND_BASIC_AUTH=$(echo -n "${BACKEND_CLIENT_ID}:${BACKEND_CLIENT_SECRET}" | base64) 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 "Using Keycloak: $KEYCLOAK_URL"
echo "realm: $REALM_NAME" echo "realm: $REALM_NAME"
@ -49,55 +50,72 @@ else
INSECURE=--insecure INSECURE=--insecure
fi fi
### Basic auth test with backend
result=$(curl -s -X POST "$KEYCLOAK_URL" "$INSECURE" \ result=$(curl -s -X POST "$KEYCLOAK_URL" "$INSECURE" \
-H "Content-Type: application/x-www-form-urlencoded" \ -H "Content-Type: application/x-www-form-urlencoded" \
-H "Authorization: Basic $BACKEND_BASIC_AUTH" \
-d "username=$USERNAME" \ -d "username=$USERNAME" \
-d "password=$PASSWORD" \ -d "password=$PASSWORD" \
-d 'grant_type=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 "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") 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" ### Get with frontend and exchange with backend - not configured to work in keycloak atm
# everything_resource_id='446bdcf4-a3bd-41c7-a0f8-67a225ba6b57' # result=$(curl -s -X POST "$KEYCLOAK_URL" "$INSECURE" \
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") # -H "Content-Type: application/x-www-form-urlencoded" \
# 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") # -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 '') ### Check fine grain permissions - does not work currently
if [[ -z "$resource_id_name_pairs" || "$resource_id_name_pairs" == "null" ]]; then # URI_TO_TEST_AGAINST=${3-'%2Fprocess-models%2Fcategory_number_one%2Fprocess-model-with-repeating-form'}
>&2 echo "ERROR: Could not find the resource id from the result: ${resource_result}" # if [[ "$backend_token" != 'null' ]]; then
exit 1 # echo "backend_token: $backend_token"
fi #
echo $resource_id_name_pairs # echo "Getting resource set"
# # everything_resource_id='446bdcf4-a3bd-41c7-a0f8-67a225ba6b57'
echo "Getting permissions" # 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")
for resource_id_name_pair in $resource_id_name_pairs ; do # # 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=$(awk -F ':' '{print $1}' <<<"$resource_id_name_pair") #
resource_name=$(awk -F ':' '{print $2}' <<<"$resource_id_name_pair") # resource_id_name_pairs=$(jq -r '.[] | "\(._id):\(.name)"' <<<"$resource_result" || echo '')
# if [[ -z "$resource_id_name_pairs" || "$resource_id_name_pairs" == "null" ]]; then
echo "Checking $resource_name" # >&2 echo "ERROR: Could not find the resource id from the result: ${resource_result}"
curl -s -X POST "$KEYCLOAK_URL" "$INSECURE" \ # exit 1
-H "Content-Type: application/x-www-form-urlencoded" \ # fi
-H "Authorization: Basic $BACKEND_BASIC_AUTH" \ # echo $resource_id_name_pairs
-d "audience=${BACKEND_CLIENT_ID}" \ #
--data-urlencode "grant_type=urn:ietf:params:oauth:grant-type:uma-ticket" \ # echo "Getting permissions"
-d "permission=${resource_id}" \ # for resource_id_name_pair in $resource_id_name_pairs ; do
-d "subject_token=${backend_token}" \ # resource_id=$(awk -F ':' '{print $1}' <<<"$resource_id_name_pair")
| jq . # resource_name=$(awk -F ':' '{print $2}' <<<"$resource_id_name_pair")
done #
else # echo "Checking $resource_name"
echo "Failed auth result: $result" # curl -s -X POST "$KEYCLOAK_URL" "$INSECURE" \
fi # -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 description: the modified process model id
schema: schema:
type: string type: string
- name: include_file_references
in: query
required: false
description: include all file references in the return
schema:
type: boolean
get: get:
operationId: spiffworkflow_backend.routes.process_models_controller.process_model_show operationId: spiffworkflow_backend.routes.process_models_controller.process_model_show
summary: Returns a single process model summary: Returns a single process model

View File

@ -15,6 +15,7 @@ from flask import jsonify
from flask import make_response from flask import make_response
from flask.wrappers import Response from flask.wrappers import Response
from flask_bpmn.api.api_error import ApiError from flask_bpmn.api.api_error import ApiError
from werkzeug.datastructures import FileStorage
from spiffworkflow_backend.interfaces import IdToProcessGroupMapping from spiffworkflow_backend.interfaces import IdToProcessGroupMapping
from spiffworkflow_backend.models.file import FileSchema from spiffworkflow_backend.models.file import FileSchema
@ -38,6 +39,9 @@ from spiffworkflow_backend.services.process_instance_report_service import (
ProcessInstanceReportService, ProcessInstanceReportService,
) )
from spiffworkflow_backend.services.process_model_service import ProcessModelService 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 from spiffworkflow_backend.services.spec_file_service import SpecFileService
@ -123,7 +127,9 @@ def process_model_update(
return ProcessModelInfoSchema().dump(process_model) 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_show."""
process_model_identifier = modified_process_model_identifier.replace(":", "/") process_model_identifier = modified_process_model_identifier.replace(":", "/")
process_model = _get_process_model(process_model_identifier) 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, key=lambda f: "" if f.name == process_model.primary_file_name else f.sort_index,
) )
process_model.files = files process_model.files = files
for file in process_model.files:
file.references = SpecFileService.get_references_for_file(file, process_model) if include_file_references:
for file in process_model.files:
file.references = SpecFileService.get_references_for_file(
file, process_model
)
process_model.parent_groups = ProcessModelService.get_parent_group_array( process_model.parent_groups = ProcessModelService.get_parent_group_array(
process_model.id process_model.id
@ -226,26 +236,11 @@ def process_model_file_update(
modified_process_model_identifier: str, file_name: str modified_process_model_identifier: str, file_name: str
) -> flask.wrappers.Response: ) -> flask.wrappers.Response:
"""Process_model_file_update.""" """Process_model_file_update."""
process_model_identifier = modified_process_model_identifier.replace(":", "/") message = f"User: {g.user.username} clicked save for"
process_model = _get_process_model(process_model_identifier) return _create_or_update_process_model_file(
modified_process_model_identifier, message, 200
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,
)
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( def process_model_file_delete(
modified_process_model_identifier: str, file_name: str modified_process_model_identifier: str, file_name: str
@ -275,28 +270,9 @@ def process_model_file_create(
modified_process_model_identifier: str, modified_process_model_identifier: str,
) -> flask.wrappers.Response: ) -> flask.wrappers.Response:
"""Process_model_file_create.""" """Process_model_file_create."""
process_model_identifier = modified_process_model_identifier.replace(":", "/") message = f"User: {g.user.username} added process model file"
process_model = _get_process_model(process_model_identifier) return _create_or_update_process_model_file(
request_file = _get_file_from_request() modified_process_model_identifier, message, 201
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"
) )
@ -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.""" """Get_file_from_request."""
request_file = connexion.request.files.get("file") request_file: FileStorage = connexion.request.files.get("file")
if not request_file: if not request_file:
raise ApiError( raise ApiError(
error_code="no_file_given", error_code="no_file_given",
@ -506,3 +482,58 @@ def _get_process_group_from_modified_identifier(
status_code=400, status_code=400,
) )
return process_group 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.BpmnProcessSpec import BpmnProcessSpec # type: ignore
from SpiffWorkflow.bpmn.specs.events.EndEvent import EndEvent # 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.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.bpmn.workflow import BpmnWorkflow # type: ignore
from SpiffWorkflow.dmn.parser.BpmnDmnParser import BpmnDmnParser # type: ignore from SpiffWorkflow.dmn.parser.BpmnDmnParser import BpmnDmnParser # type: ignore
from SpiffWorkflow.dmn.serializer.task_spec_converters import BusinessRuleTaskConverter # type: ignore from SpiffWorkflow.dmn.serializer.task_spec_converters import BusinessRuleTaskConverter # type: ignore
@ -787,7 +789,16 @@ class ProcessInstanceProcessor:
f"Manually executing Task {spiff_task.task_spec.name} of process" f"Manually executing Task {spiff_task.task_spec.name} of process"
f" instance {self.process_instance_model.id}" f" instance {self.process_instance_model.id}"
) )
spiff_task.complete() # 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: else:
spiff_logger = logging.getLogger("spiff") spiff_logger = logging.getLogger("spiff")
spiff_logger.info( spiff_logger.info(
@ -796,7 +807,20 @@ class ProcessInstanceProcessor:
spiff_task._set_state(TaskState.COMPLETED) spiff_task._set_state(TaskState.COMPLETED)
for child in spiff_task.children: for child in spiff_task.children:
child.task_spec._update(child) 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.increment_spiff_step()
self.add_step() self.add_step()
self.save() self.save()

View File

@ -6,7 +6,8 @@ from typing import List
from typing import Optional from typing import Optional
from flask_bpmn.models.db import db 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 File
from spiffworkflow_backend.models.file import FileType from spiffworkflow_backend.models.file import FileType
@ -29,6 +30,10 @@ class ProcessModelFileNotFoundError(Exception):
"""ProcessModelFileNotFoundError.""" """ProcessModelFileNotFoundError."""
class ProcessModelFileInvalidError(Exception):
"""ProcessModelFileInvalidError."""
class SpecFileService(FileSystemService): class SpecFileService(FileSystemService):
"""SpecFileService.""" """SpecFileService."""
@ -44,7 +49,6 @@ class SpecFileService(FileSystemService):
extension_filter: str = "", extension_filter: str = "",
) -> List[File]: ) -> List[File]:
"""Return all files associated with a workflow specification.""" """Return all files associated with a workflow specification."""
# path = SpecFileService.workflow_path(process_model_info)
path = os.path.join( path = os.path.join(
FileSystemService.root_path(), process_model_info.id_for_file_path() FileSystemService.root_path(), process_model_info.id_for_file_path()
) )
@ -76,9 +80,22 @@ class SpecFileService(FileSystemService):
) )
return references return references
@staticmethod @classmethod
def get_references_for_file( 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]: ) -> list[SpecReference]:
"""Uses spiffworkflow to parse BPMN and DMN files to determine how they can be externally referenced. """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' type = {str} 'process' / 'decision'
""" """
references: list[SpecReference] = [] 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 = MyCustomParser()
parser_type = None parser_type = None
sub_parser = None sub_parser = None
@ -100,14 +117,14 @@ class SpecFileService(FileSystemService):
messages = {} messages = {}
correlations = {} correlations = {}
start_messages = [] start_messages = []
if file.type == FileType.bpmn.value: if file_type.value == FileType.bpmn.value:
parser.add_bpmn_file(full_file_path) parser.add_bpmn_xml(etree.fromstring(binary_data))
parser_type = "process" parser_type = "process"
sub_parsers = list(parser.process_parsers.values()) sub_parsers = list(parser.process_parsers.values())
messages = parser.messages messages = parser.messages
correlations = parser.correlations correlations = parser.correlations
elif file.type == FileType.dmn.value: elif file_type.value == FileType.dmn.value:
parser.add_dmn_file(full_file_path) parser.add_dmn_xml(etree.fromstring(binary_data))
sub_parsers = list(parser.dmn_parsers.values()) sub_parsers = list(parser.dmn_parsers.values())
parser_type = "decision" parser_type = "decision"
else: else:
@ -127,7 +144,7 @@ class SpecFileService(FileSystemService):
display_name=sub_parser.get_name(), display_name=sub_parser.get_name(),
process_model_id=process_model_info.id, process_model_id=process_model_info.id,
type=parser_type, type=parser_type,
file_name=file.name, file_name=file_name,
relative_path=file_path, relative_path=file_path,
has_lanes=has_lanes, has_lanes=has_lanes,
is_executable=is_executable, is_executable=is_executable,
@ -147,23 +164,36 @@ class SpecFileService(FileSystemService):
# Same as update # Same as update
return SpecFileService.update_file(process_model_info, file_name, binary_data) 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( 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: ) -> File:
"""Update_file.""" """Update_file."""
SpecFileService.assert_valid_file_name(file_name) SpecFileService.assert_valid_file_name(file_name)
full_file_path = SpecFileService.full_file_path(process_model_info, file_name) cls.validate_bpmn_xml(file_name, binary_data)
SpecFileService.write_file_data_to_system(full_file_path, binary_data)
file = SpecFileService.to_file_object(file_name, full_file_path)
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( primary_process_ref = next(
(ref for ref in references if ref.is_primary and ref.is_executable), None (ref for ref in references if ref.is_primary and ref.is_executable), None
) )
SpecFileService.clear_caches_for_file(file_name, process_model_info) SpecFileService.clear_caches_for_file(file_name, process_model_info)
for ref in references: for ref in references:
# If no valid primary process is defined, default to the first process in the # If no valid primary process is defined, default to the first process in the
# updated file. # updated file.
@ -184,7 +214,11 @@ class SpecFileService(FileSystemService):
update_hash, update_hash,
) )
SpecFileService.update_caches(ref) 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 @staticmethod
def get_data(process_model_info: ProcessModelInfo, file_name: str) -> bytes: 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 # 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. # on the file system. Otherwise, assume it is a duplicate process id and error.
if os.path.isfile(full_bpmn_file_path): 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 ({ref.identifier}) has already been used for "
f"{process_id_lookup.relative_path}. It cannot be reused." f"{process_id_lookup.relative_path}. It cannot be reused."
) )
@ -314,7 +348,7 @@ class SpecFileService(FileSystemService):
identifier=message_model_identifier identifier=message_model_identifier
).first() ).first()
if message_model is None: if message_model is None:
raise ValidationException( raise ProcessModelFileInvalidError(
"Could not find message model with identifier" "Could not find message model with identifier"
f" '{message_model_identifier}'Required by a Start Event in :" f" '{message_model_identifier}'Required by a Start Event in :"
f" {ref.file_name}" f" {ref.file_name}"
@ -336,7 +370,7 @@ class SpecFileService(FileSystemService):
message_triggerable_process_model.process_model_identifier message_triggerable_process_model.process_model_identifier
!= ref.process_model_id != ref.process_model_id
): ):
raise ValidationException( raise ProcessModelFileInvalidError(
"Message model is already used to start process model" "Message model is already used to start process model"
f" {ref.process_model_id}" f" {ref.process_model_id}"
) )
@ -355,7 +389,7 @@ class SpecFileService(FileSystemService):
identifier=message_model_identifier identifier=message_model_identifier
).first() ).first()
if message_model is None: if message_model is None:
raise ValidationException( raise ProcessModelFileInvalidError(
"Could not find message model with identifier" "Could not find message model with identifier"
f" '{message_model_identifier}'specified by correlation" f" '{message_model_identifier}'specified by correlation"
f" property: {cpre}" 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: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:correlationProperty id="message_correlation_property" name="Message Correlation Property">
<bpmn:correlationPropertyRetrievalExpression messageRef="message_send"> <bpmn:correlationPropertyRetrievalExpression messageRef="message_send">
<bpmn:formalExpression>to</bpmn:formalExpression> <bpmn:messagePath>to</bpmn:messagePath>
</bpmn:correlationPropertyRetrievalExpression> </bpmn:correlationPropertyRetrievalExpression>
<bpmn:correlationPropertyRetrievalExpression messageRef="message_response"> <bpmn:correlationPropertyRetrievalExpression messageRef="message_response">
<bpmn:formalExpression>from.name</bpmn:formalExpression> <bpmn:messagePath>from.name</bpmn:messagePath>
</bpmn:correlationPropertyRetrievalExpression> </bpmn:correlationPropertyRetrievalExpression>
</bpmn:correlationProperty> </bpmn:correlationProperty>
<bpmn:message id="message_send" name="Message Send"> <bpmn:message id="message_send" name="Message Send">
@ -20,7 +20,7 @@
</bpmn:message> </bpmn:message>
<bpmn:correlationProperty id="correlation_property_one" name="Correlation Property One"> <bpmn:correlationProperty id="correlation_property_one" name="Correlation Property One">
<bpmn:correlationPropertyRetrievalExpression messageRef="message_send"> <bpmn:correlationPropertyRetrievalExpression messageRef="message_send">
<bpmn:formalExpression>new</bpmn:formalExpression> <bpmn:messagePath>new</bpmn:messagePath>
</bpmn:correlationPropertyRetrievalExpression> </bpmn:correlationPropertyRetrievalExpression>
</bpmn:correlationProperty> </bpmn:correlationProperty>
<bpmn:process id="test_dot_notation" name="Test Dot Notation" isExecutable="true"> <bpmn:process id="test_dot_notation" name="Test Dot Notation" isExecutable="true">

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?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: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_initiator" name="Message Initiator" processRef="message_send_process" />
<bpmn:participant id="message-receiver-one" name="Message Receiver One" /> <bpmn:participant id="message-receiver-one" name="Message Receiver One" />
<bpmn:participant id="message-receiver-two" name="Message Receiver Two" /> <bpmn:participant id="message-receiver-two" name="Message Receiver Two" />
@ -19,18 +19,18 @@
</bpmn:collaboration> </bpmn:collaboration>
<bpmn:correlationProperty id="mcp_topica_one" name="MCP TopicA One"> <bpmn:correlationProperty id="mcp_topica_one" name="MCP TopicA One">
<bpmn:correlationPropertyRetrievalExpression messageRef="message_send_one"> <bpmn:correlationPropertyRetrievalExpression messageRef="message_send_one">
<bpmn:formalExpression>topica_one</bpmn:formalExpression> <bpmn:messagePath>topica_one</bpmn:messagePath>
</bpmn:correlationPropertyRetrievalExpression> </bpmn:correlationPropertyRetrievalExpression>
<bpmn:correlationPropertyRetrievalExpression messageRef="message_response_one"> <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:correlationPropertyRetrievalExpression>
</bpmn:correlationProperty> </bpmn:correlationProperty>
<bpmn:correlationProperty id="mcp_topicb_one" name="MCP TopicB_one"> <bpmn:correlationProperty id="mcp_topicb_one" name="MCP TopicB_one">
<bpmn:correlationPropertyRetrievalExpression messageRef="message_send_one"> <bpmn:correlationPropertyRetrievalExpression messageRef="message_send_one">
<bpmn:formalExpression>topicb_one</bpmn:formalExpression> <bpmn:messagePath>topicb_one</bpmn:messagePath>
</bpmn:correlationPropertyRetrievalExpression> </bpmn:correlationPropertyRetrievalExpression>
<bpmn:correlationPropertyRetrievalExpression messageRef="message_response_one"> <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:correlationPropertyRetrievalExpression>
</bpmn:correlationProperty> </bpmn:correlationProperty>
<bpmn:process id="message_send_process" name="Message Send Process" isExecutable="true"> <bpmn:process id="message_send_process" name="Message Send Process" isExecutable="true">
@ -117,18 +117,18 @@ del time</bpmn:script>
</bpmn:message> </bpmn:message>
<bpmn:correlationProperty id="mcp_topica_two" name="MCP Topica Two"> <bpmn:correlationProperty id="mcp_topica_two" name="MCP Topica Two">
<bpmn:correlationPropertyRetrievalExpression messageRef="message_send_two"> <bpmn:correlationPropertyRetrievalExpression messageRef="message_send_two">
<bpmn:formalExpression>topica_two</bpmn:formalExpression> <bpmn:messagePath>topica_two</bpmn:messagePath>
</bpmn:correlationPropertyRetrievalExpression> </bpmn:correlationPropertyRetrievalExpression>
<bpmn:correlationPropertyRetrievalExpression messageRef="message_response_two"> <bpmn:correlationPropertyRetrievalExpression messageRef="message_response_two">
<bpmn:formalExpression>topica_two</bpmn:formalExpression> <bpmn:messagePath>topica_two</bpmn:messagePath>
</bpmn:correlationPropertyRetrievalExpression> </bpmn:correlationPropertyRetrievalExpression>
</bpmn:correlationProperty> </bpmn:correlationProperty>
<bpmn:correlationProperty id="mcp_topicb_two" name="MCP Topicb Two"> <bpmn:correlationProperty id="mcp_topicb_two" name="MCP Topicb Two">
<bpmn:correlationPropertyRetrievalExpression messageRef="message_send_two"> <bpmn:correlationPropertyRetrievalExpression messageRef="message_send_two">
<bpmn:formalExpression>topicb_two</bpmn:formalExpression> <bpmn:messagePath>topicb_two</bpmn:messagePath>
</bpmn:correlationPropertyRetrievalExpression> </bpmn:correlationPropertyRetrievalExpression>
<bpmn:correlationPropertyRetrievalExpression messageRef="message_response_two"> <bpmn:correlationPropertyRetrievalExpression messageRef="message_response_two">
<bpmn:formalExpression>topicb_two</bpmn:formalExpression> <bpmn:messagePath>topicb_two</bpmn:messagePath>
</bpmn:correlationPropertyRetrievalExpression> </bpmn:correlationPropertyRetrievalExpression>
</bpmn:correlationProperty> </bpmn:correlationProperty>
<bpmndi:BPMNDiagram id="BPMNDiagram_1"> <bpmndi:BPMNDiagram id="BPMNDiagram_1">

View File

@ -860,7 +860,7 @@ class TestProcessApi(BaseTest):
assert response.status_code == 200 assert response.status_code == 200
assert response.json is not None assert response.json is not None
assert response.json["ok"] assert response.json["file_contents"] is not None
response = client.get( response = client.get(
f"/v1.0/process-models/{modified_process_model_id}/files/random_fact.svg", 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 import Flask
from flask.testing import FlaskClient from flask.testing import FlaskClient
from flask_bpmn.models.db import db 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.base_test import BaseTest
from tests.spiffworkflow_backend.helpers.test_data import load_test_spec from tests.spiffworkflow_backend.helpers.test_data import load_test_spec
from spiffworkflow_backend.models.spec_reference import SpecReferenceCache from spiffworkflow_backend.models.spec_reference import SpecReferenceCache
from spiffworkflow_backend.models.user import UserModel from spiffworkflow_backend.models.user import UserModel
from spiffworkflow_backend.services.process_model_service import ProcessModelService 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 from spiffworkflow_backend.services.spec_file_service import SpecFileService
@ -74,7 +76,7 @@ class TestSpecFileService(BaseTest):
bpmn_process_id_lookups[0].relative_path bpmn_process_id_lookups[0].relative_path
== self.call_activity_nested_relative_file_path == self.call_activity_nested_relative_file_path
) )
with pytest.raises(ValidationException) as exception: with pytest.raises(ProcessModelFileInvalidError) as exception:
load_test_spec( load_test_spec(
"call_activity_nested_duplicate", "call_activity_nested_duplicate",
process_model_source_directory="call_activity_duplicate", process_model_source_directory="call_activity_duplicate",
@ -85,6 +87,14 @@ class TestSpecFileService(BaseTest):
in str(exception.value) 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( def test_updates_relative_file_path_when_appropriate(
self, self,
app: Flask, app: Flask,
@ -206,3 +216,23 @@ class TestSpecFileService(BaseTest):
assert dmn1[0].display_name == "Decision 1" assert dmn1[0].display_name == "Decision 1"
assert dmn1[0].identifier == "Decision_0vrtcmk" assert dmn1[0].identifier == "Decision_0vrtcmk"
assert dmn1[0].type == "decision" 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/core": "*",
"@rjsf/mui": "^5.0.0-beta.13", "@rjsf/mui": "^5.0.0-beta.13",
"@rjsf/utils": "^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", "@tanstack/react-table": "^8.2.2",
"@testing-library/jest-dom": "^5.16.4", "@testing-library/jest-dom": "^5.16.4",
"@testing-library/react": "^13.3.0", "@testing-library/react": "^13.3.0",
@ -4863,9 +4863,9 @@
} }
}, },
"node_modules/@rjsf/core": { "node_modules/@rjsf/core": {
"version": "5.0.0-beta.13", "version": "5.0.0-beta.16",
"resolved": "https://registry.npmjs.org/@rjsf/core/-/core-5.0.0-beta.13.tgz", "resolved": "https://registry.npmjs.org/@rjsf/core/-/core-5.0.0-beta.16.tgz",
"integrity": "sha512-uQ3A9aJhMJsz9ct5tV3ogZkSFEkKUxrM9SJ9Hc8ijxmuaW7Jv8tNv5jiWZZsLvNXlIONX83s6JqkiOJf6IOAvg==", "integrity": "sha512-TqOd3CKptWAswX9PU8pLSoAe5zI03J6Kk/aWAFbMj+xW/6hR5PXHbs5X5kxwpQx7IVXiJZZZpP5n1oDsu4GwNg==",
"dependencies": { "dependencies": {
"lodash": "^4.17.15", "lodash": "^4.17.15",
"lodash-es": "^4.17.15", "lodash-es": "^4.17.15",
@ -4881,9 +4881,9 @@
} }
}, },
"node_modules/@rjsf/mui": { "node_modules/@rjsf/mui": {
"version": "5.0.0-beta.13", "version": "5.0.0-beta.16",
"resolved": "https://registry.npmjs.org/@rjsf/mui/-/mui-5.0.0-beta.13.tgz", "resolved": "https://registry.npmjs.org/@rjsf/mui/-/mui-5.0.0-beta.16.tgz",
"integrity": "sha512-hwCtADpjNssq/CsT3Wj1FDVJfdCN3gptKedGjbusLUEwQqXoVzkzl25e/IRfN8y/JxYu4lMXDU89bN9nJSKWLA==", "integrity": "sha512-QskaSc2Zcwqz+nKoACstvn5LhrAx4EmicYc/6kNoj3jKH6MlfVCA7FYumu5g6TIqMrDEvZuZqBKtjL64Tv52PQ==",
"engines": { "engines": {
"node": ">=14" "node": ">=14"
}, },
@ -4898,9 +4898,9 @@
} }
}, },
"node_modules/@rjsf/utils": { "node_modules/@rjsf/utils": {
"version": "5.0.0-beta.13", "version": "5.0.0-beta.16",
"resolved": "https://registry.npmjs.org/@rjsf/utils/-/utils-5.0.0-beta.13.tgz", "resolved": "https://registry.npmjs.org/@rjsf/utils/-/utils-5.0.0-beta.16.tgz",
"integrity": "sha512-hWWWFD2ifjSOhqWueML4OHrZe2HW5pE2nfKGhCObFbwtggHoQlj64xDBsJ1qfUG8DGvCHztJQ/sKIaOvXnpt7w==", "integrity": "sha512-dNQ620Q6a9cB28sjjRgJkxIuD9TFd03sNMlcZVdZOuZC6wjfGc4rKG0Lc7+xgLFvSPFKwXJprzfKSM3yuy9jXg==",
"dependencies": { "dependencies": {
"json-schema-merge-allof": "^0.8.1", "json-schema-merge-allof": "^0.8.1",
"jsonpointer": "^5.0.1", "jsonpointer": "^5.0.1",
@ -4933,12 +4933,13 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz",
"integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w=="
}, },
"node_modules/@rjsf/validator-ajv6": { "node_modules/@rjsf/validator-ajv8": {
"version": "5.0.0-beta.13", "version": "5.0.0-beta.16",
"resolved": "https://registry.npmjs.org/@rjsf/validator-ajv6/-/validator-ajv6-5.0.0-beta.13.tgz", "resolved": "https://registry.npmjs.org/@rjsf/validator-ajv8/-/validator-ajv8-5.0.0-beta.16.tgz",
"integrity": "sha512-X9N3/HJYV23MjUN/VJHIdBhUdBuMTUsh4HAZm50eUvUAhWK95wIqjjhAs24rzeLajrjFeH7kFr89zAqDgIFhVQ==", "integrity": "sha512-VrQzR9HEH/1BF2TW/lRJuV+kILzR4geS+iW5Th1OlPeNp1NNWZuSO1kCU9O0JA17t2WHOEl/SFZXZBnN1/zwzQ==",
"dependencies": { "dependencies": {
"ajv": "^6.7.0", "ajv-formats": "^2.1.1",
"ajv8": "npm:ajv@^8.11.0",
"lodash": "^4.17.15", "lodash": "^4.17.15",
"lodash-es": "^4.17.15" "lodash-es": "^4.17.15"
}, },
@ -4946,7 +4947,7 @@
"node": ">=14" "node": ">=14"
}, },
"peerDependencies": { "peerDependencies": {
"@rjsf/utils": "^5.0.0-beta.1" "@rjsf/utils": "^5.0.0-beta.12"
} }
}, },
"node_modules/@rollup/plugin-babel": { "node_modules/@rollup/plugin-babel": {
@ -6822,6 +6823,27 @@
"ajv": "^6.9.1" "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": { "node_modules/ansi-align": {
"version": "3.0.1", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz", "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz",
@ -34767,9 +34789,9 @@
} }
}, },
"@rjsf/core": { "@rjsf/core": {
"version": "5.0.0-beta.13", "version": "5.0.0-beta.16",
"resolved": "https://registry.npmjs.org/@rjsf/core/-/core-5.0.0-beta.13.tgz", "resolved": "https://registry.npmjs.org/@rjsf/core/-/core-5.0.0-beta.16.tgz",
"integrity": "sha512-uQ3A9aJhMJsz9ct5tV3ogZkSFEkKUxrM9SJ9Hc8ijxmuaW7Jv8tNv5jiWZZsLvNXlIONX83s6JqkiOJf6IOAvg==", "integrity": "sha512-TqOd3CKptWAswX9PU8pLSoAe5zI03J6Kk/aWAFbMj+xW/6hR5PXHbs5X5kxwpQx7IVXiJZZZpP5n1oDsu4GwNg==",
"requires": { "requires": {
"lodash": "^4.17.15", "lodash": "^4.17.15",
"lodash-es": "^4.17.15", "lodash-es": "^4.17.15",
@ -34778,15 +34800,15 @@
} }
}, },
"@rjsf/mui": { "@rjsf/mui": {
"version": "5.0.0-beta.13", "version": "5.0.0-beta.16",
"resolved": "https://registry.npmjs.org/@rjsf/mui/-/mui-5.0.0-beta.13.tgz", "resolved": "https://registry.npmjs.org/@rjsf/mui/-/mui-5.0.0-beta.16.tgz",
"integrity": "sha512-hwCtADpjNssq/CsT3Wj1FDVJfdCN3gptKedGjbusLUEwQqXoVzkzl25e/IRfN8y/JxYu4lMXDU89bN9nJSKWLA==", "integrity": "sha512-QskaSc2Zcwqz+nKoACstvn5LhrAx4EmicYc/6kNoj3jKH6MlfVCA7FYumu5g6TIqMrDEvZuZqBKtjL64Tv52PQ==",
"requires": {} "requires": {}
}, },
"@rjsf/utils": { "@rjsf/utils": {
"version": "5.0.0-beta.13", "version": "5.0.0-beta.16",
"resolved": "https://registry.npmjs.org/@rjsf/utils/-/utils-5.0.0-beta.13.tgz", "resolved": "https://registry.npmjs.org/@rjsf/utils/-/utils-5.0.0-beta.16.tgz",
"integrity": "sha512-hWWWFD2ifjSOhqWueML4OHrZe2HW5pE2nfKGhCObFbwtggHoQlj64xDBsJ1qfUG8DGvCHztJQ/sKIaOvXnpt7w==", "integrity": "sha512-dNQ620Q6a9cB28sjjRgJkxIuD9TFd03sNMlcZVdZOuZC6wjfGc4rKG0Lc7+xgLFvSPFKwXJprzfKSM3yuy9jXg==",
"requires": { "requires": {
"json-schema-merge-allof": "^0.8.1", "json-schema-merge-allof": "^0.8.1",
"jsonpointer": "^5.0.1", "jsonpointer": "^5.0.1",
@ -34812,12 +34834,13 @@
} }
} }
}, },
"@rjsf/validator-ajv6": { "@rjsf/validator-ajv8": {
"version": "5.0.0-beta.13", "version": "5.0.0-beta.16",
"resolved": "https://registry.npmjs.org/@rjsf/validator-ajv6/-/validator-ajv6-5.0.0-beta.13.tgz", "resolved": "https://registry.npmjs.org/@rjsf/validator-ajv8/-/validator-ajv8-5.0.0-beta.16.tgz",
"integrity": "sha512-X9N3/HJYV23MjUN/VJHIdBhUdBuMTUsh4HAZm50eUvUAhWK95wIqjjhAs24rzeLajrjFeH7kFr89zAqDgIFhVQ==", "integrity": "sha512-VrQzR9HEH/1BF2TW/lRJuV+kILzR4geS+iW5Th1OlPeNp1NNWZuSO1kCU9O0JA17t2WHOEl/SFZXZBnN1/zwzQ==",
"requires": { "requires": {
"ajv": "^6.7.0", "ajv-formats": "^2.1.1",
"ajv8": "npm:ajv@^8.11.0",
"lodash": "^4.17.15", "lodash": "^4.17.15",
"lodash-es": "^4.17.15" "lodash-es": "^4.17.15"
} }
@ -36295,6 +36318,24 @@
"integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==",
"requires": {} "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": { "ansi-align": {
"version": "3.0.1", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz", "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz",

View File

@ -18,7 +18,7 @@
"@rjsf/core": "*", "@rjsf/core": "*",
"@rjsf/mui": "^5.0.0-beta.13", "@rjsf/mui": "^5.0.0-beta.13",
"@rjsf/utils": "^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", "@tanstack/react-table": "^8.2.2",
"@testing-library/jest-dom": "^5.16.4", "@testing-library/jest-dom": "^5.16.4",
"@testing-library/react": "^13.3.0", "@testing-library/react": "^13.3.0",

View File

@ -37,7 +37,9 @@ export default function ProcessModelSearch({
const shouldFilterProcessModel = (options: any) => { const shouldFilterProcessModel = (options: any) => {
const processModel: ProcessModel = options.item; const processModel: ProcessModel = options.item;
const { inputValue } = options; const { inputValue } = options;
return getFullProcessModelLabel(processModel).includes(inputValue); return getFullProcessModelLabel(processModel)
.toLowerCase()
.includes((inputValue || '').toLowerCase());
}; };
return ( return (
<ComboBox <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) => { const canEditTaskData = (task: any) => {
return ( return (
processInstance && processInstance &&
ability.can('PUT', targetUris.processInstanceTaskListDataPath) && ability.can('PUT', targetUris.processInstanceTaskListDataPath) &&
task.state === 'READY' && isCurrentTask(task) &&
processInstance.status === 'suspended' && processInstance.status === 'suspended' &&
showingLastSpiffStep() showingLastSpiffStep()
); );
@ -556,7 +569,7 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
processInstance && processInstance &&
processInstance.status === 'suspended' && processInstance.status === 'suspended' &&
ability.can('POST', targetUris.processInstanceCompleteTaskPath) && ability.can('POST', targetUris.processInstanceCompleteTaskPath) &&
task.state === 'READY' && isCurrentTask(task) &&
showingLastSpiffStep() showingLastSpiffStep()
); );
}; };

View File

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

View File

@ -374,6 +374,7 @@ export default function ProcessModelShow() {
const doFileUpload = (event: any) => { const doFileUpload = (event: any) => {
event.preventDefault(); event.preventDefault();
setErrorObject(null);
const url = `/process-models/${modifiedProcessModelId}/files`; const url = `/process-models/${modifiedProcessModelId}/files`;
const formData = new FormData(); const formData = new FormData();
formData.append('file', filesToUpload[0]); formData.append('file', filesToUpload[0]);
@ -383,6 +384,7 @@ export default function ProcessModelShow() {
successCallback: onUploadedCallback, successCallback: onUploadedCallback,
httpMethod: 'POST', httpMethod: 'POST',
postBody: formData, postBody: formData,
failureCallback: setErrorObject,
}); });
setFilesToUpload(null); 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 Editor from '@monaco-editor/react';
import { useNavigate, useParams, useSearchParams } from 'react-router-dom'; import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
// @ts-ignore // @ts-ignore
@ -8,18 +8,25 @@ import HttpService from '../services/HttpService';
import ButtonWithConfirmation from '../components/ButtonWithConfirmation'; import ButtonWithConfirmation from '../components/ButtonWithConfirmation';
import { modifyProcessIdentifierForPathParam } from '../helpers'; import { modifyProcessIdentifierForPathParam } from '../helpers';
import { ProcessFile } from '../interfaces'; 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 // 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 // possibly be merged into it. I'm leaving as a separate file now in case it does
// end up diverging greatly // end up diverging greatly
export default function ReactFormEditor() { export default function ReactFormEditor() {
const params = useParams(); const params = useParams();
const setErrorObject = (useContext as any)(ErrorContext)[1];
const [showFileNameEditor, setShowFileNameEditor] = useState(false); const [showFileNameEditor, setShowFileNameEditor] = useState(false);
const [newFileName, setNewFileName] = useState(''); const [newFileName, setNewFileName] = useState('');
const searchParams = useSearchParams()[0]; const searchParams = useSearchParams()[0];
const handleShowFileNameEditor = () => setShowFileNameEditor(true); const handleShowFileNameEditor = () => setShowFileNameEditor(true);
const navigate = useNavigate(); const navigate = useNavigate();
const [displaySaveFileMessage, setDisplaySaveFileMessage] =
useState<boolean>(false);
const [processModelFile, setProcessModelFile] = useState<ProcessFile | null>( const [processModelFile, setProcessModelFile] = useState<ProcessFile | null>(
null null
); );
@ -70,6 +77,7 @@ export default function ReactFormEditor() {
}, [params, modifiedProcessModelId]); }, [params, modifiedProcessModelId]);
const navigateToProcessModelFile = (_result: any) => { const navigateToProcessModelFile = (_result: any) => {
setDisplaySaveFileMessage(true);
if (!params.file_name) { if (!params.file_name) {
const fileNameWithExtension = `${newFileName}.${fileExtension}`; const fileNameWithExtension = `${newFileName}.${fileExtension}`;
navigate( navigate(
@ -79,6 +87,9 @@ export default function ReactFormEditor() {
}; };
const saveFile = () => { const saveFile = () => {
setErrorObject(null);
setDisplaySaveFileMessage(false);
let url = `/process-models/${modifiedProcessModelId}/files`; let url = `/process-models/${modifiedProcessModelId}/files`;
let httpMethod = 'PUT'; let httpMethod = 'PUT';
let fileNameWithExtension = params.file_name; let fileNameWithExtension = params.file_name;
@ -105,6 +116,7 @@ export default function ReactFormEditor() {
HttpService.makeCallToBackend({ HttpService.makeCallToBackend({
path: url, path: url,
successCallback: navigateToProcessModelFile, successCallback: navigateToProcessModelFile,
failureCallback: setErrorObject,
httpMethod, httpMethod,
postBody: formData, 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) { if (processModelFile || !params.file_name) {
const processModelFileName = processModelFile ? processModelFile.name : ''; const processModelFileName = processModelFile ? processModelFile.name : '';
return ( return (
@ -182,6 +208,7 @@ export default function ReactFormEditor() {
{processModelFileName} {processModelFileName}
</h1> </h1>
{newFileNameBox()} {newFileNameBox()}
{saveFileMessage()}
<Button onClick={saveFile} variant="danger" data-qa="file-save-button"> <Button onClick={saveFile} variant="danger" data-qa="file-save-button">
Save Save
</Button> </Button>

View File

@ -1,13 +1,6 @@
import { useContext, useEffect, useState } from 'react'; import { useContext, useEffect, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom'; import { useNavigate, useParams } from 'react-router-dom';
import validator from '@rjsf/validator-ajv8';
// 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 { import {
TabList, TabList,

View File

@ -1,12 +1,23 @@
import React, { CSSProperties } from 'react'; import React, { CSSProperties } from 'react';
import Box from '@mui/material/Box'; import {
import Grid from '@mui/material/Grid'; ArrayFieldTemplateItemType,
import Paper from '@mui/material/Paper'; FormContextType,
import { ArrayFieldTemplateItemType } from '@rjsf/utils'; 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 { const {
children, children,
className,
disabled, disabled,
hasToolbar, hasToolbar,
hasMoveDown, hasMoveDown,
@ -16,8 +27,8 @@ function ArrayFieldItemTemplate(props: ArrayFieldTemplateItemType) {
onDropIndexClick, onDropIndexClick,
onReorderClick, onReorderClick,
readonly, readonly,
uiSchema,
registry, registry,
uiSchema,
} = props; } = props;
const { MoveDownButton, MoveUpButton, RemoveButton } = const { MoveDownButton, MoveUpButton, RemoveButton } =
registry.templates.ButtonTemplates; registry.templates.ButtonTemplates;
@ -26,47 +37,49 @@ function ArrayFieldItemTemplate(props: ArrayFieldTemplateItemType) {
paddingLeft: 6, paddingLeft: 6,
paddingRight: 6, paddingRight: 6,
fontWeight: 'bold', fontWeight: 'bold',
minWidth: 0,
}; };
return ( return (
<Grid container alignItems="center"> <div className={className}>
<Grid item xs style={{ overflow: 'auto' }}> <div className={hasToolbar ? 'col-xs-9' : 'col-xs-12'}>{children}</div>
<Box mb={2}>
<Paper elevation={2}>
<Box p={2}>{children}</Box>
</Paper>
</Box>
</Grid>
{hasToolbar && ( {hasToolbar && (
<Grid item> <div className="col-xs-3 array-item-toolbox">
{(hasMoveUp || hasMoveDown) && ( <div
<MoveUpButton className="btn-group"
style={btnStyle} style={{
disabled={disabled || readonly || !hasMoveUp} display: 'flex',
onClick={onReorderClick(index, index - 1)} justifyContent: 'space-around',
uiSchema={uiSchema} }}
/> >
)} {(hasMoveUp || hasMoveDown) && (
{(hasMoveUp || hasMoveDown) && ( <MoveUpButton
<MoveDownButton style={btnStyle}
style={btnStyle} disabled={disabled || readonly || !hasMoveUp}
disabled={disabled || readonly || !hasMoveDown} onClick={onReorderClick(index, index - 1)}
onClick={onReorderClick(index, index + 1)} uiSchema={uiSchema}
uiSchema={uiSchema} registry={registry}
/> />
)} )}
{hasRemove && ( {(hasMoveUp || hasMoveDown) && (
<RemoveButton <MoveDownButton
style={btnStyle} style={btnStyle}
disabled={disabled || readonly} disabled={disabled || readonly || !hasMoveDown}
onClick={onDropIndexClick(index)} onClick={onReorderClick(index, index + 1)}
uiSchema={uiSchema} uiSchema={uiSchema}
/> registry={registry}
)} />
</Grid> )}
{hasRemove && (
<RemoveButton
style={btnStyle}
disabled={disabled || readonly}
onClick={onDropIndexClick(index)}
uiSchema={uiSchema}
registry={registry}
/>
)}
</div>
</div>
)} )}
</Grid> </div>
); );
} }
export default ArrayFieldItemTemplate;

View File

@ -1,17 +1,26 @@
import React from 'react'; import React from 'react';
import Box from '@mui/material/Box';
import Grid from '@mui/material/Grid';
import Paper from '@mui/material/Paper';
import { import {
ArrayFieldTemplateItemType,
ArrayFieldTemplateProps,
getTemplate, getTemplate,
getUiOptions, getUiOptions,
ArrayFieldTemplateProps,
ArrayFieldTemplateItemType,
FormContextType,
RJSFSchema,
StrictRJSFSchema,
} from '@rjsf/utils'; } 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 { const {
canAdd, canAdd,
className,
disabled, disabled,
idSchema, idSchema,
uiSchema, uiSchema,
@ -23,68 +32,62 @@ function ArrayFieldTemplate(props: ArrayFieldTemplateProps) {
schema, schema,
title, title,
} = props; } = props;
const uiOptions = getUiOptions(uiSchema); const uiOptions = getUiOptions<T, S, F>(uiSchema);
const ArrayFieldDescriptionTemplate = const ArrayFieldDescriptionTemplate = getTemplate<
getTemplate<'ArrayFieldDescriptionTemplate'>( 'ArrayFieldDescriptionTemplate',
'ArrayFieldDescriptionTemplate', T,
registry, S,
uiOptions F
); >('ArrayFieldDescriptionTemplate', registry, uiOptions);
const ArrayFieldItemTemplate = getTemplate<'ArrayFieldItemTemplate'>( const ArrayFieldItemTemplate = getTemplate<'ArrayFieldItemTemplate', T, S, F>(
'ArrayFieldItemTemplate', 'ArrayFieldItemTemplate',
registry, registry,
uiOptions uiOptions
); );
const ArrayFieldTitleTemplate = getTemplate<'ArrayFieldTitleTemplate'>( const ArrayFieldTitleTemplate = getTemplate<
'ArrayFieldTitleTemplate', 'ArrayFieldTitleTemplate',
registry, T,
uiOptions S,
); F
>('ArrayFieldTitleTemplate', registry, uiOptions);
// Button templates are not overridden in the uiSchema // Button templates are not overridden in the uiSchema
const { const {
ButtonTemplates: { AddButton }, ButtonTemplates: { AddButton },
} = registry.templates; } = registry.templates;
return ( return (
<Paper elevation={2}> <fieldset className={className} id={idSchema.$id}>
<Box p={2}> <ArrayFieldTitleTemplate
<ArrayFieldTitleTemplate idSchema={idSchema}
idSchema={idSchema} title={uiOptions.title || title}
title={uiOptions.title || title} required={required}
schema={schema} schema={schema}
uiSchema={uiSchema} uiSchema={uiSchema}
required={required} registry={registry}
registry={registry} />
/> <ArrayFieldDescriptionTemplate
<ArrayFieldDescriptionTemplate idSchema={idSchema}
idSchema={idSchema} description={uiOptions.description || schema.description}
description={uiOptions.description || schema.description} schema={schema}
schema={schema} uiSchema={uiSchema}
uiSchema={uiSchema} registry={registry}
registry={registry} />
/> <div className="row array-item-list">
<Grid container key={`array-item-list-${idSchema.$id}`}> {items &&
{items && items.map(
items.map(({ key, ...itemProps }: ArrayFieldTemplateItemType) => ( ({ key, ...itemProps }: ArrayFieldTemplateItemType<T, S, F>) => (
<ArrayFieldItemTemplate key={key} {...itemProps} /> <ArrayFieldItemTemplate key={key} {...itemProps} />
))} )
{canAdd && (
<Grid container justifyContent="flex-end">
<Grid item>
<Box mt={2}>
<AddButton
className="array-item-add"
onClick={onAddClick}
disabled={disabled || readonly}
uiSchema={uiSchema}
/>
</Box>
</Grid>
</Grid>
)} )}
</Grid> </div>
</Box> {canAdd && (
</Paper> <AddButton
className="array-item-add"
onClick={onAddClick}
disabled={disabled || readonly}
uiSchema={uiSchema}
registry={registry}
/>
)}
</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 React from 'react';
import Grid from '@mui/material/Grid';
import { import {
FormContextType,
ObjectFieldTemplatePropertyType,
ObjectFieldTemplateProps, ObjectFieldTemplateProps,
RJSFSchema,
StrictRJSFSchema,
canExpand, canExpand,
getTemplate, getTemplate,
getUiOptions, getUiOptions,
} from '@rjsf/utils'; } from '@rjsf/utils';
function ObjectFieldTemplate({ /** The `ObjectFieldTemplate` is the template to use to render all the inner properties of an object along with the
description, * title and description if available. If the object is expandable, then an `AddButton` is also rendered after all
title, * the properties.
properties, *
required, * @param props - The `ObjectFieldTemplateProps` for this component
disabled, */
readonly, export default function ObjectFieldTemplate<
uiSchema, T = any,
idSchema, S extends StrictRJSFSchema = RJSFSchema,
schema, F extends FormContextType = any
formData, >(props: ObjectFieldTemplateProps<T, S, F>) {
onAddClick, const {
registry, description,
}: ObjectFieldTemplateProps) { disabled,
const uiOptions = getUiOptions(uiSchema); formData,
const TitleFieldTemplate = getTemplate<'TitleFieldTemplate'>( idSchema,
onAddClick,
properties,
readonly,
registry,
required,
schema,
title,
uiSchema,
} = props;
const options = getUiOptions<T, S, F>(uiSchema);
const TitleFieldTemplate = getTemplate<'TitleFieldTemplate', T, S, F>(
'TitleFieldTemplate', 'TitleFieldTemplate',
registry, registry,
uiOptions options
); );
const DescriptionFieldTemplate = getTemplate<'DescriptionFieldTemplate'>( const DescriptionFieldTemplate = getTemplate<
'DescriptionFieldTemplate', 'DescriptionFieldTemplate',
registry, T,
uiOptions S,
); F
>('DescriptionFieldTemplate', registry, options);
// Button templates are not overridden in the uiSchema // Button templates are not overridden in the uiSchema
const { const {
ButtonTemplates: { AddButton }, ButtonTemplates: { AddButton },
} = registry.templates; } = registry.templates;
return ( return (
<> <fieldset id={idSchema.$id}>
{(uiOptions.title || title) && ( {(options.title || title) && (
<TitleFieldTemplate <TitleFieldTemplate
id={`${idSchema.$id}-title`} id={`${idSchema.$id}__title`}
title={title} title={options.title || title}
required={required} required={required}
schema={schema} schema={schema}
uiSchema={uiSchema} uiSchema={uiSchema}
registry={registry} registry={registry}
/> />
)} )}
{(uiOptions.description || description) && ( {(options.description || description) && (
<DescriptionFieldTemplate <DescriptionFieldTemplate
id={`${idSchema.$id}-description`} id={`${idSchema.$id}__description`}
description={uiOptions.description || description!} description={options.description || description!}
schema={schema} schema={schema}
uiSchema={uiSchema} uiSchema={uiSchema}
registry={registry} registry={registry}
/> />
)} )}
<Grid container spacing={2} style={{ marginTop: '10px' }}> {properties.map((prop: ObjectFieldTemplatePropertyType) => prop.content)}
{properties.map((element, index) => {canExpand<T, S, F>(schema, uiSchema, formData) && (
// Remove the <Grid> if the inner element is hidden as the <Grid> <AddButton
// itself would otherwise still take up space. className="object-property-expand"
element.hidden ? ( onClick={onAddClick(schema)}
element.content disabled={disabled || readonly}
) : ( uiSchema={uiSchema}
<Grid item xs={12} key={index} style={{ marginBottom: '10px' }}> registry={registry}
{element.content} />
</Grid> )}
) </fieldset>
)}
{canExpand(schema, uiSchema, formData) && (
<Grid container justifyContent="flex-end">
<Grid item>
<AddButton
className="object-property-expand"
onClick={onAddClick(schema)}
disabled={disabled || readonly}
uiSchema={uiSchema}
/>
</Grid>
</Grid>
)}
</Grid>
</>
); );
} }
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 { import {
ADDITIONAL_PROPERTY_FLAG, ADDITIONAL_PROPERTY_FLAG,
FormContextType,
RJSFSchema,
StrictRJSFSchema,
WrapIfAdditionalTemplateProps, WrapIfAdditionalTemplateProps,
} from '@rjsf/utils'; } from '@rjsf/utils';
function WrapIfAdditionalTemplate({ import Label from '../FieldTemplate/Label';
children,
classNames, /** The `WrapIfAdditional` component is used by the `FieldTemplate` to rename, or remove properties that are
disabled, * part of an `additionalProperties` part of a schema.
id, *
label, * @param props - The `WrapIfAdditionalProps` for this component
onDropPropertyClick, */
onKeyChange, export default function WrapIfAdditionalTemplate<
readonly, T = any,
required, S extends StrictRJSFSchema = RJSFSchema,
schema, F extends FormContextType = any
uiSchema, >(props: WrapIfAdditionalTemplateProps<T, S, F>) {
registry, const {
}: WrapIfAdditionalTemplateProps) { id,
classNames,
disabled,
label,
onKeyChange,
onDropPropertyClick,
readonly,
required,
schema,
children,
uiSchema,
registry,
} = props;
// Button templates are not overridden in the uiSchema // Button templates are not overridden in the uiSchema
const { RemoveButton } = registry.templates.ButtonTemplates; const { RemoveButton } = registry.templates.ButtonTemplates;
const keyLabel = `${label} Key`; // i18n ? const keyLabel = `${label} Key`; // i18n ?
const additional = ADDITIONAL_PROPERTY_FLAG in schema; const additional = ADDITIONAL_PROPERTY_FLAG in schema;
const btnStyle: CSSProperties = {
flex: 1,
paddingLeft: 6,
paddingRight: 6,
fontWeight: 'bold',
};
if (!additional) { if (!additional) {
return <div className={classNames}>{children}</div>; return <div className={classNames}>{children}</div>;
} }
const handleBlur = ({ target }: React.FocusEvent<HTMLInputElement>) =>
onKeyChange(target.value);
return ( return (
<Grid <div className={classNames}>
container <div className="row">
key={`${id}-key`} <div className="col-xs-5 form-additional">
alignItems="center" <div className="form-group">
spacing={2} <Label label={keyLabel} required={required} id={`${id}-key`} />
className={classNames} <input
> className="form-control"
<Grid item xs> type="text"
<FormControl fullWidth required={required}> id={`${id}-key`}
<InputLabel>{keyLabel}</InputLabel> onBlur={(event) => onKeyChange(event.target.value)}
<Input defaultValue={label}
defaultValue={label} />
</div>
</div>
<div className="form-additional form-group col-xs-5">{children}</div>
<div className="col-xs-2">
<RemoveButton
className="array-item-remove btn-block"
style={{ border: '0' }}
disabled={disabled || readonly} disabled={disabled || readonly}
id={`${id}-key`} onClick={onDropPropertyClick(label)}
name={`${id}-key`} uiSchema={uiSchema}
onBlur={!readonly ? handleBlur : undefined} registry={registry}
type="text"
/> />
</FormControl> </div>
</Grid> </div>
<Grid item xs> </div>
{children}
</Grid>
<Grid item>
<RemoveButton
iconType="default"
style={btnStyle}
disabled={disabled || readonly}
onClick={onDropPropertyClick(label)}
uiSchema={uiSchema}
/>
</Grid>
</Grid>
); );
} }
export default WrapIfAdditionalTemplate;