mirror of
https://github.com/sartography/spiff-arena.git
synced 2025-02-23 06:38:24 +00:00
Merge branch 'main' into feature/nested-groups
# Conflicts: # spiffworkflow-backend/src/spiffworkflow_backend/api.yml # spiffworkflow-backend/src/spiffworkflow_backend/models/process_instance_report.py # spiffworkflow-backend/src/spiffworkflow_backend/routes/process_api_blueprint.py # spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_process_api.py # spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_spec_file_service.py # spiffworkflow-frontend/src/routes/ProcessInstanceList.tsx # spiffworkflow-frontend/src/routes/ProcessInstanceReportList.tsx # spiffworkflow-frontend/src/routes/ProcessModelEditDiagram.tsx # spiffworkflow-frontend/src/routes/ProcessModelShow.tsx
This commit is contained in:
commit
b646fa98bd
@ -1,8 +1,8 @@
|
||||
"""empty message
|
||||
|
||||
Revision ID: b1647eff45c9
|
||||
Revision ID: 7c12964efde1
|
||||
Revises:
|
||||
Create Date: 2022-11-02 14:25:09.992800
|
||||
Create Date: 2022-11-08 07:48:44.265652
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
@ -10,7 +10,7 @@ import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'b1647eff45c9'
|
||||
revision = '7c12964efde1'
|
||||
down_revision = None
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
@ -115,19 +115,16 @@ def upgrade():
|
||||
op.create_table('process_instance_report',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('identifier', sa.String(length=50), nullable=False),
|
||||
sa.Column('process_model_identifier', sa.String(length=50), nullable=False),
|
||||
sa.Column('process_group_identifier', sa.String(length=50), nullable=False),
|
||||
sa.Column('report_metadata', sa.JSON(), nullable=True),
|
||||
sa.Column('created_by_id', sa.Integer(), nullable=False),
|
||||
sa.Column('created_at_in_seconds', sa.Integer(), nullable=True),
|
||||
sa.Column('updated_at_in_seconds', sa.Integer(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['created_by_id'], ['user.id'], ),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('process_group_identifier', 'process_model_identifier', 'identifier', name='process_instance_report_unique')
|
||||
sa.UniqueConstraint('created_by_id', 'identifier', name='process_instance_report_unique')
|
||||
)
|
||||
op.create_index(op.f('ix_process_instance_report_created_by_id'), 'process_instance_report', ['created_by_id'], unique=False)
|
||||
op.create_index(op.f('ix_process_instance_report_identifier'), 'process_instance_report', ['identifier'], unique=False)
|
||||
op.create_index(op.f('ix_process_instance_report_process_group_identifier'), 'process_instance_report', ['process_group_identifier'], unique=False)
|
||||
op.create_index(op.f('ix_process_instance_report_process_model_identifier'), 'process_instance_report', ['process_model_identifier'], unique=False)
|
||||
op.create_table('refresh_token',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('user_id', sa.Integer(), nullable=False),
|
||||
@ -292,9 +289,8 @@ def downgrade():
|
||||
op.drop_table('user_group_assignment')
|
||||
op.drop_table('secret')
|
||||
op.drop_table('refresh_token')
|
||||
op.drop_index(op.f('ix_process_instance_report_process_model_identifier'), table_name='process_instance_report')
|
||||
op.drop_index(op.f('ix_process_instance_report_process_group_identifier'), table_name='process_instance_report')
|
||||
op.drop_index(op.f('ix_process_instance_report_identifier'), table_name='process_instance_report')
|
||||
op.drop_index(op.f('ix_process_instance_report_created_by_id'), table_name='process_instance_report')
|
||||
op.drop_table('process_instance_report')
|
||||
op.drop_index(op.f('ix_process_instance_process_model_identifier'), table_name='process_instance')
|
||||
op.drop_index(op.f('ix_process_instance_process_group_identifier'), table_name='process_instance')
|
@ -674,14 +674,8 @@ paths:
|
||||
schema:
|
||||
$ref: "#/components/schemas/OkTrue"
|
||||
|
||||
/process-models/{modified_process_model_identifier}/process-instances/reports:
|
||||
/process-instances/reports:
|
||||
parameters:
|
||||
- name: modified_process_model_identifier
|
||||
in: path
|
||||
required: true
|
||||
description: The unique id of an existing process model
|
||||
schema:
|
||||
type: string
|
||||
- name: page
|
||||
in: query
|
||||
required: false
|
||||
@ -748,14 +742,8 @@ paths:
|
||||
schema:
|
||||
$ref: "#/components/schemas/OkTrue"
|
||||
|
||||
/process-models/{modified_process_model_identifier}/process-instances/reports/{report_identifier}:
|
||||
/process-instances/reports/{report_identifier}:
|
||||
parameters:
|
||||
- name: modified_process_model_identifier
|
||||
in: path
|
||||
required: true
|
||||
description: The unique id of an existing process model.
|
||||
schema:
|
||||
type: string
|
||||
- name: report_identifier
|
||||
in: path
|
||||
required: true
|
||||
|
@ -4,6 +4,7 @@ from dataclasses import field
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
import marshmallow
|
||||
from marshmallow import INCLUDE
|
||||
from marshmallow import Schema
|
||||
|
||||
@ -61,6 +62,20 @@ CONTENT_TYPES = {
|
||||
}
|
||||
|
||||
|
||||
@dataclass()
|
||||
class FileReference:
|
||||
"""File Reference Information.
|
||||
|
||||
Includes items such as the process id and name for a BPMN,
|
||||
or the Decision id and Decision name for a DMN file. There may be more than
|
||||
one reference that points to a particular file.
|
||||
"""
|
||||
|
||||
id: str
|
||||
name: str
|
||||
type: str # can be 'process', 'decision', or just 'file'
|
||||
|
||||
|
||||
@dataclass(order=True)
|
||||
class File:
|
||||
"""File."""
|
||||
@ -70,17 +85,12 @@ class File:
|
||||
content_type: str
|
||||
name: str
|
||||
type: str
|
||||
document: dict
|
||||
last_modified: datetime
|
||||
size: int
|
||||
process_instance_id: Optional[int] = None
|
||||
irb_doc_code: Optional[str] = None
|
||||
data_store: Optional[dict] = field(default_factory=dict)
|
||||
user_uid: Optional[str] = None
|
||||
references: Optional[list[FileReference]] = None
|
||||
file_contents: Optional[bytes] = None
|
||||
process_model_id: Optional[str] = None
|
||||
process_group_id: Optional[str] = None
|
||||
archived: bool = False
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
"""__post_init__."""
|
||||
@ -100,7 +110,6 @@ class File:
|
||||
name=file_name,
|
||||
content_type=content_type,
|
||||
type=file_type.value,
|
||||
document={},
|
||||
last_modified=last_modified,
|
||||
size=file_size,
|
||||
)
|
||||
@ -118,32 +127,29 @@ class FileSchema(Schema):
|
||||
"id",
|
||||
"name",
|
||||
"content_type",
|
||||
"process_instance_id",
|
||||
"irb_doc_code",
|
||||
"last_modified",
|
||||
"type",
|
||||
"archived",
|
||||
"size",
|
||||
"data_store",
|
||||
"document",
|
||||
"user_uid",
|
||||
"url",
|
||||
"file_contents",
|
||||
"process_model_id",
|
||||
"references",
|
||||
"process_group_id",
|
||||
"process_model_id",
|
||||
]
|
||||
unknown = INCLUDE
|
||||
references = marshmallow.fields.List(
|
||||
marshmallow.fields.Nested("FileReferenceSchema")
|
||||
)
|
||||
|
||||
# url = Method("get_url")
|
||||
#
|
||||
# def get_url(self, obj):
|
||||
# token = 'not_available'
|
||||
# if hasattr(obj, 'id') and obj.id is not None:
|
||||
# file_url = url_for("/v1_0.crc_api_file_get_file_data_link", file_id=obj.id, _external=True)
|
||||
# if hasattr(flask.g, 'user'):
|
||||
# token = flask.g.user.encode_auth_token()
|
||||
# url = file_url + '?auth_token=' + urllib.parse.quote_plus(token)
|
||||
# return url
|
||||
# else:
|
||||
# return ""
|
||||
#
|
||||
|
||||
class FileReferenceSchema(Schema):
|
||||
"""FileSchema."""
|
||||
|
||||
class Meta:
|
||||
"""Meta."""
|
||||
|
||||
model = FileReference
|
||||
fields = ["id", "name", "type"]
|
||||
unknown = INCLUDE
|
||||
|
@ -21,7 +21,6 @@ from spiffworkflow_backend.models.user import UserModel
|
||||
from spiffworkflow_backend.services.process_instance_processor import (
|
||||
ProcessInstanceProcessor,
|
||||
)
|
||||
from spiffworkflow_backend.services.process_model_service import ProcessModelService
|
||||
|
||||
|
||||
ReportMetadata = dict[str, Any]
|
||||
@ -58,8 +57,7 @@ class ProcessInstanceReportModel(SpiffworkflowBaseDBModel):
|
||||
__tablename__ = "process_instance_report"
|
||||
__table_args__ = (
|
||||
db.UniqueConstraint(
|
||||
"process_group_identifier",
|
||||
"process_model_identifier",
|
||||
"created_by_id",
|
||||
"identifier",
|
||||
name="process_instance_report_unique",
|
||||
),
|
||||
@ -67,21 +65,53 @@ class ProcessInstanceReportModel(SpiffworkflowBaseDBModel):
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
identifier: str = db.Column(db.String(50), nullable=False, index=True)
|
||||
process_model_identifier: str = db.Column(db.String(50), nullable=False, index=True)
|
||||
process_group_identifier = db.Column(db.String(50), nullable=False, index=True)
|
||||
report_metadata: dict = deferred(db.Column(db.JSON)) # type: ignore
|
||||
created_by_id = db.Column(ForeignKey(UserModel.id), nullable=False)
|
||||
created_by_id = db.Column(ForeignKey(UserModel.id), nullable=False, index=True)
|
||||
created_by = relationship("UserModel")
|
||||
created_at_in_seconds = db.Column(db.Integer)
|
||||
updated_at_in_seconds = db.Column(db.Integer)
|
||||
|
||||
@classmethod
|
||||
def default_report(cls, user: UserModel) -> ProcessInstanceReportModel:
|
||||
"""Default_report."""
|
||||
identifier = "default"
|
||||
process_instance_report = ProcessInstanceReportModel.query.filter_by(
|
||||
identifier=identifier, created_by_id=user.id
|
||||
).first()
|
||||
|
||||
if process_instance_report is None:
|
||||
report_metadata = {
|
||||
"columns": [
|
||||
{"Header": "id", "accessor": "id"},
|
||||
{
|
||||
"Header": "process_group_identifier",
|
||||
"accessor": "process_group_identifier",
|
||||
},
|
||||
{
|
||||
"Header": "process_model_identifier",
|
||||
"accessor": "process_model_identifier",
|
||||
},
|
||||
{"Header": "start_in_seconds", "accessor": "start_in_seconds"},
|
||||
{"Header": "end_in_seconds", "accessor": "end_in_seconds"},
|
||||
{"Header": "status", "accessor": "status"},
|
||||
],
|
||||
}
|
||||
|
||||
process_instance_report = cls(
|
||||
identifier=identifier,
|
||||
created_by_id=user.id,
|
||||
report_metadata=report_metadata,
|
||||
)
|
||||
|
||||
return process_instance_report # type: ignore
|
||||
|
||||
@classmethod
|
||||
def add_fixtures(cls) -> None:
|
||||
"""Add_fixtures."""
|
||||
try:
|
||||
process_model = ProcessModelService().get_process_model(
|
||||
process_model_id="sartography-admin/ticket"
|
||||
)
|
||||
# process_model = ProcessModelService().get_process_model(
|
||||
# process_model_id="sartography-admin/ticket"
|
||||
# )
|
||||
user = UserModel.query.first()
|
||||
columns = [
|
||||
{"Header": "id", "accessor": "id"},
|
||||
@ -96,29 +126,21 @@ class ProcessInstanceReportModel(SpiffworkflowBaseDBModel):
|
||||
|
||||
cls.create_report(
|
||||
identifier="standard",
|
||||
process_group_identifier=process_model.process_group_id,
|
||||
process_model_identifier=process_model.id,
|
||||
user=user,
|
||||
report_metadata=json,
|
||||
)
|
||||
cls.create_report(
|
||||
identifier="for-month",
|
||||
process_group_identifier="sartography-admin",
|
||||
process_model_identifier="ticket",
|
||||
user=user,
|
||||
report_metadata=cls.ticket_for_month_report(),
|
||||
)
|
||||
cls.create_report(
|
||||
identifier="for-month-3",
|
||||
process_group_identifier="sartography-admin",
|
||||
process_model_identifier="ticket",
|
||||
user=user,
|
||||
report_metadata=cls.ticket_for_month_3_report(),
|
||||
)
|
||||
cls.create_report(
|
||||
identifier="hot-report",
|
||||
process_group_identifier="category_number_one",
|
||||
process_model_identifier="process-model-with-form",
|
||||
user=user,
|
||||
report_metadata=cls.process_model_with_form_report_fixture(),
|
||||
)
|
||||
@ -130,23 +152,18 @@ class ProcessInstanceReportModel(SpiffworkflowBaseDBModel):
|
||||
def create_report(
|
||||
cls,
|
||||
identifier: str,
|
||||
process_group_identifier: str,
|
||||
process_model_identifier: str,
|
||||
user: UserModel,
|
||||
report_metadata: ReportMetadata,
|
||||
) -> None:
|
||||
"""Make_fixture_report."""
|
||||
process_instance_report = ProcessInstanceReportModel.query.filter_by(
|
||||
identifier=identifier,
|
||||
process_group_identifier=process_group_identifier,
|
||||
process_model_identifier=process_model_identifier,
|
||||
created_by_id=user.id,
|
||||
).first()
|
||||
|
||||
if process_instance_report is None:
|
||||
process_instance_report = cls(
|
||||
identifier=identifier,
|
||||
process_group_identifier=process_group_identifier,
|
||||
process_model_identifier=process_model_identifier,
|
||||
created_by_id=user.id,
|
||||
report_metadata=report_metadata,
|
||||
)
|
||||
@ -217,19 +234,22 @@ class ProcessInstanceReportModel(SpiffworkflowBaseDBModel):
|
||||
def create_with_attributes(
|
||||
cls,
|
||||
identifier: str,
|
||||
process_group_identifier: str,
|
||||
process_model_identifier: str,
|
||||
report_metadata: dict,
|
||||
user: UserModel,
|
||||
) -> ProcessInstanceReportModel:
|
||||
"""Create_with_attributes."""
|
||||
process_model = ProcessModelService().get_process_model(
|
||||
process_model_id=f"{process_model_identifier}"
|
||||
)
|
||||
# <<<<<<< HEAD
|
||||
# process_model = ProcessModelService().get_process_model(
|
||||
# process_model_id=f"{process_model_identifier}"
|
||||
# )
|
||||
# process_instance_report = cls(
|
||||
# identifier=identifier,
|
||||
# process_group_identifier="process_model.process_group_id",
|
||||
# process_model_identifier=process_model.id,
|
||||
# =======
|
||||
process_instance_report = cls(
|
||||
identifier=identifier,
|
||||
process_group_identifier="process_model.process_group_id",
|
||||
process_model_identifier=process_model.id,
|
||||
# >>>>>>> main
|
||||
created_by_id=user.id,
|
||||
report_metadata=report_metadata,
|
||||
)
|
||||
|
@ -65,6 +65,7 @@ from spiffworkflow_backend.services.error_handling_service import ErrorHandlingS
|
||||
from spiffworkflow_backend.services.file_system_service import FileSystemService
|
||||
from spiffworkflow_backend.services.git_service import GitService
|
||||
from spiffworkflow_backend.services.message_service import MessageService
|
||||
from spiffworkflow_backend.services.process_instance_processor import MyCustomParser
|
||||
from spiffworkflow_backend.services.process_instance_processor import (
|
||||
ProcessInstanceProcessor,
|
||||
)
|
||||
@ -280,6 +281,10 @@ def process_model_show(modified_process_model_identifier: str) -> Any:
|
||||
# process_model.id = process_model_identifier
|
||||
files = sorted(SpecFileService.get_files(process_model))
|
||||
process_model.files = files
|
||||
for file in process_model.files:
|
||||
file.references = SpecFileService.get_references_for_file(
|
||||
file, process_model, MyCustomParser
|
||||
)
|
||||
process_model_json = ProcessModelInfoSchema().dump(process_model)
|
||||
return process_model_json
|
||||
|
||||
@ -717,10 +722,29 @@ def process_instance_list(
|
||||
ProcessInstanceModel.start_in_seconds.desc(), ProcessInstanceModel.id.desc() # type: ignore
|
||||
).paginate(page=page, per_page=per_page, error_out=False)
|
||||
|
||||
process_instance_report = ProcessInstanceReportModel.default_report(g.user)
|
||||
|
||||
# TODO need to look into this more - how the filter here interacts with the
|
||||
# one defined in the report.
|
||||
# TODO need to look into test failures when the results from result_dict is
|
||||
# used instead of the process instances
|
||||
|
||||
# substitution_variables = request.args.to_dict()
|
||||
# result_dict = process_instance_report.generate_report(
|
||||
# process_instances.items, substitution_variables
|
||||
# )
|
||||
|
||||
# results = result_dict["results"]
|
||||
# report_metadata = result_dict["report_metadata"]
|
||||
|
||||
results = process_instances.items
|
||||
report_metadata = process_instance_report.report_metadata
|
||||
|
||||
response_json = {
|
||||
"results": process_instances.items,
|
||||
"report_metadata": report_metadata,
|
||||
"results": results,
|
||||
"pagination": {
|
||||
"count": len(process_instances.items),
|
||||
"count": len(results),
|
||||
"total": process_instances.total,
|
||||
"pages": process_instances.pages,
|
||||
},
|
||||
@ -769,26 +793,20 @@ def process_instance_delete(
|
||||
|
||||
|
||||
def process_instance_report_list(
|
||||
modified_process_model_identifier: str, page: int = 1, per_page: int = 100
|
||||
page: int = 1, per_page: int = 100
|
||||
) -> flask.wrappers.Response:
|
||||
"""Process_instance_report_list."""
|
||||
process_model_identifier = modified_process_model_identifier.replace(":", "/")
|
||||
|
||||
process_instance_reports = ProcessInstanceReportModel.query.filter_by(
|
||||
process_model_identifier=process_model_identifier,
|
||||
created_by_id=g.user.id,
|
||||
).all()
|
||||
|
||||
return make_response(jsonify(process_instance_reports), 200)
|
||||
|
||||
|
||||
def process_instance_report_create(
|
||||
process_group_id: str, process_model_id: str, body: Dict[str, Any]
|
||||
) -> flask.wrappers.Response:
|
||||
def process_instance_report_create(body: Dict[str, Any]) -> flask.wrappers.Response:
|
||||
"""Process_instance_report_create."""
|
||||
ProcessInstanceReportModel.create_report(
|
||||
identifier=body["identifier"],
|
||||
process_group_identifier=process_group_id,
|
||||
process_model_identifier=process_model_id,
|
||||
user=g.user,
|
||||
report_metadata=body["report_metadata"],
|
||||
)
|
||||
@ -797,16 +815,13 @@ def process_instance_report_create(
|
||||
|
||||
|
||||
def process_instance_report_update(
|
||||
process_group_id: str,
|
||||
process_model_id: str,
|
||||
report_identifier: str,
|
||||
body: Dict[str, Any],
|
||||
) -> flask.wrappers.Response:
|
||||
"""Process_instance_report_create."""
|
||||
process_instance_report = ProcessInstanceReportModel.query.filter_by(
|
||||
identifier=report_identifier,
|
||||
process_group_identifier=process_group_id,
|
||||
process_model_identifier=process_model_id,
|
||||
created_by_id=g.user.id,
|
||||
).first()
|
||||
if process_instance_report is None:
|
||||
raise ApiError(
|
||||
@ -822,15 +837,12 @@ def process_instance_report_update(
|
||||
|
||||
|
||||
def process_instance_report_delete(
|
||||
process_group_id: str,
|
||||
process_model_id: str,
|
||||
report_identifier: str,
|
||||
) -> flask.wrappers.Response:
|
||||
"""Process_instance_report_create."""
|
||||
process_instance_report = ProcessInstanceReportModel.query.filter_by(
|
||||
identifier=report_identifier,
|
||||
process_group_identifier=process_group_id,
|
||||
process_model_identifier=process_model_id,
|
||||
created_by_id=g.user.id,
|
||||
).first()
|
||||
if process_instance_report is None:
|
||||
raise ApiError(
|
||||
@ -883,24 +895,21 @@ def authentication_callback(
|
||||
|
||||
|
||||
def process_instance_report_show(
|
||||
modified_process_model_identifier: str,
|
||||
report_identifier: str,
|
||||
page: int = 1,
|
||||
per_page: int = 100,
|
||||
) -> flask.wrappers.Response:
|
||||
"""Process_instance_list."""
|
||||
process_model_identifier = modified_process_model_identifier.replace(":", "/")
|
||||
|
||||
process_instances = (
|
||||
ProcessInstanceModel.query.filter_by(process_model_identifier=process_model_identifier)
|
||||
.order_by(
|
||||
ProcessInstanceModel.start_in_seconds.desc(), ProcessInstanceModel.id.desc() # type: ignore
|
||||
)
|
||||
.paginate(page=page, per_page=per_page, error_out=False)
|
||||
process_instances = ProcessInstanceModel.query.order_by( # .filter_by(process_model_identifier=process_model.id)
|
||||
ProcessInstanceModel.start_in_seconds.desc(), ProcessInstanceModel.id.desc() # type: ignore
|
||||
).paginate(
|
||||
page=page, per_page=per_page, error_out=False
|
||||
)
|
||||
|
||||
process_instance_report = ProcessInstanceReportModel.query.filter_by(
|
||||
identifier=report_identifier
|
||||
identifier=report_identifier,
|
||||
created_by_id=g.user.id,
|
||||
).first()
|
||||
if process_instance_report is None:
|
||||
raise ApiError(
|
||||
|
@ -7,6 +7,7 @@ from typing import Optional
|
||||
from flask import current_app
|
||||
from flask_bpmn.api.api_error import ApiError
|
||||
from flask_bpmn.models.db import db
|
||||
from SpiffWorkflow.util.deep_merge import DeepMerge # type: ignore
|
||||
from SpiffWorkflow.task import Task as SpiffTask # type: ignore
|
||||
|
||||
from spiffworkflow_backend.models.process_instance import ProcessInstanceApi
|
||||
@ -104,6 +105,20 @@ class ProcessInstanceService:
|
||||
title=title_value,
|
||||
)
|
||||
|
||||
next_task_trying_again = next_task
|
||||
if (
|
||||
not next_task
|
||||
): # The Next Task can be requested to be a certain task, useful for parallel tasks.
|
||||
# This may or may not work, sometimes there is no next task to complete.
|
||||
next_task_trying_again = processor.next_task()
|
||||
|
||||
if next_task_trying_again is not None:
|
||||
process_instance_api.next_task = (
|
||||
ProcessInstanceService.spiff_task_to_api_task(
|
||||
next_task_trying_again, add_docs_and_forms=True
|
||||
)
|
||||
)
|
||||
|
||||
return process_instance_api
|
||||
|
||||
def get_process_instance(self, process_instance_id: int) -> Any:
|
||||
|
@ -2,6 +2,7 @@
|
||||
import os
|
||||
import shutil
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
|
||||
@ -14,6 +15,7 @@ from SpiffWorkflow.bpmn.parser.ValidationException import ValidationException #
|
||||
|
||||
from spiffworkflow_backend.models.bpmn_process_id_lookup import BpmnProcessIdLookup
|
||||
from spiffworkflow_backend.models.file import File
|
||||
from spiffworkflow_backend.models.file import FileReference
|
||||
from spiffworkflow_backend.models.file import FileType
|
||||
from spiffworkflow_backend.models.message_correlation_property import (
|
||||
MessageCorrelationPropertyModel,
|
||||
@ -55,6 +57,41 @@ class SpecFileService(FileSystemService):
|
||||
)
|
||||
return files
|
||||
|
||||
@staticmethod
|
||||
def get_references_for_file(
|
||||
file: File, process_model_info: ProcessModelInfo, parser_class: Any
|
||||
) -> list[FileReference]:
|
||||
"""Uses spiffworkflow to parse BPMN and DMN files to determine how they can be externally referenced.
|
||||
|
||||
Returns a list of Reference objects that contain the type of reference, the id, the name.
|
||||
Ex.
|
||||
id = {str} 'Level3'
|
||||
name = {str} 'Level 3'
|
||||
type = {str} 'process'
|
||||
"""
|
||||
references: list[FileReference] = []
|
||||
file_path = SpecFileService.file_path(process_model_info, file.name)
|
||||
parser = parser_class()
|
||||
parser_type = None
|
||||
sub_parser = None
|
||||
if file.type == FileType.bpmn.value:
|
||||
parser.add_bpmn_file(file_path)
|
||||
parser_type = "process"
|
||||
sub_parsers = list(parser.process_parsers.values())
|
||||
elif file.type == FileType.dmn.value:
|
||||
parser.add_dmn_file(file_path)
|
||||
sub_parsers = list(parser.dmn_parsers.values())
|
||||
parser_type = "decision"
|
||||
else:
|
||||
return references
|
||||
for sub_parser in sub_parsers:
|
||||
references.append(
|
||||
FileReference(
|
||||
id=sub_parser.get_id(), name=sub_parser.get_name(), type=parser_type
|
||||
)
|
||||
)
|
||||
return references
|
||||
|
||||
@staticmethod
|
||||
def add_file(
|
||||
process_model_info: ProcessModelInfo, file_name: str, binary_data: bytes
|
||||
|
@ -0,0 +1 @@
|
||||
{}
|
@ -1255,7 +1255,7 @@ class TestProcessApi(BaseTest):
|
||||
)
|
||||
|
||||
assert response.json is not None
|
||||
# assert response.json['next_task'] is not None
|
||||
assert response.json['next_task'] is not None
|
||||
|
||||
active_tasks = (
|
||||
db.session.query(ActiveTaskModel)
|
||||
@ -1518,13 +1518,11 @@ class TestProcessApi(BaseTest):
|
||||
report_metadata = {"order_by": ["month"]}
|
||||
ProcessInstanceReportModel.create_with_attributes(
|
||||
identifier=report_identifier,
|
||||
process_group_identifier="",
|
||||
process_model_identifier=process_model_identifier,
|
||||
report_metadata=report_metadata,
|
||||
user=with_super_admin_user,
|
||||
)
|
||||
response = client.get(
|
||||
f"/v1.0/process-models/{modified_process_model_identifier}/process-instances/reports",
|
||||
"/v1.0/process-instances/reports",
|
||||
headers=self.logged_in_headers(with_super_admin_user),
|
||||
)
|
||||
assert response.status_code == 200
|
||||
@ -1578,14 +1576,12 @@ class TestProcessApi(BaseTest):
|
||||
|
||||
ProcessInstanceReportModel.create_with_attributes(
|
||||
identifier="sure",
|
||||
process_group_identifier="test_process_group_id",
|
||||
process_model_identifier=process_model_identifier,
|
||||
report_metadata=report_metadata,
|
||||
user=with_super_admin_user,
|
||||
)
|
||||
|
||||
response = client.get(
|
||||
f"/v1.0/process-models/{modified_process_model_identifier}/process-instances/reports/sure",
|
||||
"/v1.0/process-instances/reports/sure",
|
||||
headers=self.logged_in_headers(with_super_admin_user),
|
||||
)
|
||||
assert response.status_code == 200
|
||||
@ -1613,10 +1609,6 @@ class TestProcessApi(BaseTest):
|
||||
setup_process_instances_for_reports: list[ProcessInstanceModel],
|
||||
) -> None:
|
||||
"""Test_process_instance_report_show_with_default_list."""
|
||||
test_process_group_id = "runs_without_input"
|
||||
process_model_dir_name = "sample"
|
||||
process_model_identifier = f"{test_process_group_id}/{process_model_dir_name}"
|
||||
modified_process_model_identifier = process_model_identifier.replace("/", ":")
|
||||
|
||||
report_metadata = {
|
||||
"filter_by": [
|
||||
@ -1630,14 +1622,12 @@ class TestProcessApi(BaseTest):
|
||||
|
||||
ProcessInstanceReportModel.create_with_attributes(
|
||||
identifier="sure",
|
||||
process_group_identifier="test_process_group_id",
|
||||
process_model_identifier=process_model_identifier,
|
||||
report_metadata=report_metadata,
|
||||
user=with_super_admin_user,
|
||||
)
|
||||
|
||||
response = client.get(
|
||||
f"/v1.0/process-models/{modified_process_model_identifier}/process-instances/reports/sure?grade_level=1",
|
||||
"/v1.0/process-instances/reports/sure?grade_level=1",
|
||||
headers=self.logged_in_headers(with_super_admin_user),
|
||||
)
|
||||
assert response.status_code == 200
|
||||
@ -1653,11 +1643,8 @@ class TestProcessApi(BaseTest):
|
||||
setup_process_instances_for_reports: list[ProcessInstanceModel],
|
||||
) -> None:
|
||||
"""Test_process_instance_report_show_with_default_list."""
|
||||
test_process_group_id = "runs_without_input"
|
||||
process_model_dir_name = "sample"
|
||||
|
||||
response = client.get(
|
||||
f"/v1.0/process-models/{test_process_group_id}:{process_model_dir_name}/process-instances/reports/sure?grade_level=1",
|
||||
"/v1.0/process-instances/reports/sure?grade_level=1",
|
||||
headers=self.logged_in_headers(with_super_admin_user),
|
||||
)
|
||||
assert response.status_code == 404
|
||||
|
@ -21,7 +21,6 @@ def create_test_file(type: str, name: str) -> File:
|
||||
type=type,
|
||||
name=name,
|
||||
content_type=type,
|
||||
document={},
|
||||
last_modified=datetime.now(),
|
||||
size=1,
|
||||
)
|
||||
|
@ -128,8 +128,6 @@ def do_report_with_metadata_and_instances(
|
||||
"""Do_report_with_metadata_and_instances."""
|
||||
process_instance_report = ProcessInstanceReportModel.create_with_attributes(
|
||||
identifier="sure",
|
||||
process_group_identifier=process_instances[0].process_group_identifier,
|
||||
process_model_identifier=process_instances[0].process_model_identifier,
|
||||
report_metadata=report_metadata,
|
||||
user=BaseTest.find_or_create_user(),
|
||||
)
|
||||
|
@ -40,7 +40,7 @@ class TestScriptUnitTestRunner(BaseTest):
|
||||
)
|
||||
)
|
||||
task = ProcessInstanceProcessor.get_task_by_bpmn_identifier(
|
||||
"Activity_RunScript", bpmn_process_instance
|
||||
"Activity_CalculateNewData", bpmn_process_instance
|
||||
)
|
||||
assert task is not None
|
||||
|
||||
@ -81,7 +81,7 @@ class TestScriptUnitTestRunner(BaseTest):
|
||||
)
|
||||
)
|
||||
task = ProcessInstanceProcessor.get_task_by_bpmn_identifier(
|
||||
"Activity_RunScript", bpmn_process_instance
|
||||
"Activity_CalculateNewData", bpmn_process_instance
|
||||
)
|
||||
assert task is not None
|
||||
|
||||
|
@ -6,11 +6,14 @@ from flask import Flask
|
||||
from flask.testing import FlaskClient
|
||||
from flask_bpmn.api.api_error import ApiError
|
||||
from flask_bpmn.models.db import db
|
||||
from SpiffWorkflow.dmn.parser.BpmnDmnParser import BpmnDmnParser # type: ignore
|
||||
from tests.spiffworkflow_backend.helpers.base_test import BaseTest
|
||||
from tests.spiffworkflow_backend.helpers.test_data import load_test_spec
|
||||
|
||||
from spiffworkflow_backend.models.bpmn_process_id_lookup import BpmnProcessIdLookup
|
||||
from spiffworkflow_backend.models.user import UserModel
|
||||
from spiffworkflow_backend.services.process_model_service import ProcessModelService
|
||||
from spiffworkflow_backend.services.spec_file_service import SpecFileService
|
||||
|
||||
|
||||
class TestSpecFileService(BaseTest):
|
||||
@ -108,3 +111,55 @@ class TestSpecFileService(BaseTest):
|
||||
bpmn_process_id_lookups[0].bpmn_file_relative_path
|
||||
== self.call_activity_nested_relative_file_path
|
||||
)
|
||||
|
||||
def test_load_reference_information(
|
||||
self, app: Flask, client: FlaskClient, with_db_and_bpmn_file_cleanup: None, with_super_admin_user: UserModel
|
||||
) -> None:
|
||||
"""Test_load_reference_information.
|
||||
|
||||
When getting files from the spec_file service, each file includes
|
||||
details about how the file can be referenced -- for instance
|
||||
it is possible to reference a DMN file with a Decision.id or
|
||||
a bpmn file with a process.id. These Decisions and processes
|
||||
can also have human readable display names, which should also be avaiable.
|
||||
Note that a single bpmn file can contain many processes, and
|
||||
a DMN file can (theoretically) contain many decisions. So this
|
||||
is an array.
|
||||
"""
|
||||
process_group_id = "test_group"
|
||||
process_model_id = "call_activity_nested"
|
||||
bpmn_file_name = "call_activity_nested.bpmn"
|
||||
process_model_identifier = self.basic_test_setup(
|
||||
client=client,
|
||||
user=with_super_admin_user,
|
||||
process_group_id=process_group_id,
|
||||
process_model_id=process_model_id,
|
||||
# bpmn_file_name=bpmn_file_name,
|
||||
bpmn_file_location=process_model_id
|
||||
)
|
||||
# load_test_spec(
|
||||
# ,
|
||||
# process_model_source_directory="call_activity_nested",
|
||||
# )
|
||||
process_model_info = ProcessModelService().get_process_model(
|
||||
process_model_identifier
|
||||
)
|
||||
files = SpecFileService.get_files(process_model_info)
|
||||
|
||||
file = next(filter(lambda f: f.name == "call_activity_level_3.bpmn", files))
|
||||
ca_3 = SpecFileService.get_references_for_file(
|
||||
file, process_model_info, BpmnDmnParser
|
||||
)
|
||||
assert len(ca_3) == 1
|
||||
assert ca_3[0].name == "Level 3"
|
||||
assert ca_3[0].id == "Level3"
|
||||
assert ca_3[0].type == "process"
|
||||
|
||||
file = next(filter(lambda f: f.name == "level2c.dmn", files))
|
||||
dmn1 = SpecFileService.get_references_for_file(
|
||||
file, process_model_info, BpmnDmnParser
|
||||
)
|
||||
assert len(dmn1) == 1
|
||||
assert dmn1[0].name == "Decision 1"
|
||||
assert dmn1[0].id == "Decision_0vrtcmk"
|
||||
assert dmn1[0].type == "decision"
|
||||
|
3
spiffworkflow-frontend/.gitignore
vendored
3
spiffworkflow-frontend/.gitignore
vendored
@ -27,3 +27,6 @@ cypress/screenshots
|
||||
|
||||
# i keep accidentally committing these
|
||||
/test*.json
|
||||
|
||||
# Editors
|
||||
.idea
|
1050
spiffworkflow-frontend/package-lock.json
generated
1050
spiffworkflow-frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -20,12 +20,13 @@
|
||||
"@types/node": "^18.6.5",
|
||||
"@types/react": "^18.0.17",
|
||||
"@types/react-dom": "^18.0.6",
|
||||
"@uiw/react-md-editor": "^3.19.5",
|
||||
"autoprefixer": "10.4.8",
|
||||
"axios": "^0.27.2",
|
||||
"bootstrap": "^5.2.0",
|
||||
"bpmn-js": "^9.3.2",
|
||||
"bpmn-js-properties-panel": "^1.10.0",
|
||||
"bpmn-js-spiffworkflow": "sartography/bpmn-js-spiffworkflow#main",
|
||||
"bpmn-js-spiffworkflow": "sartography/bpmn-js-spiffworkflow#feature/more_launch_buttons_and_dropdowns",
|
||||
"craco": "^0.0.3",
|
||||
"date-fns": "^2.28.0",
|
||||
"diagram-js": "^8.5.0",
|
||||
|
@ -4,10 +4,14 @@ import { Button, Modal } from '@carbon/react';
|
||||
|
||||
type OwnProps = {
|
||||
description?: string;
|
||||
buttonLabel: string;
|
||||
buttonLabel?: string;
|
||||
onConfirmation: (..._args: any[]) => any;
|
||||
title?: string;
|
||||
confirmButtonLabel?: string;
|
||||
kind?: string;
|
||||
renderIcon?: boolean;
|
||||
iconDescription?: string | null;
|
||||
hasIconOnly?: boolean;
|
||||
};
|
||||
|
||||
export default function ButtonWithConfirmation({
|
||||
@ -16,6 +20,10 @@ export default function ButtonWithConfirmation({
|
||||
onConfirmation,
|
||||
title = 'Are you sure?',
|
||||
confirmButtonLabel = 'OK',
|
||||
kind = 'danger',
|
||||
renderIcon = false,
|
||||
iconDescription = null,
|
||||
hasIconOnly = false,
|
||||
}: OwnProps) {
|
||||
const [showConfirmationPrompt, setShowConfirmationPrompt] = useState(false);
|
||||
|
||||
@ -49,7 +57,13 @@ export default function ButtonWithConfirmation({
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button onClick={handleShowConfirmationPrompt} kind="danger">
|
||||
<Button
|
||||
onClick={handleShowConfirmationPrompt}
|
||||
kind={kind}
|
||||
renderIcon={renderIcon}
|
||||
iconDescription={iconDescription}
|
||||
hasIconOnly={hasIconOnly}
|
||||
>
|
||||
{buttonLabel}
|
||||
</Button>
|
||||
{confirmationDialog()}
|
||||
|
@ -1,12 +1,12 @@
|
||||
import { Link } from 'react-router-dom';
|
||||
import Breadcrumb from 'react-bootstrap/Breadcrumb';
|
||||
import { BreadcrumbItem } from '../interfaces';
|
||||
// @ts-ignore
|
||||
import { Breadcrumb, BreadcrumbItem } from '@carbon/react';
|
||||
import { HotCrumbItem } from '../interfaces';
|
||||
|
||||
type OwnProps = {
|
||||
processModelId?: string;
|
||||
processGroupId?: string;
|
||||
linkProcessModel?: boolean;
|
||||
hotCrumbs?: BreadcrumbItem[];
|
||||
hotCrumbs?: HotCrumbItem[];
|
||||
};
|
||||
|
||||
export default function ProcessBreadcrumb({
|
||||
@ -22,18 +22,20 @@ export default function ProcessBreadcrumb({
|
||||
if (lastItem === undefined) {
|
||||
return null;
|
||||
}
|
||||
const lastCrumb = <Breadcrumb.Item active>{lastItem[0]}</Breadcrumb.Item>;
|
||||
const leadingCrumbLinks = hotCrumbs.map((crumb) => {
|
||||
const lastCrumb = (
|
||||
<BreadcrumbItem isCurrentPage>{lastItem[0]}</BreadcrumbItem>
|
||||
);
|
||||
const leadingCrumbLinks = hotCrumbs.map((crumb: any) => {
|
||||
const valueLabel = crumb[0];
|
||||
const url = crumb[1];
|
||||
return (
|
||||
<Breadcrumb.Item key={valueLabel} linkAs={Link} linkProps={{ to: url }}>
|
||||
<BreadcrumbItem key={valueLabel} href={url}>
|
||||
{valueLabel}
|
||||
</Breadcrumb.Item>
|
||||
</BreadcrumbItem>
|
||||
);
|
||||
});
|
||||
return (
|
||||
<Breadcrumb>
|
||||
<Breadcrumb noTrailingSlash>
|
||||
{leadingCrumbLinks}
|
||||
{lastCrumb}
|
||||
</Breadcrumb>
|
||||
@ -42,42 +44,38 @@ export default function ProcessBreadcrumb({
|
||||
if (processModelId) {
|
||||
if (linkProcessModel) {
|
||||
processModelBreadcrumb = (
|
||||
<Breadcrumb.Item
|
||||
linkAs={Link}
|
||||
linkProps={{
|
||||
to: `/admin/process-models/${processGroupId}/${processModelId}`,
|
||||
}}
|
||||
<BreadcrumbItem
|
||||
href={`/admin/process-models/${processGroupId}/${processModelId}`}
|
||||
>
|
||||
Process Model: {processModelId}
|
||||
</Breadcrumb.Item>
|
||||
{`Process Model: ${processModelId}`}
|
||||
</BreadcrumbItem>
|
||||
);
|
||||
} else {
|
||||
processModelBreadcrumb = (
|
||||
<Breadcrumb.Item active>
|
||||
Process Model: {processModelId}
|
||||
</Breadcrumb.Item>
|
||||
<BreadcrumbItem isCurrentPage>
|
||||
{`Process Model: ${processModelId}`}
|
||||
</BreadcrumbItem>
|
||||
);
|
||||
}
|
||||
processGroupBreadcrumb = (
|
||||
<Breadcrumb.Item
|
||||
linkAs={Link}
|
||||
<BreadcrumbItem
|
||||
data-qa="process-group-breadcrumb-link"
|
||||
linkProps={{ to: `/admin/process-groups/${processGroupId}` }}
|
||||
href={`/admin/process-groups/${processGroupId}`}
|
||||
>
|
||||
Process Group: {processGroupId}
|
||||
</Breadcrumb.Item>
|
||||
{`Process Group: ${processGroupId}`}
|
||||
</BreadcrumbItem>
|
||||
);
|
||||
} else if (processGroupId) {
|
||||
processGroupBreadcrumb = (
|
||||
<Breadcrumb.Item active>Process Group: {processGroupId}</Breadcrumb.Item>
|
||||
<BreadcrumbItem isCurrentPage>
|
||||
{`Process Group: ${processGroupId}`}
|
||||
</BreadcrumbItem>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Breadcrumb>
|
||||
<Breadcrumb.Item linkAs={Link} linkProps={{ to: '/admin' }}>
|
||||
Process Groups
|
||||
</Breadcrumb.Item>
|
||||
<Breadcrumb noTrailingSlash>
|
||||
<BreadcrumbItem href="/admin">Process Groups</BreadcrumbItem>
|
||||
{processGroupBreadcrumb}
|
||||
{processModelBreadcrumb}
|
||||
</Breadcrumb>
|
||||
|
@ -68,8 +68,14 @@ type OwnProps = {
|
||||
diagramXML?: string | null;
|
||||
fileName?: string;
|
||||
onLaunchScriptEditor?: (..._args: any[]) => any;
|
||||
onLaunchMarkdownEditor?: (..._args: any[]) => any;
|
||||
onLaunchBpmnEditor?: (..._args: any[]) => any;
|
||||
onLaunchJsonEditor?: (..._args: any[]) => any;
|
||||
onLaunchDmnEditor?: (..._args: any[]) => any;
|
||||
onElementClick?: (..._args: any[]) => any;
|
||||
onServiceTasksRequested?: (..._args: any[]) => any;
|
||||
onJsonFilesRequested?: (..._args: any[]) => any;
|
||||
onDmnFilesRequested?: (..._args: any[]) => any;
|
||||
url?: string;
|
||||
};
|
||||
|
||||
@ -85,8 +91,14 @@ export default function ReactDiagramEditor({
|
||||
diagramXML,
|
||||
fileName,
|
||||
onLaunchScriptEditor,
|
||||
onLaunchMarkdownEditor,
|
||||
onLaunchBpmnEditor,
|
||||
onLaunchJsonEditor,
|
||||
onLaunchDmnEditor,
|
||||
onElementClick,
|
||||
onServiceTasksRequested,
|
||||
onJsonFilesRequested,
|
||||
onDmnFilesRequested,
|
||||
url,
|
||||
}: OwnProps) {
|
||||
const [diagramXMLString, setDiagramXMLString] = useState('');
|
||||
@ -189,6 +201,17 @@ export default function ReactDiagramEditor({
|
||||
}
|
||||
}
|
||||
|
||||
function handleLaunchMarkdownEditor(
|
||||
element: any,
|
||||
value: string,
|
||||
eventBus: any
|
||||
) {
|
||||
if (onLaunchMarkdownEditor) {
|
||||
setPerformingXmlUpdates(true);
|
||||
onLaunchMarkdownEditor(element, value, eventBus);
|
||||
}
|
||||
}
|
||||
|
||||
function handleElementClick(event: any) {
|
||||
if (onElementClick) {
|
||||
onElementClick(event.element);
|
||||
@ -203,7 +226,7 @@ export default function ReactDiagramEditor({
|
||||
|
||||
setDiagramModelerState(diagramModeler);
|
||||
|
||||
diagramModeler.on('script.editor.launch', (event: any) => {
|
||||
diagramModeler.on('spiff.script.edit', (event: any) => {
|
||||
const { error, element, scriptType, script, eventBus } = event;
|
||||
if (error) {
|
||||
console.log(error);
|
||||
@ -211,6 +234,35 @@ export default function ReactDiagramEditor({
|
||||
handleLaunchScriptEditor(element, script, scriptType, eventBus);
|
||||
});
|
||||
|
||||
diagramModeler.on('spiff.markdown.edit', (event: any) => {
|
||||
const { error, element, value, eventBus } = event;
|
||||
if (error) {
|
||||
console.log(error);
|
||||
}
|
||||
handleLaunchMarkdownEditor(element, value, eventBus);
|
||||
});
|
||||
|
||||
/**
|
||||
* fixme: this is not in use yet, we need the ability to find bpmn files by id.
|
||||
*/
|
||||
diagramModeler.on('spiff.callactivity.edit', (event: any) => {
|
||||
if (onLaunchBpmnEditor) {
|
||||
onLaunchBpmnEditor(event.processId);
|
||||
}
|
||||
});
|
||||
|
||||
diagramModeler.on('spiff.file.edit', (event: any) => {
|
||||
if (onLaunchJsonEditor) {
|
||||
onLaunchJsonEditor(event.value);
|
||||
}
|
||||
});
|
||||
|
||||
diagramModeler.on('spiff.dmn.edit', (event: any) => {
|
||||
if (onLaunchDmnEditor) {
|
||||
onLaunchDmnEditor(event.value);
|
||||
}
|
||||
});
|
||||
|
||||
// 'element.hover',
|
||||
// 'element.out',
|
||||
// 'element.click',
|
||||
@ -224,12 +276,34 @@ export default function ReactDiagramEditor({
|
||||
diagramModeler.on('spiff.service_tasks.requested', (event: any) => {
|
||||
handleServiceTasksRequested(event);
|
||||
});
|
||||
|
||||
diagramModeler.on('spiff.json_files.requested', (event: any) => {
|
||||
if (onJsonFilesRequested) {
|
||||
onJsonFilesRequested(event);
|
||||
}
|
||||
});
|
||||
|
||||
diagramModeler.on('spiff.dmn_files.requested', (event: any) => {
|
||||
if (onDmnFilesRequested) {
|
||||
onDmnFilesRequested(event);
|
||||
}
|
||||
});
|
||||
|
||||
diagramModeler.on('spiff.json_files.requested', (event: any) => {
|
||||
handleServiceTasksRequested(event);
|
||||
});
|
||||
}, [
|
||||
diagramModelerState,
|
||||
diagramType,
|
||||
onLaunchScriptEditor,
|
||||
onLaunchMarkdownEditor,
|
||||
onLaunchBpmnEditor,
|
||||
onLaunchDmnEditor,
|
||||
onLaunchJsonEditor,
|
||||
onElementClick,
|
||||
onServiceTasksRequested,
|
||||
onJsonFilesRequested,
|
||||
onDmnFilesRequested,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -40,6 +40,10 @@ span.bjs-crumb {
|
||||
opacity: .4;
|
||||
}
|
||||
|
||||
.accordion-item-label {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.diagram-editor-canvas {
|
||||
border:1px solid #000000;
|
||||
height:70vh;
|
||||
|
@ -1,10 +1,22 @@
|
||||
// @use '@carbon/react/scss/themes';
|
||||
// @use '@carbon/react/scss/theme' with ($theme: themes.$g100);
|
||||
|
||||
// @use '@carbon/react/scss/theme' with
|
||||
// (
|
||||
// $theme: (
|
||||
// cds-link-primary: #525252
|
||||
// )
|
||||
// );
|
||||
|
||||
@use '@carbon/react';
|
||||
@use '@carbon/styles';
|
||||
// @include grid.flex-grid();
|
||||
|
||||
@use '@carbon/colors';
|
||||
// @use '@carbon/react/scss/colors';
|
||||
@use '@carbon/react/scss/themes';
|
||||
|
||||
// var(--cds-link-text-color, var(--cds-link-primary, #0f62fe))
|
||||
|
||||
// site is mainly using white theme.
|
||||
// header is mainly using g100
|
||||
@ -13,3 +25,60 @@
|
||||
// background-color: colors.$gray-100;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.cds--breadcrumb-item a.cds--link:hover {
|
||||
color: #525252;
|
||||
}
|
||||
.cds--breadcrumb-item a.cds--link:visited {
|
||||
color: #525252;
|
||||
}
|
||||
.cds--breadcrumb-item a.cds--link:visited:hover {
|
||||
color: #525252;
|
||||
}
|
||||
.cds--breadcrumb-item a.cds--link {
|
||||
color: #525252;
|
||||
}
|
||||
|
||||
.cds--btn--ghost {
|
||||
color: black;
|
||||
}
|
||||
.cds--btn--ghost:visited {
|
||||
color: black;
|
||||
}
|
||||
.cds--btn--ghost:hover {
|
||||
color: black;
|
||||
}
|
||||
.cds--btn--ghost:visited:hover {
|
||||
color: black;
|
||||
}
|
||||
|
||||
$slightly-lighter-gray: #474747;
|
||||
$spiff-header-background-color: #161616;
|
||||
|
||||
.cds--header__global .cds--btn--primary {
|
||||
background-color: $spiff-header-background-color;
|
||||
}
|
||||
.cds--btn--primary {
|
||||
background-color: #393939;
|
||||
}
|
||||
.cds--btn--primary:hover {
|
||||
background-color: $slightly-lighter-gray;
|
||||
}
|
||||
// .cds--btn--ghost:visited {
|
||||
// color: black;
|
||||
// }
|
||||
// .cds--btn--ghost:hover {
|
||||
// color: black;
|
||||
// }
|
||||
// .cds--btn--ghost:visited:hover {
|
||||
// color: black;
|
||||
// }
|
||||
|
||||
|
||||
// :root {
|
||||
// --cds-link-primary: #525252;
|
||||
// }
|
||||
// .card {
|
||||
// background: var(--orange);
|
||||
// --orange: hsl(255, 72%, var(--lightness));
|
||||
// }
|
||||
|
@ -17,15 +17,35 @@ export interface ProcessGroup {
|
||||
description?: string | null;
|
||||
}
|
||||
|
||||
export interface ProcessFileReference {
|
||||
id: string; // The unique id of the process or decision table.
|
||||
name: string; // The process or decision table name.
|
||||
type: string; // either "decision" or "process"
|
||||
}
|
||||
|
||||
export interface ProcessFile {
|
||||
content_type: string;
|
||||
last_modified: string;
|
||||
name: string;
|
||||
process_group_id: string;
|
||||
process_model_id: string;
|
||||
references: ProcessFileReference[];
|
||||
size: number;
|
||||
type: string;
|
||||
file_contents?: string;
|
||||
}
|
||||
|
||||
export interface ProcessModel {
|
||||
id: string;
|
||||
description: string;
|
||||
process_group_id: string;
|
||||
display_name: string;
|
||||
primary_file_name: string;
|
||||
files: ProcessFile[];
|
||||
}
|
||||
|
||||
// tuple of display value and URL
|
||||
export type BreadcrumbItem = [displayValue: string, url?: string];
|
||||
export type HotCrumbItem = [displayValue: string, url?: string];
|
||||
|
||||
export interface ErrorForDisplay {
|
||||
message: string;
|
||||
|
@ -51,6 +51,7 @@ export default function ProcessInstanceList() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [processInstances, setProcessInstances] = useState([]);
|
||||
const [reportMetadata, setReportMetadata] = useState({});
|
||||
const [pagination, setPagination] = useState<PaginationObject | null>(null);
|
||||
|
||||
const oneHourInSeconds = 3600;
|
||||
@ -97,6 +98,7 @@ export default function ProcessInstanceList() {
|
||||
function setProcessInstancesFromResult(result: any) {
|
||||
const processInstancesFromApi = result.results;
|
||||
setProcessInstances(processInstancesFromApi);
|
||||
setReportMetadata(result.report_metadata);
|
||||
setPagination(result.pagination);
|
||||
}
|
||||
function getProcessInstances() {
|
||||
@ -380,53 +382,79 @@ export default function ProcessInstanceList() {
|
||||
};
|
||||
|
||||
const buildTable = () => {
|
||||
const rows = processInstances.map((row: any) => {
|
||||
const formattedStartDate =
|
||||
convertSecondsToFormattedDate(row.start_in_seconds) || '-';
|
||||
const formattedEndDate =
|
||||
convertSecondsToFormattedDate(row.end_in_seconds) || '-';
|
||||
const headerLabels: Record<string, string> = {
|
||||
id: 'Process Instance Id',
|
||||
process_group_identifier: 'Process Group',
|
||||
process_model_identifier: 'Process Model',
|
||||
start_in_seconds: 'Start Time',
|
||||
end_in_seconds: 'End Time',
|
||||
status: 'Status',
|
||||
spiff_step: 'SpiffWorkflow Step',
|
||||
};
|
||||
const getHeaderLabel = (header: string) => {
|
||||
return headerLabels[header] ?? header;
|
||||
};
|
||||
const headers = (reportMetadata as any).columns.map((column: any) => {
|
||||
return <th>{getHeaderLabel((column as any).Header)}</th>;
|
||||
});
|
||||
|
||||
const formatProcessInstanceId = (row: any, id: any) => {
|
||||
const modifiedProcessModelId: String = modifyProcessModelPath(
|
||||
(row as any).process_model_identifier
|
||||
);
|
||||
const groupId = getGroupFromModifiedModelId(modifiedProcessModelId);
|
||||
|
||||
return (
|
||||
<tr key={row.id}>
|
||||
<td>
|
||||
<Link
|
||||
data-qa="process-instance-show-link"
|
||||
to={`/admin/process-models/${modifiedProcessModelId}/process-instances/${row.id}`}
|
||||
>
|
||||
{row.id}
|
||||
</Link>
|
||||
</td>
|
||||
<td>
|
||||
<Link to={`/admin/process-groups/${groupId}`}>{groupId}</Link>
|
||||
</td>
|
||||
<td>
|
||||
<Link to={`/admin/process-models/${modifiedProcessModelId}`}>
|
||||
{modifiedProcessModelId}
|
||||
</Link>
|
||||
</td>
|
||||
<td>{formattedStartDate}</td>
|
||||
<td>{formattedEndDate}</td>
|
||||
<td data-qa={`process-instance-status-${row.status}`}>
|
||||
{row.status}
|
||||
</td>
|
||||
</tr>
|
||||
<Link
|
||||
data-qa="process-instance-show-link"
|
||||
to={`/admin/process-models/${modifiedProcessModelId}/process-instances/${row.id}`}
|
||||
>
|
||||
{id}
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
const formatProcessGroupIdentifier = (row: any, identifier: any) => {
|
||||
return (
|
||||
<Link to={`/admin/process-groups/${identifier}`}>{identifier}</Link>
|
||||
);
|
||||
};
|
||||
const formatProcessModelIdentifier = (row: any, identifier: any) => {
|
||||
return (
|
||||
<Link
|
||||
to={`/admin/process-models/${row.process_group_identifier}/${identifier}`}
|
||||
>
|
||||
{identifier}
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
const formatSecondsForDisplay = (row: any, seconds: any) => {
|
||||
return convertSecondsToFormattedDate(seconds) || '-';
|
||||
};
|
||||
const defaultFormatter = (row: any, value: any) => {
|
||||
return value;
|
||||
};
|
||||
|
||||
const columnFormatters: Record<string, any> = {
|
||||
id: formatProcessInstanceId,
|
||||
process_group_identifier: formatProcessGroupIdentifier,
|
||||
process_model_identifier: formatProcessModelIdentifier,
|
||||
start_in_seconds: formatSecondsForDisplay,
|
||||
end_in_seconds: formatSecondsForDisplay,
|
||||
};
|
||||
const formattedColumn = (row: any, column: any) => {
|
||||
const formatter = columnFormatters[column.accessor] ?? defaultFormatter;
|
||||
const value = row[column.accessor];
|
||||
return <td>{formatter(row, value)}</td>;
|
||||
};
|
||||
|
||||
const rows = processInstances.map((row) => {
|
||||
const currentRow = (reportMetadata as any).columns.map((column: any) => {
|
||||
return formattedColumn(row, column);
|
||||
});
|
||||
return <tr key={(row as any).id}>{currentRow}</tr>;
|
||||
});
|
||||
return (
|
||||
<Table size="lg">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Process Instance Id</th>
|
||||
<th>Process Group</th>
|
||||
<th>Process Model</th>
|
||||
<th>Start Time</th>
|
||||
<th>End Time</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
<tr>{headers}</tr>
|
||||
</thead>
|
||||
<tbody>{rows}</tbody>
|
||||
</Table>
|
||||
|
@ -56,7 +56,7 @@ export default function ProcessInstanceReportEdit() {
|
||||
};
|
||||
function getProcessInstanceReport() {
|
||||
HttpService.makeCallToBackend({
|
||||
path: `/process-models/${params.process_group_id}/${params.process_model_id}/process-instances/reports/${params.report_identifier}?per_page=1`,
|
||||
path: `/process-instances/reports/${params.report_identifier}?per_page=1`,
|
||||
successCallback: processResult,
|
||||
});
|
||||
}
|
||||
@ -88,7 +88,7 @@ export default function ProcessInstanceReportEdit() {
|
||||
.filter((n) => n);
|
||||
|
||||
HttpService.makeCallToBackend({
|
||||
path: `/process-models/${params.process_group_id}/${params.process_model_id}/process-instances/reports/${params.report_identifier}`,
|
||||
path: `/process-instances/reports/${params.report_identifier}`,
|
||||
successCallback: navigateToProcessInstanceReport,
|
||||
httpMethod: 'PUT',
|
||||
postBody: {
|
||||
@ -103,7 +103,7 @@ export default function ProcessInstanceReportEdit() {
|
||||
|
||||
const deleteProcessInstanceReport = () => {
|
||||
HttpService.makeCallToBackend({
|
||||
path: `/process-models/${params.process_group_id}/${params.process_model_id}/process-instances/reports/${params.report_identifier}`,
|
||||
path: `/process-instances/reports/${params.report_identifier}`,
|
||||
successCallback: navigateToProcessInstanceReports,
|
||||
httpMethod: 'DELETE',
|
||||
});
|
||||
|
@ -15,7 +15,7 @@ export default function ProcessInstanceReportList() {
|
||||
|
||||
useEffect(() => {
|
||||
HttpService.makeCallToBackend({
|
||||
path: `/process-models/${modifiedProcessModelId}/process-instances/reports`,
|
||||
path: `/process-instances/reports`,
|
||||
successCallback: setProcessInstanceReports,
|
||||
});
|
||||
}, [params]);
|
||||
|
@ -42,7 +42,7 @@ export default function ProcessInstanceReportNew() {
|
||||
.filter((n) => n);
|
||||
|
||||
HttpService.makeCallToBackend({
|
||||
path: `/process-models/${params.process_group_id}/${params.process_model_id}/process-instances/reports`,
|
||||
path: `/process-instances/reports`,
|
||||
successCallback: navigateToNewProcessInstance,
|
||||
httpMethod: 'POST',
|
||||
postBody: {
|
||||
|
@ -39,7 +39,7 @@ export default function ProcessInstanceReport() {
|
||||
}
|
||||
});
|
||||
HttpService.makeCallToBackend({
|
||||
path: `/process-models/${params.process_group_id}/${params.process_model_id}/process-instances/reports/${params.report_identifier}?${query}`,
|
||||
path: `/process-instances/reports/${params.report_identifier}${query}`,
|
||||
successCallback: processResult,
|
||||
});
|
||||
}
|
||||
|
@ -57,6 +57,7 @@ export default function ProcessModelEdit() {
|
||||
});
|
||||
};
|
||||
|
||||
// share with or delete from ProcessModelEditDiagram
|
||||
const deleteProcessModel = () => {
|
||||
setErrorMessage(null);
|
||||
const processModelToUse = processModel as any;
|
||||
|
@ -1,24 +1,30 @@
|
||||
import { useContext, useEffect, useRef, useState } from 'react';
|
||||
import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
|
||||
import {
|
||||
generatePath,
|
||||
useNavigate,
|
||||
useParams,
|
||||
useSearchParams,
|
||||
} from 'react-router-dom';
|
||||
// @ts-ignore
|
||||
import { Button, Modal, Stack, Content } from '@carbon/react';
|
||||
// import Container from 'react-bootstrap/Container';
|
||||
import Row from 'react-bootstrap/Row';
|
||||
import Col from 'react-bootstrap/Col';
|
||||
|
||||
import Editor from '@monaco-editor/react';
|
||||
|
||||
import MDEditor from '@uiw/react-md-editor';
|
||||
import ReactDiagramEditor from '../components/ReactDiagramEditor';
|
||||
import ProcessBreadcrumb from '../components/ProcessBreadcrumb';
|
||||
import HttpService from '../services/HttpService';
|
||||
import ErrorContext from '../contexts/ErrorContext';
|
||||
import { makeid } from '../helpers';
|
||||
import { ProcessModel } from '../interfaces';
|
||||
import { modifyProcessModelPath } from '../helpers';
|
||||
import { ProcessFile, ProcessModel } from '../interfaces';
|
||||
|
||||
export default function ProcessModelEditDiagram() {
|
||||
const [showFileNameEditor, setShowFileNameEditor] = useState(false);
|
||||
const handleShowFileNameEditor = () => setShowFileNameEditor(true);
|
||||
const [processModel, setProcessModel] = useState<ProcessModel | null>(null);
|
||||
|
||||
const [scriptText, setScriptText] = useState<string>('');
|
||||
const [scriptType, setScriptType] = useState<string>('');
|
||||
@ -28,6 +34,12 @@ export default function ProcessModelEditDiagram() {
|
||||
const [showScriptEditor, setShowScriptEditor] = useState(false);
|
||||
const handleShowScriptEditor = () => setShowScriptEditor(true);
|
||||
|
||||
const [markdownText, setMarkdownText] = useState<string | undefined>('');
|
||||
const [markdownEventBus, setMarkdownEventBus] = useState<any>(null);
|
||||
const [showMarkdownEditor, setShowMarkdownEditor] = useState(false);
|
||||
|
||||
const handleShowMarkdownEditor = () => setShowMarkdownEditor(true);
|
||||
|
||||
const editorRef = useRef(null);
|
||||
const monacoRef = useRef(null);
|
||||
|
||||
@ -71,8 +83,6 @@ export default function ProcessModelEditDiagram() {
|
||||
const [bpmnXmlForDiagramRendering, setBpmnXmlForDiagramRendering] =
|
||||
useState(null);
|
||||
|
||||
const [processModel, setProcessModel] = useState<ProcessModel | null>(null);
|
||||
|
||||
const modifiedProcessModelId = modifyProcessModelPath(
|
||||
(params as any).process_model_id
|
||||
);
|
||||
@ -90,7 +100,7 @@ export default function ProcessModelEditDiagram() {
|
||||
}, [processModelPath]);
|
||||
|
||||
useEffect(() => {
|
||||
const processResult = (result: any) => {
|
||||
const fileResult = (result: any) => {
|
||||
setProcessModelFile(result);
|
||||
setBpmnXmlForDiagramRendering(result.file_contents);
|
||||
};
|
||||
@ -99,7 +109,7 @@ export default function ProcessModelEditDiagram() {
|
||||
console.log(`processModelPath: ${processModelPath}`);
|
||||
HttpService.makeCallToBackend({
|
||||
path: `/${processModelPath}/files/${params.file_name}`,
|
||||
successCallback: processResult,
|
||||
successCallback: fileResult,
|
||||
});
|
||||
}
|
||||
}, [processModelPath, params]);
|
||||
@ -162,9 +172,7 @@ export default function ProcessModelEditDiagram() {
|
||||
const httpMethod = 'DELETE';
|
||||
|
||||
const navigateToProcessModelShow = (_httpResult: any) => {
|
||||
navigate(
|
||||
`/admin/process-models/${modifiedProcessModelId}`
|
||||
);
|
||||
navigate(`/admin/process-models/${modifiedProcessModelId}`);
|
||||
};
|
||||
HttpService.makeCallToBackend({
|
||||
path: url,
|
||||
@ -252,6 +260,34 @@ export default function ProcessModelEditDiagram() {
|
||||
});
|
||||
};
|
||||
|
||||
const onJsonFilesRequested = (event: any) => {
|
||||
if (processModel) {
|
||||
const jsonFiles = processModel.files.filter((f) => f.type === 'json');
|
||||
const options = jsonFiles.map((f) => {
|
||||
return { label: f.name, value: f.name };
|
||||
});
|
||||
event.eventBus.fire('spiff.json_files.returned', { options });
|
||||
} else {
|
||||
console.log('There is no process Model.');
|
||||
}
|
||||
};
|
||||
|
||||
const onDmnFilesRequested = (event: any) => {
|
||||
if (processModel) {
|
||||
const dmnFiles = processModel.files.filter((f) => f.type === 'dmn');
|
||||
const options: any[] = [];
|
||||
dmnFiles.forEach((file) => {
|
||||
file.references.forEach((ref) => {
|
||||
options.push({ label: ref.name, value: ref.id });
|
||||
});
|
||||
});
|
||||
console.log('Options', options);
|
||||
event.eventBus.fire('spiff.dmn_files.returned', { options });
|
||||
} else {
|
||||
console.log('There is no process model.');
|
||||
}
|
||||
};
|
||||
|
||||
const getScriptUnitTestElements = (element: any) => {
|
||||
const { extensionElements } = element.businessObject;
|
||||
if (extensionElements && extensionElements.values.length > 0) {
|
||||
@ -298,7 +334,7 @@ export default function ProcessModelEditDiagram() {
|
||||
};
|
||||
|
||||
const handleScriptEditorClose = () => {
|
||||
scriptEventBus.fire('script.editor.update', {
|
||||
scriptEventBus.fire('spiff.script.update', {
|
||||
scriptType,
|
||||
script: scriptText,
|
||||
element: scriptElement,
|
||||
@ -586,6 +622,107 @@ export default function ProcessModelEditDiagram() {
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
const onLaunchMarkdownEditor = (
|
||||
element: any,
|
||||
markdown: string,
|
||||
eventBus: any
|
||||
) => {
|
||||
setMarkdownText(markdown || '');
|
||||
setMarkdownEventBus(eventBus);
|
||||
handleShowMarkdownEditor();
|
||||
};
|
||||
const handleMarkdownEditorClose = () => {
|
||||
markdownEventBus.fire('spiff.markdown.update', {
|
||||
value: markdownText,
|
||||
});
|
||||
setShowMarkdownEditor(false);
|
||||
};
|
||||
|
||||
const markdownEditor = () => {
|
||||
return (
|
||||
<Modal
|
||||
size="xl"
|
||||
show={showMarkdownEditor}
|
||||
onHide={handleMarkdownEditorClose}
|
||||
>
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title>Edit Markdown Content</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<MDEditor value={markdownText} onChange={setMarkdownText} />
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button variant="secondary" onClick={handleMarkdownEditorClose}>
|
||||
Close
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
const findFileNameForReferenceId = (
|
||||
id: string,
|
||||
type: string
|
||||
): ProcessFile | null => {
|
||||
// Given a reference id (like a process_id, or decision_id) finds the file
|
||||
// that contains that reference and returns it.
|
||||
let matchFile = null;
|
||||
if (processModel) {
|
||||
const files = processModel.files.filter((f) => f.type === type);
|
||||
files.some((file) => {
|
||||
if (file.references.some((ref) => ref.id === id)) {
|
||||
matchFile = file;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
return matchFile;
|
||||
};
|
||||
|
||||
/**
|
||||
* fixme: Not currently in use. This would only work for bpmn files within the process model. Which is right for DMN and json, but not right here. Need to merge in work on the nested process groups before tackling this.
|
||||
* @param processId
|
||||
*/
|
||||
const onLaunchBpmnEditor = (processId: string) => {
|
||||
const file = findFileNameForReferenceId(processId, 'bpmn');
|
||||
if (file) {
|
||||
const path = generatePath(
|
||||
'/admin/process-models/:process_group_id/:process_model_id/files/:file_name',
|
||||
{
|
||||
process_group_id: params.process_group_id,
|
||||
process_model_id: params.process_model_id,
|
||||
file_name: file.name,
|
||||
}
|
||||
);
|
||||
window.open(path);
|
||||
}
|
||||
};
|
||||
const onLaunchJsonEditor = (fileName: string) => {
|
||||
const path = generatePath(
|
||||
'/admin/process-models/:process_group_id/:process_model_id/form/:file_name',
|
||||
{
|
||||
process_group_id: params.process_group_id,
|
||||
process_model_id: params.process_model_id,
|
||||
file_name: fileName,
|
||||
}
|
||||
);
|
||||
window.open(path);
|
||||
};
|
||||
const onLaunchDmnEditor = (processId: string) => {
|
||||
const file = findFileNameForReferenceId(processId, 'dmn');
|
||||
if (file) {
|
||||
const path = generatePath(
|
||||
'/admin/process-models/:process_group_id/:process_model_id/files/:file_name',
|
||||
{
|
||||
process_group_id: params.process_group_id,
|
||||
process_model_id: params.process_model_id,
|
||||
file_name: file.name,
|
||||
}
|
||||
);
|
||||
window.open(path);
|
||||
}
|
||||
};
|
||||
|
||||
const isDmn = () => {
|
||||
const fileName = params.file_name || '';
|
||||
@ -626,12 +763,18 @@ export default function ProcessModelEditDiagram() {
|
||||
diagramType="bpmn"
|
||||
onLaunchScriptEditor={onLaunchScriptEditor}
|
||||
onServiceTasksRequested={onServiceTasksRequested}
|
||||
onLaunchMarkdownEditor={onLaunchMarkdownEditor}
|
||||
onLaunchBpmnEditor={onLaunchBpmnEditor}
|
||||
onLaunchJsonEditor={onLaunchJsonEditor}
|
||||
onJsonFilesRequested={onJsonFilesRequested}
|
||||
onLaunchDmnEditor={onLaunchDmnEditor}
|
||||
onDmnFilesRequested={onDmnFilesRequested}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
// if a file name is not given then this is a new model and the ReactDiagramEditor component will handle it
|
||||
if (bpmnXmlForDiagramRendering || !params.file_name) {
|
||||
if ((bpmnXmlForDiagramRendering || !params.file_name) && processModel) {
|
||||
return (
|
||||
<>
|
||||
<ProcessBreadcrumb
|
||||
@ -646,6 +789,7 @@ export default function ProcessModelEditDiagram() {
|
||||
{appropriateEditor()}
|
||||
{newFileNameBox()}
|
||||
{scriptEditor()}
|
||||
{markdownEditor()}
|
||||
|
||||
<div id="diagram-container" />
|
||||
</>
|
||||
|
@ -1,13 +1,44 @@
|
||||
import { useContext, useEffect, useState } from 'react';
|
||||
import { Link, useParams } from 'react-router-dom';
|
||||
// @ts-ignore
|
||||
import { Button, Stack } from '@carbon/react';
|
||||
import { Link, useNavigate, useParams } from 'react-router-dom';
|
||||
import {
|
||||
Add,
|
||||
Upload,
|
||||
Download,
|
||||
TrashCan,
|
||||
Favorite,
|
||||
Edit,
|
||||
// @ts-ignore
|
||||
} from '@carbon/icons-react';
|
||||
import {
|
||||
Accordion,
|
||||
AccordionItem,
|
||||
Button,
|
||||
Stack,
|
||||
ButtonSet,
|
||||
Modal,
|
||||
FileUploader,
|
||||
Table,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
TableCell,
|
||||
TableBody,
|
||||
// @ts-ignore
|
||||
} from '@carbon/react';
|
||||
import ProcessBreadcrumb from '../components/ProcessBreadcrumb';
|
||||
import FileInput from '../components/FileInput';
|
||||
import HttpService from '../services/HttpService';
|
||||
import ErrorContext from '../contexts/ErrorContext';
|
||||
import { RecentProcessModel } from '../interfaces';
|
||||
import {modifyProcessModelPath, unModifyProcessModelPath} from '../helpers';
|
||||
import { modifyProcessModelPath, unModifyProcessModelPath } from '../helpers';
|
||||
import { ProcessFile, ProcessModel, RecentProcessModel } from '../interfaces';
|
||||
import ButtonWithConfirmation from '../components/ButtonWithConfirmation';
|
||||
|
||||
// interface ProcessModelFileCarbonDropdownItem {
|
||||
// label: string;
|
||||
// action: string;
|
||||
// processModelFile: ProcessFile;
|
||||
// needsConfirmation: boolean;
|
||||
// icon: any;
|
||||
// }
|
||||
|
||||
const storeRecentProcessModelInLocalStorage = (
|
||||
processModelForStorage: any,
|
||||
@ -67,16 +98,20 @@ export default function ProcessModelShow() {
|
||||
const params = useParams();
|
||||
const setErrorMessage = (useContext as any)(ErrorContext)[1];
|
||||
|
||||
const [processModel, setProcessModel] = useState({});
|
||||
const [processModel, setProcessModel] = useState<ProcessModel | null>(null);
|
||||
const [processInstanceResult, setProcessInstanceResult] = useState(null);
|
||||
const [reloadModel, setReloadModel] = useState(false);
|
||||
const [reloadModel, setReloadModel] = useState<boolean>(false);
|
||||
const [filesToUpload, setFilesToUpload] = useState<any>(null);
|
||||
const [showFileUploadModal, setShowFileUploadModal] =
|
||||
useState<boolean>(false);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const modifiedProcessModelId = modifyProcessModelPath(
|
||||
`${params.process_model_id}`
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const processResult = (result: object) => {
|
||||
const processResult = (result: ProcessModel) => {
|
||||
setProcessModel(result);
|
||||
setReloadModel(false);
|
||||
storeRecentProcessModelInLocalStorage(result, params);
|
||||
@ -105,85 +140,241 @@ export default function ProcessModelShow() {
|
||||
});
|
||||
};
|
||||
|
||||
let processInstanceResultTag = null;
|
||||
if (processInstanceResult) {
|
||||
let takeMeToMyTaskBlurb = null;
|
||||
// FIXME: ensure that the task is actually for the current user as well
|
||||
const processInstanceId = (processInstanceResult as any).id;
|
||||
const nextTask = (processInstanceResult as any).next_task;
|
||||
if (nextTask && nextTask.state === 'READY') {
|
||||
takeMeToMyTaskBlurb = (
|
||||
<span>
|
||||
You have a task to complete. Go to{' '}
|
||||
<Link to={`/tasks/${processInstanceId}/${nextTask.id}`}>my task</Link>
|
||||
.
|
||||
</span>
|
||||
const processInstanceResultTag = () => {
|
||||
if (processModel && processInstanceResult) {
|
||||
let takeMeToMyTaskBlurb = null;
|
||||
// FIXME: ensure that the task is actually for the current user as well
|
||||
const processInstanceId = (processInstanceResult as any).id;
|
||||
const nextTask = (processInstanceResult as any).next_task;
|
||||
if (nextTask && nextTask.state === 'READY') {
|
||||
takeMeToMyTaskBlurb = (
|
||||
<span>
|
||||
You have a task to complete. Go to{' '}
|
||||
<Link to={`/tasks/${processInstanceId}/${nextTask.id}`}>
|
||||
my task
|
||||
</Link>
|
||||
.
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="alert alert-success" role="alert">
|
||||
<p>
|
||||
Process Instance {processInstanceId} kicked off (
|
||||
<Link
|
||||
to={`/admin/process-models/${modifiedProcessModelId}/process-instances/${processInstanceId}`}
|
||||
data-qa="process-instance-show-link"
|
||||
>
|
||||
view
|
||||
</Link>
|
||||
). {takeMeToMyTaskBlurb}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
processInstanceResultTag = (
|
||||
<div className="alert alert-success" role="alert">
|
||||
<p>
|
||||
Process Instance {processInstanceId} kicked off (
|
||||
<Link
|
||||
to={`/admin/process-models/${modifiedProcessModelId}/process-instances/${processInstanceId}`}
|
||||
data-qa="process-instance-show-link"
|
||||
>
|
||||
view
|
||||
</Link>
|
||||
). {takeMeToMyTaskBlurb}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const onUploadedCallback = () => {
|
||||
setReloadModel(true);
|
||||
};
|
||||
const reloadModelOhYeah = (_httpResult: any) => {
|
||||
setReloadModel(!reloadModel);
|
||||
};
|
||||
|
||||
const processModelFileList = () => {
|
||||
let constructedTag;
|
||||
const tags = (processModel as any).files.map((processModelFile: any) => {
|
||||
// Remove this code from
|
||||
const onDeleteFile = (fileName: string) => {
|
||||
const url = `/process-models/${params.process_group_id}/${params.process_model_id}/files/${fileName}`;
|
||||
const httpMethod = 'DELETE';
|
||||
HttpService.makeCallToBackend({
|
||||
path: url,
|
||||
successCallback: reloadModelOhYeah,
|
||||
httpMethod,
|
||||
});
|
||||
};
|
||||
|
||||
// const onProcessModelFileAction = (selection: any) => {
|
||||
// const { selectedItem } = selection;
|
||||
// if (selectedItem.action === 'delete') {
|
||||
// onDeleteFile(selectedItem.processModelFile.name);
|
||||
// }
|
||||
// };
|
||||
|
||||
const onSetPrimaryFile = (fileName: string) => {
|
||||
const url = `/process-models/${params.process_group_id}/${params.process_model_id}`;
|
||||
const httpMethod = 'PUT';
|
||||
|
||||
const processModelToPass = {
|
||||
primary_file_name: fileName,
|
||||
};
|
||||
HttpService.makeCallToBackend({
|
||||
path: url,
|
||||
successCallback: onUploadedCallback,
|
||||
httpMethod,
|
||||
postBody: processModelToPass,
|
||||
});
|
||||
};
|
||||
const handleProcessModelFileResult = (processModelFile: ProcessFile) => {
|
||||
if (
|
||||
!('file_contents' in processModelFile) ||
|
||||
processModelFile.file_contents === undefined
|
||||
) {
|
||||
setErrorMessage({
|
||||
message: `Could not file file contents for file: ${processModelFile.name}`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
let contentType = 'application/xml';
|
||||
if (processModelFile.type === 'json') {
|
||||
contentType = 'application/json';
|
||||
}
|
||||
const element = document.createElement('a');
|
||||
const file = new Blob([processModelFile.file_contents], {
|
||||
type: contentType,
|
||||
});
|
||||
const downloadFileName = processModelFile.name;
|
||||
element.href = URL.createObjectURL(file);
|
||||
element.download = downloadFileName;
|
||||
document.body.appendChild(element);
|
||||
element.click();
|
||||
};
|
||||
|
||||
const downloadFile = (fileName: string) => {
|
||||
setErrorMessage(null);
|
||||
const processModelPath = `process-models/${params.process_group_id}/${params.process_model_id}`;
|
||||
HttpService.makeCallToBackend({
|
||||
path: `/${processModelPath}/files/${fileName}`,
|
||||
successCallback: handleProcessModelFileResult,
|
||||
});
|
||||
};
|
||||
|
||||
const navigateToFileEdit = (processModelFile: ProcessFile) => {
|
||||
if (processModel) {
|
||||
if (processModelFile.name.match(/\.(dmn|bpmn)$/)) {
|
||||
let primarySuffix = '';
|
||||
if (processModelFile.name === (processModel as any).primary_file_name) {
|
||||
primarySuffix = '- Primary File';
|
||||
}
|
||||
// const modifiedProcessModelId = modifyProcessModelPath(
|
||||
// (processModel as any).id
|
||||
// );
|
||||
constructedTag = (
|
||||
<li key={processModelFile.name}>
|
||||
<Link
|
||||
to={`/admin/process-models/${modifiedProcessModelId}/files/${processModelFile.name}`}
|
||||
>
|
||||
{processModelFile.name}
|
||||
</Link>
|
||||
{primarySuffix}
|
||||
</li>
|
||||
navigate(
|
||||
`/admin/process-models/${modifiedProcessModelId}/files/${processModelFile.name}`
|
||||
);
|
||||
} else if (processModelFile.name.match(/\.(json|md)$/)) {
|
||||
constructedTag = (
|
||||
<li key={processModelFile.name}>
|
||||
<Link
|
||||
to={`/admin/process-models/${modifiedProcessModelId}/form/${processModelFile.name}`}
|
||||
>
|
||||
{processModelFile.name}
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
} else {
|
||||
constructedTag = (
|
||||
<li key={processModelFile.name}>{processModelFile.name}</li>
|
||||
navigate(
|
||||
`/admin/process-models/${modifiedProcessModelId}/form/${processModelFile.name}`
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const renderButtonElements = (
|
||||
processModelFile: ProcessFile,
|
||||
isPrimaryBpmnFile: boolean
|
||||
) => {
|
||||
const elements = [];
|
||||
elements.push(
|
||||
<Button
|
||||
kind="ghost"
|
||||
renderIcon={Edit}
|
||||
iconDescription="Edit File"
|
||||
hasIconOnly
|
||||
size="lg"
|
||||
onClick={() => navigateToFileEdit(processModelFile)}
|
||||
/>
|
||||
);
|
||||
elements.push(
|
||||
<Button
|
||||
kind="ghost"
|
||||
renderIcon={Download}
|
||||
iconDescription="Download File"
|
||||
hasIconOnly
|
||||
size="lg"
|
||||
onClick={() => downloadFile(processModelFile.name)}
|
||||
/>
|
||||
);
|
||||
|
||||
elements.push(
|
||||
<ButtonWithConfirmation
|
||||
kind="ghost"
|
||||
renderIcon={TrashCan}
|
||||
iconDescription="Delete File"
|
||||
hasIconOnly
|
||||
description={`Delete file: ${processModelFile.name}`}
|
||||
onConfirmation={() => {
|
||||
onDeleteFile(processModelFile.name);
|
||||
}}
|
||||
confirmButtonLabel="Delete"
|
||||
/>
|
||||
);
|
||||
if (processModelFile.name.match(/\.bpmn$/) && !isPrimaryBpmnFile) {
|
||||
elements.push(
|
||||
<Button
|
||||
kind="ghost"
|
||||
renderIcon={Favorite}
|
||||
iconDescription="Set As Primary File"
|
||||
hasIconOnly
|
||||
size="lg"
|
||||
onClick={() => onSetPrimaryFile(processModelFile.name)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return elements;
|
||||
};
|
||||
|
||||
const processModelFileList = () => {
|
||||
if (!processModel) {
|
||||
return null;
|
||||
}
|
||||
let constructedTag;
|
||||
const tags = processModel.files.map((processModelFile: ProcessFile) => {
|
||||
const isPrimaryBpmnFile =
|
||||
processModelFile.name === processModel.primary_file_name;
|
||||
|
||||
let actionsTableCell = null;
|
||||
if (processModelFile.name.match(/\.(dmn|bpmn|json|md)$/)) {
|
||||
actionsTableCell = (
|
||||
<TableCell key={`${processModelFile.name}-cell`}>
|
||||
{renderButtonElements(processModelFile, isPrimaryBpmnFile)}
|
||||
</TableCell>
|
||||
);
|
||||
}
|
||||
|
||||
let primarySuffix = '';
|
||||
if (isPrimaryBpmnFile) {
|
||||
primarySuffix = '- Primary File';
|
||||
}
|
||||
constructedTag = (
|
||||
<TableRow key={processModelFile.name}>
|
||||
<TableCell key={`${processModelFile.name}-cell`}>
|
||||
{processModelFile.name}
|
||||
{primarySuffix}
|
||||
</TableCell>
|
||||
{actionsTableCell}
|
||||
</TableRow>
|
||||
);
|
||||
return constructedTag;
|
||||
});
|
||||
|
||||
return <ul>{tags}</ul>;
|
||||
// return <ul>{tags}</ul>;
|
||||
const headers = ['Name', 'Actions'];
|
||||
return (
|
||||
<Table size="lg" useZebraStyles={false}>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
{headers.map((header) => (
|
||||
<TableHeader id={header} key={header}>
|
||||
{header}
|
||||
</TableHeader>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>{tags}</TableBody>
|
||||
</Table>
|
||||
);
|
||||
};
|
||||
|
||||
const processInstancesUl = () => {
|
||||
const unmodifiedProcessModelId: String = unModifyProcessModelPath(`${params.process_model_id}`);
|
||||
const unmodifiedProcessModelId: String = unModifyProcessModelPath(
|
||||
`${params.process_model_id}`
|
||||
);
|
||||
if (!processModel) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<ul>
|
||||
<li>
|
||||
@ -206,69 +397,149 @@ export default function ProcessModelShow() {
|
||||
);
|
||||
};
|
||||
|
||||
const processModelButtons = () => {
|
||||
const handleFileUploadCancel = () => {
|
||||
setShowFileUploadModal(false);
|
||||
};
|
||||
|
||||
const handleFileUpload = (event: any) => {
|
||||
if (processModel) {
|
||||
event.preventDefault();
|
||||
const url = `/process-models/${processModel.process_group_id}/${processModel.id}/files`;
|
||||
const formData = new FormData();
|
||||
formData.append('file', filesToUpload[0]);
|
||||
formData.append('fileName', filesToUpload[0].name);
|
||||
HttpService.makeCallToBackend({
|
||||
path: url,
|
||||
successCallback: onUploadedCallback,
|
||||
httpMethod: 'POST',
|
||||
postBody: formData,
|
||||
});
|
||||
setShowFileUploadModal(false);
|
||||
}
|
||||
};
|
||||
|
||||
const fileUploadModal = () => {
|
||||
return (
|
||||
<Stack orientation="horizontal" gap={3}>
|
||||
<Button onClick={processInstanceCreateAndRun} variant="primary">
|
||||
Run
|
||||
</Button>
|
||||
<Button
|
||||
href={`/admin/process-models/${modifiedProcessModelId}/edit`}
|
||||
variant="secondary"
|
||||
>
|
||||
Edit process model
|
||||
</Button>
|
||||
<Button
|
||||
href={`/admin/process-models/${modifiedProcessModelId}/files?file_type=bpmn`}
|
||||
variant="warning"
|
||||
>
|
||||
Add New BPMN File
|
||||
</Button>
|
||||
<Button
|
||||
href={`/admin/process-models/${modifiedProcessModelId}/files?file_type=dmn`}
|
||||
variant="success"
|
||||
>
|
||||
Add New DMN File
|
||||
</Button>
|
||||
<Button
|
||||
href={`/admin/process-models/${modifiedProcessModelId}/form?file_ext=json`}
|
||||
variant="info"
|
||||
>
|
||||
Add New JSON File
|
||||
</Button>
|
||||
<Button
|
||||
href={`/admin/process-models/${modifiedProcessModelId}/form?file_ext=md`}
|
||||
variant="info"
|
||||
>
|
||||
Add New Markdown File
|
||||
</Button>
|
||||
</Stack>
|
||||
<Modal
|
||||
open={showFileUploadModal}
|
||||
modalHeading="Upload File"
|
||||
primaryButtonText="Upload"
|
||||
secondaryButtonText="Cancel"
|
||||
onSecondarySubmit={handleFileUploadCancel}
|
||||
onRequestClose={handleFileUploadCancel}
|
||||
onRequestSubmit={handleFileUpload}
|
||||
>
|
||||
<FileUploader
|
||||
labelTitle="Upload files"
|
||||
labelDescription="Max file size is 500mb. Only .bpmn, .dmn, and .json files are supported."
|
||||
buttonLabel="Add file"
|
||||
buttonKind="primary"
|
||||
size="md"
|
||||
filenameStatus="edit"
|
||||
role="button"
|
||||
accept={['.bpmn', '.dmn', '.json']}
|
||||
disabled={false}
|
||||
iconDescription="Delete file"
|
||||
name=""
|
||||
multiple={false}
|
||||
onChange={(event: any) => setFilesToUpload(event.target.files)}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
if (Object.keys(processModel).length > 1) {
|
||||
const processModelButtons = () => {
|
||||
if (!processModel) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Accordion>
|
||||
<AccordionItem
|
||||
title={
|
||||
<Stack orientation="horizontal">
|
||||
<span>
|
||||
<Button size="sm" kind="ghost">
|
||||
Files
|
||||
</Button>
|
||||
</span>
|
||||
</Stack>
|
||||
}
|
||||
>
|
||||
<ButtonSet>
|
||||
<Button
|
||||
renderIcon={Upload}
|
||||
onClick={() => setShowFileUploadModal(true)}
|
||||
size="sm"
|
||||
kind=""
|
||||
className="button-white-background"
|
||||
>
|
||||
Upload File
|
||||
</Button>
|
||||
<Button
|
||||
renderIcon={Add}
|
||||
href={`/admin/process-models/${modifiedProcessModelId}/files?file_type=bpmn`}
|
||||
size="sm"
|
||||
>
|
||||
New BPMN File
|
||||
</Button>
|
||||
<Button
|
||||
renderIcon={Add}
|
||||
href={`/admin/process-models/${modifiedProcessModelId}/files?file_type=dmn`}
|
||||
size="sm"
|
||||
>
|
||||
New DMN File
|
||||
</Button>
|
||||
<Button
|
||||
renderIcon={Add}
|
||||
href={`/admin/process-models/${modifiedProcessModelId}/form?file_ext=json`}
|
||||
size="sm"
|
||||
>
|
||||
New JSON File
|
||||
</Button>
|
||||
<Button
|
||||
renderIcon={Add}
|
||||
href={`/admin/process-models/${modifiedProcessModelId}/form?file_ext=md`}
|
||||
size="sm"
|
||||
>
|
||||
New Markdown File
|
||||
</Button>
|
||||
</ButtonSet>
|
||||
<br />
|
||||
{processModelFileList()}
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
);
|
||||
};
|
||||
|
||||
if (processModel) {
|
||||
return (
|
||||
<>
|
||||
{fileUploadModal()}
|
||||
<ProcessBreadcrumb
|
||||
processGroupId={(processModel as any).process_group_id}
|
||||
processModelId={(processModel as any).id}
|
||||
/>
|
||||
{processInstanceResultTag}
|
||||
<FileInput
|
||||
processModelId={(processModel as any).id}
|
||||
processGroupId={(processModel as any).process_group_id}
|
||||
onUploadedCallback={onUploadedCallback}
|
||||
processGroupId={processModel.process_group_id}
|
||||
processModelId={processModel.id}
|
||||
/>
|
||||
<h1>{processModel.display_name}</h1>
|
||||
<p>{processModel.description}</p>
|
||||
<Stack orientation="horizontal" gap={3}>
|
||||
<Button onClick={processInstanceCreateAndRun} variant="primary">
|
||||
Run
|
||||
</Button>
|
||||
<Button
|
||||
href={`/admin/process-models/${modifiedProcessModelId}/edit`}
|
||||
variant="secondary"
|
||||
>
|
||||
Edit process model
|
||||
</Button>
|
||||
</Stack>
|
||||
<br />
|
||||
<br />
|
||||
{processInstanceResultTag()}
|
||||
{processModelButtons()}
|
||||
<br />
|
||||
<br />
|
||||
<h3>Process Instances</h3>
|
||||
{processInstancesUl()}
|
||||
<br />
|
||||
<br />
|
||||
<h3>Files</h3>
|
||||
{processModelFileList()}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user