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

This commit is contained in:
Dan 2022-11-30 11:18:44 -05:00
commit e26eeb3262
100 changed files with 5036 additions and 4277 deletions

3
.gitignore vendored
View File

@ -1,2 +1,3 @@
pyrightconfig.json
.idea/
.idea/
t

View File

@ -20,6 +20,12 @@ function get_python_dirs() {
(git ls-tree -r HEAD --name-only | grep -E '\.py$' | awk -F '/' '{print $1}' | sort | uniq | grep -v '\.' | grep -Ev '^(bin|migrations)$') || echo ''
}
function run_fix_docstrings() {
if command -v fix_python_docstrings >/dev/null ; then
fix_python_docstrings $(get_top_level_directories_containing_python_files)
fi
}
function run_autoflake() {
if ! command -v autoflake8 >/dev/null ; then
pip install autoflake8
@ -51,6 +57,7 @@ done
for python_project in "${python_projects[@]}" ; do
pushd "$python_project"
run_fix_docstrings || run_fix_docstrings
run_autoflake || run_autoflake
popd
done

View File

@ -74,7 +74,7 @@ def main():
print(f"ticket_identifier: {ticket_identifier}")
print(f"priority: {priority}")
process_instance = ProcessInstanceService.create_process_instance(
process_instance = ProcessInstanceService.create_process_instance_from_process_model_identifier(
process_model_identifier_ticket,
user,
process_group_identifier="sartography-admin",

View File

@ -68,7 +68,7 @@ def main():
print(f"priority: {priority}")
# if there is no month, who cares about it.
if month:
process_instance = ProcessInstanceService.create_process_instance(
process_instance = ProcessInstanceService.create_process_instance_from_process_model_identifier(
process_model_identifier=process_model_identifier_ticket,
user=user,
process_group_identifier="sartography-admin",

File diff suppressed because it is too large Load Diff

View File

@ -27,7 +27,7 @@ docker run \
-e KEYCLOAK_LOGLEVEL=ALL \
-e ROOT_LOGLEVEL=ALL \
-e KEYCLOAK_ADMIN=admin \
-e KEYCLOAK_ADMIN_PASSWORD=admin quay.io/keycloak/keycloak:19.0.3 start-dev \
-e KEYCLOAK_ADMIN_PASSWORD=admin quay.io/keycloak/keycloak:20.0.1 start-dev \
-Dkeycloak.profile.feature.token_exchange=enabled \
-Dkeycloak.profile.feature.admin_fine_grained_authz=enabled

View File

@ -96,7 +96,7 @@ def setup_process_instances_for_reports(
# )
process_instances = []
for data in [kay(), ray(), jay()]:
process_instance = ProcessInstanceService.create_process_instance(
process_instance = ProcessInstanceService.create_process_instance_from_process_model_identifier(
# process_group_identifier=process_group_id,
process_model_identifier=process_model_identifier,
user=user,

View File

@ -1,4 +1,4 @@
FROM quay.io/keycloak/keycloak:19.0.3 as builder
FROM quay.io/keycloak/keycloak:20.0.1 as builder
ENV KEYCLOAK_LOGLEVEL="ALL"
ENV ROOT_LOGLEVEL="ALL"

View File

@ -1,8 +1,8 @@
"""empty message
Revision ID: 70223f5c7b98
Revision ID: ff1c1628337c
Revises:
Create Date: 2022-11-20 19:54:45.061376
Create Date: 2022-11-28 15:08:52.014254
"""
from alembic import op
@ -10,7 +10,7 @@ import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '70223f5c7b98'
revision = 'ff1c1628337c'
down_revision = None
branch_labels = None
depends_on = None
@ -97,14 +97,12 @@ def upgrade():
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('message_model_id', sa.Integer(), nullable=False),
sa.Column('process_model_identifier', sa.String(length=50), nullable=False),
sa.Column('process_group_identifier', sa.String(length=50), nullable=False),
sa.Column('updated_at_in_seconds', sa.Integer(), nullable=True),
sa.Column('created_at_in_seconds', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['message_model_id'], ['message_model.id'], ),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('message_model_id')
)
op.create_index(op.f('ix_message_triggerable_process_model_process_group_identifier'), 'message_triggerable_process_model', ['process_group_identifier'], unique=False)
op.create_index(op.f('ix_message_triggerable_process_model_process_model_identifier'), 'message_triggerable_process_model', ['process_model_identifier'], unique=False)
op.create_table('principal',
sa.Column('id', sa.Integer(), nullable=False),
@ -120,7 +118,7 @@ def upgrade():
op.create_table('process_instance',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('process_model_identifier', sa.String(length=255), nullable=False),
sa.Column('process_group_identifier', sa.String(length=50), nullable=False),
sa.Column('process_model_display_name', sa.String(length=255), nullable=False),
sa.Column('process_initiator_id', sa.Integer(), nullable=False),
sa.Column('bpmn_json', sa.JSON(), nullable=True),
sa.Column('start_in_seconds', sa.Integer(), nullable=True),
@ -134,7 +132,7 @@ def upgrade():
sa.ForeignKeyConstraint(['process_initiator_id'], ['user.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_process_instance_process_group_identifier'), 'process_instance', ['process_group_identifier'], unique=False)
op.create_index(op.f('ix_process_instance_process_model_display_name'), 'process_instance', ['process_model_display_name'], unique=False)
op.create_index(op.f('ix_process_instance_process_model_identifier'), 'process_instance', ['process_model_identifier'], unique=False)
op.create_table('process_instance_report',
sa.Column('id', sa.Integer(), nullable=False),
@ -168,17 +166,6 @@ 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),
@ -251,6 +238,29 @@ def upgrade():
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('principal_id', 'permission_target_id', 'permission', name='permission_assignment_uniq')
)
op.create_table('process_instance_metadata',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('process_instance_id', sa.Integer(), nullable=False),
sa.Column('key', sa.String(length=255), nullable=False),
sa.Column('value', sa.String(length=255), nullable=False),
sa.Column('updated_at_in_seconds', sa.Integer(), nullable=False),
sa.Column('created_at_in_seconds', sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['process_instance_id'], ['process_instance.id'], ),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('process_instance_id', 'key', name='process_instance_metadata_unique')
)
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.ForeignKeyConstraint(['process_instance_id'], ['process_instance.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('active_task_user',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('active_task_id', sa.Integer(), nullable=False),
@ -284,6 +294,8 @@ def downgrade():
op.drop_index(op.f('ix_active_task_user_user_id'), table_name='active_task_user')
op.drop_index(op.f('ix_active_task_user_active_task_id'), table_name='active_task_user')
op.drop_table('active_task_user')
op.drop_table('spiff_step_details')
op.drop_table('process_instance_metadata')
op.drop_table('permission_assignment')
op.drop_table('message_instance')
op.drop_index(op.f('ix_message_correlation_value'), table_name='message_correlation')
@ -293,18 +305,16 @@ 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')
op.drop_index(op.f('ix_process_instance_report_created_by_id'), table_name='process_instance_report')
op.drop_table('process_instance_report')
op.drop_index(op.f('ix_process_instance_process_model_identifier'), table_name='process_instance')
op.drop_index(op.f('ix_process_instance_process_group_identifier'), table_name='process_instance')
op.drop_index(op.f('ix_process_instance_process_model_display_name'), table_name='process_instance')
op.drop_table('process_instance')
op.drop_table('principal')
op.drop_index(op.f('ix_message_triggerable_process_model_process_model_identifier'), table_name='message_triggerable_process_model')
op.drop_index(op.f('ix_message_triggerable_process_model_process_group_identifier'), table_name='message_triggerable_process_model')
op.drop_table('message_triggerable_process_model')
op.drop_index(op.f('ix_message_correlation_property_identifier'), table_name='message_correlation_property')
op.drop_table('message_correlation_property')

View File

@ -160,7 +160,7 @@ paths:
schema:
type: integer
get:
operationId: spiffworkflow_backend.routes.process_api_blueprint.process_groups_list
operationId: spiffworkflow_backend.routes.process_api_blueprint.process_group_list
summary: get list
tags:
- Process Groups
@ -278,7 +278,13 @@ paths:
required: false
description: Get all sub process models recursively if true
schema:
type: string
type: boolean
- name: filter_runnable_by_user
in: query
required: false
description: Get only the process models that the user can run
schema:
type: boolean
- name: page
in: query
required: false
@ -508,6 +514,24 @@ paths:
description: For filtering - not_started, user_input_required, waiting, complete, error, or suspended
schema:
type: string
- name: initiated_by_me
in: query
required: false
description: For filtering - show instances initiated by me
schema:
type: boolean
- name: with_tasks_completed_by_me
in: query
required: false
description: For filtering - show instances with tasks completed by me
schema:
type: boolean
- name: with_tasks_completed_by_my_group
in: query
required: false
description: For filtering - show instances with tasks completed by my group
schema:
type: boolean
- name: user_filter
in: query
required: false
@ -686,7 +710,7 @@ paths:
schema:
$ref: "#/components/schemas/Workflow"
/process-instances/{process_instance_id}/run:
/process-instances/{modified_process_model_identifier}/{process_instance_id}/run:
parameters:
- name: process_instance_id
in: path
@ -700,7 +724,6 @@ paths:
description: Defaults to true, can be set to false if you are just looking at the workflow not completeing it.
schema:
type: boolean
# process_instance_run
post:
operationId: spiffworkflow_backend.routes.process_api_blueprint.process_instance_run
summary: Run a process instance
@ -1642,10 +1665,6 @@ components:
type: integer
x-nullable: true
example: 12
study_id:
type: integer
x-nullable: true
example: 42
user_id:
type: string
x-nullable: true
@ -1770,8 +1789,6 @@ components:
type: integer
num_tasks_incomplete:
type: integer
study_id:
type: integer
example:
id: 291234
@ -1906,9 +1923,6 @@ components:
workflow_id:
example: 42
type: integer
study_id:
example: 187
type: integer
user_uid:
example: "dhf8r"
type: string

View File

@ -4,26 +4,22 @@ groups:
admin:
users:
[
admin,
jakub,
kb,
alex,
dan,
mike,
jason,
j,
amir,
jarrad,
elizabeth,
jon,
harmeet,
sasha,
manuchehr,
natalia,
]
Finance Team:
users: [finance_user1, jason]
Project Lead:
users:
[
jakub,
@ -31,18 +27,42 @@ groups:
dan,
mike,
jason,
j,
amir,
jarrad,
elizabeth,
jon,
natalia,
sasha,
fin,
fin1,
]
demo:
users:
[
core,
fin,
fin1,
harmeet,
sasha,
manuchehr,
lead,
lead1
]
core-contributor:
users:
[
core,
harmeet,
]
permissions:
admin:
groups: [admin]
users: []
allowed_permissions: [create, read, update, delete, list, instantiate]
allowed_permissions: [create, read, update, delete]
uri: /*
tasks-crud:
@ -51,45 +71,116 @@ permissions:
allowed_permissions: [create, read, update, delete]
uri: /v1.0/tasks/*
process-model-read-all:
groups: [everybody]
users: []
allowed_permissions: [read]
uri: /v1.0/process-models/*
process-group-read-all:
# read all for everybody
read-all-process-groups:
groups: [everybody]
users: []
allowed_permissions: [read]
uri: /v1.0/process-groups/*
process-instance-list:
read-all-process-models:
groups: [everybody]
users: []
allowed_permissions: [read]
uri: /v1.0/process-instances
uri: /v1.0/process-models/*
read-all-process-instance:
groups: [everybody]
users: []
allowed_permissions: [read]
uri: /v1.0/process-instances/*
read-process-instance-reports:
groups: [everybody]
users: []
allowed_permissions: [read]
uri: /v1.0/process-instances/reports/*
processes-read:
groups: [everybody]
users: []
allowed_permissions: [read]
uri: /v1.0/processes
# TODO: all uris should really have the same structure
finance-admin-group:
manage-procurement-admin:
groups: ["Project Lead"]
users: []
allowed_permissions: [create, read, update, delete]
uri: /v1.0/process-groups/manage-procurement:*
manage-procurement-admin-slash:
groups: ["Project Lead"]
users: []
allowed_permissions: [create, read, update, delete]
uri: /v1.0/process-groups/manage-procurement/*
manage-procurement-admin-models:
groups: ["Project Lead"]
users: []
allowed_permissions: [create, read, update, delete]
uri: /v1.0/process-models/manage-procurement:*
manage-procurement-admin-models-slash:
groups: ["Project Lead"]
users: []
allowed_permissions: [create, read, update, delete]
uri: /v1.0/process-models/manage-procurement/*
manage-procurement-admin-instances:
groups: ["Project Lead"]
users: []
allowed_permissions: [create, read, update, delete]
uri: /v1.0/process-instances/manage-procurement:*
manage-procurement-admin-instances-slash:
groups: ["Project Lead"]
users: []
allowed_permissions: [create, read, update, delete]
uri: /v1.0/process-instances/manage-procurement/*
finance-admin:
groups: ["Finance Team"]
users: []
allowed_permissions: [create, read, update, delete]
uri: /v1.0/process-groups/finance/*
uri: /v1.0/process-groups/manage-procurement:procurement:*
finance-admin-model:
groups: ["Finance Team"]
manage-revenue-streams-instantiate:
groups: ["core-contributor", "demo"]
users: []
allowed_permissions: [create, read, update, delete]
uri: /v1.0/process-models/finance/*
allowed_permissions: [create]
uri: /v1.0/process-models/manage-revenue-streams:product-revenue-streams:customer-contracts-trade-terms/*
manage-revenue-streams-instances:
groups: ["core-contributor", "demo"]
users: []
allowed_permissions: [create, read]
uri: /v1.0/process-instances/manage-revenue-streams:product-revenue-streams:customer-contracts-trade-terms/*
read-all:
groups: [admin, "Project Lead"]
manage-procurement-invoice-instantiate:
groups: ["core-contributor", "demo"]
users: []
allowed_permissions: [read]
uri: /*
allowed_permissions: [create]
uri: /v1.0/process-models/manage-procurement:procurement:core-contributor-invoice-management:*
manage-procurement-invoice-instances:
groups: ["core-contributor", "demo"]
users: []
allowed_permissions: [create, read]
uri: /v1.0/process-instances/manage-procurement:procurement:core-contributor-invoice-management:*
invoice-approval-tasks-read:
groups: ["Finance Team"]
manage-procurement-instantiate:
groups: ["core-contributor", "demo"]
users: []
allowed_permissions: [read]
uri: /v1.0/process-instances/category_number_one:lanes/*
allowed_permissions: [create]
uri: /v1.0/process-models/manage-procurement:vendor-lifecycle-management:*
manage-procurement-instances:
groups: ["core-contributor", "demo"]
users: []
allowed_permissions: [create, read]
uri: /v1.0/process-instances/manage-procurement:vendor-lifecycle-management:*
core1-admin-models-instantiate:
groups: ["core-contributor"]
users: []
allowed_permissions: [create]
uri: /v1.0/process-models/misc:category_number_one:process-model-with-form/process-instances
core1-admin-instances:
groups: ["core-contributor"]
users: []
allowed_permissions: [create, read]
uri: /v1.0/process-instances/misc:category_number_one:process-model-with-form:*
core1-admin-instances-slash:
groups: ["core-contributor"]
users: []
allowed_permissions: [create, read]
uri: /v1.0/process-instances/misc:category_number_one:process-model-with-form/*

View File

@ -4,20 +4,19 @@ groups:
admin:
users:
[
admin,
jakub,
kb,
alex,
dan,
mike,
jason,
j,
amir,
jarrad,
elizabeth,
jon,
natalia,
harmeet,
sasha,
manuchehr,
]
Finance Team:
@ -28,60 +27,144 @@ groups:
dan,
mike,
jason,
j,
amir,
jarrad,
elizabeth,
jon,
natalia,
sasha,
fin,
fin1,
]
Project Lead:
demo:
users:
[
jakub,
alex,
dan,
mike,
jason,
jarrad,
elizabeth,
jon,
natalia,
core,
fin,
fin1,
harmeet,
sasha,
manuchehr,
lead,
lead1
]
hr:
users: [manuchehr]
core-contributor:
users:
[
core,
harmeet,
]
permissions:
admin:
groups: [admin]
users: []
allowed_permissions: [create, read, update, delete]
uri: /*
tasks-crud:
groups: [everybody]
users: []
allowed_permissions: [create, read, update, delete]
uri: /v1.0/tasks/*
admin:
groups: [admin]
# read all for everybody
read-all-process-groups:
groups: [everybody]
users: []
allowed_permissions: [create, read, update, delete, list, instantiate]
uri: /*
allowed_permissions: [read]
uri: /v1.0/process-groups/*
read-all-process-models:
groups: [everybody]
users: []
allowed_permissions: [read]
uri: /v1.0/process-models/*
read-all-process-instance:
groups: [everybody]
users: []
allowed_permissions: [read]
uri: /v1.0/process-instances/*
read-process-instance-reports:
groups: [everybody]
users: []
allowed_permissions: [read]
uri: /v1.0/process-instances/reports/*
processes-read:
groups: [everybody]
users: []
allowed_permissions: [read]
uri: /v1.0/processes
# TODO: all uris should really have the same structure
finance-admin-group:
groups: ["Finance Team"]
manage-procurement-admin:
groups: ["Project Lead"]
users: []
allowed_permissions: [create, read, update, delete]
uri: /v1.0/process-groups/finance/*
uri: /v1.0/process-groups/manage-procurement:*
manage-procurement-admin-slash:
groups: ["Project Lead"]
users: []
allowed_permissions: [create, read, update, delete]
uri: /v1.0/process-groups/manage-procurement/*
manage-procurement-admin-models:
groups: ["Project Lead"]
users: []
allowed_permissions: [create, read, update, delete]
uri: /v1.0/process-models/manage-procurement:*
manage-procurement-admin-models-slash:
groups: ["Project Lead"]
users: []
allowed_permissions: [create, read, update, delete]
uri: /v1.0/process-models/manage-procurement/*
manage-procurement-admin-instances:
groups: ["Project Lead"]
users: []
allowed_permissions: [create, read, update, delete]
uri: /v1.0/process-instances/manage-procurement:*
manage-procurement-admin-instances-slash:
groups: ["Project Lead"]
users: []
allowed_permissions: [create, read, update, delete]
uri: /v1.0/process-instances/manage-procurement/*
finance-admin:
groups: ["Finance Team"]
users: []
allowed_permissions: [create, read, update, delete]
uri: /v1.0/process-groups/finance/*
uri: /v1.0/process-groups/manage-procurement:procurement:*
read-all:
groups: ["Finance Team", "Project Lead", hr, admin]
manage-revenue-streams-instantiate:
groups: ["core-contributor", "demo"]
users: []
allowed_permissions: [read]
uri: /*
allowed_permissions: [create]
uri: /v1.0/process-models/manage-revenue-streams:product-revenue-streams:customer-contracts-trade-terms/*
manage-revenue-streams-instances:
groups: ["core-contributor", "demo"]
users: []
allowed_permissions: [create, read]
uri: /v1.0/process-instances/manage-revenue-streams:product-revenue-streams:customer-contracts-trade-terms/*
manage-procurement-invoice-instantiate:
groups: ["core-contributor", "demo"]
users: []
allowed_permissions: [create]
uri: /v1.0/process-models/manage-procurement:procurement:core-contributor-invoice-management:*
manage-procurement-invoice-instances:
groups: ["core-contributor", "demo"]
users: []
allowed_permissions: [create, read]
uri: /v1.0/process-instances/manage-procurement:procurement:core-contributor-invoice-management:*
manage-procurement-instantiate:
groups: ["core-contributor", "demo"]
users: []
allowed_permissions: [create]
uri: /v1.0/process-models/manage-procurement:vendor-lifecycle-management:*
manage-procurement-instances:
groups: ["core-contributor", "demo"]
users: []
allowed_permissions: [create, read]
uri: /v1.0/process-instances/manage-procurement:vendor-lifecycle-management:*

View File

@ -51,5 +51,8 @@ from spiffworkflow_backend.models.spiff_step_details import (
) # noqa: F401
from spiffworkflow_backend.models.user import UserModel # noqa: F401
from spiffworkflow_backend.models.group import GroupModel # noqa: F401
from spiffworkflow_backend.models.process_instance_metadata import (
ProcessInstanceMetadataModel,
) # noqa: F401
add_listeners()

View File

@ -16,8 +16,6 @@ class MessageTriggerableProcessModel(SpiffworkflowBaseDBModel):
ForeignKey(MessageModel.id), nullable=False, unique=True
)
process_model_identifier: str = db.Column(db.String(50), nullable=False, index=True)
# fixme: Maybe we don't need this anymore?
process_group_identifier: str = db.Column(db.String(50), nullable=False, index=True)
updated_at_in_seconds: int = db.Column(db.Integer)
created_at_in_seconds: int = db.Column(db.Integer)

View File

@ -29,6 +29,7 @@ class ProcessGroup:
default_factory=list[ProcessModelInfo]
)
process_groups: list[ProcessGroup] = field(default_factory=list["ProcessGroup"])
parent_groups: list[dict] | None = None
def __post_init__(self) -> None:
"""__post_init__."""

View File

@ -1,7 +1,6 @@
"""Process_instance."""
from __future__ import annotations
from dataclasses import dataclass
from typing import Any
from typing import cast
@ -18,12 +17,15 @@ from sqlalchemy.orm import relationship
from sqlalchemy.orm import validates
from spiffworkflow_backend.helpers.spiff_enum import SpiffEnum
from spiffworkflow_backend.models.process_model import ProcessModelInfo
from spiffworkflow_backend.models.task import Task
from spiffworkflow_backend.models.task import TaskSchema
from spiffworkflow_backend.models.user import UserModel
class ProcessInstanceNotFoundError(Exception):
"""ProcessInstanceNotFoundError."""
class NavigationItemSchema(Schema):
"""NavigationItemSchema."""
@ -74,7 +76,9 @@ class ProcessInstanceModel(SpiffworkflowBaseDBModel):
process_model_identifier: str = db.Column(
db.String(255), nullable=False, index=True
)
process_group_identifier: str = db.Column(db.String(50), nullable=False, index=True)
process_model_display_name: str = db.Column(
db.String(255), nullable=False, index=True
)
process_initiator_id: int = db.Column(ForeignKey(UserModel.id), nullable=False)
process_initiator = relationship("UserModel")
@ -103,7 +107,7 @@ class ProcessInstanceModel(SpiffworkflowBaseDBModel):
return {
"id": self.id,
"process_model_identifier": self.process_model_identifier,
"process_group_identifier": self.process_group_identifier,
"process_model_display_name": self.process_model_display_name,
"status": self.status,
"start_in_seconds": self.start_in_seconds,
"end_in_seconds": self.end_in_seconds,
@ -112,6 +116,7 @@ class ProcessInstanceModel(SpiffworkflowBaseDBModel):
"bpmn_version_control_identifier": self.bpmn_version_control_identifier,
"bpmn_version_control_type": self.bpmn_version_control_type,
"spiff_step": self.spiff_step,
"username": self.process_initiator.username,
}
@property
@ -140,7 +145,7 @@ class ProcessInstanceModelSchema(Schema):
fields = [
"id",
"process_model_identifier",
"process_group_identifier",
"process_model_display_name",
"process_initiator_id",
"start_in_seconds",
"end_in_seconds",
@ -166,23 +171,18 @@ class ProcessInstanceApi:
status: ProcessInstanceStatus,
next_task: Task | None,
process_model_identifier: str,
process_group_identifier: str,
process_model_display_name: str,
completed_tasks: int,
updated_at_in_seconds: int,
is_review: bool,
title: str,
) -> None:
"""__init__."""
self.id = id
self.status = status
self.next_task = next_task # The next task that requires user input.
# self.navigation = navigation fixme: would be a hotness.
self.process_model_identifier = process_model_identifier
self.process_group_identifier = process_group_identifier
self.process_model_display_name = process_model_display_name
self.completed_tasks = completed_tasks
self.updated_at_in_seconds = updated_at_in_seconds
self.title = title
self.is_review = is_review
class ProcessInstanceApiSchema(Schema):
@ -196,24 +196,15 @@ class ProcessInstanceApiSchema(Schema):
"id",
"status",
"next_task",
"navigation",
"process_model_identifier",
"process_group_identifier",
"process_model_display_name",
"completed_tasks",
"updated_at_in_seconds",
"is_review",
"title",
"study_id",
"state",
]
unknown = INCLUDE
status = EnumField(ProcessInstanceStatus)
next_task = marshmallow.fields.Nested(TaskSchema, dump_only=True, required=False)
navigation = marshmallow.fields.List(
marshmallow.fields.Nested(NavigationItemSchema, dump_only=True)
)
state = marshmallow.fields.String(allow_none=True)
@marshmallow.post_load
def make_process_instance(
@ -224,73 +215,11 @@ class ProcessInstanceApiSchema(Schema):
"id",
"status",
"next_task",
"navigation",
"process_model_identifier",
"process_group_identifier",
"process_model_display_name",
"completed_tasks",
"updated_at_in_seconds",
"is_review",
"title",
"study_id",
"state",
]
filtered_fields = {key: data[key] for key in keys}
filtered_fields["next_task"] = TaskSchema().make_task(data["next_task"])
return ProcessInstanceApi(**filtered_fields)
@dataclass
class ProcessInstanceMetadata:
"""ProcessInstanceMetadata."""
id: int
display_name: str | None = None
description: str | None = None
spec_version: str | None = None
state: str | None = None
status: str | None = None
completed_tasks: int | None = None
is_review: bool | None = None
state_message: str | None = None
process_model_identifier: str | None = None
process_group_id: str | None = None
@classmethod
def from_process_instance(
cls, process_instance: ProcessInstanceModel, process_model: ProcessModelInfo
) -> ProcessInstanceMetadata:
"""From_process_instance."""
instance = cls(
id=process_instance.id,
display_name=process_model.display_name,
description=process_model.description,
process_group_id=process_model.process_group,
state_message=process_instance.state_message,
status=process_instance.status,
completed_tasks=process_instance.completed_tasks,
is_review=process_model.is_review,
process_model_identifier=process_instance.process_model_identifier,
)
return instance
class ProcessInstanceMetadataSchema(Schema):
"""ProcessInstanceMetadataSchema."""
status = EnumField(ProcessInstanceStatus)
class Meta:
"""Meta."""
model = ProcessInstanceMetadata
additional = [
"id",
"display_name",
"description",
"state",
"completed_tasks",
"process_group_id",
"is_review",
"state_message",
]
unknown = INCLUDE

View File

@ -0,0 +1,30 @@
"""Spiff_step_details."""
from dataclasses import dataclass
from flask_bpmn.models.db import db
from flask_bpmn.models.db import SpiffworkflowBaseDBModel
from sqlalchemy import ForeignKey
from spiffworkflow_backend.models.process_instance import ProcessInstanceModel
@dataclass
class ProcessInstanceMetadataModel(SpiffworkflowBaseDBModel):
"""ProcessInstanceMetadataModel."""
__tablename__ = "process_instance_metadata"
__table_args__ = (
db.UniqueConstraint(
"process_instance_id", "key", name="process_instance_metadata_unique"
),
)
id: int = db.Column(db.Integer, primary_key=True)
process_instance_id: int = db.Column(
ForeignKey(ProcessInstanceModel.id), nullable=False # type: ignore
)
key: str = db.Column(db.String(255), nullable=False)
value: str = db.Column(db.String(255), nullable=False)
updated_at_in_seconds: int = db.Column(db.Integer, nullable=False)
created_at_in_seconds: int = db.Column(db.Integer, nullable=False)

View File

@ -75,7 +75,7 @@ class ProcessInstanceReportModel(SpiffworkflowBaseDBModel):
def add_fixtures(cls) -> None:
"""Add_fixtures."""
try:
# process_model = ProcessModelService().get_process_model(
# process_model = ProcessModelService.get_process_model(
# process_model_id="sartography-admin/ticket"
# )
user = UserModel.query.first()
@ -205,7 +205,7 @@ class ProcessInstanceReportModel(SpiffworkflowBaseDBModel):
) -> ProcessInstanceReportModel:
"""Create_with_attributes."""
# <<<<<<< HEAD
# process_model = ProcessModelService().get_process_model(
# process_model = ProcessModelService.get_process_model(
# process_model_id=f"{process_model_identifier}"
# )
# process_instance_report = cls(

View File

@ -34,10 +34,10 @@ class ProcessModelInfo:
primary_file_name: str | None = None
primary_process_id: str | None = None
display_order: int | None = 0
is_review: bool = False
files: list[File] | None = field(default_factory=list[File])
fault_or_suspend_on_exception: str = NotificationType.fault.value
exception_notification_addresses: list[str] = field(default_factory=list)
parent_groups: list[dict] | None = None
def __post_init__(self) -> None:
"""__post_init__."""
@ -71,7 +71,6 @@ class ProcessModelInfoSchema(Schema):
display_order = marshmallow.fields.Integer(allow_none=True)
primary_file_name = marshmallow.fields.String(allow_none=True)
primary_process_id = marshmallow.fields.String(allow_none=True)
is_review = marshmallow.fields.Boolean(allow_none=True)
files = marshmallow.fields.List(marshmallow.fields.Nested("FileSchema"))
fault_or_suspend_on_exception = marshmallow.fields.String()
exception_notification_addresses = marshmallow.fields.List(

View File

@ -8,6 +8,7 @@ from sqlalchemy import ForeignKey
from sqlalchemy.orm import deferred
from spiffworkflow_backend.models.group import GroupModel
from spiffworkflow_backend.models.process_instance import ProcessInstanceModel
@dataclass
@ -16,7 +17,9 @@ class SpiffStepDetailsModel(SpiffworkflowBaseDBModel):
__tablename__ = "spiff_step_details"
id: int = db.Column(db.Integer, primary_key=True)
process_instance_id: int = db.Column(db.Integer, nullable=False)
process_instance_id: int = db.Column(
ForeignKey(ProcessInstanceModel.id), nullable=False # type: ignore
)
spiff_step: int = db.Column(db.Integer, nullable=False)
task_json: str = deferred(db.Column(db.JSON, nullable=False)) # type: ignore
timestamp: float = db.Column(db.DECIMAL(17, 6), nullable=False)

View File

@ -27,28 +27,28 @@ ALLOWED_BPMN_EXTENSIONS = {"bpmn", "dmn"}
@admin_blueprint.route("/process-groups", methods=["GET"])
def process_groups_list() -> str:
"""Process_groups_list."""
process_groups = ProcessModelService().get_process_groups()
return render_template("process_groups_list.html", process_groups=process_groups)
def process_group_list() -> str:
"""Process_group_list."""
process_groups = ProcessModelService.get_process_groups()
return render_template("process_group_list.html", process_groups=process_groups)
@admin_blueprint.route("/process-groups/<process_group_id>", methods=["GET"])
def process_group_show(process_group_id: str) -> str:
"""Show_process_group."""
process_group = ProcessModelService().get_process_group(process_group_id)
process_group = ProcessModelService.get_process_group(process_group_id)
return render_template("process_group_show.html", process_group=process_group)
@admin_blueprint.route("/process-models/<process_model_id>", methods=["GET"])
def process_model_show(process_model_id: str) -> Union[str, Response]:
"""Show_process_model."""
process_model = ProcessModelService().get_process_model(process_model_id)
process_model = ProcessModelService.get_process_model(process_model_id)
files = SpecFileService.get_files(process_model, extension_filter="bpmn")
current_file_name = process_model.primary_file_name
if current_file_name is None:
flash("No primary_file_name", "error")
return redirect(url_for("admin.process_groups_list"))
return redirect(url_for("admin.process_group_list"))
bpmn_xml = SpecFileService.get_data(process_model, current_file_name)
return render_template(
"process_model_show.html",
@ -64,7 +64,7 @@ def process_model_show(process_model_id: str) -> Union[str, Response]:
)
def process_model_show_file(process_model_id: str, file_name: str) -> str:
"""Process_model_show_file."""
process_model = ProcessModelService().get_process_model(process_model_id)
process_model = ProcessModelService.get_process_model(process_model_id)
bpmn_xml = SpecFileService.get_data(process_model, file_name)
files = SpecFileService.get_files(process_model, extension_filter="bpmn")
return render_template(
@ -81,8 +81,7 @@ def process_model_show_file(process_model_id: str, file_name: str) -> str:
)
def process_model_upload_file(process_model_id: str) -> Response:
"""Process_model_upload_file."""
process_model_service = ProcessModelService()
process_model = process_model_service.get_process_model(process_model_id)
process_model = ProcessModelService.get_process_model(process_model_id)
if "file" not in request.files:
flash("No file part", "error")
@ -97,7 +96,7 @@ def process_model_upload_file(process_model_id: str) -> Response:
SpecFileService.add_file(
process_model, request_file.filename, request_file.stream.read()
)
process_model_service.save_process_model(process_model)
ProcessModelService.save_process_model(process_model)
return redirect(
url_for("admin.process_model_show", process_model_id=process_model.id)
@ -109,7 +108,7 @@ def process_model_upload_file(process_model_id: str) -> Response:
)
def process_model_edit(process_model_id: str, file_name: str) -> str:
"""Edit_bpmn."""
process_model = ProcessModelService().get_process_model(process_model_id)
process_model = ProcessModelService.get_process_model(process_model_id)
bpmn_xml = SpecFileService.get_data(process_model, file_name)
return render_template(
@ -125,11 +124,11 @@ def process_model_edit(process_model_id: str, file_name: str) -> str:
)
def process_model_save(process_model_id: str, file_name: str) -> Union[str, Response]:
"""Process_model_save."""
process_model = ProcessModelService().get_process_model(process_model_id)
process_model = ProcessModelService.get_process_model(process_model_id)
SpecFileService.update_file(process_model, file_name, request.get_data())
if process_model.primary_file_name is None:
flash("No primary_file_name", "error")
return redirect(url_for("admin.process_groups_list"))
return redirect(url_for("admin.process_group_list"))
bpmn_xml = SpecFileService.get_data(process_model, process_model.primary_file_name)
return render_template(
"process_model_edit.html",
@ -143,19 +142,21 @@ def process_model_save(process_model_id: str, file_name: str) -> Union[str, Resp
def process_model_run(process_model_id: str) -> Union[str, Response]:
"""Process_model_run."""
user = UserService.create_user("internal", "Mr. Test", username="Mr. Test")
process_instance = ProcessInstanceService.create_process_instance(
process_model_id, user
process_instance = (
ProcessInstanceService.create_process_instance_from_process_model_identifier(
process_model_id, user
)
)
processor = ProcessInstanceProcessor(process_instance)
processor.do_engine_steps()
result = processor.get_data()
process_model = ProcessModelService().get_process_model(process_model_id)
process_model = ProcessModelService.get_process_model(process_model_id)
files = SpecFileService.get_files(process_model, extension_filter="bpmn")
current_file_name = process_model.primary_file_name
if current_file_name is None:
flash("No primary_file_name", "error")
return redirect(url_for("admin.process_groups_list"))
return redirect(url_for("admin.process_group_list"))
bpmn_xml = SpecFileService.get_data(process_model, current_file_name)
return render_template(

View File

@ -3,7 +3,7 @@
{% block content %}
<button
type="button"
onclick="window.location.href='{{ url_for( 'admin.process_groups_list') }}';"
onclick="window.location.href='{{ url_for( 'admin.process_group_list') }}';"
>
Back
</button>

View File

@ -30,6 +30,7 @@ from SpiffWorkflow.task import TaskState
from sqlalchemy import and_
from sqlalchemy import asc
from sqlalchemy import desc
from sqlalchemy.orm import joinedload
from spiffworkflow_backend.exceptions.process_entity_not_found_error import (
ProcessEntityNotFoundError,
@ -63,6 +64,7 @@ from spiffworkflow_backend.models.spec_reference import SpecReferenceSchema
from spiffworkflow_backend.models.spiff_logging import SpiffLoggingModel
from spiffworkflow_backend.models.spiff_step_details import SpiffStepDetailsModel
from spiffworkflow_backend.models.user import UserModel
from spiffworkflow_backend.models.user_group_assignment import UserGroupAssignmentModel
from spiffworkflow_backend.routes.user import verify_token
from spiffworkflow_backend.services.authorization_service import AuthorizationService
from spiffworkflow_backend.services.error_handling_service import ErrorHandlingService
@ -157,9 +159,8 @@ def un_modify_modified_process_model_id(modified_process_model_id: str) -> str:
def process_group_add(body: dict) -> flask.wrappers.Response:
"""Add_process_group."""
process_model_service = ProcessModelService()
process_group = ProcessGroup(**body)
process_model_service.add_process_group(process_group)
ProcessModelService.add_process_group(process_group)
return make_response(jsonify(process_group), 201)
@ -183,20 +184,20 @@ def process_group_update(
process_group_id = un_modify_modified_process_model_id(modified_process_group_id)
process_group = ProcessGroup(id=process_group_id, **body_filtered)
ProcessModelService().update_process_group(process_group)
ProcessModelService.update_process_group(process_group)
return make_response(jsonify(process_group), 200)
def process_groups_list(
def process_group_list(
process_group_identifier: Optional[str] = None, page: int = 1, per_page: int = 100
) -> flask.wrappers.Response:
"""Process_groups_list."""
"""Process_group_list."""
if process_group_identifier is not None:
process_groups = ProcessModelService().get_process_groups(
process_groups = ProcessModelService.get_process_groups(
process_group_identifier
)
else:
process_groups = ProcessModelService().get_process_groups()
process_groups = ProcessModelService.get_process_groups()
batch = ProcessModelService().get_batch(
items=process_groups, page=page, per_page=per_page
)
@ -222,7 +223,7 @@ def process_group_show(
"""Process_group_show."""
process_group_id = un_modify_modified_process_model_id(modified_process_group_id)
try:
process_group = ProcessModelService().get_process_group(process_group_id)
process_group = ProcessModelService.get_process_group(process_group_id)
except ProcessEntityNotFoundError as exception:
raise (
ApiError(
@ -231,13 +232,17 @@ def process_group_show(
status_code=400,
)
) from exception
process_group.parent_groups = ProcessModelService.get_parent_group_array(
process_group.id
)
return make_response(jsonify(process_group), 200)
def process_group_move(
modified_process_group_identifier: str, new_location: str
) -> flask.wrappers.Response:
"""process_group_move."""
"""Process_group_move."""
original_process_group_id = un_modify_modified_process_model_id(
modified_process_group_identifier
)
@ -268,8 +273,7 @@ def process_model_create(
unmodified_process_group_id = un_modify_modified_process_model_id(
modified_process_group_id
)
process_model_service = ProcessModelService()
process_group = process_model_service.get_process_group(unmodified_process_group_id)
process_group = ProcessModelService.get_process_group(unmodified_process_group_id)
if process_group is None:
raise ApiError(
error_code="process_model_could_not_be_created",
@ -277,7 +281,7 @@ def process_model_create(
status_code=400,
)
process_model_service.add_process_model(process_model_info)
ProcessModelService.add_process_model(process_model_info)
return Response(
json.dumps(ProcessModelInfoSchema().dump(process_model_info)),
status=201,
@ -314,7 +318,7 @@ def process_model_update(
# process_model_identifier = f"{process_group_id}/{process_model_id}"
process_model = get_process_model(process_model_identifier)
ProcessModelService().update_process_model(process_model, body_filtered)
ProcessModelService.update_process_model(process_model, body_filtered)
return ProcessModelInfoSchema().dump(process_model)
@ -329,14 +333,17 @@ def process_model_show(modified_process_model_identifier: str) -> Any:
process_model.files = files
for file in process_model.files:
file.references = SpecFileService.get_references_for_file(file, process_model)
process_model_json = ProcessModelInfoSchema().dump(process_model)
return process_model_json
process_model.parent_groups = ProcessModelService.get_parent_group_array(
process_model.id
)
return make_response(jsonify(process_model), 200)
def process_model_move(
modified_process_model_identifier: str, new_location: str
) -> flask.wrappers.Response:
"""process_model_move."""
"""Process_model_move."""
original_process_model_id = un_modify_modified_process_model_id(
modified_process_model_identifier
)
@ -349,12 +356,15 @@ def process_model_move(
def process_model_list(
process_group_identifier: Optional[str] = None,
recursive: Optional[bool] = False,
filter_runnable_by_user: Optional[bool] = False,
page: int = 1,
per_page: int = 100,
) -> flask.wrappers.Response:
"""Process model list!"""
process_models = ProcessModelService().get_process_models(
process_group_id=process_group_identifier, recursive=recursive
process_models = ProcessModelService.get_process_models(
process_group_id=process_group_identifier,
recursive=recursive,
filter_runnable_by_user=filter_runnable_by_user,
)
batch = ProcessModelService().get_batch(
process_models, page=page, per_page=per_page
@ -483,8 +493,10 @@ def process_instance_create(modified_process_model_id: str) -> flask.wrappers.Re
process_model_identifier = un_modify_modified_process_model_id(
modified_process_model_id
)
process_instance = ProcessInstanceService.create_process_instance(
process_model_identifier, g.user
process_instance = (
ProcessInstanceService.create_process_instance_from_process_model_identifier(
process_model_identifier, g.user
)
)
return Response(
json.dumps(ProcessInstanceModelSchema().dump(process_instance)),
@ -494,6 +506,7 @@ def process_instance_create(modified_process_model_id: str) -> flask.wrappers.Re
def process_instance_run(
modified_process_model_identifier: str,
process_instance_id: int,
do_engine_steps: bool = True,
) -> flask.wrappers.Response:
@ -758,6 +771,9 @@ def process_instance_list(
end_from: Optional[int] = None,
end_to: Optional[int] = None,
process_status: Optional[str] = None,
initiated_by_me: Optional[bool] = None,
with_tasks_completed_by_me: Optional[bool] = None,
with_tasks_completed_by_my_group: Optional[bool] = None,
user_filter: Optional[bool] = False,
report_identifier: Optional[str] = None,
) -> flask.wrappers.Response:
@ -774,6 +790,9 @@ def process_instance_list(
end_from,
end_to,
process_status.split(",") if process_status else None,
initiated_by_me,
with_tasks_completed_by_me,
with_tasks_completed_by_my_group,
)
else:
report_filter = (
@ -785,11 +804,19 @@ def process_instance_list(
end_from,
end_to,
process_status,
initiated_by_me,
with_tasks_completed_by_me,
with_tasks_completed_by_my_group,
)
)
# process_model_identifier = un_modify_modified_process_model_id(modified_process_model_identifier)
process_instance_query = ProcessInstanceModel.query
# Always join that hot user table for good performance at serialization time.
process_instance_query = process_instance_query.options(
joinedload(ProcessInstanceModel.process_initiator)
)
if report_filter.process_model_identifier is not None:
process_model = get_process_model(
f"{report_filter.process_model_identifier}",
@ -833,9 +860,81 @@ def process_instance_list(
ProcessInstanceModel.status.in_(report_filter.process_status) # type: ignore
)
process_instances = process_instance_query.order_by(
ProcessInstanceModel.start_in_seconds.desc(), ProcessInstanceModel.id.desc() # type: ignore
).paginate(page=page, per_page=per_page, error_out=False)
if report_filter.initiated_by_me is True:
process_instance_query = process_instance_query.filter(
ProcessInstanceModel.status.in_(["complete", "error", "terminated"]) # type: ignore
)
process_instance_query = process_instance_query.filter_by(
process_initiator=g.user
)
# TODO: not sure if this is exactly what is wanted
if report_filter.with_tasks_completed_by_me is True:
process_instance_query = process_instance_query.filter(
ProcessInstanceModel.status.in_(["complete", "error", "terminated"]) # type: ignore
)
# process_instance_query = process_instance_query.join(UserModel, UserModel.id == ProcessInstanceModel.process_initiator_id)
# process_instance_query = process_instance_query.add_columns(UserModel.username)
# search for process_instance.UserModel.username in this file for more details about why adding columns is annoying.
process_instance_query = process_instance_query.filter(
ProcessInstanceModel.process_initiator_id != g.user.id
)
process_instance_query = process_instance_query.join(
SpiffStepDetailsModel,
ProcessInstanceModel.id == SpiffStepDetailsModel.process_instance_id,
)
process_instance_query = process_instance_query.join(
SpiffLoggingModel,
ProcessInstanceModel.id == SpiffLoggingModel.process_instance_id,
)
process_instance_query = process_instance_query.filter(
SpiffLoggingModel.message.contains("COMPLETED") # type: ignore
)
process_instance_query = process_instance_query.filter(
SpiffLoggingModel.spiff_step == SpiffStepDetailsModel.spiff_step
)
process_instance_query = process_instance_query.filter(
SpiffStepDetailsModel.completed_by_user_id == g.user.id
)
if report_filter.with_tasks_completed_by_my_group is True:
process_instance_query = process_instance_query.filter(
ProcessInstanceModel.status.in_(["complete", "error", "terminated"]) # type: ignore
)
process_instance_query = process_instance_query.join(
SpiffStepDetailsModel,
ProcessInstanceModel.id == SpiffStepDetailsModel.process_instance_id,
)
process_instance_query = process_instance_query.join(
SpiffLoggingModel,
ProcessInstanceModel.id == SpiffLoggingModel.process_instance_id,
)
process_instance_query = process_instance_query.filter(
SpiffLoggingModel.message.contains("COMPLETED") # type: ignore
)
process_instance_query = process_instance_query.filter(
SpiffLoggingModel.spiff_step == SpiffStepDetailsModel.spiff_step
)
process_instance_query = process_instance_query.join(
GroupModel,
GroupModel.id == SpiffStepDetailsModel.lane_assignment_id,
)
process_instance_query = process_instance_query.join(
UserGroupAssignmentModel,
UserGroupAssignmentModel.group_id == GroupModel.id,
)
process_instance_query = process_instance_query.filter(
UserGroupAssignmentModel.user_id == g.user.id
)
process_instances = (
process_instance_query.group_by(ProcessInstanceModel.id)
.order_by(
ProcessInstanceModel.start_in_seconds.desc(), ProcessInstanceModel.id.desc() # type: ignore
)
.paginate(page=page, per_page=per_page, error_out=False)
)
results = list(
map(
@ -1001,7 +1100,7 @@ def authentication_callback(
f"{service}/{auth_method}", response, g.user.id, create_if_not_exists=True
)
return redirect(
f"{current_app.config['SPIFFWORKFLOW_FRONTEND_URL']}/admin/authentications"
f"{current_app.config['SPIFFWORKFLOW_FRONTEND_URL']}/admin/configuration"
)
@ -1056,7 +1155,7 @@ def task_list_my_tasks(page: int = 1, per_page: int = 100) -> flask.wrappers.Res
# just need this add_columns to add the process_model_identifier. Then add everything back that was removed.
.add_columns(
ProcessInstanceModel.process_model_identifier,
ProcessInstanceModel.process_group_identifier,
ProcessInstanceModel.process_model_display_name,
ProcessInstanceModel.status,
ActiveTaskModel.task_name,
ActiveTaskModel.task_title,
@ -1197,8 +1296,10 @@ def process_instance_task_list(
)
.first()
)
if step_detail is not None:
process_instance.bpmn_json = json.dumps(step_detail.task_json)
if step_detail is not None and process_instance.bpmn_json is not None:
bpmn_json = json.loads(process_instance.bpmn_json)
bpmn_json["tasks"] = step_detail.task_json
process_instance.bpmn_json = json.dumps(bpmn_json)
processor = ProcessInstanceProcessor(process_instance)
@ -1318,7 +1419,7 @@ def task_submit(
task_id, process_instance, processor=processor
)
AuthorizationService.assert_user_can_complete_spiff_task(
processor, spiff_task, principal.user
process_instance.id, spiff_task, principal.user
)
if spiff_task.state != TaskState.READY:
@ -1503,7 +1604,7 @@ def get_process_model(process_model_id: str) -> ProcessModelInfo:
"""Get_process_model."""
process_model = None
try:
process_model = ProcessModelService().get_process_model(process_model_id)
process_model = ProcessModelService.get_process_model(process_model_id)
except ProcessEntityNotFoundError as exception:
raise (
ApiError(

View File

@ -0,0 +1,42 @@
"""Get_env."""
from typing import Any
from flask_bpmn.models.db import db
from spiffworkflow_backend.models.process_instance_metadata import (
ProcessInstanceMetadataModel,
)
from spiffworkflow_backend.models.script_attributes_context import (
ScriptAttributesContext,
)
from spiffworkflow_backend.scripts.script import Script
class SaveProcessInstanceMetadata(Script):
"""SaveProcessInstanceMetadata."""
def get_description(self) -> str:
"""Get_description."""
return """Save a given dict as process instance metadata (useful for creating reports)."""
def run(
self,
script_attributes_context: ScriptAttributesContext,
*args: Any,
**kwargs: Any,
) -> Any:
"""Run."""
metadata_dict = args[0]
for key, value in metadata_dict.items():
pim = ProcessInstanceMetadataModel.query.filter_by(
process_instance_id=script_attributes_context.process_instance_id,
key=key,
).first()
if pim is None:
pim = ProcessInstanceMetadataModel(
process_instance_id=script_attributes_context.process_instance_id,
key=key,
)
pim.value = value
db.session.add(pim)
db.session.commit()

View File

@ -30,7 +30,7 @@ def load_acceptance_test_fixtures() -> list[ProcessInstanceModel]:
process_instances = []
for i in range(len(statuses)):
process_instance = ProcessInstanceService.create_process_instance(
process_instance = ProcessInstanceService.create_process_instance_from_process_model_identifier(
test_process_model_id, user
)
process_instance.status = statuses[i]

View File

@ -24,9 +24,6 @@ from spiffworkflow_backend.models.user import UserModel
from spiffworkflow_backend.models.user import UserNotFoundError
from spiffworkflow_backend.models.user_group_assignment import UserGroupAssignmentModel
from spiffworkflow_backend.services.group_service import GroupService
from spiffworkflow_backend.services.process_instance_processor import (
ProcessInstanceProcessor,
)
from spiffworkflow_backend.services.user_service import UserService
@ -393,25 +390,25 @@ class AuthorizationService:
@staticmethod
def assert_user_can_complete_spiff_task(
processor: ProcessInstanceProcessor,
process_instance_id: int,
spiff_task: SpiffTask,
user: UserModel,
) -> bool:
"""Assert_user_can_complete_spiff_task."""
active_task = ActiveTaskModel.query.filter_by(
task_name=spiff_task.task_spec.name,
process_instance_id=processor.process_instance_model.id,
process_instance_id=process_instance_id,
).first()
if active_task is None:
raise ActiveTaskNotFoundError(
f"Could find an active task with task name '{spiff_task.task_spec.name}'"
f" for process instance '{processor.process_instance_model.id}'"
f" for process instance '{process_instance_id}'"
)
if user not in active_task.potential_owners:
raise UserDoesNotHaveAccessToTaskError(
f"User {user.username} does not have access to update task'{spiff_task.task_spec.name}'"
f" for process instance '{processor.process_instance_model.id}'"
f" for process instance '{process_instance_id}'"
)
return True

View File

@ -26,7 +26,7 @@ class DataSetupService:
current_app.logger.debug("DataSetupService.save_all_process_models() start")
failing_process_models = []
process_models = ProcessModelService().get_process_models()
process_models = ProcessModelService.get_process_models(recursive=True)
SpecFileService.clear_caches()
for process_model in process_models:
current_app.logger.debug(f"Process Model: {process_model.display_name}")

View File

@ -34,7 +34,7 @@ class ErrorHandlingService:
self, _processor: ProcessInstanceProcessor, _error: Union[ApiError, Exception]
) -> None:
"""On unhandled exceptions, set instance.status based on model.fault_or_suspend_on_exception."""
process_model = ProcessModelService().get_process_model(
process_model = ProcessModelService.get_process_model(
_processor.process_model_identifier
)
if process_model.fault_or_suspend_on_exception == "suspend":

View File

@ -117,7 +117,7 @@ class MessageService:
user: UserModel,
) -> ProcessInstanceModel:
"""Process_message_triggerable_process_model."""
process_instance_receive = ProcessInstanceService.create_process_instance(
process_instance_receive = ProcessInstanceService.create_process_instance_from_process_model_identifier(
message_triggerable_process_model.process_model_identifier,
user,
)

View File

@ -349,7 +349,9 @@ class ProcessInstanceProcessor:
check_sub_specs(test_spec, 5)
self.process_model_identifier = process_instance_model.process_model_identifier
# self.process_group_identifier = process_instance_model.process_group_identifier
self.process_model_display_name = (
process_instance_model.process_model_display_name
)
try:
self.bpmn_process_instance = self.__get_bpmn_process_instance(
@ -374,7 +376,7 @@ class ProcessInstanceProcessor:
cls, process_model_identifier: str
) -> Tuple[BpmnProcessSpec, IdToBpmnProcessSpecMapping]:
"""Get_process_model_and_subprocesses."""
process_model_info = ProcessModelService().get_process_model(
process_model_info = ProcessModelService.get_process_model(
process_model_identifier
)
if process_model_info is None:
@ -540,13 +542,8 @@ class ProcessInstanceProcessor:
"""SaveSpiffStepDetails."""
bpmn_json = self.serialize()
wf_json = json.loads(bpmn_json)
task_json = "{}"
if "tasks" in wf_json:
task_json = json.dumps(wf_json["tasks"])
task_json = wf_json["tasks"]
# TODO want to just save the tasks, something wasn't immediately working
# so after the flow works with the full wf_json revisit this
task_json = wf_json
return {
"process_instance_id": self.process_instance_model.id,
"spiff_step": self.process_instance_model.spiff_step or 1,
@ -593,16 +590,12 @@ class ProcessInstanceProcessor:
if self.bpmn_process_instance.is_completed():
self.process_instance_model.end_in_seconds = round(time.time())
active_tasks = ActiveTaskModel.query.filter_by(
process_instance_id=self.process_instance_model.id
).all()
if len(active_tasks) > 0:
for at in active_tasks:
db.session.delete(at)
db.session.add(self.process_instance_model)
db.session.commit()
active_tasks = ActiveTaskModel.query.filter_by(
process_instance_id=self.process_instance_model.id
).all()
ready_or_waiting_tasks = self.get_all_ready_or_waiting_tasks()
for ready_or_waiting_task in ready_or_waiting_tasks:
# filter out non-usertasks
@ -629,27 +622,41 @@ class ProcessInstanceProcessor:
if process_model_info is not None:
process_model_display_name = process_model_info.display_name
active_task = ActiveTaskModel(
process_instance_id=self.process_instance_model.id,
process_model_display_name=process_model_display_name,
form_file_name=form_file_name,
ui_form_file_name=ui_form_file_name,
task_id=str(ready_or_waiting_task.id),
task_name=ready_or_waiting_task.task_spec.name,
task_title=ready_or_waiting_task.task_spec.description,
task_type=ready_or_waiting_task.task_spec.__class__.__name__,
task_status=ready_or_waiting_task.get_state_name(),
lane_assignment_id=potential_owner_hash["lane_assignment_id"],
)
db.session.add(active_task)
db.session.commit()
active_task = None
for at in active_tasks:
if at.task_id == str(ready_or_waiting_task.id):
active_task = at
active_tasks.remove(at)
for potential_owner_id in potential_owner_hash["potential_owner_ids"]:
active_task_user = ActiveTaskUserModel(
user_id=potential_owner_id, active_task_id=active_task.id
if active_task is None:
active_task = ActiveTaskModel(
process_instance_id=self.process_instance_model.id,
process_model_display_name=process_model_display_name,
form_file_name=form_file_name,
ui_form_file_name=ui_form_file_name,
task_id=str(ready_or_waiting_task.id),
task_name=ready_or_waiting_task.task_spec.name,
task_title=ready_or_waiting_task.task_spec.description,
task_type=ready_or_waiting_task.task_spec.__class__.__name__,
task_status=ready_or_waiting_task.get_state_name(),
lane_assignment_id=potential_owner_hash["lane_assignment_id"],
)
db.session.add(active_task_user)
db.session.commit()
db.session.add(active_task)
db.session.commit()
for potential_owner_id in potential_owner_hash[
"potential_owner_ids"
]:
active_task_user = ActiveTaskUserModel(
user_id=potential_owner_id, active_task_id=active_task.id
)
db.session.add(active_task_user)
db.session.commit()
if len(active_tasks) > 0:
for at in active_tasks:
db.session.delete(at)
db.session.commit()
@staticmethod
def get_parser() -> MyCustomParser:
@ -662,7 +669,7 @@ class ProcessInstanceProcessor:
bpmn_process_identifier: str,
) -> Optional[str]:
"""Backfill_missing_spec_reference_records."""
process_models = ProcessModelService().get_process_models(recursive=True)
process_models = ProcessModelService.get_process_models(recursive=True)
for process_model in process_models:
try:
refs = SpecFileService.reference_map(

View File

@ -18,6 +18,9 @@ class ProcessInstanceReportFilter:
end_from: Optional[int] = None
end_to: Optional[int] = None
process_status: Optional[list[str]] = None
initiated_by_me: Optional[bool] = None
with_tasks_completed_by_me: Optional[bool] = None
with_tasks_completed_by_my_group: Optional[bool] = None
def to_dict(self) -> dict[str, str]:
"""To_dict."""
@ -35,6 +38,16 @@ class ProcessInstanceReportFilter:
d["end_to"] = str(self.end_to)
if self.process_status is not None:
d["process_status"] = ",".join(self.process_status)
if self.initiated_by_me is not None:
d["initiated_by_me"] = str(self.initiated_by_me).lower()
if self.with_tasks_completed_by_me is not None:
d["with_tasks_completed_by_me"] = str(
self.with_tasks_completed_by_me
).lower()
if self.with_tasks_completed_by_my_group is not None:
d["with_tasks_completed_by_my_group"] = str(
self.with_tasks_completed_by_my_group
).lower()
return d
@ -63,48 +76,61 @@ class ProcessInstanceReportService:
"columns": [
{"Header": "id", "accessor": "id"},
{
"Header": "process_model_identifier",
"accessor": "process_model_identifier",
"Header": "process_model_display_name",
"accessor": "process_model_display_name",
},
{"Header": "start_in_seconds", "accessor": "start_in_seconds"},
{"Header": "end_in_seconds", "accessor": "end_in_seconds"},
{"Header": "username", "accessor": "username"},
{"Header": "status", "accessor": "status"},
],
},
"system_report_instances_initiated_by_me": {
"columns": [
{"Header": "id", "accessor": "id"},
{
"Header": "process_model_identifier",
"accessor": "process_model_identifier",
"Header": "process_model_display_name",
"accessor": "process_model_display_name",
},
{"Header": "start_in_seconds", "accessor": "start_in_seconds"},
{"Header": "id", "accessor": "id"},
{"Header": "end_in_seconds", "accessor": "end_in_seconds"},
{"Header": "status", "accessor": "status"},
],
"filter_by": [{"field_name": "initiated_by_me", "field_value": True}],
},
"system_report_instances_with_tasks_completed_by_me": {
"columns": [
{"Header": "start_in_seconds", "accessor": "start_in_seconds"},
{"Header": "end_in_seconds", "accessor": "end_in_seconds"},
{"Header": "status", "accessor": "status"},
{"Header": "id", "accessor": "id"},
{
"Header": "process_model_identifier",
"accessor": "process_model_identifier",
"Header": "process_model_display_name",
"accessor": "process_model_display_name",
},
{"Header": "start_in_seconds", "accessor": "start_in_seconds"},
{"Header": "end_in_seconds", "accessor": "end_in_seconds"},
{"Header": "username", "accessor": "username"},
{"Header": "status", "accessor": "status"},
],
"filter_by": [
{"field_name": "with_tasks_completed_by_me", "field_value": True}
],
},
"system_report_instances_with_tasks_completed_by_my_groups": {
"columns": [
{"Header": "id", "accessor": "id"},
{
"Header": "process_model_identifier",
"accessor": "process_model_identifier",
"Header": "process_model_display_name",
"accessor": "process_model_display_name",
},
{"Header": "start_in_seconds", "accessor": "start_in_seconds"},
{"Header": "end_in_seconds", "accessor": "end_in_seconds"},
{"Header": "username", "accessor": "username"},
{"Header": "status", "accessor": "status"},
{"Header": "id", "accessor": "id"},
],
"filter_by": [
{
"field_name": "with_tasks_completed_by_my_group",
"field_value": True,
}
],
},
}
@ -112,7 +138,7 @@ class ProcessInstanceReportService:
process_instance_report = ProcessInstanceReportModel(
identifier=report_identifier,
created_by_id=user.id,
report_metadata=temp_system_metadata_map[report_identifier],
report_metadata=temp_system_metadata_map[report_identifier], # type: ignore
)
return process_instance_report # type: ignore
@ -138,6 +164,10 @@ class ProcessInstanceReportService:
"""Filter_from_metadata."""
filters = cls.filter_by_to_dict(process_instance_report)
def bool_value(key: str) -> Optional[bool]:
"""Bool_value."""
return bool(filters[key]) if key in filters else None
def int_value(key: str) -> Optional[int]:
"""Int_value."""
return int(filters[key]) if key in filters else None
@ -152,6 +182,11 @@ class ProcessInstanceReportService:
end_from = int_value("end_from")
end_to = int_value("end_to")
process_status = list_value("process_status")
initiated_by_me = bool_value("initiated_by_me")
with_tasks_completed_by_me = bool_value("with_tasks_completed_by_me")
with_tasks_completed_by_my_group = bool_value(
"with_tasks_completed_by_my_group"
)
report_filter = ProcessInstanceReportFilter(
process_model_identifier,
@ -160,6 +195,9 @@ class ProcessInstanceReportService:
end_from,
end_to,
process_status,
initiated_by_me,
with_tasks_completed_by_me,
with_tasks_completed_by_my_group,
)
return report_filter
@ -174,6 +212,9 @@ class ProcessInstanceReportService:
end_from: Optional[int] = None,
end_to: Optional[int] = None,
process_status: Optional[str] = None,
initiated_by_me: Optional[bool] = None,
with_tasks_completed_by_me: Optional[bool] = None,
with_tasks_completed_by_my_group: Optional[bool] = None,
) -> ProcessInstanceReportFilter:
"""Filter_from_metadata_with_overrides."""
report_filter = cls.filter_from_metadata(process_instance_report)
@ -190,5 +231,13 @@ class ProcessInstanceReportService:
report_filter.end_to = end_to
if process_status is not None:
report_filter.process_status = process_status.split(",")
if initiated_by_me is not None:
report_filter.initiated_by_me = initiated_by_me
if with_tasks_completed_by_me is not None:
report_filter.with_tasks_completed_by_me = with_tasks_completed_by_me
if with_tasks_completed_by_my_group is not None:
report_filter.with_tasks_completed_by_my_group = (
with_tasks_completed_by_my_group
)
return report_filter

View File

@ -12,6 +12,7 @@ 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
from spiffworkflow_backend.models.process_model import ProcessModelInfo
from spiffworkflow_backend.models.task import MultiInstanceType
from spiffworkflow_backend.models.task import Task
from spiffworkflow_backend.models.user import UserModel
@ -28,9 +29,10 @@ class ProcessInstanceService:
TASK_STATE_LOCKED = "locked"
@staticmethod
@classmethod
def create_process_instance(
process_model_identifier: str,
cls,
process_model: ProcessModelInfo,
user: UserModel,
) -> ProcessInstanceModel:
"""Get_process_instance_from_spec."""
@ -38,8 +40,8 @@ class ProcessInstanceService:
process_instance_model = ProcessInstanceModel(
status=ProcessInstanceStatus.not_started.value,
process_initiator=user,
process_model_identifier=process_model_identifier,
process_group_identifier="",
process_model_identifier=process_model.id,
process_model_display_name=process_model.display_name,
start_in_seconds=round(time.time()),
bpmn_version_control_type="git",
bpmn_version_control_identifier=current_git_revision,
@ -48,6 +50,16 @@ class ProcessInstanceService:
db.session.commit()
return process_instance_model
@classmethod
def create_process_instance_from_process_model_identifier(
cls,
process_model_identifier: str,
user: UserModel,
) -> ProcessInstanceModel:
"""Create_process_instance_from_process_model_identifier."""
process_model = ProcessModelService.get_process_model(process_model_identifier)
return cls.create_process_instance(process_model, user)
@staticmethod
def do_waiting() -> None:
"""Do_waiting."""
@ -88,20 +100,15 @@ class ProcessInstanceService:
process_model = process_model_service.get_process_model(
processor.process_model_identifier
)
is_review_value = process_model.is_review if process_model else False
title_value = process_model.display_name if process_model else ""
process_model.display_name if process_model else ""
process_instance_api = ProcessInstanceApi(
id=processor.get_process_instance_id(),
status=processor.get_status(),
next_task=None,
# navigation=navigation,
process_model_identifier=processor.process_model_identifier,
process_group_identifier="",
# total_tasks=len(navigation),
process_model_display_name=processor.process_model_display_name,
completed_tasks=processor.process_instance_model.completed_tasks,
updated_at_in_seconds=processor.process_instance_model.updated_at_in_seconds,
is_review=is_review_value,
title=title_value,
)
next_task_trying_again = next_task
@ -197,7 +204,7 @@ class ProcessInstanceService:
a multi-instance task.
"""
AuthorizationService.assert_user_can_complete_spiff_task(
processor, spiff_task, user
processor.process_instance_model.id, spiff_task, user
)
dot_dct = ProcessInstanceService.create_dot_dict(data)
@ -320,12 +327,13 @@ class ProcessInstanceService:
def serialize_flat_with_task_data(
process_instance: ProcessInstanceModel,
) -> dict[str, Any]:
"""NOTE: This is crazy slow. Put the latest task data in the database."""
"""Serialize_flat_with_task_data."""
results = {}
try:
processor = ProcessInstanceProcessor(process_instance)
process_instance.data = processor.get_current_data()
results = process_instance.serialized_flat
except ApiError:
results = process_instance.serialized
# results = {}
# try:
# processor = ProcessInstanceProcessor(process_instance)
# process_instance.data = processor.get_current_data()
# results = process_instance.serialized_flat
# except ApiError:
results = process_instance.serialized
return results

View File

@ -18,7 +18,9 @@ from spiffworkflow_backend.models.process_group import ProcessGroupSchema
from spiffworkflow_backend.models.process_instance import ProcessInstanceModel
from spiffworkflow_backend.models.process_model import ProcessModelInfo
from spiffworkflow_backend.models.process_model import ProcessModelInfoSchema
from spiffworkflow_backend.services.authorization_service import AuthorizationService
from spiffworkflow_backend.services.file_system_service import FileSystemService
from spiffworkflow_backend.services.user_service import UserService
T = TypeVar("T")
@ -35,20 +37,54 @@ class ProcessModelService(FileSystemService):
GROUP_SCHEMA = ProcessGroupSchema()
PROCESS_MODEL_SCHEMA = ProcessModelInfoSchema()
def is_group(self, path: str) -> bool:
@classmethod
def is_group(cls, path: str) -> bool:
"""Is_group."""
group_json_path = os.path.join(path, self.PROCESS_GROUP_JSON_FILE)
group_json_path = os.path.join(path, cls.PROCESS_GROUP_JSON_FILE)
if os.path.exists(group_json_path):
return True
return False
def is_model(self, path: str) -> bool:
@classmethod
def is_group_identifier(cls, process_group_identifier: str) -> bool:
"""Is_group_identifier."""
if os.path.exists(FileSystemService.root_path()):
process_group_path = os.path.abspath(
os.path.join(
FileSystemService.root_path(),
FileSystemService.id_string_to_relative_path(
process_group_identifier
),
)
)
return cls.is_group(process_group_path)
return False
@classmethod
def is_model(cls, path: str) -> bool:
"""Is_model."""
model_json_path = os.path.join(path, self.PROCESS_MODEL_JSON_FILE)
model_json_path = os.path.join(path, cls.PROCESS_MODEL_JSON_FILE)
if os.path.exists(model_json_path):
return True
return False
@classmethod
def is_model_identifier(cls, process_model_identifier: str) -> bool:
"""Is_model_identifier."""
if os.path.exists(FileSystemService.root_path()):
process_model_path = os.path.abspath(
os.path.join(
FileSystemService.root_path(),
FileSystemService.id_string_to_relative_path(
process_model_identifier
),
)
)
return cls.is_model(process_model_path)
return False
@staticmethod
def write_json_file(
file_path: str, json_data: dict, indent: int = 4, sort_keys: bool = True
@ -68,37 +104,38 @@ class ProcessModelService(FileSystemService):
end = start + per_page
return items[start:end]
def add_process_model(self, process_model: ProcessModelInfo) -> None:
@classmethod
def add_process_model(cls, process_model: ProcessModelInfo) -> None:
"""Add_spec."""
display_order = self.next_display_order(process_model)
process_model.display_order = display_order
self.save_process_model(process_model)
cls.save_process_model(process_model)
@classmethod
def update_process_model(
self, process_model: ProcessModelInfo, attributes_to_update: dict
cls, process_model: ProcessModelInfo, attributes_to_update: dict
) -> None:
"""Update_spec."""
for atu_key, atu_value in attributes_to_update.items():
if hasattr(process_model, atu_key):
setattr(process_model, atu_key, atu_value)
self.save_process_model(process_model)
cls.save_process_model(process_model)
def save_process_model(self, process_model: ProcessModelInfo) -> None:
@classmethod
def save_process_model(cls, process_model: ProcessModelInfo) -> None:
"""Save_process_model."""
process_model_path = os.path.abspath(
os.path.join(FileSystemService.root_path(), process_model.id)
)
os.makedirs(process_model_path, exist_ok=True)
json_path = os.path.abspath(
os.path.join(process_model_path, self.PROCESS_MODEL_JSON_FILE)
os.path.join(process_model_path, cls.PROCESS_MODEL_JSON_FILE)
)
process_model_id = process_model.id
# we don't save id in the json file
# this allows us to move models around on the filesystem
# the id is determined by its location on the filesystem
delattr(process_model, "id")
json_data = self.PROCESS_MODEL_SCHEMA.dump(process_model)
self.write_json_file(json_path, json_data)
json_data = cls.PROCESS_MODEL_SCHEMA.dump(process_model)
cls.write_json_file(json_path, json_data)
process_model.id = process_model_id
def process_model_delete(self, process_model_id: str) -> None:
@ -119,7 +156,7 @@ class ProcessModelService(FileSystemService):
def process_model_move(
self, original_process_model_id: str, new_location: str
) -> ProcessModelInfo:
"""process_model_move."""
"""Process_model_move."""
original_model_path = os.path.abspath(
os.path.join(FileSystemService.root_path(), original_process_model_id)
)
@ -138,11 +175,11 @@ class ProcessModelService(FileSystemService):
) -> ProcessModelInfo:
"""Get_process_model_from_relative_path."""
process_group_identifier, _ = os.path.split(relative_path)
process_group = cls().get_process_group(process_group_identifier)
path = os.path.join(FileSystemService.root_path(), relative_path)
return cls().__scan_process_model(path, process_group=process_group)
return cls.__scan_process_model(path)
def get_process_model(self, process_model_id: str) -> ProcessModelInfo:
@classmethod
def get_process_model(cls, process_model_id: str) -> ProcessModelInfo:
"""Get a process model from a model and group id.
process_model_id is the full path to the model--including groups.
@ -153,33 +190,16 @@ class ProcessModelService(FileSystemService):
model_path = os.path.abspath(
os.path.join(FileSystemService.root_path(), process_model_id)
)
if self.is_model(model_path):
process_model = self.get_process_model_from_relative_path(process_model_id)
return process_model
# group_path, model_id = os.path.split(process_model_id)
# if group_path is not None:
# process_group = self.get_process_group(group_path)
# if process_group is not None:
# for process_model in process_group.process_models:
# if process_model_id == process_model.id:
# return process_model
# with os.scandir(FileSystemService.root_path()) as process_group_dirs:
# for item in process_group_dirs:
# process_group_dir = item
# if item.is_dir():
# with os.scandir(item.path) as spec_dirs:
# for sd in spec_dirs:
# if sd.name == process_model_id:
# # Now we have the process_group directory, and spec directory
# process_group = self.__scan_process_group(
# process_group_dir
# )
# return self.__scan_process_model(sd.path, sd.name, process_group)
if cls.is_model(model_path):
return cls.get_process_model_from_relative_path(process_model_id)
raise ProcessEntityNotFoundError("process_model_not_found")
@classmethod
def get_process_models(
self, process_group_id: Optional[str] = None, recursive: Optional[bool] = False
cls,
process_group_id: Optional[str] = None,
recursive: Optional[bool] = False,
filter_runnable_by_user: Optional[bool] = False,
) -> List[ProcessModelInfo]:
"""Get process models."""
process_models = []
@ -196,22 +216,56 @@ class ProcessModelService(FileSystemService):
process_model_relative_path = os.path.relpath(
file, start=FileSystemService.root_path()
)
process_model = self.get_process_model_from_relative_path(
process_model = cls.get_process_model_from_relative_path(
os.path.dirname(process_model_relative_path)
)
process_models.append(process_model)
process_models.sort()
if filter_runnable_by_user:
user = UserService.current_user()
new_process_model_list = []
for process_model in process_models:
uri = f"/v1.0/process-models/{process_model.id.replace('/', ':')}/process-instances"
result = AuthorizationService.user_has_permission(
user=user, permission="create", target_uri=uri
)
if result:
new_process_model_list.append(process_model)
return new_process_model_list
return process_models
@classmethod
def get_parent_group_array(cls, process_identifier: str) -> list[dict]:
"""Get_parent_group_array."""
full_group_id_path = None
parent_group_array = []
for process_group_id_segment in process_identifier.split("/")[0:-1]:
if full_group_id_path is None:
full_group_id_path = process_group_id_segment
else:
full_group_id_path = f"{full_group_id_path}/{process_group_id_segment}" # type: ignore
parent_group = ProcessModelService.get_process_group(full_group_id_path)
if parent_group:
parent_group_array.append(
{"id": parent_group.id, "display_name": parent_group.display_name}
)
return parent_group_array
@classmethod
def get_process_groups(
self, process_group_id: Optional[str] = None
cls, process_group_id: Optional[str] = None
) -> list[ProcessGroup]:
"""Returns the process_groups as a list in display order."""
process_groups = self.__scan_process_groups(process_group_id)
"""Returns the process_groups."""
process_groups = cls.__scan_process_groups(process_group_id)
process_groups.sort()
return process_groups
def get_process_group(self, process_group_id: str) -> ProcessGroup:
@classmethod
def get_process_group(
cls, process_group_id: str, find_direct_nested_items: bool = True
) -> ProcessGroup:
"""Look for a given process_group, and return it."""
if os.path.exists(FileSystemService.root_path()):
process_group_path = os.path.abspath(
@ -220,48 +274,38 @@ class ProcessModelService(FileSystemService):
FileSystemService.id_string_to_relative_path(process_group_id),
)
)
if self.is_group(process_group_path):
return self.__scan_process_group(process_group_path)
# nested_groups = []
# process_group_dir = os.scandir(process_group_path)
# for item in process_group_dir:
# if self.is_group(item.path):
# nested_group = self.get_process_group(os.path.join(process_group_path, item.path))
# nested_groups.append(nested_group)
# elif self.is_model(item.path):
# print("get_process_group: ")
# return self.__scan_process_group(process_group_path)
# with os.scandir(FileSystemService.root_path()) as directory_items:
# for item in directory_items:
# if item.is_dir() and item.name == process_group_id:
# return self.__scan_process_group(item)
if cls.is_group(process_group_path):
return cls.find_or_create_process_group(
process_group_path,
find_direct_nested_items=find_direct_nested_items,
)
raise ProcessEntityNotFoundError(
"process_group_not_found", f"Process Group Id: {process_group_id}"
)
def add_process_group(self, process_group: ProcessGroup) -> ProcessGroup:
@classmethod
def add_process_group(cls, process_group: ProcessGroup) -> ProcessGroup:
"""Add_process_group."""
display_order = len(self.get_process_groups())
process_group.display_order = display_order
return self.update_process_group(process_group)
return cls.update_process_group(process_group)
def update_process_group(self, process_group: ProcessGroup) -> ProcessGroup:
@classmethod
def update_process_group(cls, process_group: ProcessGroup) -> ProcessGroup:
"""Update_process_group."""
cat_path = self.process_group_path(process_group.id)
cat_path = cls.process_group_path(process_group.id)
os.makedirs(cat_path, exist_ok=True)
json_path = os.path.join(cat_path, self.PROCESS_GROUP_JSON_FILE)
json_path = os.path.join(cat_path, cls.PROCESS_GROUP_JSON_FILE)
serialized_process_group = process_group.serialized
# we don't store `id` in the json files
# this allows us to move groups around on the filesystem
del serialized_process_group["id"]
self.write_json_file(json_path, serialized_process_group)
cls.write_json_file(json_path, serialized_process_group)
return process_group
def process_group_move(
self, original_process_group_id: str, new_location: str
) -> ProcessGroup:
"""process_group_move."""
"""Process_group_move."""
original_group_path = self.process_group_path(original_process_group_id)
original_root, original_group_id = os.path.split(original_group_path)
new_root = f"{FileSystemService.root_path()}/{new_location}"
@ -278,7 +322,7 @@ class ProcessModelService(FileSystemService):
for _root, dirs, _files in os.walk(group_path):
for dir in dirs:
model_dir = os.path.join(group_path, dir)
if ProcessModelService().is_model(model_dir):
if ProcessModelService.is_model(model_dir):
process_model = self.get_process_model(model_dir)
all_nested_models.append(process_model)
return all_nested_models
@ -314,8 +358,9 @@ class ProcessModelService(FileSystemService):
index += 1
return process_groups
@classmethod
def __scan_process_groups(
self, process_group_id: Optional[str] = None
cls, process_group_id: Optional[str] = None
) -> list[ProcessGroup]:
"""__scan_process_groups."""
if not os.path.exists(FileSystemService.root_path()):
@ -329,14 +374,17 @@ class ProcessModelService(FileSystemService):
process_groups = []
for item in directory_items:
# if item.is_dir() and not item.name[0] == ".":
if item.is_dir() and self.is_group(item): # type: ignore
scanned_process_group = self.__scan_process_group(item.path)
if item.is_dir() and cls.is_group(item): # type: ignore
scanned_process_group = cls.find_or_create_process_group(item.path)
process_groups.append(scanned_process_group)
return process_groups
def __scan_process_group(self, dir_path: str) -> ProcessGroup:
@classmethod
def find_or_create_process_group(
cls, dir_path: str, find_direct_nested_items: bool = True
) -> ProcessGroup:
"""Reads the process_group.json file, and any nested directories."""
cat_path = os.path.join(dir_path, self.PROCESS_GROUP_JSON_FILE)
cat_path = os.path.join(dir_path, cls.PROCESS_GROUP_JSON_FILE)
if os.path.exists(cat_path):
with open(cat_path) as cat_json:
data = json.load(cat_json)
@ -357,40 +405,41 @@ class ProcessModelService(FileSystemService):
display_order=10000,
admin=False,
)
self.write_json_file(cat_path, self.GROUP_SCHEMA.dump(process_group))
cls.write_json_file(cat_path, cls.GROUP_SCHEMA.dump(process_group))
# we don't store `id` in the json files, so we add it in here
process_group.id = process_group_id
with os.scandir(dir_path) as nested_items:
process_group.process_models = []
process_group.process_groups = []
for nested_item in nested_items:
if nested_item.is_dir():
# TODO: check whether this is a group or model
if self.is_group(nested_item.path):
# This is a nested group
process_group.process_groups.append(
self.__scan_process_group(nested_item.path)
)
elif self.is_model(nested_item.path):
process_group.process_models.append(
self.__scan_process_model(
nested_item.path,
nested_item.name,
process_group=process_group,
if find_direct_nested_items:
with os.scandir(dir_path) as nested_items:
process_group.process_models = []
process_group.process_groups = []
for nested_item in nested_items:
if nested_item.is_dir():
# TODO: check whether this is a group or model
if cls.is_group(nested_item.path):
# This is a nested group
process_group.process_groups.append(
cls.find_or_create_process_group(nested_item.path)
)
)
process_group.process_models.sort()
# process_group.process_groups.sort()
elif ProcessModelService.is_model(nested_item.path):
process_group.process_models.append(
cls.__scan_process_model(
nested_item.path,
nested_item.name,
)
)
process_group.process_models.sort()
# process_group.process_groups.sort()
return process_group
@classmethod
def __scan_process_model(
self,
cls,
path: str,
name: Optional[str] = None,
process_group: Optional[ProcessGroup] = None,
) -> ProcessModelInfo:
"""__scan_process_model."""
json_file_path = os.path.join(path, self.PROCESS_MODEL_JSON_FILE)
json_file_path = os.path.join(path, cls.PROCESS_MODEL_JSON_FILE)
if os.path.exists(json_file_path):
with open(json_file_path) as wf_json:
@ -418,13 +467,10 @@ class ProcessModelService(FileSystemService):
display_name=name,
description="",
display_order=0,
is_review=False,
)
self.write_json_file(
json_file_path, self.PROCESS_MODEL_SCHEMA.dump(process_model_info)
cls.write_json_file(
json_file_path, cls.PROCESS_MODEL_SCHEMA.dump(process_model_info)
)
# we don't store `id` in the json files, so we add it in here
process_model_info.id = name
if process_group:
process_model_info.process_group = process_group.id
return process_model_info

View File

@ -65,7 +65,7 @@ class SecretService:
def update_secret(
key: str,
value: str,
user_id: int,
user_id: Optional[int] = None,
create_if_not_exists: Optional[bool] = False,
) -> None:
"""Does this pass pre commit?"""
@ -79,6 +79,12 @@ class SecretService:
db.session.rollback()
raise e
elif create_if_not_exists:
if user_id is None:
raise ApiError(
error_code="update_secret_error_no_user_id",
message=f"Cannot update secret with key: {key}. Missing user id.",
status_code=404,
)
SecretService.add_secret(key=key, value=value, user_id=user_id)
else:
raise ApiError(

View File

@ -8,6 +8,7 @@ from flask import g
from spiffworkflow_backend.services.file_system_service import FileSystemService
from spiffworkflow_backend.services.secret_service import SecretService
from spiffworkflow_backend.services.user_service import UserService
class ConnectorProxyError(Exception):
@ -65,7 +66,8 @@ class ServiceTaskDelegate:
secret_key = parsed_response["auth"]
refreshed_token_set = json.dumps(parsed_response["refreshed_token_set"])
SecretService().update_secret(secret_key, refreshed_token_set, g.user.id)
user_id = g.user.id if UserService.has_user() else None
SecretService().update_secret(secret_key, refreshed_token_set, user_id)
return json.dumps(parsed_response["api_response"])

View File

@ -171,12 +171,11 @@ class SpecFileService(FileSystemService):
ref.is_primary = True
if ref.is_primary:
ProcessModelService().update_process_model(
ProcessModelService.update_process_model(
process_model_info,
{
"primary_process_id": ref.identifier,
"primary_file_name": file_name,
"is_review": ref.has_lanes,
},
)
SpecFileService.update_caches(ref)
@ -322,7 +321,6 @@ class SpecFileService(FileSystemService):
message_triggerable_process_model = MessageTriggerableProcessModel(
message_model_id=message_model.id,
process_model_identifier=ref.process_model_id,
process_group_identifier="process_group_identifier",
)
db.session.add(message_triggerable_process_model)
db.session.commit()
@ -330,8 +328,6 @@ class SpecFileService(FileSystemService):
if (
message_triggerable_process_model.process_model_identifier
!= ref.process_model_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 {ref.process_model_id}"

View File

@ -0,0 +1,52 @@
<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" id="Definitions_96f6665" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.0.0-dev">
<bpmn:process id="test_save_process_instance_metadata" isExecutable="true">
<bpmn:startEvent id="Event_0r6oru6">
<bpmn:outgoing>Flow_1j4jzft</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:sequenceFlow id="Flow_1j4jzft" sourceRef="Event_0r6oru6" targetRef="save_key1" />
<bpmn:endEvent id="Event_1s123jg">
<bpmn:incoming>Flow_01xr2ac</bpmn:incoming>
</bpmn:endEvent>
<bpmn:scriptTask id="save_key1">
<bpmn:incoming>Flow_1j4jzft</bpmn:incoming>
<bpmn:outgoing>Flow_10xyk22</bpmn:outgoing>
<bpmn:script>save_process_instance_metadata({"key1": "value1"})</bpmn:script>
</bpmn:scriptTask>
<bpmn:sequenceFlow id="Flow_10xyk22" sourceRef="save_key1" targetRef="save_key2" />
<bpmn:scriptTask id="save_key2">
<bpmn:incoming>Flow_10xyk22</bpmn:incoming>
<bpmn:outgoing>Flow_01xr2ac</bpmn:outgoing>
<bpmn:script>save_process_instance_metadata({"key2": "value2", "key3": "value3"})</bpmn:script>
</bpmn:scriptTask>
<bpmn:sequenceFlow id="Flow_01xr2ac" sourceRef="save_key2" targetRef="Event_1s123jg" />
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="test_save_process_instance_metadata">
<bpmndi:BPMNShape id="Event_0r6oru6_di" bpmnElement="Event_0r6oru6">
<dc:Bounds x="162" y="162" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_0zfzev2_di" bpmnElement="save_key1">
<dc:Bounds x="250" y="140" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_0d1q8x4_di" bpmnElement="save_key2">
<dc:Bounds x="410" y="140" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Event_1s123jg_di" bpmnElement="Event_1s123jg">
<dc:Bounds x="582" y="162" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="Flow_1j4jzft_di" bpmnElement="Flow_1j4jzft">
<di:waypoint x="198" y="180" />
<di:waypoint x="250" y="180" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_10xyk22_di" bpmnElement="Flow_10xyk22">
<di:waypoint x="350" y="180" />
<di:waypoint x="410" y="180" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_01xr2ac_di" bpmnElement="Flow_01xr2ac">
<di:waypoint x="510" y="180" />
<di:waypoint x="582" y="180" />
</bpmndi:BPMNEdge>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>

View File

@ -140,7 +140,7 @@ class BaseTest:
process_group_path = os.path.abspath(
os.path.join(FileSystemService.root_path(), process_group_id)
)
if ProcessModelService().is_group(process_group_path):
if ProcessModelService.is_group(process_group_path):
if exception_notification_addresses is None:
exception_notification_addresses = []
@ -149,7 +149,6 @@ class BaseTest:
id=process_model_id,
display_name=process_model_display_name,
description=process_model_description,
is_review=False,
primary_process_id=primary_process_id,
primary_file_name=primary_file_name,
fault_or_suspend_on_exception=fault_or_suspend_on_exception,
@ -253,6 +252,17 @@ class BaseTest:
There must be an existing process model to instantiate.
"""
if not ProcessModelService.is_model_identifier(test_process_model_id):
dirname = os.path.dirname(test_process_model_id)
if not ProcessModelService.is_group_identifier(dirname):
process_group = ProcessGroup(id=dirname, display_name=dirname)
ProcessModelService.add_process_group(process_group)
basename = os.path.basename(test_process_model_id)
load_test_spec(
process_model_id=test_process_model_id,
process_model_source_directory=basename,
bpmn_file_name=basename,
)
modified_process_model_id = test_process_model_id.replace("/", ":")
response = client.post(
f"/v1.0/process-models/{modified_process_model_id}/process-instances",
@ -284,7 +294,7 @@ class BaseTest:
status=status,
process_initiator=user,
process_model_identifier=process_model.id,
process_group_identifier="",
process_model_display_name=process_model.display_name,
updated_at_in_seconds=round(time.time()),
start_in_seconds=current_time - (3600 * 1),
end_in_seconds=current_time - (3600 * 1 - 20),
@ -347,3 +357,16 @@ class BaseTest:
target_uri=target_uri,
)
assert has_permission is expected_result
def modify_process_identifier_for_path_param(self, identifier: str) -> str:
"""Identifier."""
if "\\" in identifier:
raise Exception(f"Found backslash in identifier: {identifier}")
return identifier.replace("/", ":")
def un_modify_modified_process_identifier_for_path_param(
self, modified_identifier: str
) -> str:
"""Un_modify_modified_process_model_id."""
return modified_identifier.replace(":", "/")

View File

@ -36,10 +36,8 @@ class ExampleDataLoader:
display_name=display_name,
description=description,
display_order=display_order,
is_review=False,
)
workflow_spec_service = ProcessModelService()
workflow_spec_service.add_process_model(spec)
ProcessModelService.add_process_model(spec)
bpmn_file_name_with_extension = bpmn_file_name
if not bpmn_file_name_with_extension:
@ -88,7 +86,7 @@ class ExampleDataLoader:
)
spec.primary_process_id = references[0].identifier
spec.primary_file_name = filename
ProcessModelService().save_process_model(spec)
ProcessModelService.save_process_model(spec)
finally:
if file:
file.close()

View File

@ -51,7 +51,7 @@ class TestLoggingService(BaseTest):
assert response.json is not None
process_instance_id = response.json["id"]
response = client.post(
f"/v1.0/process-instances/{process_instance_id}/run",
f"/v1.0/process-instances/{self.modify_process_identifier_for_path_param(process_model_identifier)}/{process_instance_id}/run",
headers=headers,
)
assert response.status_code == 200

View File

@ -46,7 +46,7 @@ class TestNestedGroups(BaseTest):
process_instance_id = response.json["id"]
client.post(
f"/v1.0/process-instances/{process_instance_id}/run",
f"/v1.0/process-instances/{self.modify_process_identifier_for_path_param(process_model_identifier)}/{process_instance_id}/run",
headers=self.logged_in_headers(with_super_admin_user),
)
process_instance = ProcessInstanceService().get_process_instance(

View File

@ -133,12 +133,12 @@ class TestProcessApi(BaseTest):
process_model_description=model_description,
user=with_super_admin_user,
)
process_model = ProcessModelService().get_process_model(
process_model = ProcessModelService.get_process_model(
process_model_identifier,
)
assert model_display_name == process_model.display_name
assert 0 == process_model.display_order
assert 1 == len(ProcessModelService().get_process_groups())
assert 1 == len(ProcessModelService.get_process_groups())
# add bpmn file to the model
bpmn_file_name = "sample.bpmn"
@ -155,9 +155,7 @@ class TestProcessApi(BaseTest):
user=with_super_admin_user,
)
# get the model, assert that primary is set
process_model = ProcessModelService().get_process_model(
process_model_identifier
)
process_model = ProcessModelService.get_process_model(process_model_identifier)
assert process_model.primary_file_name == bpmn_file_name
assert process_model.primary_process_id == "sample"
@ -208,9 +206,7 @@ class TestProcessApi(BaseTest):
headers=self.logged_in_headers(with_super_admin_user),
)
assert response.status_code == 200
process_model = ProcessModelService().get_process_model(
process_model_identifier
)
process_model = ProcessModelService.get_process_model(process_model_identifier)
assert process_model.primary_file_name == bpmn_file_name
assert process_model.primary_process_id == terminal_primary_process_id
@ -236,9 +232,7 @@ class TestProcessApi(BaseTest):
)
# assert we have a model
process_model = ProcessModelService().get_process_model(
process_model_identifier
)
process_model = ProcessModelService.get_process_model(process_model_identifier)
assert process_model is not None
assert process_model.id == process_model_identifier
@ -254,7 +248,7 @@ class TestProcessApi(BaseTest):
# assert we no longer have a model
with pytest.raises(ProcessEntityNotFoundError):
ProcessModelService().get_process_model(process_model_identifier)
ProcessModelService.get_process_model(process_model_identifier)
def test_process_model_delete_with_instances(
self,
@ -327,19 +321,15 @@ class TestProcessApi(BaseTest):
process_model_id=process_model_identifier,
user=with_super_admin_user,
)
process_model = ProcessModelService().get_process_model(
process_model_identifier
)
process_model = ProcessModelService.get_process_model(process_model_identifier)
assert process_model.id == process_model_identifier
assert process_model.display_name == "Cooooookies"
assert process_model.is_review is False
assert process_model.primary_file_name is None
assert process_model.primary_process_id is None
process_model.display_name = "Updated Display Name"
process_model.primary_file_name = "superduper.bpmn"
process_model.primary_process_id = "superduper"
process_model.is_review = True # not in the include list, so get ignored
modified_process_model_identifier = process_model_identifier.replace("/", ":")
response = client.put(
@ -353,7 +343,6 @@ class TestProcessApi(BaseTest):
assert response.json["display_name"] == "Updated Display Name"
assert response.json["primary_file_name"] == "superduper.bpmn"
assert response.json["primary_process_id"] == "superduper"
assert response.json["is_review"] is False
def test_process_model_list_all(
self,
@ -550,7 +539,7 @@ class TestProcessApi(BaseTest):
assert result.description == "Test Description"
# Check what is persisted
persisted = ProcessModelService().get_process_group("test")
persisted = ProcessModelService.get_process_group("test")
assert persisted.display_name == "Another Test Category"
assert persisted.id == "test"
assert persisted.description == "Test Description"
@ -572,7 +561,7 @@ class TestProcessApi(BaseTest):
process_group_id,
display_name=process_group_display_name,
)
persisted = ProcessModelService().get_process_group(process_group_id)
persisted = ProcessModelService.get_process_group(process_group_id)
assert persisted is not None
assert persisted.id == process_group_id
@ -582,7 +571,7 @@ class TestProcessApi(BaseTest):
)
with pytest.raises(ProcessEntityNotFoundError):
ProcessModelService().get_process_group(process_group_id)
ProcessModelService.get_process_group(process_group_id)
def test_process_group_update(
self,
@ -598,7 +587,7 @@ class TestProcessApi(BaseTest):
self.create_process_group(
client, with_super_admin_user, group_id, display_name=group_display_name
)
process_group = ProcessModelService().get_process_group(group_id)
process_group = ProcessModelService.get_process_group(group_id)
assert process_group.display_name == group_display_name
@ -612,7 +601,7 @@ class TestProcessApi(BaseTest):
)
assert response.status_code == 200
process_group = ProcessModelService().get_process_group(group_id)
process_group = ProcessModelService.get_process_group(group_id)
assert process_group.display_name == "Modified Display Name"
def test_process_group_list(
@ -979,6 +968,43 @@ class TestProcessApi(BaseTest):
assert response.json is not None
assert response.json["id"] == process_group_id
assert response.json["process_models"][0]["id"] == process_model_identifier
assert response.json["parent_groups"] == []
def test_get_process_group_show_when_nested(
self,
app: Flask,
client: FlaskClient,
with_db_and_bpmn_file_cleanup: None,
with_super_admin_user: UserModel,
) -> None:
"""Test_get_process_group_show_when_nested."""
self.create_group_and_model_with_bpmn(
client=client,
user=with_super_admin_user,
process_group_id="test_group_one",
process_model_id="simple_form",
bpmn_file_location="simple_form",
)
self.create_group_and_model_with_bpmn(
client=client,
user=with_super_admin_user,
process_group_id="test_group_one/test_group_two",
process_model_id="call_activity_nested",
bpmn_file_location="call_activity_nested",
)
response = client.get(
"/v1.0/process-groups/test_group_one:test_group_two",
headers=self.logged_in_headers(with_super_admin_user),
)
assert response.status_code == 200
assert response.json is not None
assert response.json["id"] == "test_group_one/test_group_two"
assert response.json["parent_groups"] == [
{"display_name": "test_group_one", "id": "test_group_one"}
]
def test_get_process_model_when_found(
self,
@ -997,11 +1023,15 @@ class TestProcessApi(BaseTest):
f"/v1.0/process-models/{modified_process_model_identifier}",
headers=self.logged_in_headers(with_super_admin_user),
)
assert response.status_code == 200
assert response.json is not None
assert response.json["id"] == process_model_identifier
assert len(response.json["files"]) == 1
assert response.json["files"][0]["name"] == "random_fact.bpmn"
assert response.json["parent_groups"] == [
{"display_name": "test_group", "id": "test_group"}
]
def test_get_process_model_when_not_found(
self,
@ -1069,7 +1099,7 @@ class TestProcessApi(BaseTest):
assert response.json is not None
process_instance_id = response.json["id"]
response = client.post(
f"/v1.0/process-instances/{process_instance_id}/run",
f"/v1.0/process-instances/{self.modify_process_identifier_for_path_param(process_model_identifier)}/{process_instance_id}/run",
headers=self.logged_in_headers(with_super_admin_user),
)
@ -1101,7 +1131,9 @@ class TestProcessApi(BaseTest):
process_group_id=process_group_id,
process_model_id=process_model_id,
)
modified_process_model_identifier = process_model_identifier.replace("/", ":")
modified_process_model_identifier = (
self.modify_process_identifier_for_path_param(process_model_identifier)
)
headers = self.logged_in_headers(with_super_admin_user)
create_response = self.create_process_instance_from_process_model_id(
client, process_model_identifier, headers
@ -1109,7 +1141,7 @@ class TestProcessApi(BaseTest):
assert create_response.json is not None
process_instance_id = create_response.json["id"]
client.post(
f"/v1.0/process-instances/{process_instance_id}/run",
f"/v1.0/process-instances/{modified_process_model_identifier}/{process_instance_id}/run",
headers=self.logged_in_headers(with_super_admin_user),
)
show_response = client.get(
@ -1212,7 +1244,7 @@ class TestProcessApi(BaseTest):
process_instance_id = response.json["id"]
response = client.post(
f"/v1.0/process-instances/{process_instance_id}/run",
f"/v1.0/process-instances/{self.modify_process_identifier_for_path_param(process_model_identifier)}/{process_instance_id}/run",
headers=self.logged_in_headers(with_super_admin_user),
)
@ -1272,7 +1304,7 @@ class TestProcessApi(BaseTest):
process_instance_id = response.json["id"]
response = client.post(
f"/v1.0/process-instances/{process_instance_id}/run",
f"/v1.0/process-instances/{self.modify_process_identifier_for_path_param(process_model_identifier)}/{process_instance_id}/run",
headers=self.logged_in_headers(with_super_admin_user),
)
assert response.status_code == 200
@ -1320,7 +1352,7 @@ class TestProcessApi(BaseTest):
process_instance_id = response.json["id"]
response = client.post(
f"/v1.0/process-instances/{process_instance_id}/run",
f"/v1.0/process-instances/{self.modify_process_identifier_for_path_param(process_model_identifier)}/{process_instance_id}/run",
headers=self.logged_in_headers(with_super_admin_user),
)
assert response.json is not None
@ -1359,7 +1391,7 @@ class TestProcessApi(BaseTest):
process_instance_id = response.json["id"]
response = client.post(
f"/v1.0/process-instances/{process_instance_id}/run",
f"/v1.0/process-instances/{self.modify_process_identifier_for_path_param(process_model_identifier)}/{process_instance_id}/run",
headers=self.logged_in_headers(with_super_admin_user),
)
@ -1516,7 +1548,7 @@ class TestProcessApi(BaseTest):
status=ProcessInstanceStatus[statuses[i]].value,
process_initiator=with_super_admin_user,
process_model_identifier=process_model_identifier,
process_group_identifier="test_process_group_id",
process_model_display_name=process_model_identifier,
updated_at_in_seconds=round(time.time()),
start_in_seconds=(1000 * i) + 1000,
end_in_seconds=(1000 * i) + 2000,
@ -1818,7 +1850,7 @@ class TestProcessApi(BaseTest):
assert process.status == "not_started"
response = client.post(
f"/v1.0/process-instances/{process_instance_id}/run",
f"/v1.0/process-instances/{self.modify_process_identifier_for_path_param(process_model_identifier)}/{process_instance_id}/run",
headers=self.logged_in_headers(with_super_admin_user),
)
assert response.status_code == 400
@ -1862,10 +1894,8 @@ class TestProcessApi(BaseTest):
process_instance_id = self.setup_testing_instance(
client, process_model_identifier, with_super_admin_user
)
process_model = ProcessModelService().get_process_model(
process_model_identifier
)
ProcessModelService().update_process_model(
process_model = ProcessModelService.get_process_model(process_model_identifier)
ProcessModelService.update_process_model(
process_model,
{"fault_or_suspend_on_exception": NotificationType.suspend.value},
)
@ -1879,7 +1909,7 @@ class TestProcessApi(BaseTest):
assert process.status == "not_started"
response = client.post(
f"/v1.0/process-instances/{process_instance_id}/run",
f"/v1.0/process-instances/{self.modify_process_identifier_for_path_param(process_model_identifier)}/{process_instance_id}/run",
headers=self.logged_in_headers(with_super_admin_user),
)
assert response.status_code == 400
@ -1917,10 +1947,8 @@ class TestProcessApi(BaseTest):
client, process_model_identifier, with_super_admin_user
)
process_model = ProcessModelService().get_process_model(
process_model_identifier
)
ProcessModelService().update_process_model(
process_model = ProcessModelService.get_process_model(process_model_identifier)
ProcessModelService.update_process_model(
process_model,
{"exception_notification_addresses": ["with_super_admin_user@example.com"]},
)
@ -1929,7 +1957,7 @@ class TestProcessApi(BaseTest):
with mail.record_messages() as outbox:
response = client.post(
f"/v1.0/process-instances/{process_instance_id}/run",
f"/v1.0/process-instances/{self.modify_process_identifier_for_path_param(process_model_identifier)}/{process_instance_id}/run",
headers=self.logged_in_headers(with_super_admin_user),
)
assert response.status_code == 400
@ -2114,7 +2142,7 @@ class TestProcessApi(BaseTest):
assert response.json is not None
process_instance_id = response.json["id"]
response = client.post(
f"/v1.0/process-instances/{process_instance_id}/run",
f"/v1.0/process-instances/{self.modify_process_identifier_for_path_param(process_model_identifier)}/{process_instance_id}/run",
headers=self.logged_in_headers(initiator_user),
)
assert response.status_code == 200
@ -2319,7 +2347,7 @@ class TestProcessApi(BaseTest):
process_instance_id = response.json["id"]
client.post(
f"/v1.0/process-instances/{process_instance_id}/run",
f"/v1.0/process-instances/{self.modify_process_identifier_for_path_param(process_model_identifier)}/{process_instance_id}/run",
headers=self.logged_in_headers(with_super_admin_user),
)
@ -2339,7 +2367,7 @@ class TestProcessApi(BaseTest):
# TODO: Why can I run a suspended process instance?
response = client.post(
f"/v1.0/process-instances/{process_instance_id}/run",
f"/v1.0/process-instances/{self.modify_process_identifier_for_path_param(process_model_identifier)}/{process_instance_id}/run",
headers=self.logged_in_headers(with_super_admin_user),
)
@ -2408,7 +2436,7 @@ class TestProcessApi(BaseTest):
def setup_initial_groups_for_move_tests(
self, client: FlaskClient, with_super_admin_user: UserModel
) -> None:
"""setup_initial_groups_for_move_tests."""
"""Setup_initial_groups_for_move_tests."""
groups = ["group_a", "group_b", "group_b/group_bb"]
# setup initial groups
for group in groups:
@ -2417,7 +2445,7 @@ class TestProcessApi(BaseTest):
)
# make sure initial groups exist
for group in groups:
persisted = ProcessModelService().get_process_group(group)
persisted = ProcessModelService.get_process_group(group)
assert persisted is not None
assert persisted.id == group
@ -2428,7 +2456,7 @@ class TestProcessApi(BaseTest):
with_db_and_bpmn_file_cleanup: None,
with_super_admin_user: UserModel,
) -> None:
"""test_move_model."""
"""Test_move_model."""
self.setup_initial_groups_for_move_tests(client, with_super_admin_user)
process_model_id = "test_model"
@ -2443,7 +2471,7 @@ class TestProcessApi(BaseTest):
process_model_display_name=process_model_id,
process_model_description=process_model_id,
)
persisted = ProcessModelService().get_process_model(original_process_model_path)
persisted = ProcessModelService.get_process_model(original_process_model_path)
assert persisted is not None
assert persisted.id == original_process_model_path
@ -2463,11 +2491,11 @@ class TestProcessApi(BaseTest):
# make sure the original model does not exist
with pytest.raises(ProcessEntityNotFoundError) as e:
ProcessModelService().get_process_model(original_process_model_path)
ProcessModelService.get_process_model(original_process_model_path)
assert e.value.args[0] == "process_model_not_found"
# make sure the new model does exist
new_process_model = ProcessModelService().get_process_model(
new_process_model = ProcessModelService.get_process_model(
new_process_model_path
)
assert new_process_model is not None
@ -2480,7 +2508,7 @@ class TestProcessApi(BaseTest):
with_db_and_bpmn_file_cleanup: None,
with_super_admin_user: UserModel,
) -> None:
"""test_move_group."""
"""Test_move_group."""
self.setup_initial_groups_for_move_tests(client, with_super_admin_user)
# add sub group to `group_a`
@ -2491,7 +2519,7 @@ class TestProcessApi(BaseTest):
client, with_super_admin_user, original_sub_path, display_name=sub_group_id
)
# make sure original subgroup exists
persisted = ProcessModelService().get_process_group(original_sub_path)
persisted = ProcessModelService.get_process_group(original_sub_path)
assert persisted is not None
assert persisted.id == original_sub_path
@ -2508,11 +2536,11 @@ class TestProcessApi(BaseTest):
# make sure the original subgroup does not exist
with pytest.raises(ProcessEntityNotFoundError) as e:
ProcessModelService().get_process_group(original_sub_path)
ProcessModelService.get_process_group(original_sub_path)
assert e.value.args[0] == "process_group_not_found"
assert e.value.args[1] == f"Process Group Id: {original_sub_path}"
# make sure the new subgroup does exist
new_process_group = ProcessModelService().get_process_group(new_sub_path)
new_process_group = ProcessModelService.get_process_group(new_sub_path)
assert new_process_group.id == new_sub_path

View File

@ -52,7 +52,7 @@ class SecretServiceTestHelpers(BaseTest):
process_model_description=self.test_process_model_description,
user=user,
)
process_model_info = ProcessModelService().get_process_model(
process_model_info = ProcessModelService.get_process_model(
process_model_identifier
)
return process_model_info

View File

@ -0,0 +1,45 @@
"""Test_get_localtime."""
from flask.app import Flask
from flask.testing import FlaskClient
from tests.spiffworkflow_backend.helpers.base_test import BaseTest
from tests.spiffworkflow_backend.helpers.test_data import load_test_spec
from spiffworkflow_backend.models.process_instance_metadata import (
ProcessInstanceMetadataModel,
)
from spiffworkflow_backend.models.user import UserModel
from spiffworkflow_backend.services.process_instance_processor import (
ProcessInstanceProcessor,
)
class TestSaveProcessInstanceMetadata(BaseTest):
"""TestSaveProcessInstanceMetadata."""
def test_can_save_process_instance_metadata(
self,
app: Flask,
client: FlaskClient,
with_db_and_bpmn_file_cleanup: None,
with_super_admin_user: UserModel,
) -> None:
"""Test_can_save_process_instance_metadata."""
initiator_user = self.find_or_create_user("initiator_user")
self.create_process_group(
client, with_super_admin_user, "test_group", "test_group"
)
process_model = load_test_spec(
process_model_id="save_process_instance_metadata/save_process_instance_metadata",
bpmn_file_name="save_process_instance_metadata.bpmn",
process_model_source_directory="save_process_instance_metadata",
)
process_instance = self.create_process_instance_from_process_model(
process_model=process_model, user=initiator_user
)
processor = ProcessInstanceProcessor(process_instance)
processor.do_engine_steps(save=True)
process_instance_metadata = ProcessInstanceMetadataModel.query.filter_by(
process_instance_id=process_instance.id
).all()
assert len(process_instance_metadata) == 3

View File

@ -1,13 +1,38 @@
"""Test_acceptance_test_fixtures."""
import os
from flask.app import Flask
from spiffworkflow_backend.models.process_group import ProcessGroup
from spiffworkflow_backend.models.process_model import ProcessModelInfo
from spiffworkflow_backend.services.acceptance_test_fixtures import (
load_acceptance_test_fixtures,
)
from spiffworkflow_backend.services.process_model_service import ProcessModelService
def test_start_dates_are_one_hour_apart(app: Flask) -> None:
"""Test_start_dates_are_one_hour_apart."""
process_model_identifier = (
"misc/acceptance-tests-group-one/acceptance-tests-model-1"
)
group_identifier = os.path.dirname(process_model_identifier)
parent_group_identifier = os.path.dirname(group_identifier)
if not ProcessModelService.is_group(parent_group_identifier):
process_group = ProcessGroup(
id=parent_group_identifier, display_name=parent_group_identifier
)
ProcessModelService.add_process_group(process_group)
if not ProcessModelService.is_group(group_identifier):
process_group = ProcessGroup(id=group_identifier, display_name=group_identifier)
ProcessModelService.add_process_group(process_group)
if not ProcessModelService.is_model(process_model_identifier):
process_model = ProcessModelInfo(
id=process_model_identifier,
display_name=process_model_identifier,
description="hey",
)
ProcessModelService.add_process_model(process_model)
process_instances = load_acceptance_test_fixtures()
assert len(process_instances) > 2

View File

@ -113,7 +113,7 @@ class TestAuthorizationService(BaseTest):
bpmn_file_location="model_with_lanes",
)
process_model = ProcessModelService().get_process_model(
process_model = ProcessModelService.get_process_model(
process_model_id=process_model_identifier
)
process_instance = self.create_process_instance_from_process_model(

View File

@ -44,7 +44,7 @@ class TestMessageInstance(BaseTest):
client, with_super_admin_user
)
process_model = ProcessModelService().get_process_model(
process_model = ProcessModelService.get_process_model(
process_model_id=process_model_identifier
)
process_instance = self.create_process_instance_from_process_model(
@ -81,7 +81,7 @@ class TestMessageInstance(BaseTest):
client, with_super_admin_user
)
process_model = ProcessModelService().get_process_model(
process_model = ProcessModelService.get_process_model(
process_model_id=process_model_identifier
)
process_instance = self.create_process_instance_from_process_model(
@ -127,7 +127,7 @@ class TestMessageInstance(BaseTest):
client, with_super_admin_user
)
process_model = ProcessModelService().get_process_model(
process_model = ProcessModelService.get_process_model(
process_model_id=process_model_identifier
)
process_instance = self.create_process_instance_from_process_model(
@ -174,7 +174,7 @@ class TestMessageInstance(BaseTest):
client, with_super_admin_user
)
process_model = ProcessModelService().get_process_model(
process_model = ProcessModelService.get_process_model(
process_model_id=process_model_identifier
)
process_instance = self.create_process_instance_from_process_model(

View File

@ -47,7 +47,7 @@ class TestMessageService(BaseTest):
bpmn_file_name="message_sender.bpmn",
)
process_instance_sender = ProcessInstanceService.create_process_instance(
process_instance_sender = ProcessInstanceService.create_process_instance_from_process_model_identifier(
process_model_sender.id,
with_super_admin_user,
)
@ -154,7 +154,7 @@ class TestMessageService(BaseTest):
user = self.find_or_create_user()
process_instance_sender = ProcessInstanceService.create_process_instance(
process_instance_sender = ProcessInstanceService.create_process_instance_from_process_model_identifier(
process_model_sender.id,
user,
# process_group_identifier=process_model_sender.process_group_id,

View File

@ -9,8 +9,7 @@ def test_there_is_at_least_one_group_after_we_create_one(
app: Flask, with_db_and_bpmn_file_cleanup: None
) -> None:
"""Test_there_is_at_least_one_group_after_we_create_one."""
process_model_service = ProcessModelService()
process_group = ProcessGroup(id="hey", display_name="sure")
process_model_service.add_process_group(process_group)
process_groups = ProcessModelService().get_process_groups()
ProcessModelService.add_process_group(process_group)
process_groups = ProcessModelService.get_process_groups()
assert len(process_groups) > 0

View File

@ -161,6 +161,7 @@ class TestProcessInstanceProcessor(BaseTest):
)
processor = ProcessInstanceProcessor(process_instance)
processor.do_engine_steps(save=True)
processor.save()
assert len(process_instance.active_tasks) == 1
active_task = process_instance.active_tasks[0]
@ -241,3 +242,42 @@ class TestProcessInstanceProcessor(BaseTest):
)
assert process_instance.status == ProcessInstanceStatus.complete.value
def test_does_not_recreate_active_tasks_on_multiple_saves(
self,
app: Flask,
client: FlaskClient,
with_db_and_bpmn_file_cleanup: None,
with_super_admin_user: UserModel,
) -> None:
"""Test_sets_permission_correctly_on_active_task_when_using_dict."""
self.create_process_group(
client, with_super_admin_user, "test_group", "test_group"
)
initiator_user = self.find_or_create_user("initiator_user")
finance_user_three = self.find_or_create_user("testuser3")
assert initiator_user.principal is not None
assert finance_user_three.principal is not None
AuthorizationService.import_permissions_from_yaml_file()
finance_group = GroupModel.query.filter_by(identifier="Finance Team").first()
assert finance_group is not None
process_model = load_test_spec(
process_model_id="test_group/model_with_lanes",
bpmn_file_name="lanes_with_owner_dict.bpmn",
process_model_source_directory="model_with_lanes",
)
process_instance = self.create_process_instance_from_process_model(
process_model=process_model, user=initiator_user
)
processor = ProcessInstanceProcessor(process_instance)
processor.do_engine_steps(save=True)
assert len(process_instance.active_tasks) == 1
initial_active_task_id = process_instance.active_tasks[0].id
# save again to ensure we go attempt to process the active tasks again
processor.save()
assert len(process_instance.active_tasks) == 1
assert initial_active_task_id == process_instance.active_tasks[0].id

View File

@ -32,7 +32,7 @@ class TestProcessModelService(BaseTest):
primary_process_id = process_model.primary_process_id
assert primary_process_id == "Process_HelloWorld"
ProcessModelService().update_process_model(
ProcessModelService.update_process_model(
process_model, {"display_name": "new_name"}
)

View File

@ -188,7 +188,7 @@ class TestSpecFileService(BaseTest):
# ,
# process_model_source_directory="call_activity_nested",
# )
process_model_info = ProcessModelService().get_process_model(
process_model_info = ProcessModelService.get_process_model(
process_model_identifier
)
files = SpecFileService.get_files(process_model_info)

View File

@ -28,7 +28,7 @@ class TestVariousBpmnConstructs(BaseTest):
"timer_intermediate_catch_event",
)
process_model = ProcessModelService().get_process_model(
process_model = ProcessModelService.get_process_model(
process_model_id=process_model_identifier
)

View File

@ -19,18 +19,12 @@ describe('process-groups', () => {
cy.url().should('include', `process-groups/${groupId}`);
cy.contains(`Process Group: ${groupDisplayName}`);
cy.contains('Edit process group').click();
cy.getBySel('edit-process-group-button').click();
cy.get('input[name=display_name]').clear().type(newGroupDisplayName);
cy.contains('Submit').click();
cy.contains(`Process Group: ${newGroupDisplayName}`);
cy.contains('Edit process group').click();
cy.get('input[name=display_name]').should(
'have.value',
newGroupDisplayName
);
cy.contains('Delete').click();
cy.getBySel('delete-process-group-button').click();
cy.contains('Are you sure');
cy.getBySel('delete-process-group-button-modal-confirmation-dialog')
.find('.cds--btn--danger')

View File

@ -1,4 +1,4 @@
import { modifyProcessModelPath } from '../../src/helpers';
import { modifyProcessIdentifierForPathParam } from '../../src/helpers';
describe('process-models', () => {
beforeEach(() => {
@ -16,25 +16,22 @@ describe('process-models', () => {
const modelDisplayName = `Test Model 2 ${id}`;
const modelId = `test-model-2-${id}`;
const newModelDisplayName = `${modelDisplayName} edited`;
cy.contains('Misc').click();
cy.contains('99-Shared Resources').click();
cy.wait(500);
cy.contains(groupDisplayName).click();
cy.createModel(groupId, modelId, modelDisplayName);
cy.url().should(
'include',
`process-models/${modifyProcessModelPath(groupId)}:${modelId}`
`process-models/${modifyProcessIdentifierForPathParam(
groupId
)}:${modelId}`
);
cy.contains(`Process Model: ${modelDisplayName}`);
cy.contains('Edit process model').click();
cy.getBySel('edit-process-model-button').click();
cy.get('input[name=display_name]').clear().type(newModelDisplayName);
cy.contains('Submit').click();
cy.contains(`Process Model: ${groupId}/${modelId}`);
cy.contains('Submit').click();
cy.get('input[name=display_name]').should(
'have.value',
newModelDisplayName
);
cy.contains(`Process Model: ${newModelDisplayName}`);
// go back to process model show by clicking on the breadcrumb
cy.contains(modelId).click();
@ -46,7 +43,7 @@ describe('process-models', () => {
.click();
cy.url().should(
'include',
`process-groups/${modifyProcessModelPath(groupId)}`
`process-groups/${modifyProcessIdentifierForPathParam(groupId)}`
);
cy.contains(modelId).should('not.exist');
});
@ -64,15 +61,17 @@ describe('process-models', () => {
const dmnFileName = `dmn_test_file_${id}`;
const jsonFileName = `json_test_file_${id}`;
cy.contains('Misc').click();
cy.contains('99-Shared Resources').click();
cy.wait(500);
cy.contains(groupDisplayName).click();
cy.createModel(groupId, modelId, modelDisplayName);
cy.contains(directParentGroupId).click();
cy.contains(modelId).click();
cy.contains(modelDisplayName).click();
cy.url().should(
'include',
`process-models/${modifyProcessModelPath(groupId)}:${modelId}`
`process-models/${modifyProcessIdentifierForPathParam(
groupId
)}:${modelId}`
);
cy.contains(`Process Model: ${modelDisplayName}`);
cy.contains(`${bpmnFileName}.bpmn`).should('not.exist');
@ -135,8 +134,12 @@ describe('process-models', () => {
cy.getBySel('delete-process-model-button-modal-confirmation-dialog')
.find('.cds--btn--danger')
.click();
cy.url().should('include', `process-groups/${modifyProcessModelPath(groupId)}`);
cy.url().should(
'include',
`process-groups/${modifyProcessIdentifierForPathParam(groupId)}`
);
cy.contains(modelId).should('not.exist');
cy.contains(modelDisplayName).should('not.exist');
});
it('can upload and run a bpmn file', () => {
@ -148,17 +151,19 @@ describe('process-models', () => {
const modelDisplayName = `Test Model 2 ${id}`;
const modelId = `test-model-2-${id}`;
cy.contains('Add a process group');
cy.contains('Misc').click();
cy.contains('99-Shared Resources').click();
cy.wait(500);
cy.contains(groupDisplayName).click();
cy.createModel(groupId, modelId, modelDisplayName);
cy.contains(`${directParentGroupId}`).click();
cy.contains('Add a process model');
cy.contains(modelId).click();
cy.contains(modelDisplayName).click();
cy.url().should(
'include',
`process-models/${modifyProcessModelPath(groupId)}:${modelId}`
`process-models/${modifyProcessIdentifierForPathParam(
groupId
)}:${modelId}`
);
cy.contains(`Process Model: ${modelDisplayName}`);
@ -190,17 +195,19 @@ describe('process-models', () => {
.click();
cy.url().should(
'include',
`process-groups/${modifyProcessModelPath(groupId)}`
`process-groups/${modifyProcessIdentifierForPathParam(groupId)}`
);
cy.contains(modelId).should('not.exist');
cy.contains(modelDisplayName).should('not.exist');
});
it('can paginate items', () => {
cy.contains('Misc').click();
cy.wait(500);
cy.contains('Acceptance Tests Group One').click();
cy.basicPaginationTest();
});
// process models no longer has pagination post-tiles
// it.only('can paginate items', () => {
// cy.contains('99-Shared Resources').click();
// cy.wait(500);
// cy.contains('Acceptance Tests Group One').click();
// cy.basicPaginationTest();
// });
it('can allow searching for model', () => {
cy.getBySel('process-model-selection').click().type('model-3');

View File

@ -1,5 +1,5 @@
import { string } from 'prop-types';
import { modifyProcessModelPath } from '../../src/helpers';
import { modifyProcessIdentifierForPathParam } from '../../src/helpers';
// ***********************************************
// This example commands.js shows you how to
@ -78,8 +78,7 @@ Cypress.Commands.add('createModel', (groupId, modelId, modelDisplayName) => {
cy.url().should(
'include',
`process-models/${modifyProcessModelPath(groupId)}:${modelId}`
// `process-models/${groupId}:${modelId}`
`process-models/${modifyProcessIdentifierForPathParam(groupId)}:${modelId}`
);
cy.contains(`Process Model: ${modelDisplayName}`);
});
@ -104,12 +103,12 @@ Cypress.Commands.add(
'navigateToProcessModel',
(groupDisplayName, modelDisplayName, modelIdentifier) => {
cy.navigateToAdmin();
cy.contains('Misc').click();
cy.contains(`Process Group: 99-Misc`, { timeout: 10000 });
cy.contains('99-Shared Resources').click();
cy.contains(`Process Group: 99-Shared Resources`, { timeout: 10000 });
cy.contains(groupDisplayName).click();
cy.contains(`Process Group: ${groupDisplayName}`);
// https://stackoverflow.com/q/51254946/6090676
cy.getBySel('process-model-show-link').contains(modelIdentifier).click();
cy.getBySel('process-model-show-link').contains(modelDisplayName).click();
cy.contains(`Process Model: ${modelDisplayName}`);
}
);
@ -133,8 +132,3 @@ Cypress.Commands.add('assertAtLeastOneItemInPaginatedResults', () => {
Cypress.Commands.add('assertNoItemInPaginatedResults', () => {
cy.contains(/\b00 of 0 items/);
});
Cypress.Commands.add('modifyProcessModelPath', (path) => {
path.replace('/', ':');
return path;
});

View File

@ -7980,7 +7980,7 @@
},
"node_modules/bpmn-js-spiffworkflow": {
"version": "0.0.8",
"resolved": "git+ssh://git@github.com/sartography/bpmn-js-spiffworkflow.git#e92f48da7cb4416310af71bb1699caaca87324cd",
"resolved": "git+ssh://git@github.com/sartography/bpmn-js-spiffworkflow.git#aca23dc56e5d37aa1ed0a3cf11acb55f76a36da7",
"license": "MIT",
"dependencies": {
"inherits": "^2.0.4",
@ -37138,7 +37138,7 @@
}
},
"bpmn-js-spiffworkflow": {
"version": "git+ssh://git@github.com/sartography/bpmn-js-spiffworkflow.git#e92f48da7cb4416310af71bb1699caaca87324cd",
"version": "git+ssh://git@github.com/sartography/bpmn-js-spiffworkflow.git#aca23dc56e5d37aa1ed0a3cf11acb55f76a36da7",
"from": "bpmn-js-spiffworkflow@sartography/bpmn-js-spiffworkflow#main",
"requires": {
"inherits": "^2.0.4",

View File

@ -8,6 +8,8 @@ export default function MyCompletedInstances() {
filtersEnabled={false}
paginationQueryParamPrefix={paginationQueryParamPrefix}
perPageOptions={[2, 5, 25]}
reportIdentifier="system_report_instances_initiated_by_me"
showReports={false}
/>
);
}

View File

@ -163,6 +163,7 @@ export default function NavigationBar() {
</Can>
{configurationElement()}
<HeaderMenuItem
hidden
href="/admin/process-instances/reports"
isCurrentPage={isActivePage('/admin/process-instances/reports')}
>

View File

@ -14,6 +14,7 @@ type OwnProps = {
pagination: PaginationObject | null;
tableToDisplay: any;
paginationQueryParamPrefix?: string;
paginationClassName?: string;
};
export default function PaginationForTable({
@ -23,6 +24,7 @@ export default function PaginationForTable({
pagination,
tableToDisplay,
paginationQueryParamPrefix,
paginationClassName,
}: OwnProps) {
const PER_PAGE_OPTIONS = [2, 10, 50, 100];
const [searchParams, setSearchParams] = useSearchParams();
@ -44,6 +46,7 @@ export default function PaginationForTable({
<>
{tableToDisplay}
<Pagination
className={paginationClassName}
data-qa="pagination-options"
backwardText="Previous page"
forwardText="Next page"

View File

@ -3,13 +3,13 @@ import { BrowserRouter } from 'react-router-dom';
import ProcessBreadcrumb from './ProcessBreadcrumb';
test('renders home link', () => {
render(
<BrowserRouter>
<ProcessBreadcrumb />
</BrowserRouter>
);
const homeElement = screen.getByText(/Process Groups/);
expect(homeElement).toBeInTheDocument();
// render(
// <BrowserRouter>
// <ProcessBreadcrumb />
// </BrowserRouter>
// );
// const homeElement = screen.getByText(/Process Groups/);
// expect(homeElement).toBeInTheDocument();
});
test('renders hotCrumbs', () => {

View File

@ -1,123 +1,118 @@
// @ts-ignore
import { Breadcrumb, BreadcrumbItem } from '@carbon/react';
import { splitProcessModelId } from '../helpers';
import { HotCrumbItem } from '../interfaces';
import { useEffect, useState } from 'react';
import { modifyProcessIdentifierForPathParam } from '../helpers';
import {
HotCrumbItem,
ProcessGroup,
ProcessGroupLite,
ProcessModel,
} from '../interfaces';
import HttpService from '../services/HttpService';
type OwnProps = {
processModelId?: string;
processGroupId?: string;
linkProcessModel?: boolean;
hotCrumbs?: HotCrumbItem[];
};
const explodeCrumb = (crumb: HotCrumbItem) => {
const url: string = crumb[1] || '';
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [endingUrlType, processModelId, link] = url.split(':');
const processModelIdSegments = splitProcessModelId(processModelId);
const paths: string[] = [];
const lastPathItem = processModelIdSegments.pop();
const breadcrumbItems = processModelIdSegments.map(
(processModelIdSegment: string) => {
paths.push(processModelIdSegment);
const fullUrl = `/admin/process-groups/${paths.join(':')}`;
return (
<BreadcrumbItem key={processModelIdSegment} href={fullUrl}>
{processModelIdSegment}
</BreadcrumbItem>
);
}
);
if (link === 'link') {
if (lastPathItem !== undefined) {
paths.push(lastPathItem);
}
// process_model to process-models
const lastUrl = `/admin/${endingUrlType
.replace('_', '-')
.replace(/s*$/, 's')}/${paths.join(':')}`;
breadcrumbItems.push(
<BreadcrumbItem key={lastPathItem} href={lastUrl}>
{lastPathItem}
</BreadcrumbItem>
);
} else {
breadcrumbItems.push(
<BreadcrumbItem isCurrentPage key={lastPathItem}>
{lastPathItem}
</BreadcrumbItem>
);
}
return breadcrumbItems;
};
export default function ProcessBreadcrumb({ hotCrumbs }: OwnProps) {
const [processEntity, setProcessEntity] = useState<
ProcessGroup | ProcessModel | null
>(null);
export default function ProcessBreadcrumb({
processModelId,
processGroupId,
hotCrumbs,
linkProcessModel = false,
}: OwnProps) {
let processGroupBreadcrumb = null;
let processModelBreadcrumb = null;
if (hotCrumbs) {
const leadingCrumbLinks = hotCrumbs.map((crumb: any) => {
const valueLabel = crumb[0];
const url = crumb[1];
if (!url) {
return (
<BreadcrumbItem isCurrentPage key={valueLabel}>
{valueLabel}
</BreadcrumbItem>
);
useEffect(() => {
const explodeCrumbItemObject = (crumb: HotCrumbItem) => {
if ('entityToExplode' in crumb) {
const { entityToExplode, entityType } = crumb;
if (entityType === 'process-model-id') {
HttpService.makeCallToBackend({
path: `/process-models/${modifyProcessIdentifierForPathParam(
entityToExplode as string
)}`,
successCallback: setProcessEntity,
});
} else if (entityType === 'process-group-id') {
HttpService.makeCallToBackend({
path: `/process-groups/${modifyProcessIdentifierForPathParam(
entityToExplode as string
)}`,
successCallback: setProcessEntity,
});
} else {
setProcessEntity(entityToExplode as any);
}
}
if (url && url.match(/^process[_-](model|group)s?:/)) {
return explodeCrumb(crumb);
}
return (
<BreadcrumbItem key={valueLabel} href={url}>
{valueLabel}
</BreadcrumbItem>
);
});
return <Breadcrumb noTrailingSlash>{leadingCrumbLinks}</Breadcrumb>;
}
if (processModelId) {
if (linkProcessModel) {
processModelBreadcrumb = (
<BreadcrumbItem
href={`/admin/process-models/${processGroupId}/${processModelId}`}
>
{`Process Model: ${processModelId}`}
</BreadcrumbItem>
);
} else {
processModelBreadcrumb = (
<BreadcrumbItem isCurrentPage>
{`Process Model: ${processModelId}`}
</BreadcrumbItem>
);
};
if (hotCrumbs) {
hotCrumbs.forEach(explodeCrumbItemObject);
}
processGroupBreadcrumb = (
<BreadcrumbItem
data-qa="process-group-breadcrumb-link"
href={`/admin/process-groups/${processGroupId}`}
>
{`Process Group: ${processGroupId}`}
</BreadcrumbItem>
);
} else if (processGroupId) {
processGroupBreadcrumb = (
<BreadcrumbItem isCurrentPage>
{`Process Group: ${processGroupId}`}
</BreadcrumbItem>
);
}
}, [setProcessEntity, hotCrumbs]);
return (
<Breadcrumb noTrailingSlash>
<BreadcrumbItem href="/admin">Process Groups</BreadcrumbItem>
{processGroupBreadcrumb}
{processModelBreadcrumb}
</Breadcrumb>
);
// eslint-disable-next-line sonarjs/cognitive-complexity
const hotCrumbElement = () => {
if (hotCrumbs) {
const leadingCrumbLinks = hotCrumbs.map((crumb: any) => {
if (
'entityToExplode' in crumb &&
processEntity &&
processEntity.parent_groups
) {
const breadcrumbs = processEntity.parent_groups.map(
(parentGroup: ProcessGroupLite) => {
const fullUrl = `/admin/process-groups/${modifyProcessIdentifierForPathParam(
parentGroup.id
)}`;
return (
<BreadcrumbItem key={parentGroup.id} href={fullUrl}>
{parentGroup.display_name}
</BreadcrumbItem>
);
}
);
if (crumb.linkLastItem) {
let apiBase = '/admin/process-groups';
if (crumb.entityType.startsWith('process-model')) {
apiBase = '/admin/process-models';
}
const fullUrl = `${apiBase}/${modifyProcessIdentifierForPathParam(
processEntity.id
)}`;
breadcrumbs.push(
<BreadcrumbItem key={processEntity.id} href={fullUrl}>
{processEntity.display_name}
</BreadcrumbItem>
);
} else {
breadcrumbs.push(
<BreadcrumbItem key={processEntity.id} isCurrentPage>
{processEntity.display_name}
</BreadcrumbItem>
);
}
return breadcrumbs;
}
const valueLabel = crumb[0];
const url = crumb[1];
if (!url && valueLabel) {
return (
<BreadcrumbItem isCurrentPage key={valueLabel}>
{valueLabel}
</BreadcrumbItem>
);
}
if (url && valueLabel) {
return (
<BreadcrumbItem key={valueLabel} href={url}>
{valueLabel}
</BreadcrumbItem>
);
}
return null;
});
return <Breadcrumb noTrailingSlash>{leadingCrumbLinks}</Breadcrumb>;
}
return null;
};
return <Breadcrumb noTrailingSlash>{hotCrumbElement()}</Breadcrumb>;
}

View File

@ -5,7 +5,6 @@ import { Button, ButtonSet, Form, Stack, TextInput } from '@carbon/react';
import { modifyProcessIdentifierForPathParam, slugifyString } from '../helpers';
import HttpService from '../services/HttpService';
import { ProcessGroup } from '../interfaces';
import ButtonWithConfirmation from './ButtonWithConfirmation';
type OwnProps = {
mode: string;
@ -35,24 +34,10 @@ export default function ProcessGroupForm({
}
};
const navigateToProcessGroups = (_result: any) => {
navigate(`/admin/process-groups`);
};
const hasValidIdentifier = (identifierToCheck: string) => {
return identifierToCheck.match(/^[a-z0-9][0-9a-z-]+[a-z0-9]$/);
};
const deleteProcessGroup = () => {
HttpService.makeCallToBackend({
path: `/process-groups/${modifyProcessIdentifierForPathParam(
processGroup.id
)}`,
successCallback: navigateToProcessGroups,
httpMethod: 'DELETE',
});
};
const handleFormSubmission = (event: any) => {
const searchParams = new URLSearchParams(document.location.search);
const parentGroupId = searchParams.get('parentGroupId');
@ -172,17 +157,6 @@ export default function ProcessGroupForm({
const formButtons = () => {
const buttons = [<Button type="submit">Submit</Button>];
if (mode === 'edit') {
buttons.push(
<ButtonWithConfirmation
data-qa="delete-process-group-button"
description={`Delete Process Group ${processGroup.id}?`}
onConfirmation={deleteProcessGroup}
buttonLabel="Delete"
confirmButtonLabel="Delete"
/>
);
}
return <ButtonSet>{buttons}</ButtonSet>;
};

View File

@ -33,6 +33,7 @@ import {
getPageInfoFromSearchParams,
getProcessModelFullIdentifierFromSearchParams,
modifyProcessIdentifierForPathParam,
refreshAtInterval,
} from '../helpers';
import PaginationForTable from './PaginationForTable';
@ -47,15 +48,24 @@ import {
PaginationObject,
ProcessModel,
ProcessInstanceReport,
ProcessInstance,
} from '../interfaces';
import ProcessModelSearch from './ProcessModelSearch';
import ProcessInstanceReportSearch from './ProcessInstanceReportSearch';
const REFRESH_INTERVAL = 5;
const REFRESH_TIMEOUT = 600;
type OwnProps = {
filtersEnabled?: boolean;
processModelFullIdentifier?: string;
paginationQueryParamPrefix?: string;
perPageOptions?: number[];
showReports?: boolean;
reportIdentifier?: string;
textToShowIfEmpty?: string;
paginationClassName?: string;
autoReload?: boolean;
};
interface dateParameters {
@ -67,6 +77,11 @@ export default function ProcessInstanceListTable({
processModelFullIdentifier,
paginationQueryParamPrefix,
perPageOptions,
showReports = true,
reportIdentifier,
textToShowIfEmpty,
paginationClassName,
autoReload = false,
}: OwnProps) {
const params = useParams();
const [searchParams] = useSearchParams();
@ -171,9 +186,14 @@ export default function ProcessInstanceListTable({
queryParamString += `&user_filter=${userAppliedFilter}`;
}
const reportIdentifier = searchParams.get('report_identifier');
if (reportIdentifier) {
queryParamString += `&report_identifier=${reportIdentifier}`;
let reportIdentifierToUse: any = reportIdentifier;
if (!reportIdentifierToUse) {
reportIdentifierToUse = searchParams.get('report_identifier');
}
if (reportIdentifierToUse) {
queryParamString += `&report_identifier=${reportIdentifierToUse}`;
}
Object.keys(dateParametersToAlwaysFilterBy).forEach(
@ -250,17 +270,24 @@ export default function ProcessInstanceListTable({
getProcessInstances();
}
const checkFiltersAndRun = () => {
if (filtersEnabled) {
// populate process model selection
HttpService.makeCallToBackend({
path: `/process-models?per_page=1000&recursive=true`,
successCallback: processResultForProcessModels,
});
} else {
getProcessInstances();
}
};
if (filtersEnabled) {
// populate process model selection
HttpService.makeCallToBackend({
path: `/process-models?per_page=1000&recursive=true`,
successCallback: processResultForProcessModels,
});
} else {
getProcessInstances();
checkFiltersAndRun();
if (autoReload) {
refreshAtInterval(REFRESH_INTERVAL, REFRESH_TIMEOUT, checkFiltersAndRun);
}
}, [
autoReload,
searchParams,
params,
oneMonthInSeconds,
@ -271,6 +298,7 @@ export default function ProcessInstanceListTable({
paginationQueryParamPrefix,
processModelFullIdentifier,
perPageOptions,
reportIdentifier,
]);
// This sets the filter data using the saved reports returned from the initial instance_list query.
@ -596,10 +624,12 @@ export default function ProcessInstanceListTable({
const buildTable = () => {
const headerLabels: Record<string, string> = {
id: 'Id',
process_model_identifier: 'Process Model',
process_model_identifier: 'Process',
process_model_display_name: 'Process',
start_in_seconds: 'Start Time',
end_in_seconds: 'End Time',
status: 'Status',
username: 'Started By',
spiff_step: 'SpiffWorkflow Step',
};
const getHeaderLabel = (header: string) => {
@ -610,13 +640,14 @@ export default function ProcessInstanceListTable({
return getHeaderLabel((column as any).Header);
});
const formatProcessInstanceId = (row: any, id: any) => {
const formatProcessInstanceId = (row: ProcessInstance, id: number) => {
const modifiedProcessModelId: String =
modifyProcessIdentifierForPathParam(row.process_model_identifier);
return (
<Link
data-qa="process-instance-show-link"
to={`/admin/process-models/${modifiedProcessModelId}/process-instances/${row.id}`}
to={`/admin/process-models/${modifiedProcessModelId}/process-instances/${id}`}
title={`View process instance ${id}`}
>
{id}
</Link>
@ -633,6 +664,23 @@ export default function ProcessInstanceListTable({
</Link>
);
};
const formatProcessModelDisplayName = (
row: ProcessInstance,
displayName: string
) => {
return (
<Link
to={`/admin/process-models/${modifyProcessIdentifierForPathParam(
row.process_model_identifier
)}`}
title={row.process_model_identifier}
>
{displayName}
</Link>
);
};
const formatSecondsForDisplay = (_row: any, seconds: any) => {
return convertSecondsToFormattedDateTime(seconds) || '-';
};
@ -643,6 +691,7 @@ export default function ProcessInstanceListTable({
const columnFormatters: Record<string, any> = {
id: formatProcessInstanceId,
process_model_identifier: formatProcessModelIdentifier,
process_model_display_name: formatProcessModelDisplayName,
start_in_seconds: formatSecondsForDisplay,
end_in_seconds: formatSecondsForDisplay,
};
@ -704,12 +753,15 @@ export default function ProcessInstanceListTable({
};
const reportSearchComponent = () => {
return (
<ProcessInstanceReportSearch
onChange={processInstanceReportDidChange}
selectedItem={processInstanceReportSelection}
/>
);
if (showReports) {
return (
<ProcessInstanceReportSearch
onChange={processInstanceReportDidChange}
selectedItem={processInstanceReportSelection}
/>
);
}
return null;
};
const filterComponent = () => {
@ -720,13 +772,13 @@ export default function ProcessInstanceListTable({
<>
<Grid fullWidth>
<Column
className="filterIcon"
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
@ -740,7 +792,7 @@ export default function ProcessInstanceListTable({
);
};
if (pagination) {
if (pagination && (!textToShowIfEmpty || pagination.total > 0)) {
// eslint-disable-next-line prefer-const
let { page, perPage } = getPageInfoFromSearchParams(
searchParams,
@ -763,10 +815,18 @@ export default function ProcessInstanceListTable({
tableToDisplay={buildTable()}
paginationQueryParamPrefix={paginationQueryParamPrefix}
perPageOptions={perPageOptions}
paginationClassName={paginationClassName}
/>
</>
);
}
if (textToShowIfEmpty) {
return (
<p className="no-results-message with-large-bottom-margin">
{textToShowIfEmpty}
</p>
);
}
return null;
}

View File

@ -4,21 +4,78 @@ import {
Button,
// @ts-ignore
} from '@carbon/react';
import { ProcessModel } from '../interfaces';
import { Can } from '@casl/react';
import {
PermissionsToCheck,
ProcessModel,
RecentProcessModel,
} from '../interfaces';
import HttpService from '../services/HttpService';
import ErrorContext from '../contexts/ErrorContext';
import { modifyProcessIdentifierForPathParam } from '../helpers';
import { usePermissionFetcher } from '../hooks/PermissionService';
const storeRecentProcessModelInLocalStorage = (
processModelForStorage: ProcessModel
) => {
// All values stored in localStorage are strings.
// Grab our recentProcessModels string from localStorage.
const stringFromLocalStorage = window.localStorage.getItem(
'recentProcessModels'
);
// adapted from https://stackoverflow.com/a/59424458/6090676
// If that value is null (meaning that we've never saved anything to that spot in localStorage before), use an empty array as our array. Otherwise, use the value we parse out.
let array: RecentProcessModel[] = [];
if (stringFromLocalStorage !== null) {
// Then parse that string into an actual value.
array = JSON.parse(stringFromLocalStorage);
}
// Here's the value we want to add
const value = {
processModelIdentifier: processModelForStorage.id,
processModelDisplayName: processModelForStorage.display_name,
};
// anything with a processGroupIdentifier is old and busted. leave it behind.
array = array.filter((item) => item.processGroupIdentifier === undefined);
// If our parsed/empty array doesn't already have this value in it...
const matchingItem = array.find(
(item) => item.processModelIdentifier === value.processModelIdentifier
);
if (matchingItem === undefined) {
// add the value to the beginning of the array
array.unshift(value);
// Keep the array to 3 items
if (array.length > 3) {
array.pop();
}
}
// once the old and busted serializations are gone, we can put these two statements inside the above if statement
// turn the array WITH THE NEW VALUE IN IT into a string to prepare it to be stored in localStorage
const stringRepresentingArray = JSON.stringify(array);
// and store it in localStorage as "recentProcessModels"
window.localStorage.setItem('recentProcessModels', stringRepresentingArray);
};
type OwnProps = {
processModel: ProcessModel;
onSuccessCallback: Function;
className?: string;
checkPermissions?: boolean;
};
export default function ProcessInstanceRun({
processModel,
onSuccessCallback,
className,
checkPermissions = true,
}: OwnProps) {
const navigate = useNavigate();
const setErrorMessage = (useContext as any)(ErrorContext)[1];
@ -26,6 +83,17 @@ export default function ProcessInstanceRun({
processModel.id
);
const processInstanceActionPath = `/v1.0/process-models/${modifiedProcessModelId}/process-instances`;
let permissionRequestData: PermissionsToCheck = {
[processInstanceActionPath]: ['POST'],
};
if (!checkPermissions) {
permissionRequestData = {};
}
const { ability } = usePermissionFetcher(permissionRequestData);
const onProcessInstanceRun = (processInstance: any) => {
// FIXME: ensure that the task is actually for the current user as well
const processInstanceId = (processInstance as any).id;
@ -38,8 +106,9 @@ export default function ProcessInstanceRun({
const processModelRun = (processInstance: any) => {
setErrorMessage(null);
storeRecentProcessModelInLocalStorage(processModel);
HttpService.makeCallToBackend({
path: `/process-instances/${processInstance.id}/run`,
path: `/process-instances/${modifiedProcessModelId}/${processInstance.id}/run`,
successCallback: onProcessInstanceRun,
failureCallback: setErrorMessage,
httpMethod: 'POST',
@ -48,19 +117,23 @@ export default function ProcessInstanceRun({
const processInstanceCreateAndRun = () => {
HttpService.makeCallToBackend({
path: `/process-models/${modifiedProcessModelId}/process-instances`,
path: processInstanceActionPath,
successCallback: processModelRun,
httpMethod: 'POST',
});
};
if (checkPermissions) {
return (
<Can I="POST" a={processInstanceActionPath} ability={ability}>
<Button onClick={processInstanceCreateAndRun} className={className}>
Start
</Button>
</Can>
);
}
return (
<Button
onClick={processInstanceCreateAndRun}
variant="primary"
className={className}
>
Run
<Button onClick={processInstanceCreateAndRun} className={className}>
Start
</Button>
);
}

View File

@ -51,12 +51,15 @@ export default function ProcessModelForm({
if (hasErrors) {
return;
}
const path = `/process-models/${modifyProcessIdentifierForPathParam(
let path = `/process-models/${modifyProcessIdentifierForPathParam(
processGroupId || ''
)}`;
let httpMethod = 'POST';
if (mode === 'edit') {
httpMethod = 'PUT';
path = `/process-models/${modifyProcessIdentifierForPathParam(
processModel.id
)}`;
}
const postBody = {
display_name: processModel.display_name,

View File

@ -15,11 +15,13 @@ import ProcessInstanceRun from './ProcessInstanceRun';
type OwnProps = {
headerElement?: ReactElement;
processGroup?: ProcessGroup;
checkPermissions?: boolean;
};
export default function ProcessModelListTiles({
headerElement,
processGroup,
checkPermissions = true,
}: OwnProps) {
const [searchParams] = useSearchParams();
const [processModels, setProcessModels] = useState<ProcessModel[] | null>(
@ -33,9 +35,11 @@ export default function ProcessModelListTiles({
setProcessModels(result.results);
};
// only allow 10 for now until we get the backend only returning certain models for user execution
let queryParams = '?per_page=100';
let queryParams = '?per_page=20';
if (processGroup) {
queryParams = `${queryParams}&process_group_identifier=${processGroup.id}`;
} else {
queryParams = `${queryParams}&recursive=true&filter_runnable_by_user=true`;
}
HttpService.makeCallToBackend({
path: `/process-models${queryParams}`,
@ -73,12 +77,19 @@ export default function ProcessModelListTiles({
<Tile
id={`process-model-tile-${row.id}`}
className="tile-process-group"
href={`/admin/process-models/${modifyProcessIdentifierForPathParam(
row.id
)}`}
>
<div className="tile-process-group-content-container">
<div className="tile-title-top">{row.display_name}</div>
<div className="tile-title-top">
<a
title={row.id}
data-qa="process-model-show-link"
href={`/admin/process-models/${modifyProcessIdentifierForPathParam(
row.id
)}`}
>
{row.display_name}
</a>
</div>
<p className="tile-description">
{truncateString(row.description || '', 100)}
</p>
@ -86,6 +97,7 @@ export default function ProcessModelListTiles({
processModel={row}
onSuccessCallback={setProcessInstance}
className="tile-pin-bottom"
checkPermissions={checkPermissions}
/>
</div>
</Tile>

View File

@ -569,7 +569,7 @@ export default function ReactDiagramEditor({
a={targetUris.processModelFileShowPath}
ability={ability}
>
<Button onClick={downloadXmlFile}>Download xml</Button>
<Button onClick={downloadXmlFile}>Download</Button>
</Can>
</>
);

View File

@ -0,0 +1,17 @@
// @ts-ignore
import { TimeAgo } from '../helpers/timeago';
import { convertSecondsToFormattedDateTime } from '../helpers';
type OwnProps = {
timeInSeconds: number;
};
export default function TableCellWithTimeAgoInWords({
timeInSeconds,
}: OwnProps) {
return (
<td title={convertSecondsToFormattedDateTime(timeInSeconds) || '-'}>
{timeInSeconds ? TimeAgo.inWords(timeInSeconds) : '-'}
</td>
);
}

View File

@ -7,12 +7,16 @@ import {
convertSecondsToFormattedDateTime,
getPageInfoFromSearchParams,
modifyProcessIdentifierForPathParam,
refreshAtInterval,
} from '../helpers';
import HttpService from '../services/HttpService';
import { PaginationObject } from '../interfaces';
import TableCellWithTimeAgoInWords from './TableCellWithTimeAgoInWords';
const PER_PAGE_FOR_TASKS_ON_HOME_PAGE = 5;
const paginationQueryParamPrefix = 'tasks_for_my_open_processes';
const REFRESH_INTERVAL = 5;
const REFRESH_TIMEOUT = 600;
export default function MyOpenProcesses() {
const [searchParams] = useSearchParams();
@ -20,20 +24,24 @@ export default function MyOpenProcesses() {
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);
const getTasks = () => {
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-open-processes?per_page=${perPage}&page=${page}`,
successCallback: setTasksFromResult,
});
};
HttpService.makeCallToBackend({
path: `/tasks/for-my-open-processes?per_page=${perPage}&page=${page}`,
successCallback: setTasksFromResult,
});
getTasks();
refreshAtInterval(REFRESH_INTERVAL, REFRESH_TIMEOUT, getTasks);
}, [searchParams]);
const buildTable = () => {
@ -46,18 +54,20 @@ export default function MyOpenProcesses() {
<tr key={rowToUse.id}>
<td>
<Link
data-qa="process-model-show-link"
to={`/admin/process-models/${modifiedProcessModelIdentifier}`}
data-qa="process-instance-show-link"
to={`/admin/process-models/${modifiedProcessModelIdentifier}/process-instances/${rowToUse.process_instance_id}`}
title={`View process instance ${rowToUse.process_instance_id}`}
>
{rowToUse.process_model_display_name}
{rowToUse.process_instance_id}
</Link>
</td>
<td>
<Link
data-qa="process-instance-show-link"
to={`/admin/process-models/${modifiedProcessModelIdentifier}/process-instances/${rowToUse.process_instance_id}`}
data-qa="process-model-show-link"
to={`/admin/process-models/${modifiedProcessModelIdentifier}`}
title={rowToUse.process_model_identifier}
>
View {rowToUse.process_instance_id}
{rowToUse.process_model_display_name}
</Link>
</td>
<td
@ -65,18 +75,15 @@ export default function MyOpenProcesses() {
>
{rowToUse.task_title}
</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>
<TableCellWithTimeAgoInWords
timeInSeconds={rowToUse.updated_at_in_seconds}
/>
<td>
<Button
variant="primary"
@ -94,13 +101,12 @@ export default function MyOpenProcesses() {
<Table striped bordered>
<thead>
<tr>
<th>Process Model</th>
<th>Process Instance</th>
<th>Task Name</th>
<th>Process Instance Status</th>
<th>Assigned Group</th>
<th>Process Started</th>
<th>Process Updated</th>
<th>Id</th>
<th>Process</th>
<th>Task</th>
<th>Waiting For</th>
<th>Date Started</th>
<th>Last Updated</th>
<th>Actions</th>
</tr>
</thead>
@ -111,7 +117,11 @@ export default function MyOpenProcesses() {
const tasksComponent = () => {
if (pagination && pagination.total < 1) {
return null;
return (
<p className="no-results-message with-large-bottom-margin">
There are no tasks for processes you started at this time.
</p>
);
}
const { page, perPage } = getPageInfoFromSearchParams(
searchParams,
@ -120,22 +130,27 @@ export default function MyOpenProcesses() {
paginationQueryParamPrefix
);
return (
<>
<h1>Tasks for my open processes</h1>
<PaginationForTable
page={page}
perPage={perPage}
perPageOptions={[2, PER_PAGE_FOR_TASKS_ON_HOME_PAGE, 25]}
pagination={pagination}
tableToDisplay={buildTable()}
paginationQueryParamPrefix={paginationQueryParamPrefix}
/>
</>
<PaginationForTable
page={page}
perPage={perPage}
perPageOptions={[2, PER_PAGE_FOR_TASKS_ON_HOME_PAGE, 25]}
pagination={pagination}
tableToDisplay={buildTable()}
paginationQueryParamPrefix={paginationQueryParamPrefix}
paginationClassName="with-large-bottom-margin"
/>
);
};
if (pagination) {
return tasksComponent();
}
return null;
return (
<>
<h2>My open instances</h2>
<p className="data-table-description">
These tasks are for processes you started which are not complete. You
may not have an action to take at this time. See below for tasks waiting
on you.
</p>
{tasksComponent()}
</>
);
}

View File

@ -10,6 +10,7 @@ import {
} from '../helpers';
import HttpService from '../services/HttpService';
import { PaginationObject } from '../interfaces';
import TableCellWithTimeAgoInWords from './TableCellWithTimeAgoInWords';
const PER_PAGE_FOR_TASKS_ON_HOME_PAGE = 5;
@ -45,18 +46,20 @@ export default function TasksWaitingForMe() {
<tr key={rowToUse.id}>
<td>
<Link
data-qa="process-model-show-link"
to={`/admin/process-models/${modifiedProcessModelIdentifier}`}
data-qa="process-instance-show-link"
to={`/admin/process-models/${modifiedProcessModelIdentifier}/process-instances/${rowToUse.process_instance_id}`}
title={`View process instance ${rowToUse.process_instance_id}`}
>
{rowToUse.process_model_display_name}
{rowToUse.process_instance_id}
</Link>
</td>
<td>
<Link
data-qa="process-instance-show-link"
to={`/admin/process-models/${modifiedProcessModelIdentifier}/process-instances/${rowToUse.process_instance_id}`}
data-qa="process-model-show-link"
to={`/admin/process-models/${modifiedProcessModelIdentifier}`}
title={rowToUse.process_model_identifier}
>
View {rowToUse.process_instance_id}
{rowToUse.process_model_display_name}
</Link>
</td>
<td
@ -65,18 +68,15 @@ export default function TasksWaitingForMe() {
{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>
<TableCellWithTimeAgoInWords
timeInSeconds={rowToUse.updated_at_in_seconds}
/>
<td>
<Button
variant="primary"
@ -94,14 +94,13 @@ export default function TasksWaitingForMe() {
<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>Id</th>
<th>Process</th>
<th>Task</th>
<th>Started By</th>
<th>Waiting For</th>
<th>Date Started</th>
<th>Last Updated</th>
<th>Actions</th>
</tr>
</thead>
@ -112,7 +111,11 @@ export default function TasksWaitingForMe() {
const tasksComponent = () => {
if (pagination && pagination.total < 1) {
return null;
return (
<p className="no-results-message with-large-bottom-margin">
You have no task assignments at this time.
</p>
);
}
const { page, perPage } = getPageInfoFromSearchParams(
searchParams,
@ -121,22 +124,26 @@ export default function TasksWaitingForMe() {
'tasks_waiting_for_me'
);
return (
<>
<h1>Tasks waiting for me</h1>
<PaginationForTable
page={page}
perPage={perPage}
perPageOptions={[2, PER_PAGE_FOR_TASKS_ON_HOME_PAGE, 25]}
pagination={pagination}
tableToDisplay={buildTable()}
paginationQueryParamPrefix="tasks_waiting_for_me"
/>
</>
<PaginationForTable
page={page}
perPage={perPage}
perPageOptions={[2, PER_PAGE_FOR_TASKS_ON_HOME_PAGE, 25]}
pagination={pagination}
tableToDisplay={buildTable()}
paginationQueryParamPrefix="tasks_waiting_for_me"
paginationClassName="with-large-bottom-margin"
/>
);
};
if (pagination) {
return tasksComponent();
}
return null;
return (
<>
<h2>Tasks waiting for me</h2>
<p className="data-table-description">
These processes are waiting on you to complete the next task. All are
processes created by others that are now actionable by you.
</p>
{tasksComponent()}
</>
);
}

View File

@ -7,33 +7,41 @@ import {
convertSecondsToFormattedDateTime,
getPageInfoFromSearchParams,
modifyProcessIdentifierForPathParam,
refreshAtInterval,
} from '../helpers';
import HttpService from '../services/HttpService';
import { PaginationObject } from '../interfaces';
import TableCellWithTimeAgoInWords from './TableCellWithTimeAgoInWords';
const PER_PAGE_FOR_TASKS_ON_HOME_PAGE = 5;
const paginationQueryParamPrefix = 'tasks_waiting_for_my_groups';
const REFRESH_INTERVAL = 5;
const REFRESH_TIMEOUT = 600;
export default function TasksForWaitingForMyGroups() {
export default function TasksWaitingForMyGroups() {
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);
const getTasks = () => {
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,
});
};
HttpService.makeCallToBackend({
path: `/tasks/for-my-groups?per_page=${perPage}&page=${page}`,
successCallback: setTasksFromResult,
});
getTasks();
refreshAtInterval(REFRESH_INTERVAL, REFRESH_TIMEOUT, getTasks);
}, [searchParams]);
const buildTable = () => {
@ -46,18 +54,20 @@ export default function TasksForWaitingForMyGroups() {
<tr key={rowToUse.id}>
<td>
<Link
data-qa="process-model-show-link"
to={`/admin/process-models/${modifiedProcessModelIdentifier}`}
data-qa="process-instance-show-link"
to={`/admin/process-models/${modifiedProcessModelIdentifier}/process-instances/${rowToUse.process_instance_id}`}
title={`View process instance ${rowToUse.process_instance_id}`}
>
{rowToUse.process_model_display_name}
{rowToUse.process_instance_id}
</Link>
</td>
<td>
<Link
data-qa="process-instance-show-link"
to={`/admin/process-models/${modifiedProcessModelIdentifier}/process-instances/${rowToUse.process_instance_id}`}
data-qa="process-model-show-link"
to={`/admin/process-models/${modifiedProcessModelIdentifier}`}
title={rowToUse.process_model_identifier}
>
View {rowToUse.process_instance_id}
{rowToUse.process_model_display_name}
</Link>
</td>
<td
@ -66,18 +76,15 @@ export default function TasksForWaitingForMyGroups() {
{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>
<TableCellWithTimeAgoInWords
timeInSeconds={rowToUse.updated_at_in_seconds}
/>
<td>
<Button
variant="primary"
@ -95,14 +102,13 @@ export default function TasksForWaitingForMyGroups() {
<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>Id</th>
<th>Process</th>
<th>Task</th>
<th>Started By</th>
<th>Waiting For</th>
<th>Date Started</th>
<th>Last Updated</th>
<th>Actions</th>
</tr>
</thead>
@ -113,7 +119,11 @@ export default function TasksForWaitingForMyGroups() {
const tasksComponent = () => {
if (pagination && pagination.total < 1) {
return null;
return (
<p className="no-results-message">
Your groups have no task assignments at this time.
</p>
);
}
const { page, perPage } = getPageInfoFromSearchParams(
searchParams,
@ -122,22 +132,25 @@ export default function TasksForWaitingForMyGroups() {
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}
/>
</>
<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;
return (
<>
<h2>Tasks waiting for my groups</h2>
<p className="data-table-description">
This is a list of tasks for groups you belong to that can be completed
by any member of the group.
</p>
{tasksComponent()}
</>
);
}

View File

@ -0,0 +1,62 @@
/* eslint-disable no-restricted-syntax */
// https://gist.github.com/caiotarifa/30ae974f2293c761f3139dd194abd9e5
export const TimeAgo = (function awesomeFunc() {
const self = {};
// Public Methods
self.locales = {
prefix: '',
sufix: 'ago',
seconds: 'less than a minute',
minute: 'about a minute',
minutes: '%d minutes',
hour: 'about an hour',
hours: 'about %d hours',
day: 'a day',
days: '%d days',
month: 'about a month',
months: '%d months',
year: 'about a year',
years: '%d years',
};
self.inWords = function inWords(timeAgo) {
const milliseconds = timeAgo * 1000;
const seconds = Math.floor(
(new Date() - parseInt(milliseconds, 10)) / 1000
);
const separator = this.locales.separator || ' ';
let words = this.locales.prefix + separator;
let interval = 0;
const intervals = {
year: seconds / 31536000,
month: seconds / 2592000,
day: seconds / 86400,
hour: seconds / 3600,
minute: seconds / 60,
};
let distance = this.locales.seconds;
// eslint-disable-next-line guard-for-in
for (const key in intervals) {
interval = Math.floor(intervals[key]);
if (interval > 1) {
distance = this.locales[`${key}s`];
break;
} else if (interval === 1) {
distance = this.locales[key];
break;
}
}
distance = distance.replace(/%d/i, interval);
words += distance + separator + this.locales.sufix;
return words.trim();
};
return self;
})();

View File

@ -1,7 +1,7 @@
// We may need to update usage of Ability when we update.
// They say they are going to rename PureAbility to Ability and remove the old class.
import { AbilityBuilder, Ability } from '@casl/ability';
import { useContext, useEffect } from 'react';
import { useContext, useEffect, useState } from 'react';
import { AbilityContext } from '../contexts/Can';
import { PermissionCheckResponseBody, PermissionsToCheck } from '../interfaces';
import HttpService from '../services/HttpService';
@ -10,6 +10,7 @@ export const usePermissionFetcher = (
permissionsToCheck: PermissionsToCheck
) => {
const ability = useContext(AbilityContext);
const [permissionsLoaded, setPermissionsLoaded] = useState<boolean>(false);
useEffect(() => {
const processPermissionResult = (result: PermissionCheckResponseBody) => {
@ -34,15 +35,17 @@ export const usePermissionFetcher = (
}
});
ability.update(rules);
setPermissionsLoaded(true);
};
HttpService.makeCallToBackend({
path: `/permissions-check`,
httpMethod: 'POST',
successCallback: processPermissionResult,
postBody: { requests_to_check: permissionsToCheck },
});
if (Object.keys(permissionsToCheck).length !== 0) {
HttpService.makeCallToBackend({
path: `/permissions-check`,
httpMethod: 'POST',
successCallback: processPermissionResult,
postBody: { requests_to_check: permissionsToCheck },
});
}
});
return { ability };
return { ability, permissionsLoaded };
};

View File

@ -1,20 +1,25 @@
import { useMemo } from 'react';
import { useParams } from 'react-router-dom';
export const useUriListForPermissions = () => {
const params = useParams();
const targetUris = {
authenticationListPath: `/v1.0/authentications`,
messageInstanceListPath: '/v1.0/messages',
processGroupListPath: '/v1.0/process-groups',
processGroupShowPath: `/v1.0/process-groups/${params.process_group_id}`,
processInstanceActionPath: `/v1.0/process-models/${params.process_model_id}/process-instances`,
processInstanceListPath: '/v1.0/process-instances',
processModelCreatePath: `/v1.0/process-models/${params.process_group_id}`,
processModelFileCreatePath: `/v1.0/process-models/${params.process_model_id}/files`,
processModelFileShowPath: `/v1.0/process-models/${params.process_model_id}/files/${params.file_name}`,
processModelShowPath: `/v1.0/process-models/${params.process_model_id}`,
secretListPath: `/v1.0/secrets`,
};
const targetUris = useMemo(() => {
return {
authenticationListPath: `/v1.0/authentications`,
messageInstanceListPath: '/v1.0/messages',
processGroupListPath: '/v1.0/process-groups',
processGroupShowPath: `/v1.0/process-groups/${params.process_group_id}`,
processInstanceActionPath: `/v1.0/process-models/${params.process_model_id}/process-instances`,
processInstanceListPath: '/v1.0/process-instances',
processInstanceTaskListPath: `/v1.0/process-instances/${params.process_model_id}/${params.process_instance_id}/tasks`,
processInstanceReportListPath: '/v1.0/process-instances/reports',
processModelCreatePath: `/v1.0/process-models/${params.process_group_id}`,
processModelFileCreatePath: `/v1.0/process-models/${params.process_model_id}/files`,
processModelFileShowPath: `/v1.0/process-models/${params.process_model_id}/files/${params.file_name}`,
processModelShowPath: `/v1.0/process-models/${params.process_model_id}`,
secretListPath: `/v1.0/secrets`,
};
}, [params]);
return { targetUris };
};

View File

@ -5,6 +5,10 @@
color: white;
}
.megacondensed {
padding-left: 0px;
}
/* defaults to 3rem, which isn't long sufficient for "elizabeth" */
.cds--header__action.username-header-text {
width: 5rem;
@ -143,6 +147,14 @@ h1.with-icons {
margin-bottom: 1em;
}
.with-top-margin {
margin-top: 1em;
}
.with-large-bottom-margin {
margin-bottom: 3em;
}
.diagram-viewer-canvas {
border:1px solid #000000;
height:70vh;
@ -248,3 +260,40 @@ in on this with the react-jsonschema-form repo. This is just a patch fix to allo
position: absolute;
bottom: 1em;
}
.cds--tabs .cds--tabs__nav-link {
max-width: 20rem;
}
.clear-left {
clear: left;
}
td.actions-cell {
width: 1em;
}
.no-results-message {
font-style: italic;
margin-left: 2em;
margin-top: 1em;
font-size: 14px;
}
.data-table-description {
font-size: 14px;
line-height: 18px;
letter-spacing: 0.16px;
color: #525252;
margin-bottom: 1em;
}
/* top and bottom margin since this is sort of the middle of three sections on the process model show page */
.process-model-files-section {
margin: 2em 0;
}
.filterIcon {
text-align: right;
padding-bottom: 10px;
}

View File

@ -46,12 +46,18 @@ export interface ProcessInstanceReport {
display_name: string;
}
export interface ProcessGroupLite {
id: string;
display_name: string;
}
export interface ProcessModel {
id: string;
description: string;
display_name: string;
primary_file_name: string;
files: ProcessFile[];
parent_groups?: ProcessGroupLite[];
}
export interface ProcessGroup {
@ -60,10 +66,19 @@ export interface ProcessGroup {
description?: string | null;
process_models?: ProcessModel[];
process_groups?: ProcessGroup[];
parent_groups?: ProcessGroupLite[];
}
export interface HotCrumbItemObject {
entityToExplode: ProcessModel | ProcessGroup | string;
entityType: string;
linkLastItem?: boolean;
}
export type HotCrumbItemArray = [displayValue: string, url?: string];
// tuple of display value and URL
export type HotCrumbItem = [displayValue: string, url?: string];
export type HotCrumbItem = HotCrumbItemArray | HotCrumbItemObject;
export interface ErrorForDisplay {
message: string;

View File

@ -54,7 +54,7 @@ export default function AuthenticationList() {
<Table striped bordered>
<thead>
<tr>
<th>ID</th>
<th>Id</th>
</tr>
</thead>
<tbody>{rows}</tbody>

View File

@ -1,5 +1,48 @@
import MyCompletedInstances from '../components/MyCompletedInstances';
import ProcessInstanceListTable from '../components/ProcessInstanceListTable';
export default function CompletedInstances() {
return <MyCompletedInstances />;
return (
<>
<h2>My completed instances</h2>
<p className="data-table-description">
This is a list of instances you started that are now complete.
</p>
<ProcessInstanceListTable
filtersEnabled={false}
paginationQueryParamPrefix="my_completed_instances"
perPageOptions={[2, 5, 25]}
reportIdentifier="system_report_instances_initiated_by_me"
showReports={false}
textToShowIfEmpty="You have no completed instances at this time."
paginationClassName="with-large-bottom-margin"
autoReload
/>
<h2>Tasks completed by me</h2>
<p className="data-table-description">
This is a list of instances where you have completed tasks.
</p>
<ProcessInstanceListTable
filtersEnabled={false}
paginationQueryParamPrefix="my_completed_tasks"
perPageOptions={[2, 5, 25]}
reportIdentifier="system_report_instances_with_tasks_completed_by_me"
showReports={false}
textToShowIfEmpty="You have no completed tasks at this time."
paginationClassName="with-large-bottom-margin"
/>
<h2>Tasks completed by my groups</h2>
<p className="data-table-description">
This is a list of instances with tasks that were completed by groups you
belong to.
</p>
<ProcessInstanceListTable
filtersEnabled={false}
paginationQueryParamPrefix="group_completed_tasks"
perPageOptions={[2, 5, 25]}
reportIdentifier="system_report_instances_with_tasks_completed_by_my_groups"
showReports={false}
textToShowIfEmpty="Your group has no completed tasks at this time."
/>
</>
);
}

View File

@ -3,7 +3,8 @@ import ProcessModelListTiles from '../components/ProcessModelListTiles';
export default function CreateNewInstance() {
return (
<ProcessModelListTiles
headerElement={<h1>Process models available to you</h1>}
headerElement={<h2>Processes I can start</h2>}
checkPermissions={false}
/>
);
}

View File

@ -1,15 +1,15 @@
import TasksForMyOpenProcesses from '../components/TasksForMyOpenProcesses';
import TasksWaitingForMe from '../components/TasksWaitingForMe';
import TasksForWaitingForMyGroups from '../components/TasksWaitingForMyGroups';
import TasksWaitingForMyGroups from '../components/TasksWaitingForMyGroups';
export default function GroupedTasks() {
return (
<>
{/* be careful moving these around since the first two have with-large-bottom-margin in order to get some space between the three table sections. */}
{/* i wish Stack worked to add space just between top-level elements */}
<TasksForMyOpenProcesses />
<br />
<TasksWaitingForMe />
<br />
<TasksForWaitingForMyGroups />
<TasksWaitingForMyGroups />
</>
);
}

View File

@ -18,12 +18,10 @@ export default function HomePageRoutes() {
useEffect(() => {
setErrorMessage(null);
let newSelectedTabIndex = 0;
if (location.pathname.match(/^\/tasks\/grouped\b/)) {
if (location.pathname.match(/^\/tasks\/completed-instances\b/)) {
newSelectedTabIndex = 1;
} else if (location.pathname.match(/^\/tasks\/completed-instances\b/)) {
newSelectedTabIndex = 2;
} else if (location.pathname.match(/^\/tasks\/create-new-instance\b/)) {
newSelectedTabIndex = 3;
newSelectedTabIndex = 2;
}
setSelectedTabIndex(newSelectedTabIndex);
}, [location, setErrorMessage]);
@ -36,13 +34,13 @@ export default function HomePageRoutes() {
<>
<Tabs selectedIndex={selectedTabIndex}>
<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/my-tasks')}>My Tasks</Tab> */}
<Tab onClick={() => navigate('/tasks/grouped')}>In Progress</Tab>
<Tab onClick={() => navigate('/tasks/completed-instances')}>
Completed Instances
Completed
</Tab>
<Tab onClick={() => navigate('/tasks/create-new-instance')}>
Create New Instance +
Start New +
</Tab>
</TabList>
</Tabs>
@ -55,7 +53,7 @@ export default function HomePageRoutes() {
<>
{renderTabs()}
<Routes>
<Route path="/" element={<MyTasks />} />
<Route path="/" element={<GroupedTasks />} />
<Route path="my-tasks" element={<MyTasks />} />
<Route path=":process_instance_id/:task_id" element={<TaskShow />} />
<Route path="grouped" element={<GroupedTasks />} />

View File

@ -8,7 +8,6 @@ import {
convertSecondsToFormattedDateString,
getPageInfoFromSearchParams,
modifyProcessIdentifierForPathParam,
unModifyProcessIdentifierForPathParam,
} from '../helpers';
import HttpService from '../services/HttpService';
@ -102,12 +101,11 @@ export default function MessageInstanceList() {
<ProcessBreadcrumb
hotCrumbs={[
['Process Groups', '/admin'],
[
`Process Model: ${params.process_model_id}`,
`process_model:${unModifyProcessIdentifierForPathParam(
searchParams.get('process_model_id') || ''
)}:link`,
],
{
entityToExplode: searchParams.get('process_model_id') || '',
entityType: 'process-model-id',
linkLastItem: true,
},
[
`Process Instance: ${searchParams.get('process_instance_id')}`,
`/admin/process-models/${searchParams.get(

View File

@ -9,16 +9,24 @@ import {
refreshAtInterval,
} from '../helpers';
import HttpService from '../services/HttpService';
import { PaginationObject, RecentProcessModel } from '../interfaces';
import {
PaginationObject,
ProcessInstance,
ProcessModel,
RecentProcessModel,
} from '../interfaces';
import ProcessInstanceRun from '../components/ProcessInstanceRun';
const PER_PAGE_FOR_TASKS_ON_HOME_PAGE = 5;
const REFRESH_INTERVAL = 10;
const REFRESH_INTERVAL = 5;
const REFRESH_TIMEOUT = 600;
export default function MyTasks() {
const [searchParams] = useSearchParams();
const [tasks, setTasks] = useState([]);
const [pagination, setPagination] = useState<PaginationObject | null>(null);
const [processInstance, setProcessInstance] =
useState<ProcessInstance | null>(null);
useEffect(() => {
const getTasks = () => {
@ -40,6 +48,28 @@ export default function MyTasks() {
refreshAtInterval(REFRESH_INTERVAL, REFRESH_TIMEOUT, getTasks);
}, [searchParams]);
const processInstanceRunResultTag = () => {
if (processInstance) {
return (
<div className="alert alert-success" role="alert">
<p>
Process Instance {processInstance.id} kicked off (
<Link
to={`/admin/process-models/${modifyProcessIdentifierForPathParam(
processInstance.process_model_identifier
)}/process-instances/${processInstance.id}`}
data-qa="process-instance-show-link"
>
view
</Link>
).
</p>
</div>
);
}
return null;
};
let recentProcessModels: RecentProcessModel[] = [];
const recentProcessModelsString = localStorage.getItem('recentProcessModels');
if (recentProcessModelsString !== null) {
@ -67,7 +97,7 @@ export default function MyTasks() {
data-qa="process-instance-show-link"
to={`/admin/process-models/${modifiedProcessModelIdentifier}/process-instances/${rowToUse.process_instance_id}`}
>
View {rowToUse.process_instance_id}
{rowToUse.process_instance_id}
</Link>
</td>
<td
@ -107,33 +137,44 @@ export default function MyTasks() {
};
const buildRecentProcessModelSection = () => {
const rows = recentProcessModels.map((row) => {
const rowToUse = row as any;
const rows = recentProcessModels.map((row: RecentProcessModel) => {
const processModel: ProcessModel = {
id: row.processModelIdentifier,
description: '',
display_name: '',
primary_file_name: '',
files: [],
};
const modifiedProcessModelId = modifyProcessIdentifierForPathParam(
rowToUse.processModelIdentifier
row.processModelIdentifier
);
return (
<tr
key={`${rowToUse.processGroupIdentifier}/${rowToUse.processModelIdentifier}`}
>
<tr key={`${row.processGroupIdentifier}/${row.processModelIdentifier}`}>
<td>
<Link
data-qa="process-model-show-link"
to={`/admin/process-models/${modifiedProcessModelId}`}
>
{rowToUse.processModelDisplayName}
{row.processModelDisplayName}
</Link>
</td>
<td className="actions-cell">
<ProcessInstanceRun
processModel={processModel}
onSuccessCallback={setProcessInstance}
/>
</td>
</tr>
);
});
return (
<>
<h1>Recently viewed process models</h1>
<h1>Recently instantiated process models</h1>
<Table striped bordered>
<thead>
<tr>
<th>Process Model</th>
<th>Actions</th>
</tr>
</thead>
<tbody>{rows}</tbody>
@ -175,6 +216,7 @@ export default function MyTasks() {
}
return (
<>
{processInstanceRunResultTag()}
{tasksWaitingForMe}
<br />
{relevantProcessModelSection}

View File

@ -27,10 +27,11 @@ export default function ProcessGroupEdit() {
<ProcessBreadcrumb
hotCrumbs={[
['Process Groups', '/admin'],
[
`Process Group: ${processGroup.id}:link`,
`process_group:${processGroup.id}:link`,
],
{
entityToExplode: processGroup,
entityType: 'process-group',
linkLastItem: true,
},
]}
/>
<h1>Edit Process Group: {(processGroup as any).id}</h1>

View File

@ -14,7 +14,11 @@ export default function ProcessGroupNew() {
const hotCrumbs: HotCrumbItem[] = [['Process Groups', '/admin']];
if (parentGroupId) {
hotCrumbs.push(['', `process_group:${parentGroupId}:link`]);
hotCrumbs.push({
entityToExplode: parentGroupId,
entityType: 'process-group-id',
linkLastItem: true,
});
}
return (

View File

@ -1,10 +1,19 @@
import { useEffect, useState } from 'react';
import { Link, useSearchParams, useParams } from 'react-router-dom';
import {
// Link,
useSearchParams,
useParams,
useNavigate,
} from 'react-router-dom';
import {
TrashCan,
Edit,
// @ts-ignore
} from '@carbon/icons-react';
// @ts-ignore
import { Button, Table, Stack } from '@carbon/react';
import { Button, Stack } from '@carbon/react';
import { Can } from '@casl/react';
import ProcessBreadcrumb from '../components/ProcessBreadcrumb';
import PaginationForTable from '../components/PaginationForTable';
import HttpService from '../services/HttpService';
import {
getPageInfoFromSearchParams,
@ -15,26 +24,28 @@ import {
PaginationObject,
PermissionsToCheck,
ProcessGroup,
ProcessModel,
// ProcessModel,
} from '../interfaces';
import { useUriListForPermissions } from '../hooks/UriListForPermissions';
import { usePermissionFetcher } from '../hooks/PermissionService';
import ProcessGroupListTiles from '../components/ProcessGroupListTiles';
// import ProcessModelListTiles from '../components/ProcessModelListTiles';
import ButtonWithConfirmation from '../components/ButtonWithConfirmation';
import ProcessModelListTiles from '../components/ProcessModelListTiles';
export default function ProcessGroupShow() {
const params = useParams();
const [searchParams] = useSearchParams();
const navigate = useNavigate();
const [processGroup, setProcessGroup] = useState<ProcessGroup | null>(null);
const [processModels, setProcessModels] = useState([]);
// const [processModels, setProcessModels] = useState([]);
const [modelPagination, setModelPagination] =
useState<PaginationObject | null>(null);
const { targetUris } = useUriListForPermissions();
const permissionRequestData: PermissionsToCheck = {
[targetUris.processGroupListPath]: ['POST'],
[targetUris.processGroupShowPath]: ['PUT'],
[targetUris.processGroupShowPath]: ['PUT', 'DELETE'],
[targetUris.processModelCreatePath]: ['POST'],
};
const { ability } = usePermissionFetcher(permissionRequestData);
@ -43,7 +54,7 @@ export default function ProcessGroupShow() {
const { page, perPage } = getPageInfoFromSearchParams(searchParams);
const setProcessModelFromResult = (result: any) => {
setProcessModels(result.results);
// setProcessModels(result.results);
setModelPagination(result.pagination);
};
const processResult = (result: any) => {
@ -62,45 +73,61 @@ export default function ProcessGroupShow() {
});
}, [params, searchParams]);
const buildModelTable = () => {
if (processGroup === null) {
return null;
// const buildModelTable = () => {
// if (processGroup === null) {
// return null;
// }
// const rows = processModels.map((row: ProcessModel) => {
// const modifiedProcessModelId: String =
// modifyProcessIdentifierForPathParam((row as any).id);
// return (
// <tr key={row.id}>
// <td>
// <Link
// to={`/admin/process-models/${modifiedProcessModelId}`}
// data-qa="process-model-show-link"
// >
// {row.id}
// </Link>
// </td>
// <td>{row.display_name}</td>
// </tr>
// );
// });
// return (
// <div>
// <h2>Process Models</h2>
// <Table striped bordered>
// <thead>
// <tr>
// <th>Process Model Id</th>
// <th>Display Name</th>
// </tr>
// </thead>
// <tbody>{rows}</tbody>
// </Table>
// </div>
// );
// };
const navigateToProcessGroups = (_result: any) => {
navigate(`/admin/process-groups`);
};
const deleteProcessGroup = () => {
if (processGroup) {
HttpService.makeCallToBackend({
path: `/process-groups/${modifyProcessIdentifierForPathParam(
processGroup.id
)}`,
successCallback: navigateToProcessGroups,
httpMethod: 'DELETE',
});
}
const rows = processModels.map((row: ProcessModel) => {
const modifiedProcessModelId: String =
modifyProcessIdentifierForPathParam((row as any).id);
return (
<tr key={row.id}>
<td>
<Link
to={`/admin/process-models/${modifiedProcessModelId}`}
data-qa="process-model-show-link"
>
{row.id}
</Link>
</td>
<td>{row.display_name}</td>
</tr>
);
});
return (
<div>
<h2>Process Models</h2>
<Table striped bordered>
<thead>
<tr>
<th>Process Model Id</th>
<th>Display Name</th>
</tr>
</thead>
<tbody>{rows}</tbody>
</Table>
</div>
);
};
if (processGroup && modelPagination) {
const { page, perPage } = getPageInfoFromSearchParams(searchParams);
// const { page, perPage } = getPageInfoFromSearchParams(searchParams);
const modifiedProcessGroupId = modifyProcessIdentifierForPathParam(
processGroup.id
);
@ -109,10 +136,41 @@ export default function ProcessGroupShow() {
<ProcessBreadcrumb
hotCrumbs={[
['Process Groups', '/admin'],
['', `process_group:${processGroup.id}`],
{
entityToExplode: processGroup,
entityType: 'process-group',
},
]}
/>
<h1>Process Group: {processGroup.display_name}</h1>
<Stack orientation="horizontal" gap={1}>
<h1 className="with-icons">
Process Group: {processGroup.display_name}
</h1>
<Can I="PUT" a={targetUris.processGroupShowPath} ability={ability}>
<Button
kind="ghost"
data-qa="edit-process-group-button"
renderIcon={Edit}
iconDescription="Edit Process Group"
hasIconOnly
href={`/admin/process-groups/${modifiedProcessGroupId}/edit`}
>
Edit process group
</Button>
</Can>
<Can I="DELETE" a={targetUris.processGroupShowPath} ability={ability}>
<ButtonWithConfirmation
kind="ghost"
data-qa="delete-process-group-button"
renderIcon={TrashCan}
iconDescription="Delete Process Group"
hasIconOnly
description={`Delete process group: ${processGroup.display_name}`}
onConfirmation={deleteProcessGroup}
confirmButtonLabel="Delete"
/>
</Can>
</Stack>
<p className="process-description">{processGroup.description}</p>
<ul>
<Stack orientation="horizontal" gap={3}>
@ -134,34 +192,27 @@ export default function ProcessGroupShow() {
Add a process model
</Button>
</Can>
<Can I="PUT" a={targetUris.processGroupShowPath} ability={ability}>
<Button
href={`/admin/process-groups/${modifiedProcessGroupId}/edit`}
>
Edit process group
</Button>
</Can>
</Stack>
<br />
<br />
{/* <ProcessModelListTiles
<ProcessModelListTiles
headerElement={<h2>Process Models</h2>}
processGroup={processGroup}
/> */}
/>
{/* eslint-disable-next-line sonarjs/no-gratuitous-expressions */}
{modelPagination && modelPagination.total > 0 && (
{/* {modelPagination && modelPagination.total > 0 && (
<PaginationForTable
page={page}
perPage={perPage}
pagination={modelPagination}
tableToDisplay={buildModelTable()}
/>
)}
)} */}
<br />
<br />
<ProcessGroupListTiles
processGroup={processGroup}
headerElement={<h2>Process Groups</h2>}
headerElement={<h2 className="clear-left">Process Groups</h2>}
/>
</ul>
</>

View File

@ -7,7 +7,6 @@ import ProcessBreadcrumb from '../components/ProcessBreadcrumb';
import {
getPageInfoFromSearchParams,
modifyProcessIdentifierForPathParam,
unModifyProcessIdentifierForPathParam,
convertSecondsToFormattedDateTime,
} from '../helpers';
import HttpService from '../services/HttpService';
@ -80,12 +79,11 @@ export default function ProcessInstanceLogList() {
<ProcessBreadcrumb
hotCrumbs={[
['Process Groups', '/admin'],
[
`Process Model: ${params.process_model_id}`,
`process_model:${unModifyProcessIdentifierForPathParam(
params.process_model_id || ''
)}:link`,
],
{
entityToExplode: params.process_model_id || '',
entityType: 'process-model-id',
linkLastItem: true,
},
[
`Process Instance: ${params.process_instance_id}`,
`/admin/process-models/${params.process_model_id}/process-instances/${params.process_instance_id}`,

View File

@ -2,12 +2,22 @@ import { useEffect, useState } from 'react';
// @ts-ignore
import { Button, Table } from '@carbon/react';
import { useParams, Link } from 'react-router-dom';
import { Can } from '@casl/react';
import HttpService from '../services/HttpService';
import { useUriListForPermissions } from '../hooks/UriListForPermissions';
import { PermissionsToCheck } from '../interfaces';
import { usePermissionFetcher } from '../hooks/PermissionService';
export default function ProcessInstanceReportList() {
const params = useParams();
const [processInstanceReports, setProcessInstanceReports] = useState([]);
const { targetUris } = useUriListForPermissions();
const permissionRequestData: PermissionsToCheck = {
[targetUris.processInstanceReportListPath]: ['POST'],
};
const { ability } = usePermissionFetcher(permissionRequestData);
useEffect(() => {
HttpService.makeCallToBackend({
path: `/process-instances/reports`,
@ -45,9 +55,11 @@ export default function ProcessInstanceReportList() {
const headerStuff = (
<>
<h1>Process Instance Perspectives</h1>
<Button href="/admin/process-instances/reports/new">
Add a process instance perspective
</Button>
<Can I="POST" a={targetUris.processInstanceListPath} ability={ability}>
<Button href="/admin/process-instances/reports/new">
Add a process instance perspective
</Button>
</Can>
</>
);
if (processInstanceReports?.length > 0) {

View File

@ -76,9 +76,7 @@ export default function ProcessInstanceReport() {
return (
<main>
<ProcessBreadcrumb
processModelId={params.process_model_id}
processGroupId={params.process_group_id}
linkProcessModel
hotCrumbs={[['Process Groups', '/admin'], ['Process Instance']]}
/>
<h1>Process Instance Perspective: {params.report_identifier}</h1>
<Button

View File

@ -43,6 +43,7 @@ export default function ProcessInstanceShow() {
const [processInstance, setProcessInstance] = useState(null);
const [tasks, setTasks] = useState<Array<object> | null>(null);
const [tasksCallHadError, setTasksCallHadError] = useState<boolean>(false);
const [taskToDisplay, setTaskToDisplay] = useState<object | null>(null);
const [taskDataToDisplay, setTaskDataToDisplay] = useState<string>('');
const [editingTaskData, setEditingTaskData] = useState<boolean>(false);
@ -57,8 +58,11 @@ export default function ProcessInstanceShow() {
const { targetUris } = useUriListForPermissions();
const permissionRequestData: PermissionsToCheck = {
[targetUris.messageInstanceListPath]: ['GET'],
[targetUris.processInstanceTaskListPath]: ['GET'],
};
const { ability } = usePermissionFetcher(permissionRequestData);
const { ability, permissionsLoaded } = usePermissionFetcher(
permissionRequestData
);
const navigateToProcessInstances = (_result: any) => {
navigate(
@ -67,21 +71,29 @@ export default function ProcessInstanceShow() {
};
useEffect(() => {
HttpService.makeCallToBackend({
path: `/process-models/${modifiedProcessModelId}/process-instances/${params.process_instance_id}`,
successCallback: setProcessInstance,
});
if (typeof params.spiff_step === 'undefined')
if (permissionsLoaded) {
const processTaskFailure = () => {
setTasksCallHadError(true);
};
HttpService.makeCallToBackend({
path: `/process-instances/${modifiedProcessModelId}/${params.process_instance_id}/tasks?all_tasks=true`,
successCallback: setTasks,
path: `/process-models/${modifiedProcessModelId}/process-instances/${params.process_instance_id}`,
successCallback: setProcessInstance,
});
else
HttpService.makeCallToBackend({
path: `/process-instances/${modifiedProcessModelId}/${params.process_instance_id}/tasks?all_tasks=true&spiff_step=${params.spiff_step}`,
successCallback: setTasks,
});
}, [params, modifiedProcessModelId]);
let taskParams = '?all_tasks=true';
if (typeof params.spiff_step !== 'undefined') {
taskParams = `${taskParams}&spiff_step=${params.spiff_step}`;
}
if (ability.can('GET', targetUris.processInstanceTaskListPath)) {
HttpService.makeCallToBackend({
path: `/process-instances/${modifiedProcessModelId}/${params.process_instance_id}/tasks${taskParams}`,
successCallback: setTasks,
failureCallback: processTaskFailure,
});
} else {
setTasksCallHadError(true);
}
}
}, [params, modifiedProcessModelId, permissionsLoaded, ability, targetUris]);
const deleteProcessInstance = () => {
HttpService.makeCallToBackend({
@ -550,7 +562,7 @@ export default function ProcessInstanceShow() {
return elements;
};
if (processInstance && tasks) {
if (processInstance && (tasks || tasksCallHadError)) {
const processInstanceToUse = processInstance as any;
const taskIds = getTaskIds();
const processModelId = unModifyProcessIdentifierForPathParam(
@ -562,10 +574,11 @@ export default function ProcessInstanceShow() {
<ProcessBreadcrumb
hotCrumbs={[
['Process Groups', '/admin'],
[
`Process Model: ${processModelId}`,
`process_model:${processModelId}:link`,
],
{
entityToExplode: processModelId,
entityType: 'process-model-id',
linkLastItem: true,
},
[`Process Instance Id: ${processInstanceToUse.id}`],
]}
/>

View File

@ -24,10 +24,11 @@ export default function ProcessModelEdit() {
<ProcessBreadcrumb
hotCrumbs={[
['Process Groups', '/admin'],
[
`Process Model: ${processModel.id}`,
`process_model:${processModel.id}:link`,
],
{
entityToExplode: processModel,
entityType: 'process-model',
linkLastItem: true,
},
]}
/>
<h1>Edit Process Model: {(processModel as any).id}</h1>

View File

@ -844,10 +844,11 @@ export default function ProcessModelEditDiagram() {
<ProcessBreadcrumb
hotCrumbs={[
['Process Groups', '/admin'],
[
`Process Model: ${processModel.id}`,
`process_model:${processModel.id}:link`,
],
{
entityToExplode: processModel,
entityType: 'process-model',
linkLastItem: true,
},
[processModelFileName],
]}
/>

View File

@ -20,10 +20,11 @@ export default function ProcessModelNew() {
<ProcessBreadcrumb
hotCrumbs={[
['Process Groups', '/admin'],
[
`Process Group: ${params.process_group_id}`,
`process_group:${params.process_group_id}:link`,
],
{
entityToExplode: params.process_group_id || '',
entityType: 'process-group-id',
linkLastItem: true,
},
]}
/>
<h1>Add Process Model</h1>

View File

@ -7,6 +7,7 @@ import {
TrashCan,
Favorite,
Edit,
View,
ArrowRight,
// @ts-ignore
} from '@carbon/icons-react';
@ -41,7 +42,6 @@ import {
ProcessFile,
ProcessInstance,
ProcessModel,
RecentProcessModel,
} from '../interfaces';
import ButtonWithConfirmation from '../components/ButtonWithConfirmation';
import ProcessInstanceListTable from '../components/ProcessInstanceListTable';
@ -49,55 +49,6 @@ import { usePermissionFetcher } from '../hooks/PermissionService';
import { useUriListForPermissions } from '../hooks/UriListForPermissions';
import ProcessInstanceRun from '../components/ProcessInstanceRun';
const storeRecentProcessModelInLocalStorage = (
processModelForStorage: ProcessModel
) => {
// All values stored in localStorage are strings.
// Grab our recentProcessModels string from localStorage.
const stringFromLocalStorage = window.localStorage.getItem(
'recentProcessModels'
);
// adapted from https://stackoverflow.com/a/59424458/6090676
// If that value is null (meaning that we've never saved anything to that spot in localStorage before), use an empty array as our array. Otherwise, use the value we parse out.
let array: RecentProcessModel[] = [];
if (stringFromLocalStorage !== null) {
// Then parse that string into an actual value.
array = JSON.parse(stringFromLocalStorage);
}
// Here's the value we want to add
const value = {
processModelIdentifier: processModelForStorage.id,
processModelDisplayName: processModelForStorage.display_name,
};
// anything with a processGroupIdentifier is old and busted. leave it behind.
array = array.filter((item) => item.processGroupIdentifier === undefined);
// If our parsed/empty array doesn't already have this value in it...
const matchingItem = array.find(
(item) => item.processModelIdentifier === value.processModelIdentifier
);
if (matchingItem === undefined) {
// add the value to the beginning of the array
array.unshift(value);
// Keep the array to 3 items
if (array.length > 3) {
array.pop();
}
}
// once the old and busted serializations are gone, we can put these two statements inside the above if statement
// turn the array WITH THE NEW VALUE IN IT into a string to prepare it to be stored in localStorage
const stringRepresentingArray = JSON.stringify(array);
// and store it in localStorage as "recentProcessModels"
window.localStorage.setItem('recentProcessModels', stringRepresentingArray);
};
export default function ProcessModelShow() {
const params = useParams();
const setErrorMessage = (useContext as any)(ErrorContext)[1];
@ -116,9 +67,11 @@ export default function ProcessModelShow() {
[targetUris.processModelShowPath]: ['PUT', 'DELETE'],
[targetUris.processInstanceListPath]: ['GET'],
[targetUris.processInstanceActionPath]: ['POST'],
[targetUris.processModelFileCreatePath]: ['POST', 'GET', 'DELETE'],
[targetUris.processModelFileCreatePath]: ['POST', 'PUT', 'GET', 'DELETE'],
};
const { ability } = usePermissionFetcher(permissionRequestData);
const { ability, permissionsLoaded } = usePermissionFetcher(
permissionRequestData
);
const modifiedProcessModelId = modifyProcessIdentifierForPathParam(
`${params.process_model_id}`
@ -128,7 +81,6 @@ export default function ProcessModelShow() {
const processResult = (result: ProcessModel) => {
setProcessModel(result);
setReloadModel(false);
storeRecentProcessModelInLocalStorage(result);
};
HttpService.makeCallToBackend({
path: `/process-models/${modifiedProcessModelId}`,
@ -139,7 +91,7 @@ export default function ProcessModelShow() {
const processInstanceRunResultTag = () => {
if (processInstance) {
return (
<div className="alert alert-success" role="alert">
<div className="alert alert-success with-top-margin" role="alert">
<p>
Process Instance {processInstance.id} kicked off (
<Link
@ -150,6 +102,7 @@ export default function ProcessModelShow() {
</Link>
).
</p>
<br />
</div>
);
}
@ -262,12 +215,18 @@ export default function ProcessModelShow() {
isPrimaryBpmnFile: boolean
) => {
const elements = [];
let icon = View;
let actionWord = 'View';
if (ability.can('PUT', targetUris.processModelFileCreatePath)) {
icon = Edit;
actionWord = 'Edit';
}
elements.push(
<Can I="GET" a={targetUris.processModelFileCreatePath} ability={ability}>
<Button
kind="ghost"
renderIcon={Edit}
iconDescription="Edit File"
renderIcon={icon}
iconDescription={`${actionWord} File`}
hasIconOnly
size="lg"
data-qa={`edit-file-${processModelFile.name.replace('.', '-')}`}
@ -325,7 +284,7 @@ export default function ProcessModelShow() {
};
const processModelFileList = () => {
if (!processModel) {
if (!processModel || !permissionsLoaded) {
return null;
}
let constructedTag;
@ -439,12 +398,16 @@ export default function ProcessModelShow() {
);
};
const processModelButtons = () => {
const processModelFilesSection = () => {
if (!processModel) {
return null;
}
return (
<Grid condensed fullWidth>
<Grid
condensed
fullWidth
className="megacondensed process-model-files-section"
>
<Column md={5} lg={9} sm={3}>
<Accordion align="end" open>
<AccordionItem
@ -518,7 +481,10 @@ export default function ProcessModelShow() {
const processInstanceListTableButton = () => {
if (processModel) {
return (
<Grid fullWidth>
<Grid fullWidth condensed>
<Column sm={{ span: 3 }} md={{ span: 4 }} lg={{ span: 3 }}>
<h2>Process Instances</h2>
</Column>
<Column
sm={{ span: 1, offset: 3 }}
md={{ span: 1, offset: 7 }}
@ -551,17 +517,28 @@ export default function ProcessModelShow() {
<ProcessBreadcrumb
hotCrumbs={[
['Process Groups', '/admin'],
[
`Process Model: ${processModel.id}`,
`process_model:${processModel.id}`,
],
{
entityToExplode: processModel,
entityType: 'process-model',
},
]}
/>
<Stack orientation="horizontal" gap={1}>
<h1 className="with-icons">
Process Model: {processModel.display_name}
</h1>
<Can I="PUT" a={targetUris.processModelShowPath} ability={ability}>
<Button
kind="ghost"
data-qa="edit-process-model-button"
renderIcon={Edit}
iconDescription="Edit Process Model"
hasIconOnly
href={`/admin/process-models/${modifiedProcessModelId}/edit`}
>
Edit process model
</Button>
</Can>
<Can I="DELETE" a={targetUris.processModelShowPath} ability={ability}>
<ButtonWithConfirmation
kind="ghost"
@ -582,34 +559,27 @@ export default function ProcessModelShow() {
a={targetUris.processInstanceActionPath}
ability={ability}
>
<ProcessInstanceRun
processModel={processModel}
onSuccessCallback={setProcessInstance}
/>
</Can>
<Can I="PUT" a={targetUris.processModelShowPath} ability={ability}>
<Button
href={`/admin/process-models/${modifiedProcessModelId}/edit`}
variant="secondary"
>
Edit process model
</Button>
<>
<ProcessInstanceRun
processModel={processModel}
onSuccessCallback={setProcessInstance}
/>
<br />
<br />
</>
</Can>
</Stack>
<br />
<br />
{processInstanceRunResultTag()}
<br />
{processModelFilesSection()}
<Can I="GET" a={targetUris.processInstanceListPath} ability={ability}>
{processInstanceListTableButton()}
<ProcessInstanceListTable
filtersEnabled={false}
processModelFullIdentifier={processModel.id}
perPageOptions={[2, 5, 25]}
showReports={false}
/>
<br />
</Can>
{processModelButtons()}
</>
);
}

View File

@ -6,10 +6,7 @@ import { Button, Modal } from '@carbon/react';
import ProcessBreadcrumb from '../components/ProcessBreadcrumb';
import HttpService from '../services/HttpService';
import ButtonWithConfirmation from '../components/ButtonWithConfirmation';
import {
modifyProcessIdentifierForPathParam,
unModifyProcessIdentifierForPathParam,
} from '../helpers';
import { modifyProcessIdentifierForPathParam } from '../helpers';
import { ProcessFile } from '../interfaces';
// NOTE: This is mostly the same as ProcessModelEditDiagram and if we go this route could
@ -159,14 +156,11 @@ export default function ReactFormEditor() {
<ProcessBreadcrumb
hotCrumbs={[
['Process Groups', '/admin'],
[
`Process Model: ${unModifyProcessIdentifierForPathParam(
params.process_model_id || ''
)}`,
`process_model:${unModifyProcessIdentifierForPathParam(
params.process_model_id || ''
)}:link`,
],
{
entityToExplode: params.process_model_id || '',
entityType: 'process-model-id',
linkLastItem: true,
},
[processModelFileName],
]}
/>

View File

@ -62,7 +62,7 @@ export default function SecretList() {
<Table striped bordered>
<thead>
<tr>
<th>ID</th>
<th>Id</th>
<th>Secret Key</th>
<th>Creator</th>
<th>Delete</th>

View File

@ -15,6 +15,7 @@ import {
Tabs,
Grid,
Column,
Button,
// @ts-ignore
} from '@carbon/react';
@ -167,6 +168,14 @@ export default function TaskShow() {
reactFragmentToHideSubmitButton = <div />;
}
if (taskToUse.type === 'Manual Task') {
reactFragmentToHideSubmitButton = (
<div>
<Button type="submit">Continue</Button>
</div>
);
}
return (
<Grid fullWidth condensed>
<Column md={5} lg={8} sm={4}>

View File

@ -66,9 +66,11 @@ backendCallProps) => {
method: httpMethod,
});
const updatedPath = path.replace(/^\/v1\.0/, '');
let isSuccessful = true;
let is403 = false;
fetch(`${BACKEND_BASE_URL}${path}`, httpArgs)
fetch(`${BACKEND_BASE_URL}${updatedPath}`, httpArgs)
.then((response) => {
if (response.status === 401) {
UserService.doLogin();