Merge remote-tracking branch 'origin/main' into feature/call_activity_selection

This commit is contained in:
Dan 2022-11-15 09:55:02 -05:00
commit a9df1ed23f
40 changed files with 1038 additions and 698 deletions

View File

@ -279,6 +279,8 @@ jobs:
# if: ${{ github.event_name != 'pull_request' }} # if: ${{ github.event_name != 'pull_request' }}
# so just skip everything but main # so just skip everything but main
if: github.ref_name == 'main' if: github.ref_name == 'main'
with:
projectBaseDir: spiffworkflow-backend
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}

View File

@ -1,8 +1,8 @@
"""empty message """empty message
Revision ID: 7d1662ea1227 Revision ID: 7cc9bdcc309f
Revises: Revises:
Create Date: 2022-11-14 21:48:34.469311 Create Date: 2022-11-15 09:53:53.349712
""" """
from alembic import op from alembic import op
@ -10,7 +10,7 @@ import sqlalchemy as sa
# revision identifiers, used by Alembic. # revision identifiers, used by Alembic.
revision = '7d1662ea1227' revision = '7cc9bdcc309f'
down_revision = None down_revision = None
branch_labels = None branch_labels = None
depends_on = None depends_on = None
@ -68,15 +68,6 @@ def upgrade():
sa.Column('spiff_step', sa.Integer(), nullable=False), sa.Column('spiff_step', sa.Integer(), nullable=False),
sa.PrimaryKeyConstraint('id') sa.PrimaryKeyConstraint('id')
) )
op.create_table('spiff_step_details',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('process_instance_id', sa.Integer(), nullable=False),
sa.Column('spiff_step', sa.Integer(), nullable=False),
sa.Column('task_json', sa.JSON(), nullable=False),
sa.Column('timestamp', sa.DECIMAL(precision=17, scale=6), nullable=False),
sa.Column('completed_by_user_id', sa.Integer(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_table('user', op.create_table('user',
sa.Column('id', sa.Integer(), nullable=False), sa.Column('id', sa.Integer(), nullable=False),
sa.Column('username', sa.String(length=255), nullable=False), sa.Column('username', sa.String(length=255), nullable=False),
@ -176,6 +167,17 @@ def upgrade():
sa.PrimaryKeyConstraint('id'), sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('key') sa.UniqueConstraint('key')
) )
op.create_table('spiff_step_details',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('process_instance_id', sa.Integer(), nullable=False),
sa.Column('spiff_step', sa.Integer(), nullable=False),
sa.Column('task_json', sa.JSON(), nullable=False),
sa.Column('timestamp', sa.DECIMAL(precision=17, scale=6), nullable=False),
sa.Column('completed_by_user_id', sa.Integer(), nullable=True),
sa.Column('lane_assignment_id', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['lane_assignment_id'], ['group.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('user_group_assignment', op.create_table('user_group_assignment',
sa.Column('id', sa.Integer(), nullable=False), sa.Column('id', sa.Integer(), nullable=False),
sa.Column('user_id', sa.Integer(), nullable=False), sa.Column('user_id', sa.Integer(), nullable=False),
@ -290,6 +292,7 @@ def downgrade():
op.drop_table('message_correlation') op.drop_table('message_correlation')
op.drop_table('active_task') op.drop_table('active_task')
op.drop_table('user_group_assignment') op.drop_table('user_group_assignment')
op.drop_table('spiff_step_details')
op.drop_table('secret') op.drop_table('secret')
op.drop_table('refresh_token') op.drop_table('refresh_token')
op.drop_index(op.f('ix_process_instance_report_identifier'), table_name='process_instance_report') op.drop_index(op.f('ix_process_instance_report_identifier'), table_name='process_instance_report')
@ -305,7 +308,6 @@ def downgrade():
op.drop_index(op.f('ix_message_correlation_property_identifier'), table_name='message_correlation_property') op.drop_index(op.f('ix_message_correlation_property_identifier'), table_name='message_correlation_property')
op.drop_table('message_correlation_property') op.drop_table('message_correlation_property')
op.drop_table('user') op.drop_table('user')
op.drop_table('spiff_step_details')
op.drop_table('spiff_logging') op.drop_table('spiff_logging')
op.drop_index(op.f('ix_spec_reference_cache_type'), table_name='spec_reference_cache') op.drop_index(op.f('ix_spec_reference_cache_type'), table_name='spec_reference_cache')
op.drop_index(op.f('ix_spec_reference_cache_identifier'), table_name='spec_reference_cache') op.drop_index(op.f('ix_spec_reference_cache_identifier'), table_name='spec_reference_cache')

View File

@ -918,7 +918,7 @@ paths:
items: items:
$ref: "#/components/schemas/Task" $ref: "#/components/schemas/Task"
/tasks/for-processes-started-by-others: /tasks/for-me:
parameters: parameters:
- name: page - name: page
in: query in: query
@ -935,7 +935,36 @@ paths:
get: get:
tags: tags:
- Process Instances - Process Instances
operationId: spiffworkflow_backend.routes.process_api_blueprint.task_list_for_processes_started_by_others operationId: spiffworkflow_backend.routes.process_api_blueprint.task_list_for_me
summary: returns the list of tasks for given user's open process instances
responses:
"200":
description: list of tasks
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/Task"
/tasks/for-my-groups:
parameters:
- name: page
in: query
required: false
description: The page number to return. Defaults to page 1.
schema:
type: integer
- name: per_page
in: query
required: false
description: The page number to return. Defaults to page 1.
schema:
type: integer
get:
tags:
- Process Instances
operationId: spiffworkflow_backend.routes.process_api_blueprint.task_list_for_my_groups
summary: returns the list of tasks for given user's open process instances summary: returns the list of tasks for given user's open process instances
responses: responses:
"200": "200":

View File

@ -64,6 +64,7 @@ CONTENT_TYPES = {
@dataclass(order=True) @dataclass(order=True)
class File: class File:
"""File.""" """File."""

View File

@ -59,7 +59,7 @@ class MessageInstanceModel(SpiffworkflowBaseDBModel):
updated_at_in_seconds: int = db.Column(db.Integer) updated_at_in_seconds: int = db.Column(db.Integer)
created_at_in_seconds: int = db.Column(db.Integer) created_at_in_seconds: int = db.Column(db.Integer)
message_correlations: dict | None = None message_correlations: Optional[dict] = None
@validates("message_type") @validates("message_type")
def validate_message_type(self, key: str, value: Any) -> Any: def validate_message_type(self, key: str, value: Any) -> Any:

View File

@ -1,10 +1,14 @@
"""Spiff_step_details.""" """Spiff_step_details."""
from dataclasses import dataclass from dataclasses import dataclass
from typing import Optional
from flask_bpmn.models.db import db from flask_bpmn.models.db import db
from flask_bpmn.models.db import SpiffworkflowBaseDBModel from flask_bpmn.models.db import SpiffworkflowBaseDBModel
from sqlalchemy import ForeignKey
from sqlalchemy.orm import deferred from sqlalchemy.orm import deferred
from spiffworkflow_backend.models.group import GroupModel
@dataclass @dataclass
class SpiffStepDetailsModel(SpiffworkflowBaseDBModel): class SpiffStepDetailsModel(SpiffworkflowBaseDBModel):
@ -17,3 +21,6 @@ class SpiffStepDetailsModel(SpiffworkflowBaseDBModel):
task_json: str = deferred(db.Column(db.JSON, nullable=False)) # type: ignore task_json: str = deferred(db.Column(db.JSON, nullable=False)) # type: ignore
timestamp: float = db.Column(db.DECIMAL(17, 6), nullable=False) timestamp: float = db.Column(db.DECIMAL(17, 6), nullable=False)
completed_by_user_id: int = db.Column(db.Integer, nullable=True) completed_by_user_id: int = db.Column(db.Integer, nullable=True)
lane_assignment_id: Optional[int] = db.Column(
ForeignKey(GroupModel.id), nullable=True
)

View File

@ -65,9 +65,7 @@ from spiffworkflow_backend.models.spiff_step_details import SpiffStepDetailsMode
from spiffworkflow_backend.models.user import UserModel from spiffworkflow_backend.models.user import UserModel
from spiffworkflow_backend.routes.user import verify_token from spiffworkflow_backend.routes.user import verify_token
from spiffworkflow_backend.services.authorization_service import AuthorizationService from spiffworkflow_backend.services.authorization_service import AuthorizationService
from spiffworkflow_backend.services.custom_parser import MyCustomParser
from spiffworkflow_backend.services.error_handling_service import ErrorHandlingService from spiffworkflow_backend.services.error_handling_service import ErrorHandlingService
from spiffworkflow_backend.services.file_system_service import FileSystemService
from spiffworkflow_backend.services.git_service import GitService from spiffworkflow_backend.services.git_service import GitService
from spiffworkflow_backend.services.message_service import MessageService from spiffworkflow_backend.services.message_service import MessageService
from spiffworkflow_backend.services.process_instance_processor import ( from spiffworkflow_backend.services.process_instance_processor import (
@ -1034,7 +1032,17 @@ def task_list_for_my_open_processes(
return get_tasks(page=page, per_page=per_page) return get_tasks(page=page, per_page=per_page)
def task_list_for_processes_started_by_others( def task_list_for_me(page: int = 1, per_page: int = 100) -> flask.wrappers.Response:
"""Task_list_for_processes_started_by_others."""
return get_tasks(
processes_started_by_user=False,
has_lane_assignment_id=False,
page=page,
per_page=per_page,
)
def task_list_for_my_groups(
page: int = 1, per_page: int = 100 page: int = 1, per_page: int = 100
) -> flask.wrappers.Response: ) -> flask.wrappers.Response:
"""Task_list_for_processes_started_by_others.""" """Task_list_for_processes_started_by_others."""
@ -1042,14 +1050,21 @@ def task_list_for_processes_started_by_others(
def get_tasks( def get_tasks(
processes_started_by_user: bool = True, page: int = 1, per_page: int = 100 processes_started_by_user: bool = True,
has_lane_assignment_id: bool = True,
page: int = 1,
per_page: int = 100,
) -> flask.wrappers.Response: ) -> flask.wrappers.Response:
"""Get_tasks.""" """Get_tasks."""
user_id = g.user.id user_id = g.user.id
# use distinct to ensure we only get one row per active task otherwise
# we can get back multiple for the same active task row which throws off
# pagination later on
# https://stackoverflow.com/q/34582014/6090676
active_tasks_query = ( active_tasks_query = (
ActiveTaskModel.query.outerjoin( ActiveTaskModel.query.distinct()
GroupModel, GroupModel.id == ActiveTaskModel.lane_assignment_id .outerjoin(GroupModel, GroupModel.id == ActiveTaskModel.lane_assignment_id)
)
.join(ProcessInstanceModel) .join(ProcessInstanceModel)
.join(UserModel, UserModel.id == ProcessInstanceModel.process_initiator_id) .join(UserModel, UserModel.id == ProcessInstanceModel.process_initiator_id)
) )
@ -1057,11 +1072,29 @@ def get_tasks(
if processes_started_by_user: if processes_started_by_user:
active_tasks_query = active_tasks_query.filter( active_tasks_query = active_tasks_query.filter(
ProcessInstanceModel.process_initiator_id == user_id ProcessInstanceModel.process_initiator_id == user_id
).outerjoin(ActiveTaskUserModel, and_(ActiveTaskUserModel.user_id == user_id)) ).outerjoin(
ActiveTaskUserModel,
and_(
ActiveTaskUserModel.user_id == user_id,
ActiveTaskModel.id == ActiveTaskUserModel.active_task_id,
),
)
else: else:
active_tasks_query = active_tasks_query.filter( active_tasks_query = active_tasks_query.filter(
ProcessInstanceModel.process_initiator_id != user_id ProcessInstanceModel.process_initiator_id != user_id
).join(ActiveTaskUserModel, and_(ActiveTaskUserModel.user_id == user_id)) ).join(
ActiveTaskUserModel,
and_(
ActiveTaskUserModel.user_id == user_id,
ActiveTaskModel.id == ActiveTaskUserModel.active_task_id,
),
)
if has_lane_assignment_id:
active_tasks_query = active_tasks_query.filter(
ActiveTaskModel.lane_assignment_id.is_not(None) # type: ignore
)
else:
active_tasks_query = active_tasks_query.filter(ActiveTaskModel.lane_assignment_id.is_(None)) # type: ignore
active_tasks = active_tasks_query.add_columns( active_tasks = active_tasks_query.add_columns(
ProcessInstanceModel.process_model_identifier, ProcessInstanceModel.process_model_identifier,
@ -1238,7 +1271,25 @@ def task_submit(
if terminate_loop and spiff_task.is_looping(): if terminate_loop and spiff_task.is_looping():
spiff_task.terminate_loop() spiff_task.terminate_loop()
ProcessInstanceService.complete_form_task(processor, spiff_task, body, g.user) active_task = ActiveTaskModel.query.filter_by(
process_instance_id=process_instance_id, task_id=task_id
).first()
if active_task is None:
raise (
ApiError(
error_code="no_active_task",
message="Cannot find an active task with task id '{task_id}' for process instance {process_instance_id}.",
status_code=500,
)
)
ProcessInstanceService.complete_form_task(
processor=processor,
spiff_task=spiff_task,
data=body,
user=g.user,
active_task=active_task,
)
# If we need to update all tasks, then get the next ready task and if it a multi-instance with the same # If we need to update all tasks, then get the next ready task and if it a multi-instance with the same
# task spec, complete that form as well. # task spec, complete that form as well.

View File

@ -1,5 +1,6 @@
from SpiffWorkflow.dmn.parser.BpmnDmnParser import BpmnDmnParser """Custom_parser."""
from SpiffWorkflow.spiff.parser.process import SpiffBpmnParser from SpiffWorkflow.dmn.parser.BpmnDmnParser import BpmnDmnParser # type: ignore
from SpiffWorkflow.spiff.parser.process import SpiffBpmnParser # type: ignore
class MyCustomParser(BpmnDmnParser): # type: ignore class MyCustomParser(BpmnDmnParser): # type: ignore

View File

@ -60,8 +60,9 @@ class FileSystemService:
@staticmethod @staticmethod
def workflow_path(spec: ProcessModelInfo) -> str: def workflow_path(spec: ProcessModelInfo) -> str:
"""Workflow_path.""" """Workflow_path."""
process_model_path = os.path.join(FileSystemService.root_path(), spec.id) process_model_path = os.path.join(
# process_group_path = FileSystemService.process_group_path_for_spec(spec) FileSystemService.root_path(), spec.id_for_file_path()
)
return process_model_path return process_model_path
@staticmethod @staticmethod

View File

@ -38,7 +38,6 @@ from SpiffWorkflow.dmn.parser.BpmnDmnParser import BpmnDmnParser # type: ignore
from SpiffWorkflow.dmn.serializer.task_spec_converters import BusinessRuleTaskConverter # type: ignore from SpiffWorkflow.dmn.serializer.task_spec_converters import BusinessRuleTaskConverter # type: ignore
from SpiffWorkflow.exceptions import WorkflowException # type: ignore from SpiffWorkflow.exceptions import WorkflowException # type: ignore
from SpiffWorkflow.serializer.exceptions import MissingSpecError # type: ignore from SpiffWorkflow.serializer.exceptions import MissingSpecError # type: ignore
from SpiffWorkflow.spiff.parser.process import SpiffBpmnParser # type: ignore
from SpiffWorkflow.spiff.serializer.task_spec_converters import BoundaryEventConverter # type: ignore from SpiffWorkflow.spiff.serializer.task_spec_converters import BoundaryEventConverter # type: ignore
from SpiffWorkflow.spiff.serializer.task_spec_converters import ( from SpiffWorkflow.spiff.serializer.task_spec_converters import (
CallActivityTaskConverter, CallActivityTaskConverter,
@ -95,9 +94,6 @@ from spiffworkflow_backend.services.custom_parser import MyCustomParser
from spiffworkflow_backend.services.file_system_service import FileSystemService from spiffworkflow_backend.services.file_system_service import FileSystemService
from spiffworkflow_backend.services.process_model_service import ProcessModelService from spiffworkflow_backend.services.process_model_service import ProcessModelService
from spiffworkflow_backend.services.service_task_service import ServiceTaskDelegate from spiffworkflow_backend.services.service_task_service import ServiceTaskDelegate
from spiffworkflow_backend.services.spec_file_service import (
ProcessModelFileNotFoundError,
)
from spiffworkflow_backend.services.spec_file_service import SpecFileService from spiffworkflow_backend.services.spec_file_service import SpecFileService
from spiffworkflow_backend.services.user_service import UserService from spiffworkflow_backend.services.user_service import UserService
@ -584,9 +580,10 @@ class ProcessInstanceProcessor:
) )
return details_model return details_model
def save_spiff_step_details(self) -> None: def save_spiff_step_details(self, active_task: ActiveTaskModel) -> None:
"""SaveSpiffStepDetails.""" """SaveSpiffStepDetails."""
details_model = self.spiff_step_details() details_model = self.spiff_step_details()
details_model.lane_assignment_id = active_task.lane_assignment_id
db.session.add(details_model) db.session.add(details_model)
db.session.commit() db.session.commit()
@ -679,13 +676,13 @@ class ProcessInstanceProcessor:
"""Backfill_missing_spec_reference_records.""" """Backfill_missing_spec_reference_records."""
process_models = ProcessModelService().get_process_models() process_models = ProcessModelService().get_process_models()
for process_model in process_models: for process_model in process_models:
refs = SpecFileService.reference_map(SpecFileService.get_references_for_process(process_model)) refs = SpecFileService.reference_map(
SpecFileService.get_references_for_process(process_model)
)
bpmn_process_identifiers = refs.keys() bpmn_process_identifiers = refs.keys()
if bpmn_process_identifier in bpmn_process_identifiers: if bpmn_process_identifier in bpmn_process_identifiers:
SpecFileService.update_process_cache(refs[bpmn_process_identifier]) SpecFileService.update_process_cache(refs[bpmn_process_identifier])
return FileSystemService.full_path_to_process_model_file( return FileSystemService.full_path_to_process_model_file(process_model)
process_model
)
return None return None
@staticmethod @staticmethod
@ -1130,11 +1127,11 @@ class ProcessInstanceProcessor:
) )
return user_tasks # type: ignore return user_tasks # type: ignore
def complete_task(self, task: SpiffTask) -> None: def complete_task(self, task: SpiffTask, active_task: ActiveTaskModel) -> None:
"""Complete_task.""" """Complete_task."""
self.increment_spiff_step() self.increment_spiff_step()
self.bpmn_process_instance.complete_task_from_id(task.id) self.bpmn_process_instance.complete_task_from_id(task.id)
self.save_spiff_step_details() self.save_spiff_step_details(active_task)
def get_data(self) -> dict[str, Any]: def get_data(self) -> dict[str, Any]:
"""Get_data.""" """Get_data."""

View File

@ -8,6 +8,7 @@ from flask_bpmn.api.api_error import ApiError
from flask_bpmn.models.db import db from flask_bpmn.models.db import db
from SpiffWorkflow.task import Task as SpiffTask # type: ignore from SpiffWorkflow.task import Task as SpiffTask # type: ignore
from spiffworkflow_backend.models.active_task import ActiveTaskModel
from spiffworkflow_backend.models.process_instance import ProcessInstanceApi from spiffworkflow_backend.models.process_instance import ProcessInstanceApi
from spiffworkflow_backend.models.process_instance import ProcessInstanceModel from spiffworkflow_backend.models.process_instance import ProcessInstanceModel
from spiffworkflow_backend.models.process_instance import ProcessInstanceStatus from spiffworkflow_backend.models.process_instance import ProcessInstanceStatus
@ -188,6 +189,7 @@ class ProcessInstanceService:
spiff_task: SpiffTask, spiff_task: SpiffTask,
data: dict[str, Any], data: dict[str, Any],
user: UserModel, user: UserModel,
active_task: ActiveTaskModel,
) -> None: ) -> None:
"""All the things that need to happen when we complete a form. """All the things that need to happen when we complete a form.
@ -201,7 +203,7 @@ class ProcessInstanceService:
dot_dct = ProcessInstanceService.create_dot_dict(data) dot_dct = ProcessInstanceService.create_dot_dict(data)
spiff_task.update_data(dot_dct) spiff_task.update_data(dot_dct)
# ProcessInstanceService.post_process_form(spiff_task) # some properties may update the data store. # ProcessInstanceService.post_process_form(spiff_task) # some properties may update the data store.
processor.complete_task(spiff_task) processor.complete_task(spiff_task, active_task)
processor.do_engine_steps(save=True) processor.do_engine_steps(save=True)
@staticmethod @staticmethod

View File

@ -2,17 +2,10 @@
import os import os
import shutil import shutil
from datetime import datetime from datetime import datetime
from typing import Any, Type
from typing import List from typing import List
from typing import Optional from typing import Optional
from SpiffWorkflow.dmn.parser.BpmnDmnParser import BpmnDmnParser
from SpiffWorkflow.bpmn.parser.ProcessParser import ProcessParser
from flask_bpmn.api.api_error import ApiError
from flask_bpmn.models.db import db from flask_bpmn.models.db import db
from lxml import etree # type: ignore
from lxml.etree import _Element # type: ignore
from lxml.etree import Element as EtreeElement
from SpiffWorkflow.bpmn.parser.ValidationException import ValidationException # type: ignore from SpiffWorkflow.bpmn.parser.ValidationException import ValidationException # type: ignore
from spiffworkflow_backend.models.spec_reference import SpecReferenceCache from spiffworkflow_backend.models.spec_reference import SpecReferenceCache
@ -77,10 +70,13 @@ class SpecFileService(FileSystemService):
@staticmethod @staticmethod
def get_references_for_process(process_model_info: ProcessModelInfo) -> list[SpecReference]: def get_references_for_process(process_model_info: ProcessModelInfo) -> list[SpecReference]:
"""Get_references_for_process."""
files = SpecFileService.get_files(process_model_info) files = SpecFileService.get_files(process_model_info)
references = [] references = []
for file in files: for file in files:
references.extend(SpecFileService.get_references_for_file(file, process_model_info)) references.extend(
SpecFileService.get_references_for_file(file, process_model_info)
)
return references return references
@staticmethod @staticmethod
@ -94,7 +90,7 @@ class SpecFileService(FileSystemService):
type = {str} 'process' / 'decision' type = {str} 'process' / 'decision'
""" """
references: list[SpecReference] = [] references: list[SpecReference] = []
full_file_path = SpecFileService.file_path(process_model_info, file.name) full_file_path = SpecFileService.full_file_path(process_model_info, file.name)
file_path = os.path.join(process_model_info.id, file.name) file_path = os.path.join(process_model_info.id, file.name)
parser = MyCustomParser() parser = MyCustomParser()
parser_type = None parser_type = None
@ -118,19 +114,28 @@ class SpecFileService(FileSystemService):
else: else:
return references return references
for sub_parser in sub_parsers: for sub_parser in sub_parsers:
if parser_type == 'process': if parser_type == "process":
has_lanes = sub_parser.has_lanes() has_lanes = sub_parser.has_lanes()
executable = sub_parser.process_executable executable = sub_parser.process_executable
start_messages = sub_parser.start_messages() start_messages = sub_parser.start_messages()
is_primary = sub_parser.get_id() == process_model_info.primary_process_id is_primary = sub_parser.get_id() == process_model_info.primary_process_id
references.append(SpecReference(
identifier=sub_parser.get_id(), display_name=sub_parser.get_name(), references.append(
SpecReference(
identifier=sub_parser.get_id(),
display_name=sub_parser.get_name(),
process_model_id=process_model_info.id, process_model_id=process_model_info.id,
type=parser_type, type=parser_type,
file_name=file.name, relative_path=file_path, has_lanes=has_lanes, file_name=file.name,
is_executable=is_executable, messages=messages, is_primary=is_primary, relative_path=file_path,
correlations=correlations, start_messages=start_messages has_lanes=has_lanes,
)) is_executable=is_executable,
messages=messages,
is_primary=is_primary,
correlations=correlations,
start_messages=start_messages
)
)
return references return references
@staticmethod @staticmethod
@ -142,15 +147,14 @@ class SpecFileService(FileSystemService):
return SpecFileService.update_file(process_model_info, file_name, binary_data) return SpecFileService.update_file(process_model_info, file_name, binary_data)
@staticmethod @staticmethod
def update_file(process_model_info: ProcessModelInfo, file_name: str, binary_data: bytes def update_file(
process_model_info: ProcessModelInfo, file_name: str, binary_data: bytes
) -> File: ) -> File:
"""Update_file.""" """Update_file."""
SpecFileService.assert_valid_file_name(file_name) SpecFileService.assert_valid_file_name(file_name)
file_path = os.path.join( full_file_path = SpecFileService.full_file_path(process_model_info, file_name)
FileSystemService.root_path(), process_model_info.id, file_name SpecFileService.write_file_data_to_system(full_file_path, binary_data)
) file = SpecFileService.to_file_object(file_name, full_file_path)
SpecFileService.write_file_data_to_system(file_path, binary_data)
file = SpecFileService.to_file_object(file_name, file_path)
references = SpecFileService.get_references_for_file(file, process_model_info) references = SpecFileService.get_references_for_file(file, process_model_info)
primary_process_ref = next((ref for ref in references if ref.is_primary), None) primary_process_ref = next((ref for ref in references if ref.is_primary), None)
@ -172,40 +176,36 @@ class SpecFileService(FileSystemService):
SpecFileService.update_message_cache(ref) SpecFileService.update_message_cache(ref)
SpecFileService.update_message_trigger_cache(ref, process_model_info) SpecFileService.update_message_trigger_cache(ref, process_model_info)
SpecFileService.update_correlation_cache(ref) SpecFileService.update_correlation_cache(ref)
return file return file
@staticmethod @staticmethod
def get_data(process_model_info: ProcessModelInfo, file_name: str) -> bytes: def get_data(process_model_info: ProcessModelInfo, file_name: str) -> bytes:
"""Get_data.""" """Get_data."""
# file_path = SpecFileService.file_path(process_model_info, file_name) full_file_path = SpecFileService.full_file_path(process_model_info, file_name)
file_path = os.path.join( if not os.path.exists(full_file_path):
FileSystemService.root_path(), process_model_info.id, file_name
)
if not os.path.exists(file_path):
raise ProcessModelFileNotFoundError( raise ProcessModelFileNotFoundError(
f"No file found with name {file_name} in {process_model_info.display_name}" f"No file found with name {file_name} in {process_model_info.display_name}"
) )
with open(file_path, "rb") as f_handle: with open(full_file_path, "rb") as f_handle:
spec_file_data = f_handle.read() spec_file_data = f_handle.read()
return spec_file_data return spec_file_data
@staticmethod @staticmethod
def file_path(spec: ProcessModelInfo, file_name: str) -> str: def full_file_path(spec: ProcessModelInfo, file_name: str) -> str:
"""File_path.""" """File_path."""
return os.path.join(SpecFileService.workflow_path(spec), file_name) return os.path.join(SpecFileService.workflow_path(spec), file_name)
@staticmethod @staticmethod
def last_modified(spec: ProcessModelInfo, file_name: str) -> datetime: def last_modified(spec: ProcessModelInfo, file_name: str) -> datetime:
"""Last_modified.""" """Last_modified."""
path = SpecFileService.file_path(spec, file_name) full_file_path = SpecFileService.full_file_path(spec, file_name)
return FileSystemService._last_modified(path) return FileSystemService._last_modified(full_file_path)
@staticmethod @staticmethod
def timestamp(spec: ProcessModelInfo, file_name: str) -> float: def timestamp(spec: ProcessModelInfo, file_name: str) -> float:
"""Timestamp.""" """Timestamp."""
path = SpecFileService.file_path(spec, file_name) full_file_path = SpecFileService.full_file_path(spec, file_name)
return FileSystemService._timestamp(path) return FileSystemService._timestamp(full_file_path)
@staticmethod @staticmethod
def delete_file(spec: ProcessModelInfo, file_name: str) -> None: def delete_file(spec: ProcessModelInfo, file_name: str) -> None:
@ -215,9 +215,8 @@ class SpecFileService(FileSystemService):
# for lf in lookup_files: # for lf in lookup_files:
# session.query(LookupDataModel).filter_by(lookup_file_model_id=lf.id).delete() # session.query(LookupDataModel).filter_by(lookup_file_model_id=lf.id).delete()
# session.query(LookupFileModel).filter_by(id=lf.id).delete() # session.query(LookupFileModel).filter_by(id=lf.id).delete()
# file_path = SpecFileService.file_path(spec, file_name) full_file_path = SpecFileService.full_file_path(spec, file_name)
file_path = os.path.join(FileSystemService.root_path(), spec.id, file_name) os.remove(full_file_path)
os.remove(file_path)
@staticmethod @staticmethod
def delete_all_files(spec: ProcessModelInfo) -> None: def delete_all_files(spec: ProcessModelInfo) -> None:
@ -226,10 +225,8 @@ class SpecFileService(FileSystemService):
if os.path.exists(dir_path): if os.path.exists(dir_path):
shutil.rmtree(dir_path) shutil.rmtree(dir_path)
# fixme: Place all the caching stuff in a different service. # fixme: Place all the caching stuff in a different service.
@staticmethod @staticmethod
def update_process_cache(ref: SpecReference) -> None: def update_process_cache(ref: SpecReference) -> None:
process_id_lookup = SpecReferenceCache.query.filter_by(identifier=ref.identifier).first() process_id_lookup = SpecReferenceCache.query.filter_by(identifier=ref.identifier).first()
@ -255,23 +252,26 @@ class SpecFileService(FileSystemService):
db.session.add(process_id_lookup) db.session.add(process_id_lookup)
db.session.commit() db.session.commit()
@staticmethod @staticmethod
def update_message_cache(ref: SpecReference) -> None: def update_message_cache(ref: SpecReference) -> None:
"""Assure we have a record in the database of all possible message ids and names.""" """Assure we have a record in the database of all possible message ids and names."""
for message_model_identifier in ref.messages.keys(): for message_model_identifier in ref.messages.keys():
message_model = MessageModel.query.filter_by(identifier=message_model_identifier).first() message_model = MessageModel.query.filter_by(
identifier=message_model_identifier
).first()
if message_model is None: if message_model is None:
message_model = MessageModel( message_model = MessageModel(
identifier=message_model_identifier, name=ref.messages[message_model_identifier] identifier=message_model_identifier,
name=ref.messages[message_model_identifier],
) )
db.session.add(message_model) db.session.add(message_model)
db.session.commit() db.session.commit()
@staticmethod @staticmethod
def update_message_trigger_cache(ref: SpecReference, process_model_info: ProcessModelInfo) -> None: def update_message_trigger_cache(
"""assure we know which messages can trigger the start of a process.""" ref: SpecReference, process_model_info: ProcessModelInfo
) -> None:
"""Assure we know which messages can trigger the start of a process."""
for message_model_identifier in ref.start_messages: for message_model_identifier in ref.start_messages:
message_model = MessageModel.query.filter_by( message_model = MessageModel.query.filter_by(
identifier=message_model_identifier identifier=message_model_identifier
@ -288,12 +288,10 @@ class SpecFileService(FileSystemService):
) )
if message_triggerable_process_model is None: if message_triggerable_process_model is None:
message_triggerable_process_model = ( message_triggerable_process_model = MessageTriggerableProcessModel(
MessageTriggerableProcessModel(
message_model_id=message_model.id, message_model_id=message_model.id,
process_model_identifier=process_model_info.id, process_model_identifier=process_model_info.id,
process_group_identifier="process_group_identifier" process_group_identifier="process_group_identifier",
)
) )
db.session.add(message_triggerable_process_model) db.session.add(message_triggerable_process_model)
db.session.commit() db.session.commit()
@ -310,13 +308,17 @@ class SpecFileService(FileSystemService):
@staticmethod @staticmethod
def update_correlation_cache(ref: SpecReference) -> None: def update_correlation_cache(ref: SpecReference) -> None:
"""Update_correlation_cache."""
for correlation_identifier in ref.correlations.keys(): for correlation_identifier in ref.correlations.keys():
correlation_property_retrieval_expressions = \ correlation_property_retrieval_expressions = ref.correlations[
ref.correlations[correlation_identifier]['retrieval_expressions'] correlation_identifier
]["retrieval_expressions"]
for cpre in correlation_property_retrieval_expressions: for cpre in correlation_property_retrieval_expressions:
message_model_identifier = cpre["messageRef"] message_model_identifier = cpre["messageRef"]
message_model = MessageModel.query.filter_by(identifier=message_model_identifier).first() message_model = MessageModel.query.filter_by(
identifier=message_model_identifier
).first()
if message_model is None: if message_model is None:
raise ValidationException( raise ValidationException(
f"Could not find message model with identifier '{message_model_identifier}'" f"Could not find message model with identifier '{message_model_identifier}'"

View File

@ -40,7 +40,7 @@
}</spiffworkflow:messagePayload> }</spiffworkflow:messagePayload>
</bpmn:extensionElements> </bpmn:extensionElements>
</bpmn:message> </bpmn:message>
<bpmn:process id="message_receiver_process_one" name="Message Receiver Process" isExecutable="true"> <bpmn:process id="message_receiver_process_one" name="Message Receiver Process One" isExecutable="true">
<bpmn:sequenceFlow id="Flow_11r9uiw" sourceRef="send_message_response" targetRef="Event_0q5otqd" /> <bpmn:sequenceFlow id="Flow_11r9uiw" sourceRef="send_message_response" targetRef="Event_0q5otqd" />
<bpmn:endEvent id="Event_0q5otqd"> <bpmn:endEvent id="Event_0q5otqd">
<bpmn:incoming>Flow_11r9uiw</bpmn:incoming> <bpmn:incoming>Flow_11r9uiw</bpmn:incoming>

View File

@ -40,7 +40,7 @@
}</spiffworkflow:messagePayload> }</spiffworkflow:messagePayload>
</bpmn:extensionElements> </bpmn:extensionElements>
</bpmn:message> </bpmn:message>
<bpmn:process id="message_receiver_process_two" name="Message Receiver Process" isExecutable="true"> <bpmn:process id="message_receiver_process_two" name="Message Receiver Process Two" isExecutable="true">
<bpmn:sequenceFlow id="Flow_11r9uiw" sourceRef="send_message_response" targetRef="Event_0q5otqd" /> <bpmn:sequenceFlow id="Flow_11r9uiw" sourceRef="send_message_response" targetRef="Event_0q5otqd" />
<bpmn:endEvent id="Event_0q5otqd"> <bpmn:endEvent id="Event_0q5otqd">
<bpmn:incoming>Flow_11r9uiw</bpmn:incoming> <bpmn:incoming>Flow_11r9uiw</bpmn:incoming>

View File

@ -25,11 +25,9 @@ class ExampleDataLoader:
"""Assumes that process_model_source_directory exists in static/bpmn and contains bpmn_file_name. """Assumes that process_model_source_directory exists in static/bpmn and contains bpmn_file_name.
further assumes that bpmn_file_name is the primary file for the process model. further assumes that bpmn_file_name is the primary file for the process model.
if bpmn_file_name is None we load all files in process_model_source_directory, if bpmn_file_name is None we load all files in process_model_source_directory,
otherwise, we only load bpmn_file_name otherwise, we only load bpmn_file_name
""" """
if process_model_source_directory is None: if process_model_source_directory is None:
raise Exception("You must include `process_model_source_directory`.") raise Exception("You must include `process_model_source_directory`.")
@ -85,7 +83,9 @@ class ExampleDataLoader:
process_model_info=spec, file_name=filename, binary_data=data process_model_info=spec, file_name=filename, binary_data=data
) )
if is_primary: if is_primary:
references = SpecFileService.get_references_for_file(file_info, spec) references = SpecFileService.get_references_for_file(
file_info, spec
)
spec.primary_process_id = references[0].identifier spec.primary_process_id = references[0].identifier
spec.primary_file_name = filename spec.primary_file_name = filename
ProcessModelService().save_process_model(spec) ProcessModelService().save_process_model(spec)

View File

@ -2367,4 +2367,3 @@ class TestProcessApi(BaseTest):
) )
print("test_script_unit_test_run") print("test_script_unit_test_run")

View File

@ -74,7 +74,11 @@ class TestGetLocaltime(BaseTest):
) )
ProcessInstanceService.complete_form_task( ProcessInstanceService.complete_form_task(
processor, spiff_task, {"timezone": "US/Pacific"}, initiator_user processor,
spiff_task,
{"timezone": "US/Pacific"},
initiator_user,
active_task,
) )
active_task = process_instance.active_tasks[0] active_task = process_instance.active_tasks[0]

View File

@ -126,7 +126,7 @@ class TestAuthorizationService(BaseTest):
active_task.task_name, processor.bpmn_process_instance active_task.task_name, processor.bpmn_process_instance
) )
ProcessInstanceService.complete_form_task( ProcessInstanceService.complete_form_task(
processor, spiff_task, {}, initiator_user processor, spiff_task, {}, initiator_user, active_task
) )
active_task = process_instance.active_tasks[0] active_task = process_instance.active_tasks[0]
@ -137,5 +137,5 @@ class TestAuthorizationService(BaseTest):
{"username": "testuser2", "sub": "open_id"} {"username": "testuser2", "sub": "open_id"}
) )
ProcessInstanceService.complete_form_task( ProcessInstanceService.complete_form_task(
processor, spiff_task, {}, finance_user processor, spiff_task, {}, finance_user, active_task
) )

View File

@ -47,6 +47,7 @@ class TestDotNotation(BaseTest):
processor = ProcessInstanceProcessor(process_instance) processor = ProcessInstanceProcessor(process_instance)
processor.do_engine_steps(save=True) processor.do_engine_steps(save=True)
active_task = process_instance.active_tasks[0]
user_task = processor.get_ready_user_tasks()[0] user_task = processor.get_ready_user_tasks()[0]
form_data = { form_data = {
@ -57,7 +58,7 @@ class TestDotNotation(BaseTest):
"invoice.dueDate": "09/30/2022", "invoice.dueDate": "09/30/2022",
} }
ProcessInstanceService.complete_form_task( ProcessInstanceService.complete_form_task(
processor, user_task, form_data, with_super_admin_user processor, user_task, form_data, with_super_admin_user, active_task
) )
expected = { expected = {

View File

@ -91,10 +91,10 @@ class TestProcessInstanceProcessor(BaseTest):
) )
with pytest.raises(UserDoesNotHaveAccessToTaskError): with pytest.raises(UserDoesNotHaveAccessToTaskError):
ProcessInstanceService.complete_form_task( ProcessInstanceService.complete_form_task(
processor, spiff_task, {}, finance_user processor, spiff_task, {}, finance_user, active_task
) )
ProcessInstanceService.complete_form_task( ProcessInstanceService.complete_form_task(
processor, spiff_task, {}, initiator_user processor, spiff_task, {}, initiator_user, active_task
) )
assert len(process_instance.active_tasks) == 1 assert len(process_instance.active_tasks) == 1
@ -108,11 +108,11 @@ class TestProcessInstanceProcessor(BaseTest):
) )
with pytest.raises(UserDoesNotHaveAccessToTaskError): with pytest.raises(UserDoesNotHaveAccessToTaskError):
ProcessInstanceService.complete_form_task( ProcessInstanceService.complete_form_task(
processor, spiff_task, {}, initiator_user processor, spiff_task, {}, initiator_user, active_task
) )
ProcessInstanceService.complete_form_task( ProcessInstanceService.complete_form_task(
processor, spiff_task, {}, finance_user processor, spiff_task, {}, finance_user, active_task
) )
assert len(process_instance.active_tasks) == 1 assert len(process_instance.active_tasks) == 1
active_task = process_instance.active_tasks[0] active_task = process_instance.active_tasks[0]
@ -124,7 +124,7 @@ class TestProcessInstanceProcessor(BaseTest):
active_task.task_name, processor.bpmn_process_instance active_task.task_name, processor.bpmn_process_instance
) )
ProcessInstanceService.complete_form_task( ProcessInstanceService.complete_form_task(
processor, spiff_task, {}, initiator_user processor, spiff_task, {}, initiator_user, active_task
) )
assert process_instance.status == ProcessInstanceStatus.complete.value assert process_instance.status == ProcessInstanceStatus.complete.value
@ -173,10 +173,10 @@ class TestProcessInstanceProcessor(BaseTest):
) )
with pytest.raises(UserDoesNotHaveAccessToTaskError): with pytest.raises(UserDoesNotHaveAccessToTaskError):
ProcessInstanceService.complete_form_task( ProcessInstanceService.complete_form_task(
processor, spiff_task, {}, finance_user_three processor, spiff_task, {}, finance_user_three, active_task
) )
ProcessInstanceService.complete_form_task( ProcessInstanceService.complete_form_task(
processor, spiff_task, {}, initiator_user processor, spiff_task, {}, initiator_user, active_task
) )
assert len(process_instance.active_tasks) == 1 assert len(process_instance.active_tasks) == 1
@ -190,12 +190,12 @@ class TestProcessInstanceProcessor(BaseTest):
) )
with pytest.raises(UserDoesNotHaveAccessToTaskError): with pytest.raises(UserDoesNotHaveAccessToTaskError):
ProcessInstanceService.complete_form_task( ProcessInstanceService.complete_form_task(
processor, spiff_task, {}, initiator_user processor, spiff_task, {}, initiator_user, active_task
) )
g.user = finance_user_three g.user = finance_user_three
ProcessInstanceService.complete_form_task( ProcessInstanceService.complete_form_task(
processor, spiff_task, {}, finance_user_three processor, spiff_task, {}, finance_user_three, active_task
) )
assert len(process_instance.active_tasks) == 1 assert len(process_instance.active_tasks) == 1
active_task = process_instance.active_tasks[0] active_task = process_instance.active_tasks[0]
@ -208,11 +208,11 @@ class TestProcessInstanceProcessor(BaseTest):
) )
with pytest.raises(UserDoesNotHaveAccessToTaskError): with pytest.raises(UserDoesNotHaveAccessToTaskError):
ProcessInstanceService.complete_form_task( ProcessInstanceService.complete_form_task(
processor, spiff_task, {}, initiator_user processor, spiff_task, {}, initiator_user, active_task
) )
ProcessInstanceService.complete_form_task( ProcessInstanceService.complete_form_task(
processor, spiff_task, {}, finance_user_four processor, spiff_task, {}, finance_user_four, active_task
) )
assert len(process_instance.active_tasks) == 1 assert len(process_instance.active_tasks) == 1
active_task = process_instance.active_tasks[0] active_task = process_instance.active_tasks[0]
@ -224,7 +224,7 @@ class TestProcessInstanceProcessor(BaseTest):
active_task.task_name, processor.bpmn_process_instance active_task.task_name, processor.bpmn_process_instance
) )
ProcessInstanceService.complete_form_task( ProcessInstanceService.complete_form_task(
processor, spiff_task, {}, initiator_user processor, spiff_task, {}, initiator_user, active_task
) )
assert len(process_instance.active_tasks) == 1 assert len(process_instance.active_tasks) == 1
@ -234,8 +234,10 @@ class TestProcessInstanceProcessor(BaseTest):
) )
with pytest.raises(UserDoesNotHaveAccessToTaskError): with pytest.raises(UserDoesNotHaveAccessToTaskError):
ProcessInstanceService.complete_form_task( ProcessInstanceService.complete_form_task(
processor, spiff_task, {}, initiator_user processor, spiff_task, {}, initiator_user, active_task
)
ProcessInstanceService.complete_form_task(
processor, spiff_task, {}, testadmin1, active_task
) )
ProcessInstanceService.complete_form_task(processor, spiff_task, {}, testadmin1)
assert process_instance.status == ProcessInstanceStatus.complete.value assert process_instance.status == ProcessInstanceStatus.complete.value

View File

@ -4,9 +4,8 @@ import os
import pytest import pytest
from flask import Flask from flask import Flask
from flask.testing import FlaskClient from flask.testing import FlaskClient
from flask_bpmn.api.api_error import ApiError
from flask_bpmn.models.db import db from flask_bpmn.models.db import db
from SpiffWorkflow.dmn.parser.BpmnDmnParser import BpmnDmnParser # type: ignore from SpiffWorkflow.bpmn.parser.ValidationException import ValidationException # type: ignore
from tests.spiffworkflow_backend.helpers.base_test import BaseTest from tests.spiffworkflow_backend.helpers.base_test import BaseTest
from tests.spiffworkflow_backend.helpers.test_data import load_test_spec from tests.spiffworkflow_backend.helpers.test_data import load_test_spec
@ -14,9 +13,7 @@ from spiffworkflow_backend.models.spec_reference import SpecReferenceCache
from spiffworkflow_backend.models.user import UserModel from spiffworkflow_backend.models.user import UserModel
from spiffworkflow_backend.services.process_model_service import ProcessModelService from spiffworkflow_backend.services.process_model_service import ProcessModelService
from spiffworkflow_backend.services.spec_file_service import SpecFileService from spiffworkflow_backend.services.spec_file_service import SpecFileService
from tests.spiffworkflow_backend.helpers.base_test import BaseTest
from tests.spiffworkflow_backend.helpers.test_data import load_test_spec
from SpiffWorkflow.bpmn.parser.ValidationException import ValidationException
class TestSpecFileService(BaseTest): class TestSpecFileService(BaseTest):
"""TestSpecFileService.""" """TestSpecFileService."""
@ -86,8 +83,9 @@ class TestSpecFileService(BaseTest):
process_model_source_directory="call_activity_duplicate", process_model_source_directory="call_activity_duplicate",
bpmn_file_name="call_activity_nested_duplicate", bpmn_file_name="call_activity_nested_duplicate",
) )
assert f"Process id ({bpmn_process_identifier}) has already been used" in str( assert (
exception.value f"Process id ({bpmn_process_identifier}) has already been used"
in str(exception.value)
) )
def test_updates_relative_file_path_when_appropriate( def test_updates_relative_file_path_when_appropriate(

View File

@ -0,0 +1,13 @@
import ProcessInstanceListTable from './ProcessInstanceListTable';
const paginationQueryParamPrefix = 'my_completed_instances';
export default function MyCompletedInstances() {
return (
<ProcessInstanceListTable
filtersEnabled={false}
paginationQueryParamPrefix={paginationQueryParamPrefix}
perPageOptions={[2, 5, 25]}
/>
);
}

View File

@ -1,4 +1,4 @@
import { useNavigate } from 'react-router-dom'; import { useSearchParams } from 'react-router-dom';
// @ts-ignore // @ts-ignore
import { Pagination } from '@carbon/react'; import { Pagination } from '@carbon/react';
@ -13,8 +13,7 @@ type OwnProps = {
perPageOptions?: number[]; perPageOptions?: number[];
pagination: PaginationObject | null; pagination: PaginationObject | null;
tableToDisplay: any; tableToDisplay: any;
queryParamString?: string; paginationQueryParamPrefix?: string;
path: string;
}; };
export default function PaginationForTable({ export default function PaginationForTable({
@ -23,16 +22,21 @@ export default function PaginationForTable({
perPageOptions, perPageOptions,
pagination, pagination,
tableToDisplay, tableToDisplay,
queryParamString = '', paginationQueryParamPrefix,
path,
}: OwnProps) { }: OwnProps) {
const PER_PAGE_OPTIONS = [2, 10, 50, 100]; const PER_PAGE_OPTIONS = [2, 10, 50, 100];
const navigate = useNavigate(); const [searchParams, setSearchParams] = useSearchParams();
const paginationQueryParamPrefixToUse = paginationQueryParamPrefix
? `${paginationQueryParamPrefix}_`
: '';
const updateRows = (args: any) => { const updateRows = (args: any) => {
const newPage = args.page; const newPage = args.page;
const { pageSize } = args; const { pageSize } = args;
navigate(`${path}?page=${newPage}&per_page=${pageSize}${queryParamString}`);
searchParams.set(`${paginationQueryParamPrefixToUse}page`, newPage);
searchParams.set(`${paginationQueryParamPrefixToUse}per_page`, pageSize);
setSearchParams(searchParams);
}; };
if (pagination) { if (pagination) {

View File

@ -0,0 +1,557 @@
import { useContext, useEffect, useMemo, useState } from 'react';
import {
Link,
useNavigate,
useParams,
useSearchParams,
} from 'react-router-dom';
// @ts-ignore
import { Filter } from '@carbon/icons-react';
import {
Button,
ButtonSet,
DatePicker,
DatePickerInput,
Table,
Grid,
Column,
MultiSelect,
TableHeader,
TableHead,
TableRow,
// @ts-ignore
} from '@carbon/react';
import { PROCESS_STATUSES, DATE_FORMAT, DATE_FORMAT_CARBON } from '../config';
import {
convertDateStringToSeconds,
convertSecondsToFormattedDate,
getPageInfoFromSearchParams,
getProcessModelFullIdentifierFromSearchParams,
modifyProcessModelPath,
} from '../helpers';
import PaginationForTable from './PaginationForTable';
import 'react-datepicker/dist/react-datepicker.css';
import ErrorContext from '../contexts/ErrorContext';
import HttpService from '../services/HttpService';
import 'react-bootstrap-typeahead/css/Typeahead.css';
import 'react-bootstrap-typeahead/css/Typeahead.bs5.css';
import { PaginationObject, ProcessModel } from '../interfaces';
import ProcessModelSearch from './ProcessModelSearch';
type OwnProps = {
filtersEnabled?: boolean;
processModelFullIdentifier?: string;
paginationQueryParamPrefix?: string;
perPageOptions?: number[];
};
export default function ProcessInstanceListTable({
filtersEnabled = true,
processModelFullIdentifier,
paginationQueryParamPrefix,
perPageOptions,
}: OwnProps) {
const params = useParams();
const [searchParams] = useSearchParams();
const navigate = useNavigate();
const [processInstances, setProcessInstances] = useState([]);
const [reportMetadata, setReportMetadata] = useState({});
const [pagination, setPagination] = useState<PaginationObject | null>(null);
const oneHourInSeconds = 3600;
const oneMonthInSeconds = oneHourInSeconds * 24 * 30;
const [startFrom, setStartFrom] = useState<string>('');
const [startTo, setStartTo] = useState<string>('');
const [endFrom, setEndFrom] = useState<string>('');
const [endTo, setEndTo] = useState<string>('');
const [showFilterOptions, setShowFilterOptions] = useState<boolean>(false);
const setErrorMessage = (useContext as any)(ErrorContext)[1];
const [processStatusAllOptions, setProcessStatusAllOptions] = useState<any[]>(
[]
);
const [processStatusSelection, setProcessStatusSelection] = useState<
string[]
>([]);
const [processModelAvailableItems, setProcessModelAvailableItems] = useState<
ProcessModel[]
>([]);
const [processModelSelection, setProcessModelSelection] =
useState<ProcessModel | null>(null);
const parametersToAlwaysFilterBy = useMemo(() => {
return {
start_from: setStartFrom,
start_to: setStartTo,
end_from: setEndFrom,
end_to: setEndTo,
};
}, [setStartFrom, setStartTo, setEndFrom, setEndTo]);
const parametersToGetFromSearchParams = useMemo(() => {
return {
process_model_identifier: null,
process_status: null,
};
}, []);
// eslint-disable-next-line sonarjs/cognitive-complexity
useEffect(() => {
function setProcessInstancesFromResult(result: any) {
const processInstancesFromApi = result.results;
setProcessInstances(processInstancesFromApi);
setReportMetadata(result.report_metadata);
setPagination(result.pagination);
}
function getProcessInstances() {
// eslint-disable-next-line prefer-const
let { page, perPage } = getPageInfoFromSearchParams(
searchParams,
undefined,
undefined,
paginationQueryParamPrefix
);
if (perPageOptions && !perPageOptions.includes(perPage)) {
// eslint-disable-next-line prefer-destructuring
perPage = perPageOptions[1];
}
let queryParamString = `per_page=${perPage}&page=${page}`;
Object.keys(parametersToAlwaysFilterBy).forEach((paramName: string) => {
// @ts-expect-error TS(7053) FIXME:
const functionToCall = parametersToAlwaysFilterBy[paramName];
const searchParamValue = searchParams.get(paramName);
if (searchParamValue) {
queryParamString += `&${paramName}=${searchParamValue}`;
const dateString = convertSecondsToFormattedDate(
searchParamValue as any
);
functionToCall(dateString);
setShowFilterOptions(true);
}
});
Object.keys(parametersToGetFromSearchParams).forEach(
(paramName: string) => {
if (
paramName === 'process_model_identifier' &&
processModelFullIdentifier
) {
queryParamString += `&process_model_identifier=${processModelFullIdentifier}`;
} else if (searchParams.get(paramName)) {
// @ts-expect-error TS(7053) FIXME:
const functionToCall = parametersToGetFromSearchParams[paramName];
queryParamString += `&${paramName}=${searchParams.get(paramName)}`;
if (functionToCall !== null) {
functionToCall(searchParams.get(paramName) || '');
}
setShowFilterOptions(true);
}
}
);
HttpService.makeCallToBackend({
path: `/process-instances?${queryParamString}`,
successCallback: setProcessInstancesFromResult,
});
}
function processResultForProcessModels(result: any) {
const processModelFullIdentifierFromSearchParams =
getProcessModelFullIdentifierFromSearchParams(searchParams);
const selectionArray = result.results.map((item: any) => {
const label = `${item.id}`;
Object.assign(item, { label });
if (label === processModelFullIdentifierFromSearchParams) {
setProcessModelSelection(item);
}
return item;
});
setProcessModelAvailableItems(selectionArray);
const processStatusSelectedArray: string[] = [];
const processStatusAllOptionsArray = PROCESS_STATUSES.map(
(processStatusOption: any) => {
const regex = new RegExp(`\\b${processStatusOption}\\b`);
if ((searchParams.get('process_status') || '').match(regex)) {
processStatusSelectedArray.push(processStatusOption);
}
return processStatusOption;
}
);
setProcessStatusSelection(processStatusSelectedArray);
setProcessStatusAllOptions(processStatusAllOptionsArray);
getProcessInstances();
}
if (filtersEnabled) {
// populate process model selection
HttpService.makeCallToBackend({
path: `/process-models?per_page=1000`,
successCallback: processResultForProcessModels,
});
} else {
getProcessInstances();
}
}, [
searchParams,
params,
oneMonthInSeconds,
oneHourInSeconds,
parametersToAlwaysFilterBy,
parametersToGetFromSearchParams,
filtersEnabled,
paginationQueryParamPrefix,
processModelFullIdentifier,
perPageOptions,
]);
// does the comparison, but also returns false if either argument
// is not truthy and therefore not comparable.
const isTrueComparison = (param1: any, operation: any, param2: any) => {
if (param1 && param2) {
switch (operation) {
case '<':
return param1 < param2;
case '>':
return param1 > param2;
default:
return false;
}
} else {
return false;
}
};
const applyFilter = (event: any) => {
event.preventDefault();
const { page, perPage } = getPageInfoFromSearchParams(
searchParams,
undefined,
undefined,
paginationQueryParamPrefix
);
let queryParamString = `per_page=${perPage}&page=${page}`;
const startFromSeconds = convertDateStringToSeconds(startFrom);
const endFromSeconds = convertDateStringToSeconds(endFrom);
const startToSeconds = convertDateStringToSeconds(startTo);
const endToSeconds = convertDateStringToSeconds(endTo);
if (isTrueComparison(startFromSeconds, '>', startToSeconds)) {
setErrorMessage({
message: '"Start date from" cannot be after "start date to"',
});
return;
}
if (isTrueComparison(endFromSeconds, '>', endToSeconds)) {
setErrorMessage({
message: '"End date from" cannot be after "end date to"',
});
return;
}
if (isTrueComparison(startFromSeconds, '>', endFromSeconds)) {
setErrorMessage({
message: '"Start date from" cannot be after "end date from"',
});
return;
}
if (isTrueComparison(startToSeconds, '>', endToSeconds)) {
setErrorMessage({
message: '"Start date to" cannot be after "end date to"',
});
return;
}
if (startFromSeconds) {
queryParamString += `&start_from=${startFromSeconds}`;
}
if (startToSeconds) {
queryParamString += `&start_to=${startToSeconds}`;
}
if (endFromSeconds) {
queryParamString += `&end_from=${endFromSeconds}`;
}
if (endToSeconds) {
queryParamString += `&end_to=${endToSeconds}`;
}
if (processStatusSelection.length > 0) {
queryParamString += `&process_status=${processStatusSelection}`;
}
if (processModelSelection) {
queryParamString += `&process_model_identifier=${processModelSelection.id}`;
}
setErrorMessage(null);
navigate(`/admin/process-instances?${queryParamString}`);
};
const dateComponent = (
labelString: any,
name: any,
initialDate: any,
onChangeFunction: any
) => {
return (
<DatePicker dateFormat={DATE_FORMAT_CARBON} datePickerType="single">
<DatePickerInput
id={`date-picker-${name}`}
placeholder={DATE_FORMAT}
labelText={labelString}
type="text"
size="md"
autocomplete="off"
allowInput={false}
onChange={(dateChangeEvent: any) => {
onChangeFunction(dateChangeEvent.srcElement.value);
}}
value={initialDate}
/>
</DatePicker>
);
};
const processStatusSearch = () => {
return (
<MultiSelect
label="Choose Status"
className="our-class"
id="process-instance-status-select"
titleText="Status"
items={processStatusAllOptions}
onChange={(selection: any) => {
setProcessStatusSelection(selection.selectedItems);
}}
itemToString={(item: any) => {
return item || '';
}}
selectionFeedback="top-after-reopen"
selectedItems={processStatusSelection}
/>
);
};
const clearFilters = () => {
setProcessModelSelection(null);
setProcessStatusSelection([]);
setStartFrom('');
setStartTo('');
setEndFrom('');
setEndTo('');
};
const filterOptions = () => {
if (!showFilterOptions) {
return null;
}
return (
<>
<Grid fullWidth className="with-bottom-margin">
<Column md={8}>
<ProcessModelSearch
onChange={(selection: any) =>
setProcessModelSelection(selection.selectedItem)
}
processModels={processModelAvailableItems}
selectedItem={processModelSelection}
/>
</Column>
<Column md={8}>{processStatusSearch()}</Column>
</Grid>
<Grid fullWidth className="with-bottom-margin">
<Column md={4}>
{dateComponent(
'Start date from',
'start-from',
startFrom,
setStartFrom
)}
</Column>
<Column md={4}>
{dateComponent('Start date to', 'start-to', startTo, setStartTo)}
</Column>
<Column md={4}>
{dateComponent('End date from', 'end-from', endFrom, setEndFrom)}
</Column>
<Column md={4}>
{dateComponent('End date to', 'end-to', endTo, setEndTo)}
</Column>
</Grid>
<Grid fullWidth className="with-bottom-margin">
<Column md={4}>
<ButtonSet>
<Button
kind=""
className="button-white-background"
onClick={clearFilters}
>
Clear
</Button>
<Button
kind="secondary"
onClick={applyFilter}
data-qa="filter-button"
>
Filter
</Button>
</ButtonSet>
</Column>
</Grid>
</>
);
};
const buildTable = () => {
const headerLabels: Record<string, string> = {
id: 'Process Instance Id',
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>;
return getHeaderLabel((column as any).Header);
});
const formatProcessInstanceId = (row: any, id: any) => {
const modifiedProcessModelId: String = modifyProcessModelPath(
row.process_model_identifier
);
return (
<Link
data-qa="process-instance-show-link"
to={`/admin/process-models/${modifiedProcessModelId}/process-instances/${row.id}`}
>
{id}
</Link>
);
};
const formatProcessModelIdentifier = (_row: any, identifier: any) => {
return (
<Link
to={`/admin/process-models/${modifyProcessModelPath(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_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];
if (column.accessor === 'status') {
return (
<td data-qa={`process-instance-status-${value}`}>
{formatter(row, value)}
</td>
);
}
return <td>{formatter(row, value)}</td>;
};
const rows = processInstances.map((row: any) => {
const currentRow = (reportMetadata as any).columns.map((column: any) => {
return formattedColumn(row, column);
});
return <tr key={row.id}>{currentRow}</tr>;
});
return (
<Table size="lg">
<TableHead>
<TableRow>
{headers.map((header: any) => (
<TableHeader key={header}>{header}</TableHeader>
))}
</TableRow>
</TableHead>
<tbody>{rows}</tbody>
</Table>
);
};
const toggleShowFilterOptions = () => {
setShowFilterOptions(!showFilterOptions);
};
const filterComponent = () => {
if (!filtersEnabled) {
return null;
}
return (
<>
<Grid fullWidth>
<Column
sm={{ span: 1, offset: 3 }}
md={{ span: 1, offset: 7 }}
lg={{ span: 1, offset: 15 }}
>
<Button
data-qa="filter-section-expand-toggle"
kind="ghost"
renderIcon={Filter}
iconDescription="Filter Options"
hasIconOnly
size="lg"
onClick={toggleShowFilterOptions}
/>
</Column>
</Grid>
{filterOptions()}
</>
);
};
if (pagination) {
// eslint-disable-next-line prefer-const
let { page, perPage } = getPageInfoFromSearchParams(
searchParams,
undefined,
undefined,
paginationQueryParamPrefix
);
if (perPageOptions && !perPageOptions.includes(perPage)) {
// eslint-disable-next-line prefer-destructuring
perPage = perPageOptions[1];
}
return (
<>
{filterComponent()}
<br />
<PaginationForTable
page={page}
perPage={perPage}
pagination={pagination}
tableToDisplay={buildTable()}
paginationQueryParamPrefix={paginationQueryParamPrefix}
perPageOptions={perPageOptions}
/>
</>
);
}
return null;
}

View File

@ -12,6 +12,7 @@ import HttpService from '../services/HttpService';
import { PaginationObject } from '../interfaces'; import { PaginationObject } from '../interfaces';
const PER_PAGE_FOR_TASKS_ON_HOME_PAGE = 5; const PER_PAGE_FOR_TASKS_ON_HOME_PAGE = 5;
const paginationQueryParamPrefix = 'tasks_for_my_open_processes';
export default function MyOpenProcesses() { export default function MyOpenProcesses() {
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
@ -21,7 +22,9 @@ export default function MyOpenProcesses() {
useEffect(() => { useEffect(() => {
const { page, perPage } = getPageInfoFromSearchParams( const { page, perPage } = getPageInfoFromSearchParams(
searchParams, searchParams,
PER_PAGE_FOR_TASKS_ON_HOME_PAGE PER_PAGE_FOR_TASKS_ON_HOME_PAGE,
undefined,
paginationQueryParamPrefix
); );
const setTasksFromResult = (result: any) => { const setTasksFromResult = (result: any) => {
setTasks(result.results); setTasks(result.results);
@ -113,7 +116,9 @@ export default function MyOpenProcesses() {
} }
const { page, perPage } = getPageInfoFromSearchParams( const { page, perPage } = getPageInfoFromSearchParams(
searchParams, searchParams,
PER_PAGE_FOR_TASKS_ON_HOME_PAGE PER_PAGE_FOR_TASKS_ON_HOME_PAGE,
undefined,
paginationQueryParamPrefix
); );
return ( return (
<> <>
@ -124,7 +129,7 @@ export default function MyOpenProcesses() {
perPageOptions={[2, PER_PAGE_FOR_TASKS_ON_HOME_PAGE, 25]} perPageOptions={[2, PER_PAGE_FOR_TASKS_ON_HOME_PAGE, 25]}
pagination={pagination} pagination={pagination}
tableToDisplay={buildTable()} tableToDisplay={buildTable()}
path="/tasks/for-my-open-processes" paginationQueryParamPrefix={paginationQueryParamPrefix}
/> />
</> </>
); );

View File

@ -13,7 +13,7 @@ import { PaginationObject } from '../interfaces';
const PER_PAGE_FOR_TASKS_ON_HOME_PAGE = 5; const PER_PAGE_FOR_TASKS_ON_HOME_PAGE = 5;
export default function MyTasksForProcessesStartedByOthers() { export default function TasksWaitingForMe() {
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const [tasks, setTasks] = useState([]); const [tasks, setTasks] = useState([]);
const [pagination, setPagination] = useState<PaginationObject | null>(null); const [pagination, setPagination] = useState<PaginationObject | null>(null);
@ -21,14 +21,16 @@ export default function MyTasksForProcessesStartedByOthers() {
useEffect(() => { useEffect(() => {
const { page, perPage } = getPageInfoFromSearchParams( const { page, perPage } = getPageInfoFromSearchParams(
searchParams, searchParams,
PER_PAGE_FOR_TASKS_ON_HOME_PAGE PER_PAGE_FOR_TASKS_ON_HOME_PAGE,
undefined,
'tasks_waiting_for_me'
); );
const setTasksFromResult = (result: any) => { const setTasksFromResult = (result: any) => {
setTasks(result.results); setTasks(result.results);
setPagination(result.pagination); setPagination(result.pagination);
}; };
HttpService.makeCallToBackend({ HttpService.makeCallToBackend({
path: `/tasks/for-processes-started-by-others?per_page=${perPage}&page=${page}`, path: `/tasks/for-me?per_page=${perPage}&page=${page}`,
successCallback: setTasksFromResult, successCallback: setTasksFromResult,
}); });
}, [searchParams]); }, [searchParams]);
@ -115,7 +117,9 @@ export default function MyTasksForProcessesStartedByOthers() {
} }
const { page, perPage } = getPageInfoFromSearchParams( const { page, perPage } = getPageInfoFromSearchParams(
searchParams, searchParams,
PER_PAGE_FOR_TASKS_ON_HOME_PAGE PER_PAGE_FOR_TASKS_ON_HOME_PAGE,
undefined,
'tasks_waiting_for_me'
); );
return ( return (
<> <>
@ -126,7 +130,7 @@ export default function MyTasksForProcessesStartedByOthers() {
perPageOptions={[2, PER_PAGE_FOR_TASKS_ON_HOME_PAGE, 25]} perPageOptions={[2, PER_PAGE_FOR_TASKS_ON_HOME_PAGE, 25]}
pagination={pagination} pagination={pagination}
tableToDisplay={buildTable()} tableToDisplay={buildTable()}
path="/tasks/for-my-open-processes" paginationQueryParamPrefix="tasks_waiting_for_me"
/> />
</> </>
); );

View File

@ -0,0 +1,144 @@
import { useEffect, useState } from 'react';
// @ts-ignore
import { Button, Table } from '@carbon/react';
import { Link, useSearchParams } from 'react-router-dom';
import PaginationForTable from './PaginationForTable';
import {
convertSecondsToFormattedDateTime,
getPageInfoFromSearchParams,
modifyProcessModelPath,
} from '../helpers';
import HttpService from '../services/HttpService';
import { PaginationObject } from '../interfaces';
const PER_PAGE_FOR_TASKS_ON_HOME_PAGE = 5;
const paginationQueryParamPrefix = 'tasks_waiting_for_my_groups';
export default function TasksForWaitingForMyGroups() {
const [searchParams] = useSearchParams();
const [tasks, setTasks] = useState([]);
const [pagination, setPagination] = useState<PaginationObject | null>(null);
useEffect(() => {
const { page, perPage } = getPageInfoFromSearchParams(
searchParams,
PER_PAGE_FOR_TASKS_ON_HOME_PAGE,
undefined,
paginationQueryParamPrefix
);
const setTasksFromResult = (result: any) => {
setTasks(result.results);
setPagination(result.pagination);
};
HttpService.makeCallToBackend({
path: `/tasks/for-my-groups?per_page=${perPage}&page=${page}`,
successCallback: setTasksFromResult,
});
}, [searchParams]);
const buildTable = () => {
const rows = tasks.map((row) => {
const rowToUse = row as any;
const taskUrl = `/tasks/${rowToUse.process_instance_id}/${rowToUse.task_id}`;
const modifiedProcessModelIdentifier = modifyProcessModelPath(
rowToUse.process_model_identifier
);
return (
<tr key={rowToUse.id}>
<td>
<Link
data-qa="process-model-show-link"
to={`/admin/process-models/${modifiedProcessModelIdentifier}`}
>
{rowToUse.process_model_display_name}
</Link>
</td>
<td>
<Link
data-qa="process-instance-show-link"
to={`/admin/process-models/${modifiedProcessModelIdentifier}/process-instances/${rowToUse.process_instance_id}`}
>
View {rowToUse.process_instance_id}
</Link>
</td>
<td
title={`task id: ${rowToUse.name}, spiffworkflow task guid: ${rowToUse.id}`}
>
{rowToUse.task_title}
</td>
<td>{rowToUse.username}</td>
<td>{rowToUse.process_instance_status}</td>
<td>{rowToUse.group_identifier || '-'}</td>
<td>
{convertSecondsToFormattedDateTime(
rowToUse.created_at_in_seconds
) || '-'}
</td>
<td>
{convertSecondsToFormattedDateTime(
rowToUse.updated_at_in_seconds
) || '-'}
</td>
<td>
<Button
variant="primary"
href={taskUrl}
hidden={rowToUse.process_instance_status === 'suspended'}
disabled={!rowToUse.current_user_is_potential_owner}
>
Go
</Button>
</td>
</tr>
);
});
return (
<Table striped bordered>
<thead>
<tr>
<th>Process Model</th>
<th>Process Instance</th>
<th>Task Name</th>
<th>Process Started By</th>
<th>Process Instance Status</th>
<th>Assigned Group</th>
<th>Process Started</th>
<th>Process Updated</th>
<th>Actions</th>
</tr>
</thead>
<tbody>{rows}</tbody>
</Table>
);
};
const tasksComponent = () => {
if (pagination && pagination.total < 1) {
return null;
}
const { page, perPage } = getPageInfoFromSearchParams(
searchParams,
PER_PAGE_FOR_TASKS_ON_HOME_PAGE,
undefined,
paginationQueryParamPrefix
);
return (
<>
<h1>Tasks waiting for my groups</h1>
<PaginationForTable
page={page}
perPage={perPage}
perPageOptions={[2, PER_PAGE_FOR_TASKS_ON_HOME_PAGE, 25]}
pagination={pagination}
tableToDisplay={buildTable()}
paginationQueryParamPrefix={paginationQueryParamPrefix}
/>
</>
);
};
if (pagination) {
return tasksComponent();
}
return null;
}

View File

@ -79,11 +79,20 @@ export const objectIsEmpty = (obj: object) => {
export const getPageInfoFromSearchParams = ( export const getPageInfoFromSearchParams = (
searchParams: any, searchParams: any,
defaultPerPage: string | number = DEFAULT_PER_PAGE, defaultPerPage: string | number = DEFAULT_PER_PAGE,
defaultPage: string | number = DEFAULT_PAGE defaultPage: string | number = DEFAULT_PAGE,
paginationQueryParamPrefix: string | null = null
) => { ) => {
const page = parseInt(searchParams.get('page') || defaultPage.toString(), 10); const paginationQueryParamPrefixToUse = paginationQueryParamPrefix
? `${paginationQueryParamPrefix}_`
: '';
const page = parseInt(
searchParams.get(`${paginationQueryParamPrefixToUse}page`) ||
defaultPage.toString(),
10
);
const perPage = parseInt( const perPage = parseInt(
searchParams.get('per_page') || defaultPerPage.toString(), searchParams.get(`${paginationQueryParamPrefixToUse}per_page`) ||
defaultPerPage.toString(),
10 10
); );

View File

@ -0,0 +1,5 @@
import MyCompletedInstances from '../components/MyCompletedInstances';
export default function CompletedInstances() {
return <MyCompletedInstances />;
}

View File

@ -1,12 +1,15 @@
import MyTasksForProcessesStartedByOthers from '../components/MyTasksForProcessesStartedByOthers';
import TasksForMyOpenProcesses from '../components/TasksForMyOpenProcesses'; import TasksForMyOpenProcesses from '../components/TasksForMyOpenProcesses';
import TasksWaitingForMe from '../components/TasksWaitingForMe';
import TasksForWaitingForMyGroups from '../components/TasksWaitingForMyGroups';
export default function GroupedTasks() { export default function GroupedTasks() {
return ( return (
<> <>
<TasksForMyOpenProcesses /> <TasksForMyOpenProcesses />
<br /> <br />
<MyTasksForProcessesStartedByOthers /> <TasksWaitingForMe />
<br />
<TasksForWaitingForMyGroups />
</> </>
); );
} }

View File

@ -6,6 +6,7 @@ import TaskShow from './TaskShow';
import ErrorContext from '../contexts/ErrorContext'; import ErrorContext from '../contexts/ErrorContext';
import MyTasks from './MyTasks'; import MyTasks from './MyTasks';
import GroupedTasks from './GroupedTasks'; import GroupedTasks from './GroupedTasks';
import CompletedInstances from './CompletedInstances';
export default function HomePageRoutes() { export default function HomePageRoutes() {
const location = useLocation(); const location = useLocation();
@ -18,6 +19,8 @@ export default function HomePageRoutes() {
let newSelectedTabIndex = 0; let newSelectedTabIndex = 0;
if (location.pathname.match(/^\/tasks\/grouped\b/)) { if (location.pathname.match(/^\/tasks\/grouped\b/)) {
newSelectedTabIndex = 1; newSelectedTabIndex = 1;
} else if (location.pathname.match(/^\/tasks\/completed-instances\b/)) {
newSelectedTabIndex = 2;
} }
setSelectedTabIndex(newSelectedTabIndex); setSelectedTabIndex(newSelectedTabIndex);
}, [location, setErrorMessage]); }, [location, setErrorMessage]);
@ -28,6 +31,9 @@ export default function HomePageRoutes() {
<TabList aria-label="List of tabs"> <TabList aria-label="List of tabs">
<Tab onClick={() => navigate('/tasks/my-tasks')}>My Tasks</Tab> <Tab onClick={() => navigate('/tasks/my-tasks')}>My Tasks</Tab>
<Tab onClick={() => navigate('/tasks/grouped')}>Grouped Tasks</Tab> <Tab onClick={() => navigate('/tasks/grouped')}>Grouped Tasks</Tab>
<Tab onClick={() => navigate('/tasks/completed-instances')}>
Completed Instances
</Tab>
</TabList> </TabList>
</Tabs> </Tabs>
<br /> <br />
@ -36,6 +42,7 @@ export default function HomePageRoutes() {
<Route path="my-tasks" element={<MyTasks />} /> <Route path="my-tasks" element={<MyTasks />} />
<Route path=":process_instance_id/:task_id" element={<TaskShow />} /> <Route path=":process_instance_id/:task_id" element={<TaskShow />} />
<Route path="grouped" element={<GroupedTasks />} /> <Route path="grouped" element={<GroupedTasks />} />
<Route path="completed-instances" element={<CompletedInstances />} />
</Routes> </Routes>
</> </>
); );

View File

@ -94,14 +94,8 @@ export default function MessageInstanceList() {
if (pagination) { if (pagination) {
const { page, perPage } = getPageInfoFromSearchParams(searchParams); const { page, perPage } = getPageInfoFromSearchParams(searchParams);
let queryParamString = '';
let breadcrumbElement = null; let breadcrumbElement = null;
if (searchParams.get('process_instance_id')) { if (searchParams.get('process_instance_id')) {
queryParamString += `&process_group_id=${searchParams.get(
'process_group_id'
)}&process_model_id=${searchParams.get(
'process_model_id'
)}&process_instance_id=${searchParams.get('process_instance_id')}`;
breadcrumbElement = ( breadcrumbElement = (
<ProcessBreadcrumb <ProcessBreadcrumb
hotCrumbs={[ hotCrumbs={[
@ -132,8 +126,6 @@ export default function MessageInstanceList() {
perPage={perPage} perPage={perPage}
pagination={pagination} pagination={pagination}
tableToDisplay={buildTable()} tableToDisplay={buildTable()}
queryParamString={queryParamString}
path="/admin/messages"
/> />
</> </>
); );

View File

@ -152,7 +152,6 @@ export default function MyTasks() {
perPageOptions={[2, PER_PAGE_FOR_TASKS_ON_HOME_PAGE, 25]} perPageOptions={[2, PER_PAGE_FOR_TASKS_ON_HOME_PAGE, 25]}
pagination={pagination} pagination={pagination}
tableToDisplay={buildTable()} tableToDisplay={buildTable()}
path="/tasks"
/> />
</> </>
); );

View File

@ -109,7 +109,6 @@ export default function ProcessGroupList() {
perPage={perPage} perPage={perPage}
pagination={pagination as any} pagination={pagination as any}
tableToDisplay={buildTable()} tableToDisplay={buildTable()}
path="/admin/process-groups"
/> />
</> </>
); );

View File

@ -170,7 +170,6 @@ export default function ProcessGroupShow() {
perPage={perPage} perPage={perPage}
pagination={modelPagination} pagination={modelPagination}
tableToDisplay={buildModelTable()} tableToDisplay={buildModelTable()}
path={`/admin/process-groups/${processGroup.id}`}
/> />
)} )}
<br /> <br />
@ -182,7 +181,6 @@ export default function ProcessGroupShow() {
perPage={perPage} perPage={perPage}
pagination={groupPagination} pagination={groupPagination}
tableToDisplay={buildGroupTable()} tableToDisplay={buildGroupTable()}
path={`/admin/process-groups/${processGroup.id}`}
/> />
)} )}
</ul> </ul>

View File

@ -1,482 +1,15 @@
import { useContext, useEffect, useMemo, useState } from 'react'; import { useSearchParams } from 'react-router-dom';
import {
Link,
useNavigate,
useParams,
useSearchParams,
} from 'react-router-dom';
// @ts-ignore
import { Filter } from '@carbon/icons-react';
import {
Button,
ButtonSet,
DatePicker,
DatePickerInput,
Table,
Grid,
Column,
MultiSelect,
TableHeader,
TableHead,
TableRow,
// @ts-ignore
} from '@carbon/react';
import { PROCESS_STATUSES, DATE_FORMAT, DATE_FORMAT_CARBON } from '../config';
import {
convertDateStringToSeconds,
convertSecondsToFormattedDate,
getPageInfoFromSearchParams,
getProcessModelFullIdentifierFromSearchParams,
modifyProcessModelPath,
} from '../helpers';
import PaginationForTable from '../components/PaginationForTable';
import 'react-datepicker/dist/react-datepicker.css'; import 'react-datepicker/dist/react-datepicker.css';
import ErrorContext from '../contexts/ErrorContext';
import HttpService from '../services/HttpService';
import 'react-bootstrap-typeahead/css/Typeahead.css'; import 'react-bootstrap-typeahead/css/Typeahead.css';
import 'react-bootstrap-typeahead/css/Typeahead.bs5.css'; import 'react-bootstrap-typeahead/css/Typeahead.bs5.css';
import { PaginationObject, ProcessModel } from '../interfaces';
import ProcessModelSearch from '../components/ProcessModelSearch';
import ProcessBreadcrumb from '../components/ProcessBreadcrumb'; import ProcessBreadcrumb from '../components/ProcessBreadcrumb';
import ProcessInstanceListTable from '../components/ProcessInstanceListTable';
import { getProcessModelFullIdentifierFromSearchParams } from '../helpers';
export default function ProcessInstanceList() { export default function ProcessInstanceList() {
const params = useParams();
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const navigate = useNavigate();
const [processInstances, setProcessInstances] = useState([]);
const [reportMetadata, setReportMetadata] = useState({});
const [pagination, setPagination] = useState<PaginationObject | null>(null);
const oneHourInSeconds = 3600;
const oneMonthInSeconds = oneHourInSeconds * 24 * 30;
const [startFrom, setStartFrom] = useState<string>('');
const [startTo, setStartTo] = useState<string>('');
const [endFrom, setEndFrom] = useState<string>('');
const [endTo, setEndTo] = useState<string>('');
const [showFilterOptions, setShowFilterOptions] = useState<boolean>(false);
const setErrorMessage = (useContext as any)(ErrorContext)[1];
const [processStatusAllOptions, setProcessStatusAllOptions] = useState<any[]>(
[]
);
const [processStatusSelection, setProcessStatusSelection] = useState<
string[]
>([]);
const [processModelAvailableItems, setProcessModelAvailableItems] = useState<
ProcessModel[]
>([]);
const [processModelSelection, setProcessModelSelection] =
useState<ProcessModel | null>(null);
const parametersToAlwaysFilterBy = useMemo(() => {
return {
start_from: setStartFrom,
start_to: setStartTo,
end_from: setEndFrom,
end_to: setEndTo,
};
}, [setStartFrom, setStartTo, setEndFrom, setEndTo]);
const parametersToGetFromSearchParams = useMemo(() => {
return {
process_model_identifier: null,
process_status: null,
};
}, []);
// eslint-disable-next-line sonarjs/cognitive-complexity
useEffect(() => {
function setProcessInstancesFromResult(result: any) {
const processInstancesFromApi = result.results;
setProcessInstances(processInstancesFromApi);
setReportMetadata(result.report_metadata);
setPagination(result.pagination);
}
function getProcessInstances() {
const { page, perPage } = getPageInfoFromSearchParams(searchParams);
let queryParamString = `per_page=${perPage}&page=${page}`;
Object.keys(parametersToAlwaysFilterBy).forEach((paramName: string) => {
// @ts-expect-error TS(7053) FIXME:
const functionToCall = parametersToAlwaysFilterBy[paramName];
const searchParamValue = searchParams.get(paramName);
if (searchParamValue) {
queryParamString += `&${paramName}=${searchParamValue}`;
const dateString = convertSecondsToFormattedDate(
searchParamValue as any
);
functionToCall(dateString);
setShowFilterOptions(true);
}
});
Object.keys(parametersToGetFromSearchParams).forEach(
(paramName: string) => {
if (searchParams.get(paramName)) {
// @ts-expect-error TS(7053) FIXME:
const functionToCall = parametersToGetFromSearchParams[paramName];
queryParamString += `&${paramName}=${searchParams.get(paramName)}`;
if (functionToCall !== null) {
functionToCall(searchParams.get(paramName) || '');
}
setShowFilterOptions(true);
}
}
);
HttpService.makeCallToBackend({
path: `/process-instances?${queryParamString}`,
successCallback: setProcessInstancesFromResult,
});
}
function processResultForProcessModels(result: any) {
const processModelFullIdentifier =
getProcessModelFullIdentifierFromSearchParams(searchParams);
const selectionArray = result.results.map((item: any) => {
const label = `${item.id}`;
Object.assign(item, { label });
if (label === processModelFullIdentifier) {
setProcessModelSelection(item);
}
return item;
});
setProcessModelAvailableItems(selectionArray);
const processStatusSelectedArray: string[] = [];
const processStatusAllOptionsArray = PROCESS_STATUSES.map(
(processStatusOption: any) => {
const regex = new RegExp(`\\b${processStatusOption}\\b`);
if ((searchParams.get('process_status') || '').match(regex)) {
processStatusSelectedArray.push(processStatusOption);
}
return processStatusOption;
}
);
setProcessStatusSelection(processStatusSelectedArray);
setProcessStatusAllOptions(processStatusAllOptionsArray);
getProcessInstances();
}
// populate process model selection
HttpService.makeCallToBackend({
path: `/process-models?per_page=1000`,
successCallback: processResultForProcessModels,
});
}, [
searchParams,
params,
oneMonthInSeconds,
oneHourInSeconds,
parametersToAlwaysFilterBy,
parametersToGetFromSearchParams,
]);
// does the comparison, but also returns false if either argument
// is not truthy and therefore not comparable.
const isTrueComparison = (param1: any, operation: any, param2: any) => {
if (param1 && param2) {
switch (operation) {
case '<':
return param1 < param2;
case '>':
return param1 > param2;
default:
return false;
}
} else {
return false;
}
};
const applyFilter = (event: any) => {
event.preventDefault();
const { page, perPage } = getPageInfoFromSearchParams(searchParams);
let queryParamString = `per_page=${perPage}&page=${page}`;
const startFromSeconds = convertDateStringToSeconds(startFrom);
const endFromSeconds = convertDateStringToSeconds(endFrom);
const startToSeconds = convertDateStringToSeconds(startTo);
const endToSeconds = convertDateStringToSeconds(endTo);
if (isTrueComparison(startFromSeconds, '>', startToSeconds)) {
setErrorMessage({
message: '"Start date from" cannot be after "start date to"',
});
return;
}
if (isTrueComparison(endFromSeconds, '>', endToSeconds)) {
setErrorMessage({
message: '"End date from" cannot be after "end date to"',
});
return;
}
if (isTrueComparison(startFromSeconds, '>', endFromSeconds)) {
setErrorMessage({
message: '"Start date from" cannot be after "end date from"',
});
return;
}
if (isTrueComparison(startToSeconds, '>', endToSeconds)) {
setErrorMessage({
message: '"Start date to" cannot be after "end date to"',
});
return;
}
if (startFromSeconds) {
queryParamString += `&start_from=${startFromSeconds}`;
}
if (startToSeconds) {
queryParamString += `&start_to=${startToSeconds}`;
}
if (endFromSeconds) {
queryParamString += `&end_from=${endFromSeconds}`;
}
if (endToSeconds) {
queryParamString += `&end_to=${endToSeconds}`;
}
if (processStatusSelection.length > 0) {
queryParamString += `&process_status=${processStatusSelection}`;
}
if (processModelSelection) {
queryParamString += `&process_model_identifier=${processModelSelection.id}`;
}
setErrorMessage(null);
navigate(`/admin/process-instances?${queryParamString}`);
};
const dateComponent = (
labelString: any,
name: any,
initialDate: any,
onChangeFunction: any
) => {
return (
<DatePicker dateFormat={DATE_FORMAT_CARBON} datePickerType="single">
<DatePickerInput
id={`date-picker-${name}`}
placeholder={DATE_FORMAT}
labelText={labelString}
type="text"
size="md"
autocomplete="off"
allowInput={false}
onChange={(dateChangeEvent: any) => {
onChangeFunction(dateChangeEvent.srcElement.value);
}}
value={initialDate}
/>
</DatePicker>
);
};
const processStatusSearch = () => {
return (
<MultiSelect
label="Choose Status"
className="our-class"
id="process-instance-status-select"
titleText="Status"
items={processStatusAllOptions}
onChange={(selection: any) => {
setProcessStatusSelection(selection.selectedItems);
}}
itemToString={(item: any) => {
return item || '';
}}
selectionFeedback="top-after-reopen"
selectedItems={processStatusSelection}
/>
);
};
const clearFilters = () => {
setProcessModelSelection(null);
setProcessStatusSelection([]);
setStartFrom('');
setStartTo('');
setEndFrom('');
setEndTo('');
};
const filterOptions = () => {
if (!showFilterOptions) {
return null;
}
return (
<>
<Grid fullWidth className="with-bottom-margin">
<Column md={8}>
<ProcessModelSearch
onChange={(selection: any) =>
setProcessModelSelection(selection.selectedItem)
}
processModels={processModelAvailableItems}
selectedItem={processModelSelection}
/>
</Column>
<Column md={8}>{processStatusSearch()}</Column>
</Grid>
<Grid fullWidth className="with-bottom-margin">
<Column md={4}>
{dateComponent(
'Start date from',
'start-from',
startFrom,
setStartFrom
)}
</Column>
<Column md={4}>
{dateComponent('Start date to', 'start-to', startTo, setStartTo)}
</Column>
<Column md={4}>
{dateComponent('End date from', 'end-from', endFrom, setEndFrom)}
</Column>
<Column md={4}>
{dateComponent('End date to', 'end-to', endTo, setEndTo)}
</Column>
</Grid>
<Grid fullWidth className="with-bottom-margin">
<Column md={4}>
<ButtonSet>
<Button
kind=""
className="button-white-background"
onClick={clearFilters}
>
Clear
</Button>
<Button
kind="secondary"
onClick={applyFilter}
data-qa="filter-button"
>
Filter
</Button>
</ButtonSet>
</Column>
</Grid>
</>
);
};
const toggleShowFilterOptions = () => {
setShowFilterOptions(!showFilterOptions);
};
const filterComponent = () => {
return (
<>
<Grid fullWidth>
<Column
sm={{ span: 1, offset: 3 }}
md={{ span: 1, offset: 7 }}
lg={{ span: 1, offset: 15 }}
>
<Button
data-qa="filter-section-expand-toggle"
kind="ghost"
renderIcon={Filter}
iconDescription="Filter Options"
hasIconOnly
size="lg"
onClick={toggleShowFilterOptions}
/>
</Column>
</Grid>
{filterOptions()}
</>
);
};
const buildTable = () => {
const headerLabels: Record<string, string> = {
id: 'Process Instance Id',
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>;
return getHeaderLabel((column as any).Header);
});
const formatProcessInstanceId = (row: any, id: any) => {
const modifiedProcessModelId: String = modifyProcessModelPath(
row.process_model_identifier
);
return (
<Link
data-qa="process-instance-show-link"
to={`/admin/process-models/${modifiedProcessModelId}/process-instances/${row.id}`}
>
{id}
</Link>
);
};
const formatProcessModelIdentifier = (_row: any, identifier: any) => {
return (
<Link
to={`/admin/process-models/${modifyProcessModelPath(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_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];
if (column.accessor === 'status') {
return (
<td data-qa={`process-instance-status-${value}`}>
{formatter(row, value)}
</td>
);
}
return <td>{formatter(row, value)}</td>;
};
const rows = processInstances.map((row: any) => {
const currentRow = (reportMetadata as any).columns.map((column: any) => {
return formattedColumn(row, column);
});
return <tr key={row.id}>{currentRow}</tr>;
});
return (
<Table size="lg">
<TableHead>
<TableRow>
{headers.map((header: any) => (
<TableHeader key={header}>{header}</TableHeader>
))}
</TableRow>
</TableHead>
<tbody>{rows}</tbody>
</Table>
);
};
const processInstanceBreadcrumbElement = () => { const processInstanceBreadcrumbElement = () => {
const processModelFullIdentifier = const processModelFullIdentifier =
getProcessModelFullIdentifierFromSearchParams(searchParams); getProcessModelFullIdentifierFromSearchParams(searchParams);
@ -498,48 +31,14 @@ export default function ProcessInstanceList() {
); );
}; };
const getSearchParamsAsQueryString = () => {
let queryParamString = '';
Object.keys(parametersToAlwaysFilterBy).forEach((paramName) => {
const searchParamValue = searchParams.get(paramName);
if (searchParamValue) {
queryParamString += `&${paramName}=${searchParamValue}`;
}
});
Object.keys(parametersToGetFromSearchParams).forEach(
(paramName: string) => {
if (searchParams.get(paramName)) {
queryParamString += `&${paramName}=${searchParams.get(paramName)}`;
}
}
);
return queryParamString;
};
const processInstanceTitleElement = () => { const processInstanceTitleElement = () => {
return <h1>Process Instances</h1>; return <h1>Process Instances</h1>;
}; };
if (pagination) {
const { page, perPage } = getPageInfoFromSearchParams(searchParams);
return ( return (
<> <>
{processInstanceBreadcrumbElement()} {processInstanceBreadcrumbElement()}
{processInstanceTitleElement()} {processInstanceTitleElement()}
{filterComponent()} <ProcessInstanceListTable />
<br />
<PaginationForTable
page={page}
perPage={perPage}
pagination={pagination}
tableToDisplay={buildTable()}
queryParamString={getSearchParamsAsQueryString()}
path="/admin/process-instances"
/>
</> </>
); );
}
return null;
} }

View File

@ -99,7 +99,6 @@ export default function ProcessInstanceLogList() {
perPage={perPage} perPage={perPage}
pagination={pagination} pagination={pagination}
tableToDisplay={buildTable()} tableToDisplay={buildTable()}
path={`/admin/process-models/${modifiedProcessModelId}/process-instances/${params.process_instance_id}/logs`}
/> />
</main> </main>
); );

View File

@ -91,7 +91,6 @@ export default function ProcessInstanceReport() {
perPage={perPage} perPage={perPage}
pagination={pagination} pagination={pagination}
tableToDisplay={buildTable()} tableToDisplay={buildTable()}
path={`/admin/process-models/${params.process_group_id}/${params.process_model_id}/process-instances/report`}
/> />
</main> </main>
); );

View File

@ -33,6 +33,7 @@ import ErrorContext from '../contexts/ErrorContext';
import { modifyProcessModelPath, unModifyProcessModelPath } from '../helpers'; import { modifyProcessModelPath, unModifyProcessModelPath } from '../helpers';
import { ProcessFile, ProcessModel, RecentProcessModel } from '../interfaces'; import { ProcessFile, ProcessModel, RecentProcessModel } from '../interfaces';
import ButtonWithConfirmation from '../components/ButtonWithConfirmation'; import ButtonWithConfirmation from '../components/ButtonWithConfirmation';
import ProcessInstanceListTable from '../components/ProcessInstanceListTable';
const storeRecentProcessModelInLocalStorage = ( const storeRecentProcessModelInLocalStorage = (
processModelForStorage: ProcessModel processModelForStorage: ProcessModel
@ -538,6 +539,11 @@ export default function ProcessModelShow() {
{processInstancesUl()} {processInstancesUl()}
<br /> <br />
{processModelButtons()} {processModelButtons()}
<br />
<ProcessInstanceListTable
filtersEnabled={false}
processModelFullIdentifier={processModel.id}
/>
</> </>
); );
} }

View File

@ -83,7 +83,6 @@ export default function SecretList() {
perPage={perPage} perPage={perPage}
pagination={pagination as any} pagination={pagination as any}
tableToDisplay={buildTable()} tableToDisplay={buildTable()}
path="/admin/secrets"
/> />
); );
} else { } else {