diff --git a/spiffworkflow-backend/bin/save_all_bpmn.py b/spiffworkflow-backend/bin/save_all_bpmn.py old mode 100644 new mode 100755 diff --git a/spiffworkflow-backend/migrations/versions/43afc70a7016_.py b/spiffworkflow-backend/migrations/versions/43afc70a7016_.py new file mode 100644 index 00000000..16f52687 --- /dev/null +++ b/spiffworkflow-backend/migrations/versions/43afc70a7016_.py @@ -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 ### diff --git a/spiffworkflow-backend/poetry.lock b/spiffworkflow-backend/poetry.lock index 56a8ab46..d738d966 100644 --- a/spiffworkflow-backend/poetry.lock +++ b/spiffworkflow-backend/poetry.lock @@ -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" diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/api.yml b/spiffworkflow-backend/src/spiffworkflow_backend/api.yml index a30a8f5d..f254a4bd 100755 --- a/spiffworkflow-backend/src/spiffworkflow_backend/api.yml +++ b/spiffworkflow-backend/src/spiffworkflow_backend/api.yml @@ -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 diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/load_database_models.py b/spiffworkflow-backend/src/spiffworkflow_backend/load_database_models.py index 5007133c..9967742c 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/load_database_models.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/load_database_models.py @@ -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 diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/models/correlation_property_cache.py b/spiffworkflow-backend/src/spiffworkflow_backend/models/correlation_property_cache.py deleted file mode 100644 index 89149777..00000000 --- a/spiffworkflow-backend/src/spiffworkflow_backend/models/correlation_property_cache.py +++ /dev/null @@ -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)) diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/models/message_model.py b/spiffworkflow-backend/src/spiffworkflow_backend/models/message_model.py new file mode 100644 index 00000000..52fc7f01 --- /dev/null +++ b/spiffworkflow-backend/src/spiffworkflow_backend/models/message_model.py @@ -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") diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/models/process_group.py b/spiffworkflow-backend/src/spiffworkflow_backend/models/process_group.py index 229a9dce..db70cace 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/models/process_group.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/models/process_group.py @@ -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: diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/routes/messages_controller.py b/spiffworkflow-backend/src/spiffworkflow_backend/routes/messages_controller.py index 595a0c48..6bbbc707 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/routes/messages_controller.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/routes/messages_controller.py @@ -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( diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_groups_controller.py b/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_groups_controller.py index ed5502e7..f9ece4a6 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_groups_controller.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_groups_controller.py @@ -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) diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/authorization_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/authorization_service.py index 25c96bfe..17390ded 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/authorization_service.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/authorization_service.py @@ -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"], diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/data_setup_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/data_setup_service.py index 44ef33c4..ba200095 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/data_setup_service.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/data_setup_service.py @@ -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() diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/message_definition_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/message_definition_service.py new file mode 100644 index 00000000..a9c21e27 --- /dev/null +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/message_definition_service.py @@ -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) diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/spec_file_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/spec_file_service.py index 60e25150..a215d45a 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/spec_file_service.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/spec_file_service.py @@ -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) diff --git a/spiffworkflow-backend/tests/data/message_send_one_conversation/message_receiver.bpmn b/spiffworkflow-backend/tests/data/message_send_one_conversation/message_receiver.bpmn index 371fa031..46ce0ef8 100644 --- a/spiffworkflow-backend/tests/data/message_send_one_conversation/message_receiver.bpmn +++ b/spiffworkflow-backend/tests/data/message_send_one_conversation/message_receiver.bpmn @@ -2,9 +2,6 @@ - - - customer_id po_number diff --git a/spiffworkflow-backend/tests/data/message_send_one_conversation/message_sender.bpmn b/spiffworkflow-backend/tests/data/message_send_one_conversation/message_sender.bpmn index c0911ea6..b1bf8c5c 100644 --- a/spiffworkflow-backend/tests/data/message_send_one_conversation/message_sender.bpmn +++ b/spiffworkflow-backend/tests/data/message_send_one_conversation/message_sender.bpmn @@ -2,15 +2,6 @@ - - - - - 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. - - po_number customer_id diff --git a/spiffworkflow-backend/tests/process_models_example_dir/examples/1-basic-concepts/1-1-content/README.md b/spiffworkflow-backend/tests/process_models_example_dir/examples/1-basic-concepts/1-1-content/README.md new file mode 100644 index 00000000..e40c4a87 --- /dev/null +++ b/spiffworkflow-backend/tests/process_models_example_dir/examples/1-basic-concepts/1-1-content/README.md @@ -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. diff --git a/spiffworkflow-backend/tests/process_models_example_dir/examples/1-basic-concepts/1-1-content/a-simple-form-schema.json b/spiffworkflow-backend/tests/process_models_example_dir/examples/1-basic-concepts/1-1-content/a-simple-form-schema.json new file mode 100644 index 00000000..c76fb455 --- /dev/null +++ b/spiffworkflow-backend/tests/process_models_example_dir/examples/1-basic-concepts/1-1-content/a-simple-form-schema.json @@ -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"] + } diff --git a/spiffworkflow-backend/tests/process_models_example_dir/examples/1-basic-concepts/1-1-content/a-simple-form-uischema.json b/spiffworkflow-backend/tests/process_models_example_dir/examples/1-basic-concepts/1-1-content/a-simple-form-uischema.json new file mode 100644 index 00000000..99c9fd13 --- /dev/null +++ b/spiffworkflow-backend/tests/process_models_example_dir/examples/1-basic-concepts/1-1-content/a-simple-form-uischema.json @@ -0,0 +1,13 @@ +{ + "is_joke": { + "ui:widget": "radio", + "ui:options": { + "inline": true + } + }, + "ui:order": [ + "first_name", + "is_joke", + "color" + ] +} diff --git a/spiffworkflow-backend/tests/process_models_example_dir/examples/1-basic-concepts/1-1-content/process_model.json b/spiffworkflow-backend/tests/process_models_example_dir/examples/1-basic-concepts/1-1-content/process_model.json new file mode 100644 index 00000000..b7a6e824 --- /dev/null +++ b/spiffworkflow-backend/tests/process_models_example_dir/examples/1-basic-concepts/1-1-content/process_model.json @@ -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" +} diff --git a/spiffworkflow-backend/tests/process_models_example_dir/examples/1-basic-concepts/1-1-content/show-content.bpmn b/spiffworkflow-backend/tests/process_models_example_dir/examples/1-basic-concepts/1-1-content/show-content.bpmn new file mode 100644 index 00000000..cc45fec4 --- /dev/null +++ b/spiffworkflow-backend/tests/process_models_example_dir/examples/1-basic-concepts/1-1-content/show-content.bpmn @@ -0,0 +1,220 @@ + + + + + Flow_1wvtd9f + + + + + ### 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. + + + + Flow_1thoqro + + + + + 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: + +> 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. + + Flow_1wvtd9f + Flow_0r9coud + + + + + Please complete the simple form below. We'll show you these values in the next manual task. + + + + + + Flow_0r9coud + Flow_0796xeb + + + + + + Generating some colors for you, give us just a second .... + + Flow_0796xeb + Flow_0wr8gfr + 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" +} + + + + 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: + +> When Chuck Norris falls into the water, Chuck Norris doesn’t 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. + +> 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 | <div style="background-color:blue; color:blue"> blue </div> | +{%- for name, hex in colors.items() %} + +| {{name}} | {{hex}} | <div style="background-color:{{hex}}; color:{{hex}}"> {{name}} </div> | +{%- endfor %} + + + + + + Flow_0fdwa26 + Flow_1thoqro + + + + + + Making the colors more silly ... + + Flow_0wr8gfr + Flow_1rgip89 + time.sleep(1) + + + + Turning the silly level all the way up to 11, almost done .... + + Flow_1rgip89 + Flow_0fdwa26 + time.sleep(2) + + + 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. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spiffworkflow-backend/tests/process_models_example_dir/examples/1-basic-concepts/process_group.json b/spiffworkflow-backend/tests/process_models_example_dir/examples/1-basic-concepts/process_group.json new file mode 100644 index 00000000..11d65874 --- /dev/null +++ b/spiffworkflow-backend/tests/process_models_example_dir/examples/1-basic-concepts/process_group.json @@ -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": {} } } +} diff --git a/spiffworkflow-backend/tests/process_models_example_dir/examples/process_group.json b/spiffworkflow-backend/tests/process_models_example_dir/examples/process_group.json new file mode 100644 index 00000000..8ca186b9 --- /dev/null +++ b/spiffworkflow-backend/tests/process_models_example_dir/examples/process_group.json @@ -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": {} + } + } +} diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/helpers/base_test.py b/spiffworkflow-backend/tests/spiffworkflow_backend/helpers/base_test.py index 1fc160e4..141fb159 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/helpers/base_test.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/helpers/base_test.py @@ -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. diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_messages.py b/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_messages.py index 50bc8f7c..4deb3743 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_messages.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_messages.py @@ -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"]] diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_authorization_service.py b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_authorization_service.py index d6c0a0dd..9cff5843 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_authorization_service.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_authorization_service.py @@ -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/*", diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_data_setup_service.py b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_data_setup_service.py new file mode 100644 index 00000000..e6c637bf --- /dev/null +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_data_setup_service.py @@ -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 == [] diff --git a/spiffworkflow-frontend/package-lock.json b/spiffworkflow-frontend/package-lock.json index c088f3ed..d92188b8 100644 --- a/spiffworkflow-frontend/package-lock.json +++ b/spiffworkflow-frontend/package-lock.json @@ -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", diff --git a/spiffworkflow-frontend/package.json b/spiffworkflow-frontend/package.json index d5a392e0..af38ce67 100644 --- a/spiffworkflow-frontend/package.json +++ b/spiffworkflow-frontend/package.json @@ -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", diff --git a/spiffworkflow-frontend/src/components/CustomForm.tsx b/spiffworkflow-frontend/src/components/CustomForm.tsx index 105d2a48..dd1dc05d 100644 --- a/spiffworkflow-frontend/src/components/CustomForm.tsx +++ b/spiffworkflow-frontend/src/components/CustomForm.tsx @@ -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 && diff --git a/spiffworkflow-frontend/src/components/ExtensionUxElementForDisplay.tsx b/spiffworkflow-frontend/src/components/ExtensionUxElementForDisplay.tsx index 94d391c8..ba55ba06 100644 --- a/spiffworkflow-frontend/src/components/ExtensionUxElementForDisplay.tsx +++ b/spiffworkflow-frontend/src/components/ExtensionUxElementForDisplay.tsx @@ -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}; } diff --git a/spiffworkflow-frontend/src/components/NavigationBar.tsx b/spiffworkflow-frontend/src/components/NavigationBar.tsx index 34c95f45..45036e84 100644 --- a/spiffworkflow-frontend/src/components/NavigationBar.tsx +++ b/spiffworkflow-frontend/src/components/NavigationBar.tsx @@ -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'; diff --git a/spiffworkflow-frontend/src/components/ProcessGroupForm.tsx b/spiffworkflow-frontend/src/components/ProcessGroupForm.tsx index c4e6cad9..3d1bfba7 100644 --- a/spiffworkflow-frontend/src/components/ProcessGroupForm.tsx +++ b/spiffworkflow-frontend/src/components/ProcessGroupForm.tsx @@ -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; }; diff --git a/spiffworkflow-frontend/src/components/ReactDiagramEditor.tsx b/spiffworkflow-frontend/src/components/ReactDiagramEditor.tsx index 06245cd2..6538e325 100644 --- a/spiffworkflow-frontend/src/components/ReactDiagramEditor.tsx +++ b/spiffworkflow-frontend/src/components/ReactDiagramEditor.tsx @@ -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; diff --git a/spiffworkflow-frontend/src/components/messages/MessageEditor.tsx b/spiffworkflow-frontend/src/components/messages/MessageEditor.tsx new file mode 100644 index 00000000..5c899e8f --- /dev/null +++ b/spiffworkflow-frontend/src/components/messages/MessageEditor.tsx @@ -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(null); + const [currentFormData, setCurrentFormData] = useState(null); + const [displaySaveMessageMessage, setDisplaySaveMessageMessage] = + useState(false); + const [currentMessageId, setCurrentMessageId] = useState(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 ? ( + setDisplaySaveMessageMessage(false)} + > + Message has been saved + + ) : null} + + + ); + } + return null; +} diff --git a/spiffworkflow-frontend/src/components/messages/MessageHelper.tsx b/spiffworkflow-frontend/src/components/messages/MessageHelper.tsx new file mode 100644 index 00000000..19505705 --- /dev/null +++ b/spiffworkflow-frontend/src/components/messages/MessageHelper.tsx @@ -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; +}; diff --git a/spiffworkflow-frontend/src/components/MessageInstanceList.tsx b/spiffworkflow-frontend/src/components/messages/MessageInstanceList.tsx similarity index 92% rename from spiffworkflow-frontend/src/components/MessageInstanceList.tsx rename to spiffworkflow-frontend/src/components/messages/MessageInstanceList.tsx index 4562d222..5928fd4a 100644 --- a/spiffworkflow-frontend/src/components/MessageInstanceList.tsx +++ b/spiffworkflow-frontend/src/components/messages/MessageInstanceList.tsx @@ -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) { {instanceLink} {row.name} {row.message_type} + {row.counterpart_id}