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 2b300d0d4e
40 changed files with 1038 additions and 698 deletions

View File

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

View File

@ -1,8 +1,8 @@
"""empty message
Revision ID: 7d1662ea1227
Revision ID: 7cc9bdcc309f
Revises:
Create Date: 2022-11-14 21:48:34.469311
Create Date: 2022-11-15 09:53:53.349712
"""
from alembic import op
@ -10,7 +10,7 @@ import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '7d1662ea1227'
revision = '7cc9bdcc309f'
down_revision = None
branch_labels = None
depends_on = None
@ -68,15 +68,6 @@ def upgrade():
sa.Column('spiff_step', sa.Integer(), nullable=False),
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',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('username', sa.String(length=255), nullable=False),
@ -176,6 +167,17 @@ def upgrade():
sa.PrimaryKeyConstraint('id'),
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',
sa.Column('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('active_task')
op.drop_table('user_group_assignment')
op.drop_table('spiff_step_details')
op.drop_table('secret')
op.drop_table('refresh_token')
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_table('message_correlation_property')
op.drop_table('user')
op.drop_table('spiff_step_details')
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_identifier'), table_name='spec_reference_cache')

View File

@ -918,7 +918,7 @@ paths:
items:
$ref: "#/components/schemas/Task"
/tasks/for-processes-started-by-others:
/tasks/for-me:
parameters:
- name: page
in: query
@ -935,7 +935,36 @@ paths:
get:
tags:
- 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
responses:
"200":

View File

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

View File

@ -59,7 +59,7 @@ class MessageInstanceModel(SpiffworkflowBaseDBModel):
updated_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")
def validate_message_type(self, key: str, value: Any) -> Any:

View File

@ -1,10 +1,14 @@
"""Spiff_step_details."""
from dataclasses import dataclass
from typing import Optional
from flask_bpmn.models.db import db
from flask_bpmn.models.db import SpiffworkflowBaseDBModel
from sqlalchemy import ForeignKey
from sqlalchemy.orm import deferred
from spiffworkflow_backend.models.group import GroupModel
@dataclass
class SpiffStepDetailsModel(SpiffworkflowBaseDBModel):
@ -17,3 +21,6 @@ class SpiffStepDetailsModel(SpiffworkflowBaseDBModel):
task_json: str = deferred(db.Column(db.JSON, nullable=False)) # type: ignore
timestamp: float = db.Column(db.DECIMAL(17, 6), nullable=False)
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.routes.user import verify_token
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.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 (
@ -1034,7 +1032,17 @@ def task_list_for_my_open_processes(
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
) -> flask.wrappers.Response:
"""Task_list_for_processes_started_by_others."""
@ -1042,14 +1050,21 @@ def task_list_for_processes_started_by_others(
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:
"""Get_tasks."""
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 = (
ActiveTaskModel.query.outerjoin(
GroupModel, GroupModel.id == ActiveTaskModel.lane_assignment_id
)
ActiveTaskModel.query.distinct()
.outerjoin(GroupModel, GroupModel.id == ActiveTaskModel.lane_assignment_id)
.join(ProcessInstanceModel)
.join(UserModel, UserModel.id == ProcessInstanceModel.process_initiator_id)
)
@ -1057,11 +1072,29 @@ def get_tasks(
if processes_started_by_user:
active_tasks_query = active_tasks_query.filter(
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:
active_tasks_query = active_tasks_query.filter(
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(
ProcessInstanceModel.process_model_identifier,
@ -1238,7 +1271,25 @@ def task_submit(
if terminate_loop and spiff_task.is_looping():
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
# task spec, complete that form as well.

View File

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

View File

@ -60,8 +60,9 @@ class FileSystemService:
@staticmethod
def workflow_path(spec: ProcessModelInfo) -> str:
"""Workflow_path."""
process_model_path = os.path.join(FileSystemService.root_path(), spec.id)
# process_group_path = FileSystemService.process_group_path_for_spec(spec)
process_model_path = os.path.join(
FileSystemService.root_path(), spec.id_for_file_path()
)
return process_model_path
@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.exceptions import WorkflowException # 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 (
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.process_model_service import ProcessModelService
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.user_service import UserService
@ -584,9 +580,10 @@ class ProcessInstanceProcessor:
)
return details_model
def save_spiff_step_details(self) -> None:
def save_spiff_step_details(self, active_task: ActiveTaskModel) -> None:
"""SaveSpiffStepDetails."""
details_model = self.spiff_step_details()
details_model.lane_assignment_id = active_task.lane_assignment_id
db.session.add(details_model)
db.session.commit()
@ -679,13 +676,13 @@ class ProcessInstanceProcessor:
"""Backfill_missing_spec_reference_records."""
process_models = ProcessModelService().get_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()
if bpmn_process_identifier in bpmn_process_identifiers:
SpecFileService.update_process_cache(refs[bpmn_process_identifier])
return FileSystemService.full_path_to_process_model_file(
process_model
)
return FileSystemService.full_path_to_process_model_file(process_model)
return None
@staticmethod
@ -1130,11 +1127,11 @@ class ProcessInstanceProcessor:
)
return user_tasks # type: ignore
def complete_task(self, task: SpiffTask) -> None:
def complete_task(self, task: SpiffTask, active_task: ActiveTaskModel) -> None:
"""Complete_task."""
self.increment_spiff_step()
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]:
"""Get_data."""

View File

@ -8,6 +8,7 @@ from flask_bpmn.api.api_error import ApiError
from flask_bpmn.models.db import db
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 ProcessInstanceModel
from spiffworkflow_backend.models.process_instance import ProcessInstanceStatus
@ -188,6 +189,7 @@ class ProcessInstanceService:
spiff_task: SpiffTask,
data: dict[str, Any],
user: UserModel,
active_task: ActiveTaskModel,
) -> None:
"""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)
spiff_task.update_data(dot_dct)
# 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)
@staticmethod

View File

@ -2,17 +2,10 @@
import os
import shutil
from datetime import datetime
from typing import Any, Type
from typing import List
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 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_backend.models.spec_reference import SpecReferenceCache
@ -77,10 +70,13 @@ class SpecFileService(FileSystemService):
@staticmethod
def get_references_for_process(process_model_info: ProcessModelInfo) -> list[SpecReference]:
"""Get_references_for_process."""
files = SpecFileService.get_files(process_model_info)
references = []
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
@staticmethod
@ -94,7 +90,7 @@ class SpecFileService(FileSystemService):
type = {str} 'process' / 'decision'
"""
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)
parser = MyCustomParser()
parser_type = None
@ -118,19 +114,28 @@ class SpecFileService(FileSystemService):
else:
return references
for sub_parser in sub_parsers:
if parser_type == 'process':
if parser_type == "process":
has_lanes = sub_parser.has_lanes()
executable = sub_parser.process_executable
start_messages = sub_parser.start_messages()
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,
type=parser_type,
file_name=file.name, relative_path=file_path, has_lanes=has_lanes,
is_executable=is_executable, messages=messages, is_primary=is_primary,
correlations=correlations, start_messages=start_messages
))
file_name=file.name,
relative_path=file_path,
has_lanes=has_lanes,
is_executable=is_executable,
messages=messages,
is_primary=is_primary,
correlations=correlations,
start_messages=start_messages
)
)
return references
@staticmethod
@ -142,15 +147,14 @@ class SpecFileService(FileSystemService):
return SpecFileService.update_file(process_model_info, file_name, binary_data)
@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:
"""Update_file."""
SpecFileService.assert_valid_file_name(file_name)
file_path = os.path.join(
FileSystemService.root_path(), process_model_info.id, file_name
)
SpecFileService.write_file_data_to_system(file_path, binary_data)
file = SpecFileService.to_file_object(file_name, file_path)
full_file_path = SpecFileService.full_file_path(process_model_info, file_name)
SpecFileService.write_file_data_to_system(full_file_path, binary_data)
file = SpecFileService.to_file_object(file_name, full_file_path)
references = SpecFileService.get_references_for_file(file, process_model_info)
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_trigger_cache(ref, process_model_info)
SpecFileService.update_correlation_cache(ref)
return file
@staticmethod
def get_data(process_model_info: ProcessModelInfo, file_name: str) -> bytes:
"""Get_data."""
# file_path = SpecFileService.file_path(process_model_info, file_name)
file_path = os.path.join(
FileSystemService.root_path(), process_model_info.id, file_name
)
if not os.path.exists(file_path):
full_file_path = SpecFileService.full_file_path(process_model_info, file_name)
if not os.path.exists(full_file_path):
raise ProcessModelFileNotFoundError(
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()
return spec_file_data
@staticmethod
def file_path(spec: ProcessModelInfo, file_name: str) -> str:
def full_file_path(spec: ProcessModelInfo, file_name: str) -> str:
"""File_path."""
return os.path.join(SpecFileService.workflow_path(spec), file_name)
@staticmethod
def last_modified(spec: ProcessModelInfo, file_name: str) -> datetime:
"""Last_modified."""
path = SpecFileService.file_path(spec, file_name)
return FileSystemService._last_modified(path)
full_file_path = SpecFileService.full_file_path(spec, file_name)
return FileSystemService._last_modified(full_file_path)
@staticmethod
def timestamp(spec: ProcessModelInfo, file_name: str) -> float:
"""Timestamp."""
path = SpecFileService.file_path(spec, file_name)
return FileSystemService._timestamp(path)
full_file_path = SpecFileService.full_file_path(spec, file_name)
return FileSystemService._timestamp(full_file_path)
@staticmethod
def delete_file(spec: ProcessModelInfo, file_name: str) -> None:
@ -215,9 +215,8 @@ class SpecFileService(FileSystemService):
# for lf in lookup_files:
# session.query(LookupDataModel).filter_by(lookup_file_model_id=lf.id).delete()
# session.query(LookupFileModel).filter_by(id=lf.id).delete()
# file_path = SpecFileService.file_path(spec, file_name)
file_path = os.path.join(FileSystemService.root_path(), spec.id, file_name)
os.remove(file_path)
full_file_path = SpecFileService.full_file_path(spec, file_name)
os.remove(full_file_path)
@staticmethod
def delete_all_files(spec: ProcessModelInfo) -> None:
@ -226,10 +225,8 @@ class SpecFileService(FileSystemService):
if os.path.exists(dir_path):
shutil.rmtree(dir_path)
# fixme: Place all the caching stuff in a different service.
@staticmethod
def update_process_cache(ref: SpecReference) -> None:
process_id_lookup = SpecReferenceCache.query.filter_by(identifier=ref.identifier).first()
@ -255,68 +252,73 @@ class SpecFileService(FileSystemService):
db.session.add(process_id_lookup)
db.session.commit()
@staticmethod
def update_message_cache(ref: SpecReference) -> None:
"""Assure we have a record in the database of all possible message ids and names."""
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:
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.commit()
@staticmethod
def update_message_trigger_cache(ref: SpecReference, process_model_info: ProcessModelInfo) -> None:
"""assure we know which messages can trigger the start of a process."""
def update_message_trigger_cache(
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:
message_model = MessageModel.query.filter_by(
identifier=message_model_identifier
).first()
if message_model is None:
raise ValidationException(
f"Could not find message model with identifier '{message_model_identifier}'"
f"Required by a Start Event in : {ref.file_name}"
)
message_triggerable_process_model = (
MessageTriggerableProcessModel.query.filter_by(
message_model_id=message_model.id,
).first()
message_model = MessageModel.query.filter_by(
identifier=message_model_identifier
).first()
if message_model is None:
raise ValidationException(
f"Could not find message model with identifier '{message_model_identifier}'"
f"Required by a Start Event in : {ref.file_name}"
)
message_triggerable_process_model = (
MessageTriggerableProcessModel.query.filter_by(
message_model_id=message_model.id,
).first()
)
if message_triggerable_process_model is None:
message_triggerable_process_model = (
MessageTriggerableProcessModel(
message_model_id=message_model.id,
process_model_identifier=process_model_info.id,
process_group_identifier="process_group_identifier"
)
if message_triggerable_process_model is None:
message_triggerable_process_model = MessageTriggerableProcessModel(
message_model_id=message_model.id,
process_model_identifier=process_model_info.id,
process_group_identifier="process_group_identifier",
)
db.session.add(message_triggerable_process_model)
db.session.commit()
else:
if (
message_triggerable_process_model.process_model_identifier
!= process_model_info.id
# or message_triggerable_process_model.process_group_identifier
# != process_model_info.process_group_id
):
raise ValidationException(
f"Message model is already used to start process model {process_model_info.id}"
)
db.session.add(message_triggerable_process_model)
db.session.commit()
else:
if (
message_triggerable_process_model.process_model_identifier
!= process_model_info.id
# or message_triggerable_process_model.process_group_identifier
# != process_model_info.process_group_id
):
raise ValidationException(
f"Message model is already used to start process model {process_model_info.id}"
)
@staticmethod
def update_correlation_cache(ref: SpecReference) -> None:
"""Update_correlation_cache."""
for correlation_identifier in ref.correlations.keys():
correlation_property_retrieval_expressions = \
ref.correlations[correlation_identifier]['retrieval_expressions']
correlation_property_retrieval_expressions = ref.correlations[
correlation_identifier
]["retrieval_expressions"]
for cpre in correlation_property_retrieval_expressions:
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:
raise ValidationException(
f"Could not find message model with identifier '{message_model_identifier}'"

View File

@ -40,7 +40,7 @@
}</spiffworkflow:messagePayload>
</bpmn:extensionElements>
</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:endEvent id="Event_0q5otqd">
<bpmn:incoming>Flow_11r9uiw</bpmn:incoming>

View File

@ -40,7 +40,7 @@
}</spiffworkflow:messagePayload>
</bpmn:extensionElements>
</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:endEvent id="Event_0q5otqd">
<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.
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,
otherwise, we only load bpmn_file_name
"""
if process_model_source_directory is None:
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
)
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_file_name = filename
ProcessModelService().save_process_model(spec)

View File

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

View File

@ -74,7 +74,11 @@ class TestGetLocaltime(BaseTest):
)
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]

View File

@ -126,7 +126,7 @@ class TestAuthorizationService(BaseTest):
active_task.task_name, processor.bpmn_process_instance
)
ProcessInstanceService.complete_form_task(
processor, spiff_task, {}, initiator_user
processor, spiff_task, {}, initiator_user, active_task
)
active_task = process_instance.active_tasks[0]
@ -137,5 +137,5 @@ class TestAuthorizationService(BaseTest):
{"username": "testuser2", "sub": "open_id"}
)
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.do_engine_steps(save=True)
active_task = process_instance.active_tasks[0]
user_task = processor.get_ready_user_tasks()[0]
form_data = {
@ -57,7 +58,7 @@ class TestDotNotation(BaseTest):
"invoice.dueDate": "09/30/2022",
}
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 = {

View File

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

View File

@ -4,9 +4,8 @@ import os
import pytest
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 SpiffWorkflow.bpmn.parser.ValidationException import ValidationException # type: ignore
from tests.spiffworkflow_backend.helpers.base_test import BaseTest
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.services.process_model_service import ProcessModelService
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):
"""TestSpecFileService."""
@ -86,8 +83,9 @@ class TestSpecFileService(BaseTest):
process_model_source_directory="call_activity_duplicate",
bpmn_file_name="call_activity_nested_duplicate",
)
assert f"Process id ({bpmn_process_identifier}) has already been used" in str(
exception.value
assert (
f"Process id ({bpmn_process_identifier}) has already been used"
in str(exception.value)
)
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
import { Pagination } from '@carbon/react';
@ -13,8 +13,7 @@ type OwnProps = {
perPageOptions?: number[];
pagination: PaginationObject | null;
tableToDisplay: any;
queryParamString?: string;
path: string;
paginationQueryParamPrefix?: string;
};
export default function PaginationForTable({
@ -23,16 +22,21 @@ export default function PaginationForTable({
perPageOptions,
pagination,
tableToDisplay,
queryParamString = '',
path,
paginationQueryParamPrefix,
}: OwnProps) {
const PER_PAGE_OPTIONS = [2, 10, 50, 100];
const navigate = useNavigate();
const [searchParams, setSearchParams] = useSearchParams();
const paginationQueryParamPrefixToUse = paginationQueryParamPrefix
? `${paginationQueryParamPrefix}_`
: '';
const updateRows = (args: any) => {
const newPage = args.page;
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) {

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';
const PER_PAGE_FOR_TASKS_ON_HOME_PAGE = 5;
const paginationQueryParamPrefix = 'tasks_for_my_open_processes';
export default function MyOpenProcesses() {
const [searchParams] = useSearchParams();
@ -21,7 +22,9 @@ export default function MyOpenProcesses() {
useEffect(() => {
const { page, perPage } = getPageInfoFromSearchParams(
searchParams,
PER_PAGE_FOR_TASKS_ON_HOME_PAGE
PER_PAGE_FOR_TASKS_ON_HOME_PAGE,
undefined,
paginationQueryParamPrefix
);
const setTasksFromResult = (result: any) => {
setTasks(result.results);
@ -113,7 +116,9 @@ export default function MyOpenProcesses() {
}
const { page, perPage } = getPageInfoFromSearchParams(
searchParams,
PER_PAGE_FOR_TASKS_ON_HOME_PAGE
PER_PAGE_FOR_TASKS_ON_HOME_PAGE,
undefined,
paginationQueryParamPrefix
);
return (
<>
@ -124,7 +129,7 @@ export default function MyOpenProcesses() {
perPageOptions={[2, PER_PAGE_FOR_TASKS_ON_HOME_PAGE, 25]}
pagination={pagination}
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;
export default function MyTasksForProcessesStartedByOthers() {
export default function TasksWaitingForMe() {
const [searchParams] = useSearchParams();
const [tasks, setTasks] = useState([]);
const [pagination, setPagination] = useState<PaginationObject | null>(null);
@ -21,14 +21,16 @@ export default function MyTasksForProcessesStartedByOthers() {
useEffect(() => {
const { page, perPage } = getPageInfoFromSearchParams(
searchParams,
PER_PAGE_FOR_TASKS_ON_HOME_PAGE
PER_PAGE_FOR_TASKS_ON_HOME_PAGE,
undefined,
'tasks_waiting_for_me'
);
const setTasksFromResult = (result: any) => {
setTasks(result.results);
setPagination(result.pagination);
};
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,
});
}, [searchParams]);
@ -115,7 +117,9 @@ export default function MyTasksForProcessesStartedByOthers() {
}
const { page, perPage } = getPageInfoFromSearchParams(
searchParams,
PER_PAGE_FOR_TASKS_ON_HOME_PAGE
PER_PAGE_FOR_TASKS_ON_HOME_PAGE,
undefined,
'tasks_waiting_for_me'
);
return (
<>
@ -126,7 +130,7 @@ export default function MyTasksForProcessesStartedByOthers() {
perPageOptions={[2, PER_PAGE_FOR_TASKS_ON_HOME_PAGE, 25]}
pagination={pagination}
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 = (
searchParams: any,
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(
searchParams.get('per_page') || defaultPerPage.toString(),
searchParams.get(`${paginationQueryParamPrefixToUse}per_page`) ||
defaultPerPage.toString(),
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 TasksWaitingForMe from '../components/TasksWaitingForMe';
import TasksForWaitingForMyGroups from '../components/TasksWaitingForMyGroups';
export default function GroupedTasks() {
return (
<>
<TasksForMyOpenProcesses />
<br />
<MyTasksForProcessesStartedByOthers />
<TasksWaitingForMe />
<br />
<TasksForWaitingForMyGroups />
</>
);
}

View File

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

View File

@ -94,14 +94,8 @@ export default function MessageInstanceList() {
if (pagination) {
const { page, perPage } = getPageInfoFromSearchParams(searchParams);
let queryParamString = '';
let breadcrumbElement = null;
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 = (
<ProcessBreadcrumb
hotCrumbs={[
@ -132,8 +126,6 @@ export default function MessageInstanceList() {
perPage={perPage}
pagination={pagination}
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]}
pagination={pagination}
tableToDisplay={buildTable()}
path="/tasks"
/>
</>
);

View File

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

View File

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

View File

@ -1,482 +1,15 @@
import { useContext, useEffect, useMemo, useState } from 'react';
import {
Link,
useNavigate,
useParams,
useSearchParams,
} from 'react-router-dom';
import { 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 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 '../components/ProcessModelSearch';
import ProcessBreadcrumb from '../components/ProcessBreadcrumb';
import ProcessInstanceListTable from '../components/ProcessInstanceListTable';
import { getProcessModelFullIdentifierFromSearchParams } from '../helpers';
export default function ProcessInstanceList() {
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() {
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 processModelFullIdentifier =
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 = () => {
return <h1>Process Instances</h1>;
};
if (pagination) {
const { page, perPage } = getPageInfoFromSearchParams(searchParams);
return (
<>
{processInstanceBreadcrumbElement()}
{processInstanceTitleElement()}
{filterComponent()}
<br />
<PaginationForTable
page={page}
perPage={perPage}
pagination={pagination}
tableToDisplay={buildTable()}
queryParamString={getSearchParamsAsQueryString()}
path="/admin/process-instances"
/>
</>
);
}
return null;
return (
<>
{processInstanceBreadcrumbElement()}
{processInstanceTitleElement()}
<ProcessInstanceListTable />
</>
);
}

View File

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

View File

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

View File

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

View File

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