the big message improvements branch (#1549)

* imported patch from old message_improvements branch w/ burnettk

* wip.

* merging in changes from message_improvements

* remove patch files that were accidendetally added.

* Added a modal for editing a correlation.  Added ability to delete whole correlation keys.
A little css cleanup.

* * Removing migration - will add back in at the end.
* The Message models API should not require page and per_age parameters, it will return all.
* The Message model list should return a full json description of all messages/correlations for all containing groups.
*

* wip

* Add import, fix class name

* Getting ./bin/pyl to pass

* Getting ./bin/pyl to pass

* Some fe lint fixes

* Some ruff fixes

* Commands to nuke poetry dirs

* Temp skipping of a couple tests

* Getting ./bin/pyl to pass

* This needs to be back in

* Revert back to main

* Factored out data store handling

* Working on factoring out collecting messages, has test failure

* Formatting

* Fixed up test failures

* Remove commentted out lines

* Adding fields

* Fix merge issue

* Re-enable modal

* WIP

* Untested relationships

* Remove correlation key table

* Remove retrieval expression from uniqueness

* Remove commentted out lines

* WIP

* WIP

* WIP

* WIP

* WIP

* Make mypy pass

* Getting formatters to pass

* Add migration

* WIP fixing tests

* WIP fixing tests

* WIP fixing tests

* WIP fixing tests

* WIP fixing tests

* Getting ./bin/pyl to pass

* Fix skipped test

* Fix skipped test

* Getting ./bin/pyl to pass

* Remove unused method

* Remove unused methods

* Clean up unused code

* Refactor to support creating single messages from the UI

* Untested support for processing one process_group

* WIP test

* WIP test

* Filled out test

* Getting ./bin/pyl to pass

* Message Editor Modal Work

* Change migration and add in schemas.

* Swtich to using the associated branch of the process BPMN.io mods

* Get the backend returning messages created from the frontend to the drop down list in the BPMN.io editor.

* Merge main, fix up test

* Getting ./bin/pyl to pass

* Show path in location

* Rename var

* install packages from bpmn-js-spiffworkflow as well for local development

* process group api can add and update message models now w/ burnettk

* backend tests are passing now w/ burnettk

* the launch message edit button is loading the editor w/ burnettk

* updated bpmn-js-spiffworkflow

* pyl is passing w/ burnettk

* updated bpmn-js-spiffworkflow w/ burnettk

* updated bpmn-js-spiffworkflow w/ burnettk

* fixed console errors w/ burnettk

* a couple tweaks w/ burnettk

* save the message json in the new format from the mform w/ burnettk

* display the correlation props in the form w/ burnettk

* default to empty schema so the format is obvious

* allow removing correlation props from web ui w/ burnettk

* added save notification when saving a message on a process model w/ burnettk

* fixed broken test w/ burnettk

* Updating test cases to new message format, tests are failing

* support schema from messages in frontend

* Fixing tests

* Fixing tests

* Fixing tests

* removed references to correlation keys and removed unused components w/ burnettk

* removed temp mesasge model edit button w/ burnettk

* Make mypy pass

* Fixing tests

* Fixing tests

* Getting ./bin/pyl to pass

* save deleted messages before attempting to add new ones w/ burnettk

* set state for the message id so it can be changed w/ burnettk

* do not wait for the message id to be set since it is not necessary w/ burnettk

* updated bpmn-js-spiffworkflow w/ burnettk

* build images for this branch w/ burnettk

* put location in path of message-models so we can control permissions on it w/ burnettk

* fix black

* some coderabbit suggestions

* pull in spiff fix

* Default schema to {}

* Temp fix for invalid schema

* updated bpmn-js-spiffworkflow

* some updates for issue 1626

* minor name tweaks and attempts to update message dropdown in panel when message changes - does not work yet w/ burnettk

* updated bpmn-js-spiffworkflow w/ burnettk

* attempt to call add_message.returned event when message updates w/ burnettk

* treat formData as a state in the MesasgeEditor so it can be updated when the form contents is modified w/ burnettk

* updated bpmn-js-spiffworkflow w/ burnettk

* Feature/merge correlation properties (#1693)

* Merge XML Correlation properties with Process group properties

* updates for messages w/ burnettk

---------

Co-authored-by: theaubmov <ayoubaitlachgar98@gmail.com>
Co-authored-by: jasquat <jasquat@users.noreply.github.com>

* do not wait for message id state to be set to better support new messages w/ burnettk

* updated SpiffWorkflow w/ burnettk

* some cleanup from coderabbit and linting

* added index to message tables, run typecheck in ci, and other updates while code reviewing w/ burnettk

* updated bpmn-js-spiffworkflow w/ burnettk

* remove branch to build

---------

Co-authored-by: jasquat <jasquat@users.noreply.github.com>
Co-authored-by: danfunk <daniel.h.funk@gmail.com>
Co-authored-by: Jon Herron <jon.herron@yahoo.com>
Co-authored-by: burnettk <burnettk@users.noreply.github.com>
Co-authored-by: jasquat <2487833+jasquat@users.noreply.github.com>
Co-authored-by: theaubmov <ayoubaitlachgar98@gmail.com>
This commit is contained in:
Kevin Burnett 2024-06-10 16:15:54 +00:00 committed by GitHub
parent a63e8a34a1
commit eae5f7dd2d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
45 changed files with 1982 additions and 213 deletions

0
spiffworkflow-backend/bin/save_all_bpmn.py Normal file → Executable file
View File

View File

@ -0,0 +1,85 @@
"""empty message
Revision ID: 43afc70a7016
Revises: d4b900e71852
Create Date: 2024-06-10 11:17:19.060779
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision = '43afc70a7016'
down_revision = 'd4b900e71852'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('message',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('identifier', sa.String(length=255), nullable=False),
sa.Column('location', sa.String(length=255), nullable=False),
sa.Column('schema', sa.JSON(), nullable=False),
sa.Column('updated_at_in_seconds', sa.Integer(), nullable=False),
sa.Column('created_at_in_seconds', sa.Integer(), nullable=False),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('identifier', 'location', name='message_identifier_location_unique')
)
with op.batch_alter_table('message', schema=None) as batch_op:
batch_op.create_index(batch_op.f('ix_message_identifier'), ['identifier'], unique=False)
batch_op.create_index(batch_op.f('ix_message_location'), ['location'], unique=False)
op.create_table('message_correlation_property',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('message_id', sa.Integer(), nullable=False),
sa.Column('identifier', sa.String(length=255), nullable=False),
sa.Column('retrieval_expression', sa.String(length=255), nullable=False),
sa.Column('updated_at_in_seconds', sa.Integer(), nullable=False),
sa.Column('created_at_in_seconds', sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['message_id'], ['message.id'], name='message_correlation_property_message_id_fk'),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('message_id', 'identifier', name='message_correlation_property_unique')
)
with op.batch_alter_table('message_correlation_property', schema=None) as batch_op:
batch_op.create_index(batch_op.f('ix_message_correlation_property_identifier'), ['identifier'], unique=False)
batch_op.create_index(batch_op.f('ix_message_correlation_property_message_id'), ['message_id'], unique=False)
with op.batch_alter_table('correlation_property_cache', schema=None) as batch_op:
batch_op.drop_index('ix_correlation_property_cache_message_name')
batch_op.drop_index('ix_correlation_property_cache_name')
op.drop_table('correlation_property_cache')
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('correlation_property_cache',
sa.Column('id', mysql.INTEGER(), autoincrement=True, nullable=False),
sa.Column('name', mysql.VARCHAR(collation='utf8mb4_0900_as_cs', length=50), nullable=False),
sa.Column('message_name', mysql.VARCHAR(collation='utf8mb4_0900_as_cs', length=50), nullable=False),
sa.Column('process_model_id', mysql.VARCHAR(collation='utf8mb4_0900_as_cs', length=255), nullable=False),
sa.Column('retrieval_expression', mysql.VARCHAR(collation='utf8mb4_0900_as_cs', length=255), nullable=True),
sa.PrimaryKeyConstraint('id'),
mysql_collate='utf8mb4_0900_as_cs',
mysql_default_charset='utf8mb4',
mysql_engine='InnoDB'
)
with op.batch_alter_table('correlation_property_cache', schema=None) as batch_op:
batch_op.create_index('ix_correlation_property_cache_name', ['name'], unique=False)
batch_op.create_index('ix_correlation_property_cache_message_name', ['message_name'], unique=False)
with op.batch_alter_table('message_correlation_property', schema=None) as batch_op:
batch_op.drop_index(batch_op.f('ix_message_correlation_property_message_id'))
batch_op.drop_index(batch_op.f('ix_message_correlation_property_identifier'))
op.drop_table('message_correlation_property')
with op.batch_alter_table('message', schema=None) as batch_op:
batch_op.drop_index(batch_op.f('ix_message_location'))
batch_op.drop_index(batch_op.f('ix_message_identifier'))
op.drop_table('message')
# ### end Alembic commands ###

View File

@ -2959,7 +2959,7 @@ doc = ["sphinx", "sphinx_rtd_theme"]
type = "git"
url = "https://github.com/sartography/SpiffWorkflow"
reference = "main"
resolved_reference = "faf859fefcb21eecd6038f32efb48eb19d09ae28"
resolved_reference = "6cf13eb18cd77f17b6c06e99c6756c0dce26675b"
[[package]]
name = "spiffworkflow-connector-command"

View File

@ -2580,6 +2580,41 @@ paths:
schema:
$ref: "#/components/schemas/Workflow"
/message-models:
get:
tags:
- Messages
operationId: spiffworkflow_backend.routes.messages_controller.message_model_list_from_root
summary: Get a list of message models
responses:
"200":
description: A list of message models
content:
application/json:
schema:
$ref: "#/components/schemas/Workflow"
/message-models/{relative_location}:
parameters:
- name: relative_location
in: path
required: true
description: The location of the message model relative to the root of the project, defaults to /
schema:
type: string
get:
tags:
- Messages
operationId: spiffworkflow_backend.routes.messages_controller.message_model_list
summary: Get a list of message models
responses:
"200":
description: A list of message models
content:
application/json:
schema:
$ref: "#/components/schemas/Workflow"
/messages:
parameters:
- name: process_instance_id

View File

@ -106,6 +106,10 @@ from spiffworkflow_backend.models.user_property import (
from spiffworkflow_backend.models.service_account import (
ServiceAccountModel,
) # noqa: F401
from spiffworkflow_backend.models.message_model import (
MessageModel,
MessageCorrelationPropertyModel,
) # noqa: F401
from spiffworkflow_backend.models.future_task import (
FutureTaskModel,
) # noqa: F401

View File

@ -1,23 +0,0 @@
from dataclasses import dataclass
from spiffworkflow_backend.models.db import SpiffworkflowBaseDBModel
from spiffworkflow_backend.models.db import db
@dataclass
class CorrelationPropertyCache(SpiffworkflowBaseDBModel):
"""A list of known correlation properties as read from BPMN files.
This correlation properties are not directly linked to anything
but it provides a way to know what processes are talking about
what messages and correlation keys. And could be useful as an
api endpoint if you wanted to know what another process model
is using.
"""
__tablename__ = "correlation_property_cache"
id = db.Column(db.Integer, primary_key=True)
name: str = db.Column(db.String(50), nullable=False, index=True)
message_name: str = db.Column(db.String(50), nullable=False, index=True)
process_model_id: str = db.Column(db.String(255), nullable=False)
retrieval_expression: str = db.Column(db.String(255))

View File

@ -0,0 +1,44 @@
from __future__ import annotations
from dataclasses import dataclass
from sqlalchemy import ForeignKey
from sqlalchemy import UniqueConstraint
from sqlalchemy.orm import relationship
from spiffworkflow_backend.models.db import SpiffworkflowBaseDBModel
from spiffworkflow_backend.models.db import db
@dataclass
class MessageModel(SpiffworkflowBaseDBModel):
__tablename__ = "message"
__table_args__ = (UniqueConstraint("identifier", "location", name="message_identifier_location_unique"),)
id: int = db.Column(db.Integer, primary_key=True)
identifier: str = db.Column(db.String(255), index=True, nullable=False)
location: str = db.Column(db.String(255), index=True, nullable=False)
schema: dict = db.Column(db.JSON, nullable=False)
updated_at_in_seconds: int = db.Column(db.Integer, nullable=False)
created_at_in_seconds: int = db.Column(db.Integer, nullable=False)
correlation_properties = relationship("MessageCorrelationPropertyModel", cascade="delete")
@dataclass
class MessageCorrelationPropertyModel(SpiffworkflowBaseDBModel):
__tablename__ = "message_correlation_property"
__table_args__ = (UniqueConstraint("message_id", "identifier", name="message_correlation_property_unique"),)
id: int = db.Column(db.Integer, primary_key=True)
message_id: int = db.Column(
ForeignKey(MessageModel.id, name="message_correlation_property_message_id_fk"), # type: ignore
nullable=False,
index=True,
)
identifier: str = db.Column(db.String(255), index=True, nullable=False)
retrieval_expression: str = db.Column(db.String(255), nullable=False)
updated_at_in_seconds: int = db.Column(db.Integer, nullable=False)
created_at_in_seconds: int = db.Column(db.Integer, nullable=False)
message = relationship("MessageModel", back_populates="correlation_properties")

View File

@ -18,6 +18,19 @@ PROCESS_GROUP_SUPPORTED_KEYS_FOR_DISK_SERIALIZATION = [
"display_name",
"description",
"data_store_specifications",
"messages",
"correlation_keys",
"correlation_properties",
]
PROCESS_GROUP_KEYS_TO_UPDATE_FROM_API = [
"display_name",
"description",
"messages",
"data_store_specifications",
"correlation_keys",
"correlation_properties",
]
@ -32,6 +45,9 @@ class ProcessGroup:
process_groups: list[ProcessGroup] = field(default_factory=list["ProcessGroup"])
data_store_specifications: dict[str, Any] = field(default_factory=dict)
parent_groups: list[ProcessGroupLite] | None = None
messages: dict[str, Any] | None = None
correlation_keys: list[dict[str, Any]] | None = None
correlation_properties: list[dict[str, Any]] | None = None
# TODO: delete these once they no no longer mentioned in current
# process_group.json files
@ -62,6 +78,23 @@ class ProcessGroup:
return list(dict_keys)
class MessageSchema(Schema):
class Meta:
fields = ["id", "schema"]
class RetrievalExpressionSchema(Schema):
class Meta:
fields = ["message_ref", "formal_expression"]
class CorrelationPropertySchema(Schema):
class Meta:
fields = ["id", "retrieval_expression"]
retrieval_expression = marshmallow.fields.Nested(RetrievalExpressionSchema, required=False)
class ProcessGroupSchema(Schema):
class Meta:
model = ProcessGroup
@ -71,10 +104,16 @@ class ProcessGroupSchema(Schema):
"process_models",
"description",
"process_groups",
"messages",
"correlation_properties",
]
process_models = marshmallow.fields.List(marshmallow.fields.Nested("ProcessModelInfoSchema", dump_only=True, required=False))
process_groups = marshmallow.fields.List(marshmallow.fields.Nested("ProcessGroupSchema", dump_only=True, required=False))
messages = marshmallow.fields.List(marshmallow.fields.Nested(MessageSchema, dump_only=True, required=False))
correlation_properties = marshmallow.fields.List(
marshmallow.fields.Nested(CorrelationPropertySchema, dump_only=True, required=False)
)
@post_load
def make_process_group(self, data: dict[str, str | bool | int], **kwargs: dict) -> ProcessGroup:

View File

@ -6,10 +6,43 @@ from flask import jsonify
from flask import make_response
from flask.wrappers import Response
from spiffworkflow_backend.models.db import db
from spiffworkflow_backend.models.message_instance import MessageInstanceModel
from spiffworkflow_backend.models.message_model import MessageCorrelationPropertyModel
from spiffworkflow_backend.models.message_model import MessageModel
from spiffworkflow_backend.models.process_instance import ProcessInstanceModel
from spiffworkflow_backend.models.process_instance import ProcessInstanceModelSchema
from spiffworkflow_backend.services.message_service import MessageService
from spiffworkflow_backend.services.upsearch_service import UpsearchService
def message_model_list_from_root() -> flask.wrappers.Response:
return message_model_list()
def message_model_list(relative_location: str | None = None) -> flask.wrappers.Response:
# Returns all the messages and correlation properties that exist at the given
# relative location or higher in the directory tree.
loc = relative_location.replace(":", "/") if relative_location else ""
locations = UpsearchService.upsearch_locations(loc)
messages = db.session.query(MessageModel).filter(MessageModel.location.in_(locations)).order_by(MessageModel.identifier).all() # type: ignore
def correlation_property_response(correlation_property: MessageCorrelationPropertyModel) -> dict[str, str]:
return {
"identifier": correlation_property.identifier,
"retrieval_expression": correlation_property.retrieval_expression,
}
def message_response(message: MessageModel) -> dict[str, Any]:
return {
"identifier": message.identifier,
"location": message.location,
"schema": message.schema,
"correlation_properties": [correlation_property_response(cp) for cp in message.correlation_properties],
}
return make_response(jsonify({"messages": [message_response(m) for m in messages]}), 200)
def message_instance_list(

View File

@ -13,11 +13,14 @@ from spiffworkflow_backend.exceptions.api_error import ApiError
from spiffworkflow_backend.exceptions.error import NotAuthorizedError
from spiffworkflow_backend.exceptions.process_entity_not_found_error import ProcessEntityNotFoundError
from spiffworkflow_backend.models.db import db
from spiffworkflow_backend.models.message_model import MessageModel
from spiffworkflow_backend.models.process_group import PROCESS_GROUP_KEYS_TO_UPDATE_FROM_API
from spiffworkflow_backend.models.process_group import ProcessGroup
from spiffworkflow_backend.models.process_group import ProcessGroupSchema
from spiffworkflow_backend.routes.process_api_blueprint import _commit_and_push_to_git
from spiffworkflow_backend.routes.process_api_blueprint import _un_modify_modified_process_model_id
from spiffworkflow_backend.services.authorization_service import AuthorizationService
from spiffworkflow_backend.services.message_definition_service import MessageDefinitionService
from spiffworkflow_backend.services.process_model_service import ProcessModelService
from spiffworkflow_backend.services.process_model_service import ProcessModelWithInstancesNotDeletableError
from spiffworkflow_backend.services.spec_file_service import SpecFileService
@ -68,9 +71,9 @@ def process_group_delete(modified_process_group_id: str) -> flask.wrappers.Respo
def process_group_update(modified_process_group_id: str, body: dict) -> flask.wrappers.Response:
"""Process Group Update."""
body_include_list = ["display_name", "description"]
body_filtered = {include_item: body[include_item] for include_item in body_include_list if include_item in body}
body_filtered = {
include_item: body[include_item] for include_item in PROCESS_GROUP_KEYS_TO_UPDATE_FROM_API if include_item in body
}
process_group_id = _un_modify_modified_process_model_id(modified_process_group_id)
if not ProcessModelService.is_process_group_identifier(process_group_id):
@ -82,6 +85,14 @@ def process_group_update(modified_process_group_id: str, body: dict) -> flask.wr
process_group = ProcessGroup(id=process_group_id, **body_filtered)
ProcessModelService.update_process_group(process_group)
all_message_models: dict[tuple[str, str], MessageModel] = {}
MessageDefinitionService.collect_message_models(process_group, process_group_id, all_message_models)
MessageDefinitionService.delete_message_models_at_location(process_group_id)
db.session.commit()
MessageDefinitionService.save_all_message_models(all_message_models)
db.session.commit()
_commit_and_push_to_git(f"User: {g.user.username} updated process group {process_group_id}")
return make_response(jsonify(process_group), 200)

View File

@ -59,6 +59,7 @@ PATH_SEGMENTS_FOR_PERMISSION_ALL = [
{"path": "/event-error-details", "relevant_permissions": ["read"]},
{"path": "/logs", "relevant_permissions": ["read"]},
{"path": "/logs/typeahead-filter-values", "relevant_permissions": ["read"]},
{"path": "/message-models", "relevant_permissions": ["read"]},
{
"path": "/process-instances",
"relevant_permissions": ["create", "read", "delete"],

View File

@ -8,8 +8,11 @@ from spiffworkflow_backend.data_stores.kkv import KKVDataStore
from spiffworkflow_backend.models.db import db
from spiffworkflow_backend.models.json_data_store import JSONDataStoreModel
from spiffworkflow_backend.models.kkv_data_store import KKVDataStoreModel
from spiffworkflow_backend.models.message_model import MessageModel
from spiffworkflow_backend.models.process_group import ProcessGroup
from spiffworkflow_backend.models.reference_cache import ReferenceCacheModel
from spiffworkflow_backend.services.file_system_service import FileSystemService
from spiffworkflow_backend.services.message_definition_service import MessageDefinitionService
from spiffworkflow_backend.services.process_model_service import ProcessModelService
from spiffworkflow_backend.services.reference_cache_service import ReferenceCacheService
from spiffworkflow_backend.services.spec_file_service import SpecFileService
@ -33,6 +36,7 @@ class DataSetupService:
files = FileSystemService.walk_files_from_root_path(True, None)
reference_objects: dict[str, ReferenceCacheModel] = {}
all_data_store_specifications: dict[tuple[str, str, str], Any] = {}
all_message_models: dict[tuple[str, str], MessageModel] = {}
references = []
for file in files:
@ -42,7 +46,6 @@ class DataSetupService:
try:
# FIXME: get_references_for_file_contents is erroring out for elements in the list
refs = SpecFileService.get_references_for_process(process_model)
for ref in refs:
try:
reference_cache = ReferenceCacheModel.from_spec_reference(ref)
@ -80,29 +83,19 @@ class DataSetupService:
elif FileSystemService.is_process_group_json_file(file):
try:
process_group = ProcessModelService.find_or_create_process_group(os.path.dirname(file))
cls._collect_data_store_specifications(process_group, file, all_data_store_specifications)
MessageDefinitionService.collect_message_models(process_group, process_group.id, all_message_models)
except Exception:
current_app.logger.debug(f"Failed to load process group from file @ '{file}'")
continue
for data_store_type, specs_by_id in process_group.data_store_specifications.items():
if not isinstance(specs_by_id, dict):
current_app.logger.debug(f"Expected dictionary as value for key '{data_store_type}' in file @ '{file}'")
continue
for identifier, specification in specs_by_id.items():
location = specification.get("location")
if location is None:
current_app.logger.debug(
f"Location missing from data store specification '{identifier}' in file @ '{file}'"
)
continue
all_data_store_specifications[(data_store_type, location, identifier)] = specification
current_app.logger.debug("DataSetupService.save_all_process_models() end")
ReferenceCacheService.add_new_generation(reference_objects)
cls._sync_data_store_models_with_specifications(all_data_store_specifications)
MessageDefinitionService.delete_all_message_models()
db.session.commit()
MessageDefinitionService.save_all_message_models(all_message_models)
db.session.commit()
for ref in references:
try:
@ -118,6 +111,25 @@ class DataSetupService:
return failing_process_models
@classmethod
def _collect_data_store_specifications(
cls, process_group: ProcessGroup, file_name: str, all_data_store_specifications: dict[tuple[str, str, str], Any]
) -> None:
for data_store_type, specs_by_id in process_group.data_store_specifications.items():
if not isinstance(specs_by_id, dict):
current_app.logger.debug(f"Expected dictionary as value for key '{data_store_type}' in file @ '{file_name}'")
continue
for identifier, specification in specs_by_id.items():
location = specification.get("location")
if location is None:
current_app.logger.debug(
f"Location missing from data store specification '{identifier}' in file @ '{file_name}'"
)
continue
all_data_store_specifications[(data_store_type, location, identifier)] = specification
@classmethod
def _sync_data_store_models_with_specifications(cls, all_data_store_specifications: dict[tuple[str, str, str], Any]) -> None:
all_data_store_models: dict[tuple[str, str, str], Any] = {}
@ -208,5 +220,3 @@ class DataSetupService:
current_app.logger.debug(f"DataSetupService: was expecting key '{key}' to point to a data store model to delete.")
continue
db.session.delete(model)
db.session.commit()

View File

@ -0,0 +1,74 @@
from typing import Any
from flask import current_app
from spiffworkflow_backend.models.db import db
from spiffworkflow_backend.models.message_model import MessageCorrelationPropertyModel
from spiffworkflow_backend.models.message_model import MessageModel
from spiffworkflow_backend.models.process_group import ProcessGroup
class MessageDefinitionService:
@classmethod
def _message_model_from_message(
cls, identifier: str, message_definition: dict[str, Any], process_group: ProcessGroup
) -> MessageModel | None:
schema = message_definition.get("schema", "{}")
return MessageModel(identifier=identifier, location=process_group.id, schema=schema)
@classmethod
def _correlation_property_models_from_message_definition(
cls, correlation_property_group: dict[str, Any], location: str
) -> list[MessageCorrelationPropertyModel]:
models: list[MessageCorrelationPropertyModel] = []
for identifier, definition in correlation_property_group.items():
retrieval_expressions = definition.get("retrieval_expressions")
if not retrieval_expressions:
current_app.logger.debug(f"Malformed correlation property: '{identifier}' in file @ '{location}'")
continue
for retrieval_expression in retrieval_expressions:
models.append(MessageCorrelationPropertyModel(identifier=identifier, retrieval_expression=retrieval_expression))
return models
@classmethod
def collect_message_models(
cls, process_group: ProcessGroup, location: str, all_message_models: dict[tuple[str, str], MessageModel]
) -> None:
messages: dict[str, Any] = process_group.messages or {}
for message_identifier, message_definition in messages.items():
message_model = cls._message_model_from_message(message_identifier, message_definition, process_group)
if message_model is None:
continue
all_message_models[(message_model.identifier, message_model.location)] = message_model
correlation_property_models = cls._correlation_property_models_from_message_definition(
message_definition.get("correlation_properties", {}), location
)
message_model.correlation_properties = correlation_property_models # type: ignore
@classmethod
def delete_all_message_models(cls) -> None:
messages = MessageModel.query.all()
for message in messages:
db.session.delete(message)
@classmethod
def delete_message_models_at_location(cls, location: str) -> None:
messages = MessageModel.query.filter_by(location=location).all()
for message in messages:
db.session.delete(message)
@classmethod
def save_all_message_models(cls, all_message_models: dict[tuple[str, str], MessageModel]) -> None:
for message_model in all_message_models.values():
db.session.add(message_model)
for correlation_property_model in message_model.correlation_properties:
db.session.add(correlation_property_model)

View File

@ -9,7 +9,6 @@ from lxml import etree # type: ignore
from SpiffWorkflow.bpmn.parser.BpmnParser import BpmnValidator # type: ignore
from spiffworkflow_backend.exceptions.error import NotAuthorizedError
from spiffworkflow_backend.models.correlation_property_cache import CorrelationPropertyCache
from spiffworkflow_backend.models.db import db
from spiffworkflow_backend.models.file import File
from spiffworkflow_backend.models.file import FileType
@ -255,7 +254,6 @@ class SpecFileService(FileSystemService):
def update_caches_except_process(ref: Reference) -> None:
SpecFileService.update_process_caller_cache(ref)
SpecFileService.update_message_trigger_cache(ref)
SpecFileService.update_correlation_cache(ref)
@staticmethod
def clear_caches_for_item(
@ -336,28 +334,3 @@ class SpecFileService(FileSystemService):
current_triggerable_processes.remove(message_triggerable_process_model)
for trigger_pm in current_triggerable_processes:
db.session.delete(trigger_pm)
@staticmethod
def update_correlation_cache(ref: Reference) -> None:
for name in ref.correlations.keys():
correlation_property_retrieval_expressions = ref.correlations[name]["retrieval_expressions"]
for cpre in correlation_property_retrieval_expressions:
message_name = ref.messages.get(cpre["messageRef"], None)
retrieval_expression = cpre["expression"]
process_model_id = ref.relative_location
existing = CorrelationPropertyCache.query.filter_by(
name=name,
message_name=message_name,
process_model_id=process_model_id,
retrieval_expression=retrieval_expression,
).first()
if existing is None:
new_cache = CorrelationPropertyCache(
name=name,
message_name=message_name,
process_model_id=process_model_id,
retrieval_expression=retrieval_expression,
)
db.session.add(new_cache)

View File

@ -2,9 +2,6 @@
<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">
<bpmn:participant id="message_receiver" name="Message Receiver (invoice approver)" processRef="message_receiver_process" />
<bpmn:participant id="message_sender" name="Message Sender" />
<bpmn:messageFlow id="message_send_flow" name="Message Send Flow" sourceRef="message_sender" targetRef="receive_message" />
<bpmn:messageFlow id="Flow_0ds946g" sourceRef="send_message_response" targetRef="message_sender" />
<bpmn:correlationKey name="invoice">
<bpmn:correlationPropertyRef>customer_id</bpmn:correlationPropertyRef>
<bpmn:correlationPropertyRef>po_number</bpmn:correlationPropertyRef>

View File

@ -2,15 +2,6 @@
<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">
<bpmn:participant id="message_initiator" name="Message Initiator" processRef="message_send_process" />
<bpmn:participant id="message-receiver" name="Message Receiver" />
<bpmn:messageFlow id="message_send_flow" name="Message Send Flow" sourceRef="send_message" targetRef="message-receiver" />
<bpmn:messageFlow id="message_response_flow" name="Message Response Flow" sourceRef="message-receiver" targetRef="receive_message_response" />
<bpmn:textAnnotation id="TextAnnotation_0oxbpew">
<bpmn:text>The messages sent here are about an Invoice that can be uniquely identified by the customer_id ("sartography") and a purchase order number (1001)
It will fire a message connected to the invoice keys above, starting another process, which can communicate back to this specific process instance using the correct key.</bpmn:text>
</bpmn:textAnnotation>
<bpmn:association id="Association_1d6q7zd" sourceRef="message_initiator" targetRef="TextAnnotation_0oxbpew" />
<bpmn:correlationKey name="invoice">
<bpmn:correlationPropertyRef>po_number</bpmn:correlationPropertyRef>
<bpmn:correlationPropertyRef>customer_id</bpmn:correlationPropertyRef>

View File

@ -0,0 +1,2 @@
# Creating Content
This section will explain how we combine **Markdown** and **Jinja** to make it easy to display content to participants in a workflow.

View File

@ -0,0 +1,41 @@
{
"title": "A Simple Form",
"description": "Provide a little information, and we will display it back to you in a second",
"properties": {
"first_name": {
"type": "string",
"title": "First Name"
},
"is_joke": {
"type": "boolean",
"title": "Would you like to hear a Chuck Norris joke?"
},
"color": {
"type": "string",
"title": "What is your favorite color?",
"enum": [
"Midnight Blue",
"Crimson Red",
"Emerald Green",
"Sunflower Yellow",
"Mauve Mist",
"Mint Ice",
"Burnt Sienna",
"Powder Pink",
"Royal Purple",
"Peach Fuzz",
"Goldenrod",
"Turquoise Splash",
"Tangerine Tango",
"Cerulean Sky",
"Sagebrush Green",
"Lavender Blush",
"Cocoa Bean Brown",
"Teal Lagoon",
"Electric Indigo",
"Antique Brass"
]
}
},
"required": ["first_name","is_joke","color"]
}

View File

@ -0,0 +1,13 @@
{
"is_joke": {
"ui:widget": "radio",
"ui:options": {
"inline": true
}
},
"ui:order": [
"first_name",
"is_joke",
"color"
]
}

View File

@ -0,0 +1,9 @@
{
"description": "This process demonstrates how you can display content to the user.",
"display_name": "Displaying Content",
"exception_notification_addresses": [],
"fault_or_suspend_on_exception": "fault",
"metadata_extraction_paths": null,
"primary_file_name": "show-content.bpmn",
"primary_process_id": "Process_puxk39c"
}

View File

@ -0,0 +1,220 @@
<?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:process id="Process_puxk39c" isExecutable="true">
<bpmn:startEvent id="StartEvent_1">
<bpmn:outgoing>Flow_1wvtd9f</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:sequenceFlow id="Flow_1wvtd9f" sourceRef="StartEvent_1" targetRef="Activity_1f9v5wa" />
<bpmn:endEvent id="Event_1wckt3w">
<bpmn:extensionElements>
<spiffworkflow:instructionsForEndUser>### Checkout the BPMN!
Open up this diagram in edit mode where you can move things around and view task properties, but you won't be able to save these demos. To do this, click on "Displaying Content" in the Breadcrumbs above, then open the file "show-content.bpmn". Click on the manual tasks and will see they each have content in their "Instructions" in the properties panel on the right. You can open these instructions in the markdown editor by clicking on the "Launch Editor" button which provides a great way to see how Markdown and Jinja can be used to create content.
### Build a BPMN process yourself!
Complete the [Request a Playground](/process-models/examples:0-4-request-permissions) task to get access to your own private area of this Demo SpiffWorkflow instance. There you can build your own documentation!
### Get Involved!
Please get in touch with us! We would love to help you build beautiful documentation into your workflows. There is no end to what we can accomplish if we work together. Please reach out to Dan at [dan@sartography.com](mailto:dan@sartography.com) to get started.
### What's Next?
Check out the some of the other [basic examples](/process-groups/examples:1-basic-concepts) to learn more.
</spiffworkflow:instructionsForEndUser>
</bpmn:extensionElements>
<bpmn:incoming>Flow_1thoqro</bpmn:incoming>
</bpmn:endEvent>
<bpmn:sequenceFlow id="Flow_0r9coud" sourceRef="Activity_1f9v5wa" targetRef="Activity_1r4u2ny" />
<bpmn:manualTask id="Activity_1f9v5wa" name="Introduction">
<bpmn:extensionElements>
<spiffworkflow:instructionsForEndUser>We use two open standards for displaying content.
### 1. Markdown
Markdown provides some minimal tools for formatting text. It can handle basic formatting from **bold text** to headings, tables, lists, etc... It is not the most expressive possible way of writing content, but it does help standardize your documentation and assure it can be easily updated by anyone else. The [Markdown Guilde](https://www.markdownguide.org/getting-started/) provides excellent information on the standard, how to use it, and why it is useful.
### 2. Jinja
As a workflow process runs, it collects information from many sources. User Tasks collect information from Web Forms, Service Tasks can pull information from other applications - these are just two examples of many. This data is collected and can be used in later tasks in the process. One way to use the information is to display it, which is where Jinja comes into play. While there is a lot that can be done in Jinja, you can go a very long way knowing one simple rule:
&gt; Wrap variables you want to display in double curly brackets like this \{\{my_variable\}\}
This will allow you to take any information you have available and present it back to the end user. You can learn more about the Jinja syntax in the [official documentation](https://jinja.palletsprojects.com/en/3.1.x/templates/). It isn't just variable replacement. You can use it to hide information under certain circumstances, or display lots of data in a list or table. It is very powerful.
## Whats Next:
Let's complete a simple form so you can enter some information. We'll also run a script in the background that will generate some additional variables. We'll take this data, and present it in another manual task.</spiffworkflow:instructionsForEndUser>
</bpmn:extensionElements>
<bpmn:incoming>Flow_1wvtd9f</bpmn:incoming>
<bpmn:outgoing>Flow_0r9coud</bpmn:outgoing>
</bpmn:manualTask>
<bpmn:sequenceFlow id="Flow_0796xeb" sourceRef="Activity_1r4u2ny" targetRef="Activity_0pdnphi" />
<bpmn:userTask id="Activity_1r4u2ny" name="A Simple Form">
<bpmn:extensionElements>
<spiffworkflow:instructionsForEndUser>Please complete the simple form below. We'll show you these values in the next manual task.</spiffworkflow:instructionsForEndUser>
<spiffworkflow:properties>
<spiffworkflow:property name="formJsonSchemaFilename" value="a-simple-form-schema.json" />
<spiffworkflow:property name="formUiSchemaFilename" value="a-simple-form-uischema.json" />
</spiffworkflow:properties>
</bpmn:extensionElements>
<bpmn:incoming>Flow_0r9coud</bpmn:incoming>
<bpmn:outgoing>Flow_0796xeb</bpmn:outgoing>
</bpmn:userTask>
<bpmn:sequenceFlow id="Flow_0wr8gfr" sourceRef="Activity_0pdnphi" targetRef="Activity_0l7raxy" />
<bpmn:sequenceFlow id="Flow_1thoqro" sourceRef="Activity_1jr7x9o" targetRef="Event_1wckt3w" />
<bpmn:scriptTask id="Activity_0pdnphi" name="Create more content">
<bpmn:extensionElements>
<spiffworkflow:instructionsForEndUser>Generating some colors for you, give us just a second ....</spiffworkflow:instructionsForEndUser>
</bpmn:extensionElements>
<bpmn:incoming>Flow_0796xeb</bpmn:incoming>
<bpmn:outgoing>Flow_0wr8gfr</bpmn:outgoing>
<bpmn:script>time.sleep(3)
colors = {
"Ninja Black": "#1E1E1E",
"Zombie Green": "#6ACD58",
"Toasted Marshmallow": "#F9E5C6",
"Alien Armpit": "#84D314",
"Granny Smith Apple Accident": "#A8E4A0",
"Teleportation Blue": "#7FDBFF",
"Invisible Pink Unicorn": "#FF77A9",
"Flamingo Fandango": "#F6A8D6",
"Laser Lemon": "#FFFF66",
"Yeti Frostbite": "#D5F2E3"
}</bpmn:script>
</bpmn:scriptTask>
<bpmn:manualTask id="Activity_1jr7x9o" name="Displaying Data">
<bpmn:extensionElements>
<spiffworkflow:instructionsForEndUser>We just did two things:
1. Completed a form, which allowed us to collect some information from you.
2. We ran a script, that created a list of colors for us to display.
Now we can present that information back to you using Markdown and Jinja .....
### Form Data
You provided the following information the form:
**Your Name**: {{first_name}}
**Favorite Color**: {{color}}
{% if is_joke %}
You said you would like to hear a joke about Chuck Norris, so here you go:
&gt; When Chuck Norris falls into the water, Chuck Norris doesnt get wet. Water gets Chuck Norris.
{% else %}
You said you didn't want to year a joke about Chuck Norris, so here is a joke that does not include old Chuck.
&gt; What is an astronaut's favorite part on a computer? The space bar.
{% endif %}
### Automatic Task Messages
We also created a list of silly color names in a script task. You should have seen that flash by as it was happening. We intentionally made it slow (we ask Python to sleep for 2 seconds), so you could see that even automatic tasks (if they take a long time) can be configured to display a message as they run in the background. Here is the list of colors we created rendered as a table :
| Color Name | Color Hex Value | Color |
|----------|-----------|-----|
| Blue | #0000ff | &lt;div style="background-color:blue; color:blue"&gt; blue &lt;/div&gt; |
{%- for name, hex in colors.items() %}
| {{name}} | {{hex}} | &lt;div style="background-color:{{hex}}; color:{{hex}}"&gt; {{name}} &lt;/div&gt; |
{%- endfor %}
</spiffworkflow:instructionsForEndUser>
</bpmn:extensionElements>
<bpmn:incoming>Flow_0fdwa26</bpmn:incoming>
<bpmn:outgoing>Flow_1thoqro</bpmn:outgoing>
</bpmn:manualTask>
<bpmn:sequenceFlow id="Flow_1rgip89" sourceRef="Activity_0l7raxy" targetRef="Activity_0jfo38l" />
<bpmn:sequenceFlow id="Flow_0fdwa26" sourceRef="Activity_0jfo38l" targetRef="Activity_1jr7x9o" />
<bpmn:scriptTask id="Activity_0l7raxy" name="More processing">
<bpmn:extensionElements>
<spiffworkflow:instructionsForEndUser>Making the colors more silly ...</spiffworkflow:instructionsForEndUser>
</bpmn:extensionElements>
<bpmn:incoming>Flow_0wr8gfr</bpmn:incoming>
<bpmn:outgoing>Flow_1rgip89</bpmn:outgoing>
<bpmn:script>time.sleep(1)</bpmn:script>
</bpmn:scriptTask>
<bpmn:scriptTask id="Activity_0jfo38l" name="Even More Processing">
<bpmn:extensionElements>
<spiffworkflow:instructionsForEndUser>Turning the silly level all the way up to 11, almost done ....</spiffworkflow:instructionsForEndUser>
</bpmn:extensionElements>
<bpmn:incoming>Flow_1rgip89</bpmn:incoming>
<bpmn:outgoing>Flow_0fdwa26</bpmn:outgoing>
<bpmn:script>time.sleep(2)</bpmn:script>
</bpmn:scriptTask>
<bpmn:textAnnotation id="TextAnnotation_05wkdje">
<bpmn:text>These three script tasks would execute nearly instantly, we are forcing them to run slowly (asking python to go to sleep for a few seconds) to demonstrate long running background tasks, so you can see how to let your end users know what is going on when you are doing complex things in the background.</bpmn:text>
</bpmn:textAnnotation>
<bpmn:association id="Association_1cnnzc4" sourceRef="Activity_0l7raxy" targetRef="TextAnnotation_05wkdje" />
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_puxk39c">
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
<dc:Bounds x="-38" y="159" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Event_1wckt3w_di" bpmnElement="Event_1wckt3w">
<dc:Bounds x="932" y="159" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_0rez9jv_di" bpmnElement="Activity_1f9v5wa">
<dc:Bounds x="50" y="137" width="100" height="80" />
<bpmndi:BPMNLabel />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_0i1lx1p_di" bpmnElement="Activity_1r4u2ny">
<dc:Bounds x="200" y="137" width="100" height="80" />
<bpmndi:BPMNLabel />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_0atremz_di" bpmnElement="Activity_0pdnphi">
<dc:Bounds x="360" y="137" width="100" height="80" />
<bpmndi:BPMNLabel />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_0q99tkd_di" bpmnElement="Activity_1jr7x9o">
<dc:Bounds x="800" y="137" width="100" height="80" />
<bpmndi:BPMNLabel />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_0e1jkdk_di" bpmnElement="Activity_0l7raxy">
<dc:Bounds x="500" y="137" width="100" height="80" />
<bpmndi:BPMNLabel />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_072c998_di" bpmnElement="Activity_0jfo38l">
<dc:Bounds x="650" y="137" width="100" height="80" />
<bpmndi:BPMNLabel />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="TextAnnotation_05wkdje_di" bpmnElement="TextAnnotation_05wkdje">
<dc:Bounds x="420" y="0" width="320" height="98" />
<bpmndi:BPMNLabel />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="Flow_1wvtd9f_di" bpmnElement="Flow_1wvtd9f">
<di:waypoint x="-2" y="177" />
<di:waypoint x="50" y="177" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0r9coud_di" bpmnElement="Flow_0r9coud">
<di:waypoint x="150" y="177" />
<di:waypoint x="200" y="177" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0796xeb_di" bpmnElement="Flow_0796xeb">
<di:waypoint x="300" y="177" />
<di:waypoint x="360" y="177" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0wr8gfr_di" bpmnElement="Flow_0wr8gfr">
<di:waypoint x="460" y="177" />
<di:waypoint x="500" y="177" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1thoqro_di" bpmnElement="Flow_1thoqro">
<di:waypoint x="900" y="177" />
<di:waypoint x="932" y="177" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1rgip89_di" bpmnElement="Flow_1rgip89">
<di:waypoint x="600" y="177" />
<di:waypoint x="650" y="177" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0fdwa26_di" bpmnElement="Flow_0fdwa26">
<di:waypoint x="750" y="177" />
<di:waypoint x="800" y="177" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Association_1cnnzc4_di" bpmnElement="Association_1cnnzc4">
<di:waypoint x="530" y="137" />
<di:waypoint x="511" y="98" />
</bpmndi:BPMNEdge>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>

View File

@ -0,0 +1,5 @@
{
"description": "These process models are the basic building blocks of our system. Understand these individually, then put them together anyway you can imagine.",
"display_name": "1. Basics",
"messages": {"basic_message": { "schema": {} } }
}

View File

@ -0,0 +1,36 @@
{
"admin": false,
"description": "These examples are provided to help you understand how SpiffWorkflow functions.",
"display_name": "Examples",
"display_order": 40,
"parent_groups": null,
"messages": {
"table_seated": {
"correlation_properties": {
"table_number": {
"retrieval_expressions": ["table_number"]
},
"franchise_id": {
"retrieval_expressions": ["franchise_id"]
}
},
"schema": {}
},
"order_ready": {
"correlation_properties": {
"table_number": {
"retrieval_expressions": ["table_number"]
},
"franchise_id": {
"retrieval_expressions": ["franchise_id"]
}
},
"schema": {}
},
"end_of_day_receipts": {
"schema": {}
}
}
}

View File

@ -1,6 +1,7 @@
import io
import json
import os
import shutil
import time
from collections.abc import Generator
from contextlib import contextmanager
@ -578,6 +579,12 @@ class BaseTest:
finally:
app.config[config_identifier] = initial_value
@staticmethod
def copy_example_process_models() -> None:
source = os.path.abspath(os.path.join(FileSystemService.root_path(), "..", "..", "..", "process_models_example_dir"))
destination = current_app.config["SPIFFWORKFLOW_BACKEND_BPMN_SPEC_ABSOLUTE_DIR"]
shutil.copytree(source, destination)
def round_last_state_change(self, bpmn_process_dict: dict | list) -> None:
"""Round last state change to the nearest 4 significant digits.

View File

@ -1,10 +1,14 @@
import json
import pytest
from flask import Flask
from flask import g
from flask.testing import FlaskClient
from spiffworkflow_backend.exceptions.api_error import ApiError
from spiffworkflow_backend.models.message_instance import MessageInstanceModel
from spiffworkflow_backend.models.user import UserModel
from spiffworkflow_backend.routes.messages_controller import message_send
from spiffworkflow_backend.services.data_setup_service import DataSetupService
from tests.spiffworkflow_backend.helpers.base_test import BaseTest
@ -66,3 +70,118 @@ class TestMessages(BaseTest):
assert len(waiting_messages) == 0
# The process has completed
assert process_instance.status == "complete"
def test_message_model_list_up_search(
self,
app: Flask,
client: FlaskClient,
with_db_and_bpmn_file_cleanup: None,
with_super_admin_user: UserModel,
) -> None:
self.copy_example_process_models()
DataSetupService.save_all_process_models()
response = client.get(
"/v1.0/message-models/examples:1-basic-concepts",
headers=self.logged_in_headers(with_super_admin_user),
content_type="application/json",
)
assert response.status_code == 200
assert response.json is not None
assert len(response.json["messages"]) == 4
response = client.get(
"/v1.0/message-models",
headers=self.logged_in_headers(with_super_admin_user),
content_type="application/json",
)
assert response.status_code == 200
assert response.json is not None
assert len(response.json["messages"]) == 0, "should not have access to messages defined in a sub directory"
def test_process_group_update_syncs_message_models(
self,
app: Flask,
client: FlaskClient,
with_db_and_bpmn_file_cleanup: None,
with_super_admin_user: UserModel,
) -> None:
self.create_process_group("bob")
response = client.get(
"/v1.0/message-models/bob",
headers=self.logged_in_headers(with_super_admin_user),
content_type="application/json",
)
assert response.status_code == 200
assert response.json is not None
assert len(response.json["messages"]) == 0
process_group = {
"admin": False,
"description": "Bob's Group",
"display_name": "Bob",
"display_order": 40,
"parent_groups": None,
"messages": {
"table_seated": {
"correlation_properties": {
"table_number": {
"retrieval_expressions": ["table_number"],
},
"franchise_id": {
"retrieval_expressions": ["franchise_id"],
},
},
"schema": {},
},
"order_ready": {
"correlation_properties": {
"table_number": {
"retrieval_expressions": ["table_number"],
},
"franchise_id": {
"retrieval_expressions": ["franchise_id"],
},
},
"schema": {},
},
"end_of_day_receipts": {
"schema": {},
},
},
}
response = client.put(
"/v1.0/process-groups/bob",
headers=self.logged_in_headers(with_super_admin_user),
content_type="application/json",
data=json.dumps(process_group),
)
assert response.status_code == 200
response = client.get(
"/v1.0/message-models/bob",
headers=self.logged_in_headers(with_super_admin_user),
content_type="application/json",
)
assert response.status_code == 200
assert response.json is not None
assert "messages" in response.json
messages = response.json["messages"]
expected_message_identifiers = {"table_seated", "order_ready", "end_of_day_receipts"}
assert len(messages) == len(expected_message_identifiers)
expected_correlation_properties = {
"table_seated": {"table_number": "table_number", "franchise_id": "franchise_id"},
"order_ready": {"table_number": "table_number", "franchise_id": "franchise_id"},
"end_of_day_receipts": {},
}
for message in messages:
assert message["identifier"] in expected_message_identifiers
assert message["location"] == "bob"
assert message["schema"] == {}
cp = {p["identifier"]: p["retrieval_expression"] for p in message["correlation_properties"]}
assert cp == expected_correlation_properties[message["identifier"]]

View File

@ -98,6 +98,7 @@ class TestAuthorizationService(BaseTest):
("/event-error-details/some-process-group:some-process-model:*", "read"),
("/logs/some-process-group:some-process-model:*", "read"),
("/logs/typeahead-filter-values/some-process-group:some-process-model:*", "read"),
("/message-models/some-process-group:some-process-model:*", "read"),
("/process-data/some-process-group:some-process-model:*", "read"),
(
"/process-data-file-download/some-process-group:some-process-model:*",
@ -189,6 +190,7 @@ class TestAuthorizationService(BaseTest):
"read",
),
("/logs/typeahead-filter-values/some-process-group:some-process-model/*", "read"),
("/message-models/some-process-group:some-process-model/*", "read"),
("/process-data/some-process-group:some-process-model/*", "read"),
(
"/process-instance-suspend/some-process-group:some-process-model/*",

View File

@ -0,0 +1,45 @@
from flask.app import Flask
from flask.testing import FlaskClient
from spiffworkflow_backend.models.message_model import MessageModel
from spiffworkflow_backend.models.reference_cache import ReferenceCacheModel
from spiffworkflow_backend.services.data_setup_service import DataSetupService
from tests.spiffworkflow_backend.helpers.base_test import BaseTest
class TestDataSetupService(BaseTest):
def test_data_setup_service_finds_models(
self,
app: Flask,
client: FlaskClient,
with_db_and_bpmn_file_cleanup: None,
) -> None:
self.copy_example_process_models()
DataSetupService.save_all_process_models()
cache = ReferenceCacheModel.query.filter(ReferenceCacheModel.type == "process").all()
assert len(cache) == 1
def test_data_setup_service_finds_messages(
self,
app: Flask,
client: FlaskClient,
with_db_and_bpmn_file_cleanup: None,
) -> None:
self.copy_example_process_models()
DataSetupService.save_all_process_models()
message_models = MessageModel.query.all()
assert len(message_models) == 4
message_map = {model.identifier: model for model in message_models}
assert "table_seated" in message_map
assert "order_ready" in message_map
assert "end_of_day_receipts" in message_map
assert "basic_message" in message_map
assert message_map["order_ready"].location == "examples"
correlations = {cp.identifier: cp.retrieval_expression for cp in message_map["order_ready"].correlation_properties}
assert correlations == {"table_number": "table_number", "franchise_id": "franchise_id"}
assert message_map["basic_message"].location == "examples/1-basic-concepts"
assert message_map["basic_message"].correlation_properties == []

View File

@ -98,6 +98,7 @@
"eslint-plugin-unused-imports": "^3.2.0",
"inherits-browser": "^0.0.1",
"jsdom": "^24.0.0",
"nice-select2": "^2.1.0",
"prettier": "^3.3.0",
"safe-regex": "^2.1.1",
"tiny-svg": "^2.2.3",
@ -7668,7 +7669,7 @@
},
"node_modules/bpmn-js-spiffworkflow": {
"version": "0.0.8",
"resolved": "git+ssh://git@github.com/sartography/bpmn-js-spiffworkflow.git#38f5b5d7563f24608e7b3b177547e273f7cc94a0",
"resolved": "git+ssh://git@github.com/sartography/bpmn-js-spiffworkflow.git#ee86b31cd7fc6f42f92f56171a9945a8ff22773a",
"license": "LGPL",
"dependencies": {
"inherits": "^2.0.4",
@ -7676,6 +7677,7 @@
"min-dash": "^3.8.1",
"min-dom": "^3.2.1",
"moddle": "^5.0.3",
"nice-select2": "^2.1.0",
"react": "^18.2.0",
"react-dom": "18.2.0",
"tiny-svg": "^2.2.3"
@ -9129,6 +9131,23 @@
"resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.5.tgz",
"integrity": "sha512-+BO9wPPi+DWTDcNYhr/W90myha8ptzftZT+LwcmUbbok0rcP/fequmFYCw8NMoH7pkAZQzU78b3kYrlua5a9eA=="
},
"node_modules/cross-env": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz",
"integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==",
"dependencies": {
"cross-spawn": "^7.0.1"
},
"bin": {
"cross-env": "src/bin/cross-env.js",
"cross-env-shell": "src/bin/cross-env-shell.js"
},
"engines": {
"node": ">=10.14",
"npm": ">=6",
"yarn": ">=1"
}
},
"node_modules/cross-spawn": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
@ -9150,6 +9169,70 @@
"node": ">=4"
}
},
"node_modules/css-loader": {
"version": "6.11.0",
"resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.11.0.tgz",
"integrity": "sha512-CTJ+AEQJjq5NzLga5pE39qdiSV56F8ywCIsqNIRF0r7BDgWsN25aazToqAFg7ZrtA/U016xudB3ffgweORxX7g==",
"dependencies": {
"icss-utils": "^5.1.0",
"postcss": "^8.4.33",
"postcss-modules-extract-imports": "^3.1.0",
"postcss-modules-local-by-default": "^4.0.5",
"postcss-modules-scope": "^3.2.0",
"postcss-modules-values": "^4.0.0",
"postcss-value-parser": "^4.2.0",
"semver": "^7.5.4"
},
"engines": {
"node": ">= 12.13.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/webpack"
},
"peerDependencies": {
"@rspack/core": "0.x || 1.x",
"webpack": "^5.0.0"
},
"peerDependenciesMeta": {
"@rspack/core": {
"optional": true
},
"webpack": {
"optional": true
}
}
},
"node_modules/css-loader/node_modules/lru-cache": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
"dependencies": {
"yallist": "^4.0.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/css-loader/node_modules/semver": {
"version": "7.6.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz",
"integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==",
"dependencies": {
"lru-cache": "^6.0.0"
},
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/css-loader/node_modules/yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
},
"node_modules/css-selector-parser": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/css-selector-parser/-/css-selector-parser-1.4.1.tgz",
@ -9172,6 +9255,17 @@
"resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz",
"integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg=="
},
"node_modules/cssesc": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
"integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
"bin": {
"cssesc": "bin/cssesc"
},
"engines": {
"node": ">=4"
}
},
"node_modules/cssstyle": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.0.1.tgz",
@ -12045,8 +12139,7 @@
"node_modules/fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
"devOptional": true
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="
},
"node_modules/fsevents": {
"version": "2.3.3",
@ -12224,7 +12317,6 @@
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"devOptional": true,
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
@ -12890,6 +12982,17 @@
"node": ">=0.10.0"
}
},
"node_modules/icss-utils": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz",
"integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==",
"engines": {
"node": "^10 || ^12 || >= 14"
},
"peerDependencies": {
"postcss": "^8.1.0"
}
},
"node_modules/ids": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/ids/-/ids-1.0.5.tgz",
@ -13027,7 +13130,6 @@
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
"devOptional": true,
"dependencies": {
"once": "^1.3.0",
"wrappy": "1"
@ -13071,6 +13173,14 @@
"node": ">= 0.4"
}
},
"node_modules/interpret": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz",
"integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==",
"engines": {
"node": ">= 0.10"
}
},
"node_modules/invariant": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
@ -19119,6 +19229,16 @@
"integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
"dev": true
},
"node_modules/nice-select2": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/nice-select2/-/nice-select2-2.2.0.tgz",
"integrity": "sha512-UVlqc/ReJPFnQhESbbJrFHCjVe74OWrewh/8w2A+zLlJ98BI4FVDVALV1tOfYCmhNcZtH4L1lQispCUIWX11nQ==",
"dependencies": {
"cross-env": "^7.0.3",
"css-loader": "^6.7.3",
"shx": "^0.3.4"
}
},
"node_modules/node-dir": {
"version": "0.1.17",
"resolved": "https://registry.npmjs.org/node-dir/-/node-dir-0.1.17.tgz",
@ -19810,7 +19930,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
"devOptional": true,
"engines": {
"node": ">=0.10.0"
}
@ -19975,6 +20094,73 @@
"node": "^10 || ^12 || >=14"
}
},
"node_modules/postcss-modules-extract-imports": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz",
"integrity": "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==",
"engines": {
"node": "^10 || ^12 || >= 14"
},
"peerDependencies": {
"postcss": "^8.1.0"
}
},
"node_modules/postcss-modules-local-by-default": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.5.tgz",
"integrity": "sha512-6MieY7sIfTK0hYfafw1OMEG+2bg8Q1ocHCpoWLqOKj3JXlKu4G7btkmM/B7lFubYkYWmRSPLZi5chid63ZaZYw==",
"dependencies": {
"icss-utils": "^5.0.0",
"postcss-selector-parser": "^6.0.2",
"postcss-value-parser": "^4.1.0"
},
"engines": {
"node": "^10 || ^12 || >= 14"
},
"peerDependencies": {
"postcss": "^8.1.0"
}
},
"node_modules/postcss-modules-scope": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.0.tgz",
"integrity": "sha512-oq+g1ssrsZOsx9M96c5w8laRmvEu9C3adDSjI8oTcbfkrTE8hx/zfyobUoWIxaKPO8bt6S62kxpw5GqypEw1QQ==",
"dependencies": {
"postcss-selector-parser": "^6.0.4"
},
"engines": {
"node": "^10 || ^12 || >= 14"
},
"peerDependencies": {
"postcss": "^8.1.0"
}
},
"node_modules/postcss-modules-values": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz",
"integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==",
"dependencies": {
"icss-utils": "^5.0.0"
},
"engines": {
"node": "^10 || ^12 || >= 14"
},
"peerDependencies": {
"postcss": "^8.1.0"
}
},
"node_modules/postcss-selector-parser": {
"version": "6.0.16",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.16.tgz",
"integrity": "sha512-A0RVJrX+IUkVZbW3ClroRWurercFhieevHB38sr2+l9eUClMqome3LmEmnhlNy+5Mr2EYN6B2Kaw9wYdd+VHiw==",
"dependencies": {
"cssesc": "^3.0.0",
"util-deprecate": "^1.0.2"
},
"engines": {
"node": ">=4"
}
},
"node_modules/postcss-value-parser": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
@ -20657,6 +20843,17 @@
"node": ">=0.10.0"
}
},
"node_modules/rechoir": {
"version": "0.6.2",
"resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz",
"integrity": "sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==",
"dependencies": {
"resolve": "^1.1.6"
},
"engines": {
"node": ">= 0.10"
}
},
"node_modules/redent": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz",
@ -21683,6 +21880,22 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/shelljs": {
"version": "0.8.5",
"resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.5.tgz",
"integrity": "sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==",
"dependencies": {
"glob": "^7.0.0",
"interpret": "^1.0.0",
"rechoir": "^0.6.2"
},
"bin": {
"shjs": "bin/shjs"
},
"engines": {
"node": ">=4"
}
},
"node_modules/shortid": {
"version": "2.2.16",
"resolved": "https://registry.npmjs.org/shortid/-/shortid-2.2.16.tgz",
@ -21696,6 +21909,21 @@
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-2.1.11.tgz",
"integrity": "sha512-s/snB+WGm6uwi0WjsZdaVcuf3KJXlfGl2LcxgwkEwJF0D/BWzVWAZW/XY4bFaiR7s0Jk3FPvlnepg1H1b1UwlA=="
},
"node_modules/shx": {
"version": "0.3.4",
"resolved": "https://registry.npmjs.org/shx/-/shx-0.3.4.tgz",
"integrity": "sha512-N6A9MLVqjxZYcVn8hLmtneQWIJtp8IKzMP4eMnx+nqkvXoqinUPCbUFLp2UcWTEIUONhlk0ewxr/jaVGlc+J+g==",
"dependencies": {
"minimist": "^1.2.3",
"shelljs": "^0.8.5"
},
"bin": {
"shx": "lib/cli.js"
},
"engines": {
"node": ">=6"
}
},
"node_modules/side-channel": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz",
@ -24617,6 +24845,11 @@
"react-dom": ">=16.8.0"
}
},
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
},
"node_modules/uuid": {
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
@ -30983,7 +31216,7 @@
}
},
"bpmn-js-spiffworkflow": {
"version": "git+ssh://git@github.com/sartography/bpmn-js-spiffworkflow.git#38f5b5d7563f24608e7b3b177547e273f7cc94a0",
"version": "git+ssh://git@github.com/sartography/bpmn-js-spiffworkflow.git#ee86b31cd7fc6f42f92f56171a9945a8ff22773a",
"from": "bpmn-js-spiffworkflow@github:sartography/bpmn-js-spiffworkflow#main",
"requires": {
"inherits": "^2.0.4",
@ -30991,6 +31224,7 @@
"min-dash": "^3.8.1",
"min-dom": "^3.2.1",
"moddle": "^5.0.3",
"nice-select2": "^2.1.0",
"react": "^18.2.0",
"react-dom": "18.2.0",
"tiny-svg": "^2.2.3"
@ -32056,6 +32290,14 @@
"resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.5.tgz",
"integrity": "sha512-+BO9wPPi+DWTDcNYhr/W90myha8ptzftZT+LwcmUbbok0rcP/fequmFYCw8NMoH7pkAZQzU78b3kYrlua5a9eA=="
},
"cross-env": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz",
"integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==",
"requires": {
"cross-spawn": "^7.0.1"
}
},
"cross-spawn": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
@ -32071,6 +32313,44 @@
"resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-1.0.0.tgz",
"integrity": "sha512-GsVpkFPlycH7/fRR7Dhcmnoii54gV1nz7y4CWyeFS14N+JVBBhY+r8amRHE4BwSYal7BPTDp8isvAlCxyFt3Hg=="
},
"css-loader": {
"version": "6.11.0",
"resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.11.0.tgz",
"integrity": "sha512-CTJ+AEQJjq5NzLga5pE39qdiSV56F8ywCIsqNIRF0r7BDgWsN25aazToqAFg7ZrtA/U016xudB3ffgweORxX7g==",
"requires": {
"icss-utils": "^5.1.0",
"postcss": "^8.4.33",
"postcss-modules-extract-imports": "^3.1.0",
"postcss-modules-local-by-default": "^4.0.5",
"postcss-modules-scope": "^3.2.0",
"postcss-modules-values": "^4.0.0",
"postcss-value-parser": "^4.2.0",
"semver": "^7.5.4"
},
"dependencies": {
"lru-cache": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
"requires": {
"yallist": "^4.0.0"
}
},
"semver": {
"version": "7.6.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz",
"integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==",
"requires": {
"lru-cache": "^6.0.0"
}
},
"yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
}
}
},
"css-selector-parser": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/css-selector-parser/-/css-selector-parser-1.4.1.tgz",
@ -32087,6 +32367,11 @@
"resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz",
"integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg=="
},
"cssesc": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
"integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="
},
"cssstyle": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.0.1.tgz",
@ -34286,8 +34571,7 @@
"fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
"devOptional": true
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="
},
"fsevents": {
"version": "2.3.3",
@ -34413,7 +34697,6 @@
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"devOptional": true,
"requires": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
@ -34912,6 +35195,12 @@
"safer-buffer": ">= 2.1.2 < 3.0.0"
}
},
"icss-utils": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz",
"integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==",
"requires": {}
},
"ids": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/ids/-/ids-1.0.5.tgz",
@ -35006,7 +35295,6 @@
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
"devOptional": true,
"requires": {
"once": "^1.3.0",
"wrappy": "1"
@ -35044,6 +35332,11 @@
"side-channel": "^1.0.4"
}
},
"interpret": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz",
"integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA=="
},
"invariant": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
@ -39520,6 +39813,16 @@
"integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
"dev": true
},
"nice-select2": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/nice-select2/-/nice-select2-2.2.0.tgz",
"integrity": "sha512-UVlqc/ReJPFnQhESbbJrFHCjVe74OWrewh/8w2A+zLlJ98BI4FVDVALV1tOfYCmhNcZtH4L1lQispCUIWX11nQ==",
"requires": {
"cross-env": "^7.0.3",
"css-loader": "^6.7.3",
"shx": "^0.3.4"
}
},
"node-dir": {
"version": "0.1.17",
"resolved": "https://registry.npmjs.org/node-dir/-/node-dir-0.1.17.tgz",
@ -40040,8 +40343,7 @@
"path-is-absolute": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
"devOptional": true
"integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="
},
"path-is-inside": {
"version": "1.0.2",
@ -40156,6 +40458,47 @@
"source-map-js": "^1.2.0"
}
},
"postcss-modules-extract-imports": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz",
"integrity": "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==",
"requires": {}
},
"postcss-modules-local-by-default": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.5.tgz",
"integrity": "sha512-6MieY7sIfTK0hYfafw1OMEG+2bg8Q1ocHCpoWLqOKj3JXlKu4G7btkmM/B7lFubYkYWmRSPLZi5chid63ZaZYw==",
"requires": {
"icss-utils": "^5.0.0",
"postcss-selector-parser": "^6.0.2",
"postcss-value-parser": "^4.1.0"
}
},
"postcss-modules-scope": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.0.tgz",
"integrity": "sha512-oq+g1ssrsZOsx9M96c5w8laRmvEu9C3adDSjI8oTcbfkrTE8hx/zfyobUoWIxaKPO8bt6S62kxpw5GqypEw1QQ==",
"requires": {
"postcss-selector-parser": "^6.0.4"
}
},
"postcss-modules-values": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz",
"integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==",
"requires": {
"icss-utils": "^5.0.0"
}
},
"postcss-selector-parser": {
"version": "6.0.16",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.16.tgz",
"integrity": "sha512-A0RVJrX+IUkVZbW3ClroRWurercFhieevHB38sr2+l9eUClMqome3LmEmnhlNy+5Mr2EYN6B2Kaw9wYdd+VHiw==",
"requires": {
"cssesc": "^3.0.0",
"util-deprecate": "^1.0.2"
}
},
"postcss-value-parser": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
@ -40651,6 +40994,14 @@
}
}
},
"rechoir": {
"version": "0.6.2",
"resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz",
"integrity": "sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==",
"requires": {
"resolve": "^1.1.6"
}
},
"redent": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz",
@ -41430,6 +41781,16 @@
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.1.tgz",
"integrity": "sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA=="
},
"shelljs": {
"version": "0.8.5",
"resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.5.tgz",
"integrity": "sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==",
"requires": {
"glob": "^7.0.0",
"interpret": "^1.0.0",
"rechoir": "^0.6.2"
}
},
"shortid": {
"version": "2.2.16",
"resolved": "https://registry.npmjs.org/shortid/-/shortid-2.2.16.tgz",
@ -41445,6 +41806,15 @@
}
}
},
"shx": {
"version": "0.3.4",
"resolved": "https://registry.npmjs.org/shx/-/shx-0.3.4.tgz",
"integrity": "sha512-N6A9MLVqjxZYcVn8hLmtneQWIJtp8IKzMP4eMnx+nqkvXoqinUPCbUFLp2UcWTEIUONhlk0ewxr/jaVGlc+J+g==",
"requires": {
"minimist": "^1.2.3",
"shelljs": "^0.8.5"
}
},
"side-channel": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz",
@ -43725,6 +44095,11 @@
"resize-observer-polyfill": "^1.5.1"
}
},
"util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
},
"uuid": {
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",

View File

@ -76,8 +76,10 @@
"serve": "vite preview",
"test": "vitest run --coverage",
"format": "prettier --write src/**/*.[tj]s{,x}",
"lint": "./node_modules/.bin/eslint src",
"lint:fix": "./node_modules/.bin/eslint --fix src"
"eslint": "./node_modules/.bin/eslint src",
"lint": "npm run eslint && npm run typecheck",
"lint:fix": "./node_modules/.bin/eslint --fix src",
"typecheck": "./node_modules/.bin/tsc --noEmit"
},
"eslintConfig": {
"extends": [
@ -113,7 +115,6 @@
"cypress-slow-down": "^1.3.1",
"cypress-vite": "^1.5.0",
"eslint": "^8.56.0",
"eslint_d": "^12.2.0",
"eslint-config-airbnb": "^19.0.4",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-cypress": "^2.15.1",
@ -124,8 +125,10 @@
"eslint-plugin-react-hooks": "^4.6.2",
"eslint-plugin-sonarjs": "^1.0.3",
"eslint-plugin-unused-imports": "^3.2.0",
"eslint_d": "^12.2.0",
"inherits-browser": "^0.0.1",
"jsdom": "^24.0.0",
"nice-select2": "^2.1.0",
"prettier": "^3.3.0",
"safe-regex": "^2.1.1",
"tiny-svg": "^2.2.3",

View File

@ -264,6 +264,22 @@ export default function CustomForm({
}
};
const checkJsonField = (
formDataToCheck: any,
propertyKey: string,
errors: any,
_jsonSchema: any,
_uiSchemaPassedIn?: any
) => {
if (propertyKey in formDataToCheck) {
try {
JSON.parse(formDataToCheck[propertyKey]);
} catch (e) {
errors[propertyKey].addError(`has invalid JSON: ${e}`);
}
}
};
const checkNumericRange = (
formDataToCheck: any,
propertyKey: string,
@ -407,6 +423,19 @@ export default function CustomForm({
currentUiSchema,
);
}
if (
currentUiSchema &&
'ui:options' in currentUiSchema &&
currentUiSchema['ui:options']['validateJson'] === true
) {
checkJsonField(
formDataToCheck,
propertyKey,
errors,
jsonSchemaToUse,
currentUiSchema
);
}
if (
currentUiSchema &&

View File

@ -1,3 +1,4 @@
import { ReactElement } from 'react';
import { UiSchemaUxElement } from '../extension_ui_schema_interfaces';
type OwnProps = {
@ -30,10 +31,12 @@ export function ExtensionUxElementMap({
return mainElement();
}
export default function ExtensionUxElementForDisplay(args: OwnProps) {
export default function ExtensionUxElementForDisplay(
args: OwnProps,
): ReactElement | null {
const result = ExtensionUxElementMap(args);
if (result === null) {
return null;
}
return result;
return <>{result}</>;
}

View File

@ -19,7 +19,6 @@ import {
import { Logout } from '@carbon/icons-react';
import { useEffect, useState } from 'react';
import { useLocation, Link, LinkProps } from 'react-router-dom';
// @ts-expect-error TS(2307) FIXME: Cannot find module '../logo.svg' or its correspond... Remove this comment to see the full error message
import logo from '../logo.svg';
import UserService from '../services/UserService';
import { UiSchemaUxElement } from '../extension_ui_schema_interfaces';

View File

@ -1,10 +1,10 @@
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
// @ts-ignore
import { Button, Form, Stack, TextInput, TextArea } from '@carbon/react';
import { modifyProcessIdentifierForPathParam, slugifyString } from '../helpers';
import HttpService from '../services/HttpService';
import { ProcessGroup } from '../interfaces';
import useProcessGroupFetcher from '../hooks/useProcessGroupFetcher';
type OwnProps = {
@ -72,6 +72,7 @@ export default function ProcessGroupForm({
const postBody = {
display_name: processGroup.display_name,
description: processGroup.description,
messages: processGroup.messages,
};
if (mode === 'new') {
if (parentGroupId) {
@ -155,7 +156,6 @@ export default function ProcessGroupForm({
}
/>,
);
return textInputs;
};

View File

@ -70,60 +70,64 @@ import { usePermissionFetcher } from '../hooks/PermissionService';
type OwnProps = {
processModelId: string;
diagramType: string;
tasks?: Task[] | null;
saveDiagram?: (..._args: any[]) => any;
onDeleteFile?: (..._args: any[]) => any;
isPrimaryFile?: boolean;
onSetPrimaryFile?: (..._args: any[]) => any;
diagramXML?: string | null;
fileName?: string;
onLaunchScriptEditor?: (..._args: any[]) => any;
onLaunchMarkdownEditor?: (..._args: any[]) => any;
onLaunchBpmnEditor?: (..._args: any[]) => any;
onLaunchJsonSchemaEditor?: (..._args: any[]) => any;
onLaunchDmnEditor?: (..._args: any[]) => any;
onElementClick?: (..._args: any[]) => any;
onServiceTasksRequested?: (..._args: any[]) => any;
onDataStoresRequested?: (..._args: any[]) => any;
onJsonSchemaFilesRequested?: (..._args: any[]) => any;
onDmnFilesRequested?: (..._args: any[]) => any;
onSearchProcessModels?: (..._args: any[]) => any;
onElementsChanged?: (..._args: any[]) => any;
url?: string;
callers?: ProcessReference[];
activeUserElement?: React.ReactElement;
callers?: ProcessReference[];
diagramXML?: string | null;
disableSaveButton?: boolean;
fileName?: string;
isPrimaryFile?: boolean;
onDataStoresRequested?: (..._args: any[]) => any;
onDeleteFile?: (..._args: any[]) => any;
onDmnFilesRequested?: (..._args: any[]) => any;
onElementClick?: (..._args: any[]) => any;
onElementsChanged?: (..._args: any[]) => any;
onJsonSchemaFilesRequested?: (..._args: any[]) => any;
onLaunchBpmnEditor?: (..._args: any[]) => any;
onLaunchDmnEditor?: (..._args: any[]) => any;
onLaunchJsonSchemaEditor?: (..._args: any[]) => any;
onLaunchMarkdownEditor?: (..._args: any[]) => any;
onLaunchScriptEditor?: (..._args: any[]) => any;
onLaunchMessageEditor?: (..._args: any[]) => any;
onMessagesRequested?: (..._args: any[]) => any;
onSearchProcessModels?: (..._args: any[]) => any;
onServiceTasksRequested?: (..._args: any[]) => any;
onSetPrimaryFile?: (..._args: any[]) => any;
saveDiagram?: (..._args: any[]) => any;
tasks?: Task[] | null;
url?: string;
};
const FitViewport = 'fit-viewport';
// https://codesandbox.io/s/quizzical-lake-szfyo?file=/src/App.js was a handy reference
export default function ReactDiagramEditor({
processModelId,
diagramType,
tasks,
saveDiagram,
onDeleteFile,
isPrimaryFile,
onSetPrimaryFile,
diagramXML,
fileName,
onLaunchScriptEditor,
onLaunchMarkdownEditor,
onLaunchBpmnEditor,
onLaunchJsonSchemaEditor,
onLaunchDmnEditor,
onElementClick,
onServiceTasksRequested,
onDataStoresRequested,
onJsonSchemaFilesRequested,
onDmnFilesRequested,
onSearchProcessModels,
onElementsChanged,
url,
callers,
activeUserElement,
callers,
diagramType,
diagramXML,
disableSaveButton,
fileName,
isPrimaryFile,
onDataStoresRequested,
onDeleteFile,
onDmnFilesRequested,
onElementClick,
onElementsChanged,
onJsonSchemaFilesRequested,
onLaunchBpmnEditor,
onLaunchDmnEditor,
onLaunchJsonSchemaEditor,
onLaunchMarkdownEditor,
onLaunchScriptEditor,
onLaunchMessageEditor,
onMessagesRequested,
onSearchProcessModels,
onServiceTasksRequested,
onSetPrimaryFile,
processModelId,
saveDiagram,
tasks,
url,
}: OwnProps) {
const [diagramXMLString, setDiagramXMLString] = useState('');
const [diagramModelerState, setDiagramModelerState] = useState(null);
@ -409,6 +413,12 @@ export default function ReactDiagramEditor({
}
});
diagramModeler.on('spiff.messages.requested', (event: any) => {
if (onMessagesRequested) {
onMessagesRequested(event);
}
});
diagramModeler.on('spiff.json_schema_files.requested', (event: any) => {
handleServiceTasksRequested(event);
});
@ -418,21 +428,29 @@ export default function ReactDiagramEditor({
onSearchProcessModels(event.value, event.eventBus, event.element);
}
});
diagramModeler.on('spiff.message.edit', (event: any) => {
if (onLaunchMessageEditor) {
onLaunchMessageEditor(event);
}
});
}, [
diagramModelerState,
diagramType,
onLaunchScriptEditor,
onLaunchMarkdownEditor,
onDataStoresRequested,
onDmnFilesRequested,
onElementClick,
onElementsChanged,
onJsonSchemaFilesRequested,
onLaunchBpmnEditor,
onLaunchDmnEditor,
onLaunchJsonSchemaEditor,
onElementClick,
onServiceTasksRequested,
onDataStoresRequested,
onJsonSchemaFilesRequested,
onDmnFilesRequested,
onLaunchMarkdownEditor,
onLaunchScriptEditor,
onLaunchMessageEditor,
onMessagesRequested,
onSearchProcessModels,
onElementsChanged,
onServiceTasksRequested,
]);
useEffect(() => {
@ -549,13 +567,13 @@ export default function ReactDiagramEditor({
alreadyImportedXmlRef.current = true;
}
function dmnTextHandler(text: str) {
function dmnTextHandler(text: string) {
const decisionId = `decision_${makeid(7)}`;
const newText = text.replaceAll('{{DECISION_ID}}', decisionId);
setDiagramXMLString(newText);
}
function bpmnTextHandler(text: str) {
function bpmnTextHandler(text: string) {
const processId = `Process_${makeid(7)}`;
const newText = text.replaceAll('{{PROCESS_ID}}', processId);
setDiagramXMLString(newText);
@ -563,7 +581,7 @@ export default function ReactDiagramEditor({
function fetchDiagramFromURL(
urlToUse: any,
textHandler?: (text: str) => void,
textHandler?: (text: string) => void,
) {
fetch(urlToUse)
.then((response) => response.text())
@ -603,7 +621,7 @@ export default function ReactDiagramEditor({
return undefined;
}
let newDiagramFileName = 'new_bpmn_diagram.bpmn';
let textHandler = null;
let textHandler = undefined;
if (diagramType === 'dmn') {
newDiagramFileName = 'new_dmn_diagram.dmn';
textHandler = dmnTextHandler;

View File

@ -0,0 +1,279 @@
import { useEffect, useState } from 'react';
import CustomForm from '../CustomForm';
import {
ProcessGroup,
RJSFFormObject,
MessageDefinition,
CorrelationProperties,
} from '../../interfaces';
import {
unModifyProcessIdentifierForPathParam,
setPageTitle,
} from '../../helpers';
import HttpService from '../../services/HttpService';
import {
convertCorrelationPropertiesToRJSF,
mergeCorrelationProperties,
} from './MessageHelper';
import { Notification } from '../Notification';
type OwnProps = {
modifiedProcessGroupIdentifier: string;
messageId: string;
messageEvent: any;
correlationProperties: any;
};
export function MessageEditor({
modifiedProcessGroupIdentifier,
messageId,
messageEvent,
correlationProperties,
}: OwnProps) {
const [processGroup, setProcessGroup] = useState<ProcessGroup | null>(null);
const [currentFormData, setCurrentFormData] = useState<any>(null);
const [displaySaveMessageMessage, setDisplaySaveMessageMessage] =
useState<boolean>(false);
const [currentMessageId, setCurrentMessageId] = useState<string | null>(null);
useEffect(() => {
const setInitialFormData = (newProcessGroup: ProcessGroup) => {
let newCorrelationProperties = convertCorrelationPropertiesToRJSF(
messageId,
newProcessGroup,
);
newCorrelationProperties = mergeCorrelationProperties(
correlationProperties,
newCorrelationProperties,
);
const jsonSchema =
(newProcessGroup.messages || {})[messageId]?.schema || {};
const newFormData = {
processGroupIdentifier: unModifyProcessIdentifierForPathParam(
modifiedProcessGroupIdentifier,
),
messageId: messageId,
correlation_properties: newCorrelationProperties,
schema: JSON.stringify(jsonSchema, null, 2),
};
setCurrentFormData(newFormData);
};
const processResult = (result: ProcessGroup) => {
setProcessGroup(result);
setCurrentMessageId(messageId);
setPageTitle([result.display_name]);
setInitialFormData(result);
};
HttpService.makeCallToBackend({
path: `/process-groups/${modifiedProcessGroupIdentifier}`,
successCallback: processResult,
});
}, [modifiedProcessGroupIdentifier]);
const handleProcessGroupUpdateResponse = (
response: ProcessGroup,
messageIdentifier: string,
updatedMessagesForId: MessageDefinition,
) => {
setProcessGroup(response);
setDisplaySaveMessageMessage(true);
messageEvent.eventBus.fire('spiff.add_message.returned', {
name: messageIdentifier,
correlation_properties: updatedMessagesForId.correlation_properties,
});
};
const updateCorrelationPropertiesOnProcessGroup = (
currentMessagesForId: MessageDefinition,
formData: any,
) => {
const newCorrelationProperties: CorrelationProperties = {
...currentMessagesForId.correlation_properties,
};
(formData.correlation_properties || []).forEach((formProp: any) => {
if (!(formProp.id in newCorrelationProperties)) {
newCorrelationProperties[formProp.id] = {
retrieval_expressions: [],
};
}
if (
!newCorrelationProperties[formProp.id].retrieval_expressions.includes(
formProp.retrievalExpression,
)
) {
newCorrelationProperties[formProp.id].retrieval_expressions.push(
formProp.retrievalExpression,
);
}
});
Object.keys(currentMessagesForId.correlation_properties || []).forEach(
(propId: string) => {
const foundProp = (formData.correlation_properties || []).find(
(formProp: any) => {
return propId === formProp.id;
},
);
if (!foundProp) {
delete newCorrelationProperties[propId];
}
},
);
return newCorrelationProperties;
};
const updateProcessGroupWithMessages = (formObject: RJSFFormObject) => {
const { formData } = formObject;
if (!processGroup) {
return;
}
// keep track of new and old message ids so we can handle renaming a message
const newMessageId = formData.messageId;
const oldMessageId = currentMessageId || newMessageId;
const processGroupForUpdate = { ...processGroup };
if (!processGroupForUpdate.messages) {
processGroupForUpdate.messages = {};
}
const currentMessagesForId: MessageDefinition =
(processGroupForUpdate.messages || {})[oldMessageId] || {};
const updatedMessagesForId = { ...currentMessagesForId };
const newCorrelationProperties = updateCorrelationPropertiesOnProcessGroup(
currentMessagesForId,
formData,
);
updatedMessagesForId.correlation_properties = newCorrelationProperties;
try {
updatedMessagesForId.schema = JSON.parse(formData.schema || '{}');
} catch (e) {
alert(`Invalid schema: ${e}`);
return;
}
processGroupForUpdate.messages[newMessageId] = updatedMessagesForId;
setCurrentMessageId(newMessageId);
const path = `/process-groups/${modifiedProcessGroupIdentifier}`;
HttpService.makeCallToBackend({
path,
successCallback: (response: ProcessGroup) =>
handleProcessGroupUpdateResponse(
response,
newMessageId,
updatedMessagesForId,
),
httpMethod: 'PUT',
postBody: processGroupForUpdate,
});
};
const schema = {
type: 'object',
required: ['processGroupIdentifier', 'messageId'],
properties: {
processGroupIdentifier: {
type: 'string',
title: 'Location',
default: '/',
pattern: '^[\\/\\w-]+$',
validationErrorMessage:
'must contain only alphanumeric characters, "/", underscores, or hyphens',
description:
'Only process models within this path will have access to this message.',
},
messageId: {
type: 'string',
title: 'Message Name',
pattern: '^[\\w -]+$',
validationErrorMessage:
'must contain only alphanumeric characters, underscores, or hyphens',
description:
'The mesage name should contain no spaces or special characters',
},
correlation_properties: {
type: 'array',
title: 'Correlation Properties',
items: {
type: 'object',
required: ['id', 'retrievalExpression'],
properties: {
id: {
type: 'string',
title: 'Property Name',
description: '',
},
retrievalExpression: {
type: 'string',
title: 'Retrieval Expression',
description:
'This is how to extract the property from the body of the message',
},
},
},
},
schema: {
type: 'string',
title: 'Json Schema',
default: '{}',
description: 'The payload must conform to this schema if defined.',
},
},
};
const uischema = {
schema: {
'ui:widget': 'textarea',
'ui:rows': 5,
'ui:options': { validateJson: true },
},
'ui:layout': [
{
processGroupIdentifier: { sm: 2, md: 4, lg: 8 },
messageId: { sm: 2, md: 4, lg: 8 },
schema: { sm: 4, md: 4, lg: 8 },
correlation_properties: {
sm: 4,
md: 4,
lg: 8,
id: { sm: 2, md: 4, lg: 8 },
extractionExpression: { sm: 2, md: 4, lg: 8 },
},
},
],
};
const updateFormData = (formObject: any) => {
setCurrentFormData(formObject.formData);
};
if (processGroup && currentFormData) {
return (
<>
{displaySaveMessageMessage ? (
<Notification
title="Save successful"
hideCloseButton
timeout={4000}
onClose={() => setDisplaySaveMessageMessage(false)}
>
Message has been saved
</Notification>
) : null}
<CustomForm
id={currentMessageId || ''}
schema={schema}
uiSchema={uischema}
formData={currentFormData}
onSubmit={updateProcessGroupWithMessages}
submitButtonText="Save"
onChange={updateFormData}
/>
</>
);
}
return null;
}

View File

@ -0,0 +1,62 @@
import { ProcessGroup } from '../../interfaces';
const arrayCompare = (array1: string[], array2: string[]) => {
return (
array1.length === array2.length &&
array1.every((value, index) => value === array2[index])
);
};
export const getPropertiesForMessage = (
messageId: string,
processGroup: ProcessGroup,
) => {
const message = (processGroup.messages || {})[messageId];
if (message) {
return message.correlation_properties;
}
return null;
};
export const convertCorrelationPropertiesToRJSF = (
messageId: string,
processGroup: ProcessGroup,
) => {
const correlationProperties = getPropertiesForMessage(
messageId,
processGroup,
);
const correlationPropertiesToUse = correlationProperties || {};
const returnArray: any = [];
Object.keys(correlationPropertiesToUse).forEach((propIdentifier: string) => {
const property = correlationPropertiesToUse[propIdentifier];
return property.retrieval_expressions.forEach((retExp: string) => {
returnArray.push({
id: propIdentifier,
retrievalExpression: retExp,
});
});
});
return returnArray;
};
export const mergeCorrelationProperties = (
xmlProperties: any,
apiProperties: any,
) => {
const mergedProperties = xmlProperties ? [...xmlProperties] : [];
apiProperties.forEach((apiProperty: any) => {
const existingProperty = mergedProperties.find(
(prop) => prop.id === apiProperty.id,
);
if (existingProperty) {
existingProperty.retrievalExpression = apiProperty.retrievalExpression;
} else {
mergedProperties.push(apiProperty);
}
});
return mergedProperties;
};

View File

@ -4,16 +4,16 @@ import { ErrorOutline } from '@carbon/icons-react';
// @ts-ignore
import { Table, Modal, Button } from '@carbon/react';
import { Link, useSearchParams } from 'react-router-dom';
import PaginationForTable from './PaginationForTable';
import ProcessBreadcrumb from './ProcessBreadcrumb';
import PaginationForTable from '../PaginationForTable';
import ProcessBreadcrumb from '../ProcessBreadcrumb';
import {
getPageInfoFromSearchParams,
modifyProcessIdentifierForPathParam,
} from '../helpers';
import HttpService from '../services/HttpService';
import { FormatProcessModelDisplayName } from './MiniComponents';
import { MessageInstance } from '../interfaces';
import DateAndTimeService from '../services/DateAndTimeService';
} from '../../helpers';
import HttpService from '../../services/HttpService';
import { FormatProcessModelDisplayName } from '../MiniComponents';
import { MessageInstance } from '../../interfaces';
import DateAndTimeService from '../../services/DateAndTimeService';
type OwnProps = {
processInstanceId?: number;
@ -122,6 +122,7 @@ export default function MessageInstanceList({ processInstanceId }: OwnProps) {
<td>{instanceLink}</td>
<td>{row.name}</td>
<td>{row.message_type}</td>
<td>{row.counterpart_id}</td>
<td>
<Button
kind="ghost"
@ -151,6 +152,7 @@ export default function MessageInstanceList({ processInstanceId }: OwnProps) {
<th>Process instance</th>
<th>Name</th>
<th>Type</th>
<th>Corresponding Message Instance</th>
<th>Details</th>
<th>Status</th>
<th>Created at</th>

View File

@ -0,0 +1,103 @@
import React, { useEffect, useState } from 'react';
import { Table } from '@carbon/react';
import { useSearchParams } from 'react-router-dom';
import PaginationForTable from '../PaginationForTable';
import {
getPageInfoFromSearchParams,
modifyProcessIdentifierForPathParam,
} from '../../helpers';
import HttpService from '../../services/HttpService';
import { PaginationObject, ReferenceCache } from '../../interfaces';
// TODO: update to work with current message-models api
type OwnProps = {
processGroupId?: string;
};
export default function MessageModelList({ processGroupId }: OwnProps) {
const [messageModels, setMessageModels] = useState<ReferenceCache[]>([]);
const [pagination, setPagination] = useState<PaginationObject | null>(null);
const [searchParams] = useSearchParams();
useEffect(() => {
const setMessageInstanceListFromResult = (result: any) => {
setMessageModels(result.results);
setPagination(result.pagination);
};
const { page, perPage } = getPageInfoFromSearchParams(searchParams);
const queryParamString = `per_page=${perPage}&page=${page}`;
let modifiedProcessIdentifierForPathParam = '';
if (processGroupId) {
modifiedProcessIdentifierForPathParam = `/${modifyProcessIdentifierForPathParam(
processGroupId
)}`;
}
HttpService.makeCallToBackend({
path: `/message-models${modifiedProcessIdentifierForPathParam}?${queryParamString}`,
successCallback: setMessageInstanceListFromResult,
});
}, [processGroupId, searchParams]);
const correlation = (row: ReferenceCache): string => {
let keys = '';
const cProps: string[] = [];
if ('correlation_keys' in row.properties) {
keys = row.properties.correlation_keys;
}
if ('correlations' in row.properties) {
row.properties.correlations.forEach((cor: any) => {
cProps.push(cor.correlation_property);
});
}
if (cProps.length > 0) {
keys += ` (${cProps.join(', ')})`;
}
return keys;
};
const buildTable = () => {
const rows = messageModels.map((row: ReferenceCache) => {
return (
<tr key={row.identifier}>
<td>{row.identifier}</td>
<td>
<a
href={`/process-groups/${modifyProcessIdentifierForPathParam(
row.relative_location
)}`}
>
{row.relative_location}
</a>
</td>
<td>{correlation(row)}</td>
</tr>
);
});
return (
<Table striped bordered>
<thead>
<tr>
<th>Id</th>
<th>Location</th>
<th>Correlation</th>
</tr>
</thead>
<tbody>{rows}</tbody>
</Table>
);
};
if (pagination) {
const { page, perPage } = getPageInfoFromSearchParams(searchParams);
return (
<PaginationForTable
page={page}
perPage={perPage}
pagination={pagination}
tableToDisplay={buildTable()}
paginationQueryParamPrefix="message-model-list"
/>
);
}
return null;
}

View File

@ -314,3 +314,4 @@ export const renderElementsForArray = (elements: ElementForArray[]) => {
<div key={element.key}>{element.component}</div>
));
};

View File

@ -276,6 +276,10 @@ h1.with-icons {
margin-top: 5px;
}
h3.with-icons {
margin-top: 12px;
}
.with-icons {
margin-top: 10px;
}
@ -1004,9 +1008,18 @@ div.onboarding {
text-align: right; /* Aligns text to the right within the container */
}
div.retrievalExpressionsForm {
border: 1px solid #999;
border-radius: 15px;
background: #ddd;
padding: 20px;
margin: 20px;
}
.task-info-modal-accordion .cds--accordion__content {
padding-right: 1rem;
}
.task-instance-modal-row-item {
height: 48px;
line-height: 48px;

View File

@ -212,26 +212,46 @@ export interface ProcessInstance {
process_model_uses_queued_execution?: boolean;
}
export interface MessageCorrelationProperties {
[key: string]: string;
export interface CorrelationProperty {
retrieval_expressions: string[];
}
export interface MessageCorrelations {
[key: string]: MessageCorrelationProperties;
export interface CorrelationProperties {
[key: string]: CorrelationProperty;
}
export interface MessageDefinition {
correlation_properties: CorrelationProperties;
schema: any;
}
export interface Messages {
[key: string]: MessageDefinition;
}
type ReferenceCacheType = 'decision' | 'process' | 'data_store' | 'message';
export interface ReferenceCache {
identifier: string;
display_name: string;
relative_location: string;
type: ReferenceCacheType;
file_name: string;
properties: any;
}
export interface MessageInstance {
id: number;
process_model_identifier: string;
process_model_display_name: string;
process_instance_id: number;
name: string;
message_type: string;
failure_cause: string;
status: string;
created_at_in_seconds: number;
message_correlations?: MessageCorrelations;
correlation_keys: any;
counterpart_id: number;
created_at_in_seconds: number;
failure_cause: string;
id: number;
message_type: string;
name: string;
process_instance_id: number;
process_model_display_name: string;
process_model_identifier: string;
status: string;
}
export interface ReportFilter {
@ -299,6 +319,7 @@ export interface ProcessGroup {
process_models?: ProcessModel[];
process_groups?: ProcessGroup[];
parent_groups?: ProcessGroupLite[];
messages?: Messages;
}
export interface HotCrumbItemObject {
@ -523,3 +544,7 @@ export interface PublicTask {
process_instance_id: number;
confirmation_message_markdown: string;
}
export interface RJSFFormObject {
formData: any;
}

View File

@ -12,7 +12,7 @@ export default function DataStoreNew() {
id: '',
name: '',
type: '',
schema: '',
schema: '{}',
description: '',
});
useEffect(() => {

View File

@ -1,8 +1,31 @@
import MessageInstanceList from '../components/MessageInstanceList';
import MessageInstanceList from '../components/messages/MessageInstanceList';
import { setPageTitle } from '../helpers';
export default function MessageListPage() {
setPageTitle(['Messages']);
// TODO: add tabs back in when MessageModelList is working again
// TODO: only load the component for the tab we are currently on
// return (
// <>
// <h1>Messages</h1>
// <Tabs>
// <TabList aria-label="List of tabs">
// <Tab>Message Instances</Tab>
// <Tab>Message Models</Tab>
// </TabList>
// <TabPanels>
// <TabPanel>
// <MessageInstanceList />
// </TabPanel>
// <TabPanel>
// <MessageModelList />
// </TabPanel>
// </TabPanels>
// </Tabs>
// </>
// );
return (
<>
<h1>Messages</h1>

View File

@ -76,7 +76,7 @@ import TaskListTable from '../components/TaskListTable';
import useAPIError from '../hooks/UseApiError';
import UserSearch from '../components/UserSearch';
import ProcessInstanceLogList from '../components/ProcessInstanceLogList';
import MessageInstanceList from '../components/MessageInstanceList';
import MessageInstanceList from '../components/messages/MessageInstanceList';
import {
childrenForErrorObject,
errorForDisplayFromString,
@ -1800,11 +1800,7 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
<Tab>Tasks</Tab>
</TabList>
<TabPanels>
<TabPanel>
{selectedTabIndex === 0 ? (
<TabPanel>{diagramArea()}</TabPanel>
) : null}
</TabPanel>
<TabPanel>{selectedTabIndex === 0 ? diagramArea() : null}</TabPanel>
<TabPanel>
{selectedTabIndex === 1 ? (
<ProcessInstanceLogList

View File

@ -39,6 +39,7 @@ import ReactFormBuilder from '../components/ReactFormBuilder/ReactFormBuilder';
import ProcessBreadcrumb from '../components/ProcessBreadcrumb';
import useAPIError from '../hooks/UseApiError';
import {
getGroupFromModifiedModelId,
makeid,
modifyProcessIdentifierForPathParam,
setPageTitle,
@ -56,6 +57,7 @@ import { useFocusedTabStatus } from '../hooks/useFocusedTabStatus';
import useScriptAssistEnabled from '../hooks/useScriptAssistEnabled';
import useProcessScriptAssistMessage from '../hooks/useProcessScriptAssistQuery';
import SpiffTooltip from '../components/SpiffTooltip';
import { MessageEditor } from '../components/messages/MessageEditor';
export default function ProcessModelEditDiagram() {
const [showFileNameEditor, setShowFileNameEditor] = useState(false);
@ -79,6 +81,9 @@ export default function ProcessModelEditDiagram() {
const [markdownText, setMarkdownText] = useState<string | undefined>('');
const [markdownEventBus, setMarkdownEventBus] = useState<any>(null);
const [showMarkdownEditor, setShowMarkdownEditor] = useState(false);
const [showMessageEditor, setShowMessageEditor] = useState(false);
const [messageId, setMessageId] = useState<string>('');
const [correlationProperties, setCorrelationProperties] = useState<any>([]);
const [showProcessSearch, setShowProcessSearch] = useState(false);
const [processSearchEventBus, setProcessSearchEventBus] = useState<any>(null);
const [processSearchElement, setProcessSearchElement] = useState<any>(null);
@ -88,8 +93,12 @@ export default function ProcessModelEditDiagram() {
const [processModelFileInvalidText, setProcessModelFileInvalidText] =
useState<string>('');
const [messageEvent, setMessageEvent] = useState<any>(null);
const handleShowMarkdownEditor = () => setShowMarkdownEditor(true);
const handleShowMessageEditor = () => setShowMessageEditor(true);
const editorRef = useRef(null);
const monacoRef = useRef(null);
@ -435,6 +444,20 @@ export default function ProcessModelEditDiagram() {
}
};
const makeMessagesRequestedHandler = (event: any) => {
return function fireEvent(results: any) {
event.eventBus.fire('spiff.messages.returned', {
configuration: results,
});
};
};
const onMessagesRequested = (event: any) => {
HttpService.makeCallToBackend({
path: `/message-models/${modifiedProcessModelId}`,
successCallback: makeMessagesRequestedHandler(event),
});
};
useEffect(() => {
const updateDiagramFiles = (pm: ProcessModel) => {
setProcessModel(pm);
@ -1036,6 +1059,46 @@ export default function ProcessModelEditDiagram() {
);
};
const onLaunchMessageEditor = (event: any) => {
setMessageEvent(event);
setMessageId(event.value.messageId);
setCorrelationProperties(event.value.correlation_properties);
handleShowMessageEditor();
};
const handleMessageEditorClose = () => {
setShowMessageEditor(false);
onMessagesRequested(messageEvent);
};
const messageEditor = () => {
// do not render this component until we actually want to display it
if (!showMessageEditor) {
return null;
}
return (
<Modal
open={showMessageEditor}
modalHeading="Create/Edit Message"
primaryButtonText="Close (this does not save)"
onRequestSubmit={handleMessageEditorClose}
onRequestClose={handleMessageEditorClose}
size="lg"
preventCloseOnClickOutside
>
<div data-color-mode="light">
<MessageEditor
modifiedProcessGroupIdentifier={getGroupFromModifiedModelId(
modifiedProcessModelId,
)}
messageId={messageId}
correlationProperties={correlationProperties}
messageEvent={messageEvent}
/>
</div>
</Modal>
);
};
const onSearchProcessModels = (
_processId: string,
eventBus: any,
@ -1141,7 +1204,7 @@ export default function ProcessModelEditDiagram() {
};
const onLaunchJsonSchemaEditor = (
element: any,
_element: any,
fileName: string,
eventBus: any,
) => {
@ -1232,28 +1295,30 @@ export default function ProcessModelEditDiagram() {
}
return (
<ReactDiagramEditor
activeUserElement={<ActiveUsers />}
callers={callers}
diagramType="bpmn"
diagramXML={bpmnXmlForDiagramRendering}
disableSaveButton={!diagramHasChanges}
fileName={params.file_name}
isPrimaryFile={params.file_name === processModel?.primary_file_name}
onDataStoresRequested={onDataStoresRequested}
onDeleteFile={onDeleteFile}
onDmnFilesRequested={onDmnFilesRequested}
onElementsChanged={onElementsChanged}
onJsonSchemaFilesRequested={onJsonSchemaFilesRequested}
onLaunchBpmnEditor={onLaunchBpmnEditor}
onLaunchDmnEditor={onLaunchDmnEditor}
onLaunchJsonSchemaEditor={onLaunchJsonSchemaEditor}
onLaunchMarkdownEditor={onLaunchMarkdownEditor}
onLaunchScriptEditor={onLaunchScriptEditor}
onLaunchMessageEditor={onLaunchMessageEditor}
onMessagesRequested={onMessagesRequested}
onSearchProcessModels={onSearchProcessModels}
onServiceTasksRequested={onServiceTasksRequested}
onSetPrimaryFile={onSetPrimaryFileCallback}
processModelId={params.process_model_id || ''}
saveDiagram={saveDiagram}
onDeleteFile={onDeleteFile}
isPrimaryFile={params.file_name === processModel?.primary_file_name}
onSetPrimaryFile={onSetPrimaryFileCallback}
diagramXML={bpmnXmlForDiagramRendering}
fileName={params.file_name}
diagramType="bpmn"
onLaunchScriptEditor={onLaunchScriptEditor}
onServiceTasksRequested={onServiceTasksRequested}
onDataStoresRequested={onDataStoresRequested}
onLaunchMarkdownEditor={onLaunchMarkdownEditor}
onLaunchBpmnEditor={onLaunchBpmnEditor}
onLaunchJsonSchemaEditor={onLaunchJsonSchemaEditor}
onJsonSchemaFilesRequested={onJsonSchemaFilesRequested}
onLaunchDmnEditor={onLaunchDmnEditor}
onDmnFilesRequested={onDmnFilesRequested}
onSearchProcessModels={onSearchProcessModels}
onElementsChanged={onElementsChanged}
callers={callers}
activeUserElement={<ActiveUsers />}
disableSaveButton={!diagramHasChanges}
/>
);
};
@ -1298,6 +1363,7 @@ export default function ProcessModelEditDiagram() {
{markdownEditor()}
{jsonSchemaEditor()}
{processModelSelector()}
{messageEditor()}
</>
);
};
@ -1327,7 +1393,6 @@ export default function ProcessModelEditDiagram() {
{unsavedChangesMessage()}
{saveFileMessage()}
{appropriateEditor()}
<div id="diagram-container" />
</>