From a5fa3f8cde9e75dbb448cf1ec261c5cd22c94eb2 Mon Sep 17 00:00:00 2001 From: jasquat Date: Wed, 12 Apr 2023 14:32:34 -0400 Subject: [PATCH 1/7] some initial work for home page refactor w/ burnettk --- .../process_instance_report_service.py | 60 +++++++++++++- .../src/routes/HomePageRoutes.tsx | 5 +- .../src/routes/InProgressInstances.tsx | 79 +++++++++++++++++++ 3 files changed, 141 insertions(+), 3 deletions(-) create mode 100644 spiffworkflow-frontend/src/routes/InProgressInstances.tsx diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_report_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_report_service.py index cbf25bb6e..85424c21e 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_report_service.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_report_service.py @@ -47,6 +47,7 @@ class ProcessInstanceReportFilter: initiated_by_me: Optional[bool] = None has_terminal_status: Optional[bool] = None with_tasks_completed_by_me: Optional[bool] = None + with_tasks_i_can_complete: Optional[bool] = None with_tasks_assigned_to_my_group: Optional[bool] = None with_relation_to_me: Optional[bool] = None process_initiator_username: Optional[str] = None @@ -77,6 +78,8 @@ class ProcessInstanceReportFilter: d["has_terminal_status"] = str(self.has_terminal_status).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_i_can_complete is not None: + d["with_tasks_i_can_complete"] = str(self.with_tasks_i_can_complete).lower() if self.with_tasks_assigned_to_my_group is not None: d["with_tasks_assigned_to_my_group"] = str(self.with_tasks_assigned_to_my_group).lower() if self.with_relation_to_me is not None: @@ -140,6 +143,31 @@ class ProcessInstanceReportService: ], "order_by": ["-start_in_seconds", "-id"], }, + "system_report_in_progress_instances_initiated_by_me": { + "columns": [ + {"Header": "id", "accessor": "id"}, + { + "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": "status", "accessor": "status"}, + ], + "filter_by": [ + {"field_name": "initiated_by_me", "field_value": "true"}, + {"field_name": "has_terminal_status", "field_value": "false"}, + ], + "order_by": ["-start_in_seconds", "-id"], + }, + "system_report_in_progress_instances_with_tasks_for_me": { + "columns": cls.builtin_column_options(), + "filter_by": [ + {"field_name": "with_tasks_i_can_complete", "field_value": "true"}, + {"field_name": "has_terminal_status", "field_value": "false"}, + ], + "order_by": ["-start_in_seconds", "-id"], + }, } if metadata_key not in temp_system_metadata_map: @@ -199,7 +227,12 @@ class ProcessInstanceReportService: def bool_value(key: str) -> Optional[bool]: """Bool_value.""" - return bool(filters[key]) if key in filters else None + if key not in filters: + return None + # bool returns True if not an empty string so check explicitly for false + if filters[key] in ['false', 'False']: + return False + return bool(filters[key]) def int_value(key: str) -> Optional[int]: """Int_value.""" @@ -219,6 +252,7 @@ class ProcessInstanceReportService: initiated_by_me = bool_value("initiated_by_me") has_terminal_status = bool_value("has_terminal_status") with_tasks_completed_by_me = bool_value("with_tasks_completed_by_me") + with_tasks_i_can_complete = bool_value("with_tasks_i_can_complete") with_tasks_assigned_to_my_group = bool_value("with_tasks_assigned_to_my_group") with_relation_to_me = bool_value("with_relation_to_me") process_initiator_username = filters.get("process_initiator_username") @@ -236,6 +270,7 @@ class ProcessInstanceReportService: initiated_by_me=initiated_by_me, has_terminal_status=has_terminal_status, with_tasks_completed_by_me=with_tasks_completed_by_me, + with_tasks_i_can_complete=with_tasks_i_can_complete, with_tasks_assigned_to_my_group=with_tasks_assigned_to_my_group, with_relation_to_me=with_relation_to_me, process_initiator_username=process_initiator_username, @@ -259,6 +294,7 @@ class ProcessInstanceReportService: initiated_by_me: Optional[bool] = None, has_terminal_status: Optional[bool] = None, with_tasks_completed_by_me: Optional[bool] = None, + with_tasks_i_can_complete: Optional[bool] = None, with_tasks_assigned_to_my_group: Optional[bool] = None, with_relation_to_me: Optional[bool] = None, process_initiator_username: Optional[str] = None, @@ -288,6 +324,8 @@ class ProcessInstanceReportService: report_filter.has_terminal_status = has_terminal_status if with_tasks_completed_by_me is not None: report_filter.with_tasks_completed_by_me = with_tasks_completed_by_me + if with_tasks_i_can_complete is not None: + report_filter.with_tasks_i_can_complete = with_tasks_i_can_complete if process_initiator_username is not None: report_filter.process_initiator_username = process_initiator_username if report_column_list is not None: @@ -405,6 +443,10 @@ class ProcessInstanceReportService: process_instance_query = process_instance_query.filter( ProcessInstanceModel.status.in_(ProcessInstanceModel.terminal_statuses()) # type: ignore ) + elif report_filter.has_terminal_status is False: + process_instance_query = process_instance_query.filter( + ProcessInstanceModel.status.not_in(ProcessInstanceModel.terminal_statuses()) # type: ignore + ) if report_filter.process_initiator_username is not None: initiator = UserModel.query.filter_by(username=report_filter.process_initiator_username).first() @@ -416,6 +458,7 @@ class ProcessInstanceReportService: if ( not report_filter.with_tasks_completed_by_me and not report_filter.with_tasks_assigned_to_my_group + and not report_filter.with_tasks_i_can_complete and report_filter.with_relation_to_me is True ): process_instance_query = process_instance_query.outerjoin(HumanTaskModel).outerjoin( @@ -444,6 +487,21 @@ class ProcessInstanceReportService: ), ) + if report_filter.with_tasks_i_can_complete is True: + process_instance_query = process_instance_query.filter( + ProcessInstanceModel.process_initiator_id != user.id + ) + process_instance_query = process_instance_query.join( + HumanTaskModel, + HumanTaskModel.process_instance_id == ProcessInstanceModel.id, + ).join( + HumanTaskUserModel, + and_( + HumanTaskUserModel.human_task_id == HumanTaskModel.id, + HumanTaskUserModel.user_id == user.id + ) + ) + if report_filter.with_tasks_assigned_to_my_group is True: group_model_join_conditions = [GroupModel.id == HumanTaskModel.lane_assignment_id] if report_filter.user_group_identifier: diff --git a/spiffworkflow-frontend/src/routes/HomePageRoutes.tsx b/spiffworkflow-frontend/src/routes/HomePageRoutes.tsx index fb7c70440..faca2f581 100644 --- a/spiffworkflow-frontend/src/routes/HomePageRoutes.tsx +++ b/spiffworkflow-frontend/src/routes/HomePageRoutes.tsx @@ -7,6 +7,7 @@ import MyTasks from './MyTasks'; import GroupedTasks from './GroupedTasks'; import CompletedInstances from './CompletedInstances'; import CreateNewInstance from './CreateNewInstance'; +import InProgressInstances from './InProgressInstances'; export default function HomePageRoutes() { const location = useLocation(); @@ -51,10 +52,10 @@ export default function HomePageRoutes() { <> {renderTabs()} - } /> + } /> } /> } /> - } /> + } /> } /> } /> diff --git a/spiffworkflow-frontend/src/routes/InProgressInstances.tsx b/spiffworkflow-frontend/src/routes/InProgressInstances.tsx new file mode 100644 index 000000000..e80160cc9 --- /dev/null +++ b/spiffworkflow-frontend/src/routes/InProgressInstances.tsx @@ -0,0 +1,79 @@ +import { useEffect, useState } from 'react'; +import ProcessInstanceListTable from '../components/ProcessInstanceListTable'; +import HttpService from '../services/HttpService'; + +export default function InProgressInstances() { + const [userGroups, setUserGroups] = useState(null); + + useEffect(() => { + HttpService.makeCallToBackend({ + path: `/user-groups/for-current-user`, + successCallback: setUserGroups, + }); + }, [setUserGroups]); + + const groupTableComponents = () => { + if (!userGroups) { + return null; + } + + return userGroups.map((userGroup: string) => { + return ( + <> +

With tasks completed by group: {userGroup}

+

+ This is a list of instances with tasks that were completed by the{' '} + {userGroup} group. +

+ + + ); + }); + }; + + return ( + <> +

My open instances

+

+ This is a list of instances you started that are now complete. +

+ +

With tasks I can complete

+

+ This is a list of instances that have tasks that you can complete. +

+ + {/** + {groupTableComponents()} + * */} + + ); +} From 17e266f9ee54ac3ebb718e2f9912499d63da9613 Mon Sep 17 00:00:00 2001 From: jasquat Date: Thu, 13 Apr 2023 07:42:41 -0400 Subject: [PATCH 2/7] updated all instance reports for the in progress instances page --- .../config/permissions/local_development.yml | 6 +++++- .../process_instance_report_service.py | 18 +++++++++++++++++- .../src/routes/InProgressInstances.tsx | 4 +--- 3 files changed, 23 insertions(+), 5 deletions(-) diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/config/permissions/local_development.yml b/spiffworkflow-backend/src/spiffworkflow_backend/config/permissions/local_development.yml index 049c991ed..eb9ce4b7a 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/config/permissions/local_development.yml +++ b/spiffworkflow-backend/src/spiffworkflow_backend/config/permissions/local_development.yml @@ -3,10 +3,14 @@ default_group: everybody groups: admin: users: [admin@spiffworkflow.org] + group1: + users: [jason@sartography.com, kb@sartography.com] + group2: + users: [dan@sartography.com] permissions: admin: - groups: [admin] + groups: [admin, group1, group2] users: [] allowed_permissions: [create, read, update, delete] uri: /* diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_report_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_report_service.py index 85424c21e..04f8b4b5d 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_report_service.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_report_service.py @@ -168,6 +168,17 @@ class ProcessInstanceReportService: ], "order_by": ["-start_in_seconds", "-id"], }, + "system_report_in_progress_instances_with_tasks_for_my_group": { + "columns": cls.builtin_column_options(), + "filter_by": [ + { + "field_name": "with_tasks_assigned_to_my_group", + "field_value": "true", + }, + {"field_name": "has_terminal_status", "field_value": "false"}, + ], + "order_by": ["-start_in_seconds", "-id"], + }, } if metadata_key not in temp_system_metadata_map: @@ -350,6 +361,8 @@ class ProcessInstanceReportService: for process_instance_row in process_instance_sqlalchemy_rows: process_instance_mapping = process_instance_row._mapping process_instance_dict = process_instance_row[0].serialized + if 'task_guid' in process_instance_mapping: + process_instance_dict['task_guid'] = process_instance_mapping['task_guid'] for metadata_column in metadata_columns: if metadata_column["accessor"] not in process_instance_dict: process_instance_dict[metadata_column["accessor"]] = process_instance_mapping[ @@ -493,7 +506,10 @@ class ProcessInstanceReportService: ) process_instance_query = process_instance_query.join( HumanTaskModel, - HumanTaskModel.process_instance_id == ProcessInstanceModel.id, + and_( + HumanTaskModel.process_instance_id == ProcessInstanceModel.id, + HumanTaskModel.lane_assignment_id.is_(None), # type: ignore + ) ).join( HumanTaskUserModel, and_( diff --git a/spiffworkflow-frontend/src/routes/InProgressInstances.tsx b/spiffworkflow-frontend/src/routes/InProgressInstances.tsx index e80160cc9..a66815973 100644 --- a/spiffworkflow-frontend/src/routes/InProgressInstances.tsx +++ b/spiffworkflow-frontend/src/routes/InProgressInstances.tsx @@ -30,7 +30,7 @@ export default function InProgressInstances() { paginationQueryParamPrefix="group_completed_instances" paginationClassName="with-large-bottom-margin" perPageOptions={[2, 5, 25]} - reportIdentifier="system_report_completed_instances_with_tasks_completed_by_my_groups" + reportIdentifier="system_report_in_progress_instances_with_tasks_for_my_group" showReports={false} textToShowIfEmpty="This group has no completed instances at this time." additionalParams={`user_group_identifier=${userGroup}`} @@ -71,9 +71,7 @@ export default function InProgressInstances() { paginationClassName="with-large-bottom-margin" autoReload={false} /> - {/** {groupTableComponents()} - * */} ); } From 5d1dc716fdf30230e524a2ba7243581485219477 Mon Sep 17 00:00:00 2001 From: jasquat Date: Thu, 13 Apr 2023 14:12:09 -0400 Subject: [PATCH 3/7] displaying process instances instead of tasks functionality is mostly complete. still need to update text on homepage and some cleanup --- .../models/process_instance.py | 5 +- .../process_instance_report_service.py | 122 ++++++++++++++++-- .../components/ProcessInstanceListTable.tsx | 73 ++++++++++- .../src/routes/InProgressInstances.tsx | 5 + 4 files changed, 190 insertions(+), 15 deletions(-) diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/models/process_instance.py b/spiffworkflow-backend/src/spiffworkflow_backend/models/process_instance.py index a67b7d5aa..5b35df3e1 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/models/process_instance.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/models/process_instance.py @@ -161,9 +161,12 @@ class ProcessInstanceModel(SpiffworkflowBaseDBModel): @classmethod def terminal_statuses(cls) -> list[str]: - """Terminal_statuses.""" return ["complete", "error", "terminated"] + @classmethod + def active_statuses(cls) -> list[str]: + return ["user_input_required", "waiting"] + class ProcessInstanceModelSchema(Schema): """ProcessInstanceModelSchema.""" diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_report_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_report_service.py index 04f8b4b5d..673834f85 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_report_service.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_report_service.py @@ -1,5 +1,7 @@ """Process_instance_report_service.""" import re +from flask import current_app +from sqlalchemy.orm.util import AliasedClass from dataclasses import dataclass from typing import Any from typing import Optional @@ -17,7 +19,7 @@ from spiffworkflow_backend.models.db import SpiffworkflowBaseDBModel from spiffworkflow_backend.models.group import GroupModel from spiffworkflow_backend.models.human_task import HumanTaskModel from spiffworkflow_backend.models.human_task_user import HumanTaskUserModel -from spiffworkflow_backend.models.process_instance import ProcessInstanceModel +from spiffworkflow_backend.models.process_instance import ProcessInstanceModel, ProcessInstanceStatus from spiffworkflow_backend.models.process_instance_metadata import ( ProcessInstanceMetadataModel, ) @@ -46,6 +48,7 @@ class ProcessInstanceReportFilter: process_status: Optional[list[str]] = None initiated_by_me: Optional[bool] = None has_terminal_status: Optional[bool] = None + has_active_status: Optional[bool] = None with_tasks_completed_by_me: Optional[bool] = None with_tasks_i_can_complete: Optional[bool] = None with_tasks_assigned_to_my_group: Optional[bool] = None @@ -53,6 +56,7 @@ class ProcessInstanceReportFilter: process_initiator_username: Optional[str] = None report_column_list: Optional[list] = None report_filter_by_list: Optional[list] = None + oldest_open_human_task_fields: Optional[list] = None def to_dict(self) -> dict[str, str]: """To_dict.""" @@ -76,6 +80,8 @@ class ProcessInstanceReportFilter: d["initiated_by_me"] = str(self.initiated_by_me).lower() if self.has_terminal_status is not None: d["has_terminal_status"] = str(self.has_terminal_status).lower() + if self.has_active_status is not None: + d["has_active_status"] = str(self.has_active_status).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_i_can_complete is not None: @@ -90,6 +96,8 @@ class ProcessInstanceReportFilter: d["report_column_list"] = str(self.report_column_list) if self.report_filter_by_list is not None: d["report_filter_by_list"] = str(self.report_filter_by_list) + if self.oldest_open_human_task_fields is not None: + d["oldest_open_human_task_fields"] = str(self.oldest_open_human_task_fields) return d @@ -150,32 +158,57 @@ class ProcessInstanceReportService: "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": "Task", "accessor": "task_title"}, + {"Header": "Waiting For", "accessor": "waiting_for"}, + {"Header": "Started", "accessor": "start_in_seconds"}, + {"Header": "Last Updated", "accessor": "updated_at_in_seconds"}, {"Header": "status", "accessor": "status"}, ], "filter_by": [ {"field_name": "initiated_by_me", "field_value": "true"}, {"field_name": "has_terminal_status", "field_value": "false"}, + {"field_name": "oldest_open_human_task_fields", "field_value": ['task_id', 'task_title', 'task_name', 'potential_owner_usernames', 'assigned_user_group_identifier']}, ], "order_by": ["-start_in_seconds", "-id"], }, "system_report_in_progress_instances_with_tasks_for_me": { - "columns": cls.builtin_column_options(), + "columns": [ + {"Header": "id", "accessor": "id"}, + { + "Header": "process_model_display_name", + "accessor": "process_model_display_name", + }, + {"Header": "Task", "accessor": "task_title"}, + {"Header": "Started By", "accessor": "process_initiator_username"}, + {"Header": "Started", "accessor": "start_in_seconds"}, + {"Header": "Last Updated", "accessor": "updated_at_in_seconds"}, + ], "filter_by": [ {"field_name": "with_tasks_i_can_complete", "field_value": "true"}, - {"field_name": "has_terminal_status", "field_value": "false"}, + {"field_name": "has_active_status", "field_value": "true"}, + {"field_name": "oldest_open_human_task_fields", "field_value": ['task_id', 'task_title', 'task_name']}, ], "order_by": ["-start_in_seconds", "-id"], }, "system_report_in_progress_instances_with_tasks_for_my_group": { - "columns": cls.builtin_column_options(), + "columns": [ + {"Header": "id", "accessor": "id"}, + { + "Header": "process_model_display_name", + "accessor": "process_model_display_name", + }, + {"Header": "Task", "accessor": "task_title"}, + {"Header": "Started By", "accessor": "process_initiator_username"}, + {"Header": "Started", "accessor": "start_in_seconds"}, + {"Header": "Last Updated", "accessor": "updated_at_in_seconds"}, + ], "filter_by": [ { "field_name": "with_tasks_assigned_to_my_group", "field_value": "true", }, - {"field_name": "has_terminal_status", "field_value": "false"}, + {"field_name": "has_active_status", "field_value": "true"}, + {"field_name": "oldest_open_human_task_fields", "field_value": ['task_id', 'task_title', 'task_name']}, ], "order_by": ["-start_in_seconds", "-id"], }, @@ -251,7 +284,11 @@ class ProcessInstanceReportService: def list_value(key: str) -> Optional[list[str]]: """List_value.""" - return filters[key].split(",") if key in filters else None + if key not in filters: + return None + if isinstance(filters[key], list): + return filters[key] + return filters[key].split(",") process_model_identifier = filters.get("process_model_identifier") user_group_identifier = filters.get("user_group_identifier") @@ -262,6 +299,7 @@ class ProcessInstanceReportService: process_status = list_value("process_status") initiated_by_me = bool_value("initiated_by_me") has_terminal_status = bool_value("has_terminal_status") + has_active_status = bool_value("has_active_status") with_tasks_completed_by_me = bool_value("with_tasks_completed_by_me") with_tasks_i_can_complete = bool_value("with_tasks_i_can_complete") with_tasks_assigned_to_my_group = bool_value("with_tasks_assigned_to_my_group") @@ -269,6 +307,7 @@ class ProcessInstanceReportService: process_initiator_username = filters.get("process_initiator_username") report_column_list = list_value("report_column_list") report_filter_by_list = list_value("report_filter_by_list") + oldest_open_human_task_fields = list_value("oldest_open_human_task_fields") report_filter = ProcessInstanceReportFilter( process_model_identifier=process_model_identifier, @@ -280,6 +319,7 @@ class ProcessInstanceReportService: process_status=process_status, initiated_by_me=initiated_by_me, has_terminal_status=has_terminal_status, + has_active_status=has_active_status, with_tasks_completed_by_me=with_tasks_completed_by_me, with_tasks_i_can_complete=with_tasks_i_can_complete, with_tasks_assigned_to_my_group=with_tasks_assigned_to_my_group, @@ -287,6 +327,7 @@ class ProcessInstanceReportService: process_initiator_username=process_initiator_username, report_column_list=report_column_list, report_filter_by_list=report_filter_by_list, + oldest_open_human_task_fields=oldest_open_human_task_fields, ) return report_filter @@ -304,6 +345,7 @@ class ProcessInstanceReportService: process_status: Optional[str] = None, initiated_by_me: Optional[bool] = None, has_terminal_status: Optional[bool] = None, + has_active_status: Optional[bool] = None, with_tasks_completed_by_me: Optional[bool] = None, with_tasks_i_can_complete: Optional[bool] = None, with_tasks_assigned_to_my_group: Optional[bool] = None, @@ -311,6 +353,7 @@ class ProcessInstanceReportService: process_initiator_username: Optional[str] = None, report_column_list: Optional[list] = None, report_filter_by_list: Optional[list] = None, + oldest_open_human_task_fields: Optional[list] = None, ) -> ProcessInstanceReportFilter: """Filter_from_metadata_with_overrides.""" report_filter = cls.filter_from_metadata(process_instance_report) @@ -333,6 +376,8 @@ class ProcessInstanceReportService: report_filter.initiated_by_me = initiated_by_me if has_terminal_status is not None: report_filter.has_terminal_status = has_terminal_status + if has_active_status is not None: + report_filter.has_active_status = has_active_status if with_tasks_completed_by_me is not None: report_filter.with_tasks_completed_by_me = with_tasks_completed_by_me if with_tasks_i_can_complete is not None: @@ -343,6 +388,8 @@ class ProcessInstanceReportService: report_filter.report_column_list = report_column_list if report_filter_by_list is not None: report_filter.report_filter_by_list = report_filter_by_list + if oldest_open_human_task_fields is not None: + report_filter.oldest_open_human_task_fields = oldest_open_human_task_fields if with_tasks_assigned_to_my_group is not None: report_filter.with_tasks_assigned_to_my_group = with_tasks_assigned_to_my_group if with_relation_to_me is not None: @@ -361,8 +408,6 @@ class ProcessInstanceReportService: for process_instance_row in process_instance_sqlalchemy_rows: process_instance_mapping = process_instance_row._mapping process_instance_dict = process_instance_row[0].serialized - if 'task_guid' in process_instance_mapping: - process_instance_dict['task_guid'] = process_instance_mapping['task_guid'] for metadata_column in metadata_columns: if metadata_column["accessor"] not in process_instance_dict: process_instance_dict[metadata_column["accessor"]] = process_instance_mapping[ @@ -372,6 +417,50 @@ class ProcessInstanceReportService: results.append(process_instance_dict) return results + @classmethod + def add_human_task_fields(cls, process_instance_dicts: list[dict], oldest_open_human_task_fields: list) -> list[dict]: + for process_instance_dict in process_instance_dicts: + assigned_user = aliased(UserModel) + human_task_query = ( + HumanTaskModel.query.filter_by(process_instance_id=process_instance_dict['id']) + .group_by(HumanTaskModel.id) + .outerjoin( + HumanTaskUserModel, + HumanTaskModel.id == HumanTaskUserModel.human_task_id, + ) + .outerjoin(assigned_user, assigned_user.id == HumanTaskUserModel.user_id) + .outerjoin(GroupModel, GroupModel.id == HumanTaskModel.lane_assignment_id) + ) + potential_owner_usernames_from_group_concat_or_similar = cls._get_potential_owner_usernames(assigned_user) + human_task = ( + human_task_query.add_columns( + HumanTaskModel.task_id, + HumanTaskModel.task_name, + HumanTaskModel.task_title, + func.max(GroupModel.identifier).label("assigned_user_group_identifier"), + potential_owner_usernames_from_group_concat_or_similar + ).order_by(HumanTaskModel.id.asc()).first() # type: ignore + ) + if human_task is not None: + for field in oldest_open_human_task_fields: + process_instance_dict[field] = getattr(human_task, field) + return process_instance_dicts + + @classmethod + def _get_potential_owner_usernames(cls, assigned_user: AliasedClass) -> Any: + """_get_potential_owner_usernames.""" + potential_owner_usernames_from_group_concat_or_similar = func.group_concat( + assigned_user.username.distinct() + ).label("potential_owner_usernames") + db_type = current_app.config.get("SPIFFWORKFLOW_BACKEND_DATABASE_TYPE") + + if db_type == "postgres": + potential_owner_usernames_from_group_concat_or_similar = func.string_agg( + assigned_user.username.distinct(), ", " + ).label("potential_owner_usernames") + + return potential_owner_usernames_from_group_concat_or_similar + @classmethod def get_column_names_for_model(cls, model: Type[SpiffworkflowBaseDBModel]) -> list[str]: """Get_column_names_for_model.""" @@ -460,6 +549,10 @@ class ProcessInstanceReportService: process_instance_query = process_instance_query.filter( ProcessInstanceModel.status.not_in(ProcessInstanceModel.terminal_statuses()) # type: ignore ) + if report_filter.has_active_status is True: + process_instance_query = process_instance_query.filter( + ProcessInstanceModel.status.in_(ProcessInstanceModel.active_statuses()) # type: ignore + ) if report_filter.process_initiator_username is not None: initiator = UserModel.query.filter_by(username=report_filter.process_initiator_username).first() @@ -531,7 +624,7 @@ class ProcessInstanceReportService: process_instance_query = process_instance_query.filter(UserGroupAssignmentModel.user_id == user.id) instance_metadata_aliases = {} - stock_columns = ProcessInstanceReportService.get_column_names_for_model(ProcessInstanceModel) + stock_columns = cls.get_column_names_for_model(ProcessInstanceModel) if isinstance(report_filter.report_column_list, list): process_instance_report.report_metadata["columns"] = report_filter.report_column_list if isinstance(report_filter.report_filter_by_list, list): @@ -581,16 +674,19 @@ class ProcessInstanceReportService: order_by_query_array.append(func.max(instance_metadata_aliases[attribute].value).desc()) else: order_by_query_array.append(func.max(instance_metadata_aliases[attribute].value).asc()) - # return process_instance_query + process_instances = ( process_instance_query.group_by(ProcessInstanceModel.id) .add_columns(ProcessInstanceModel.id) .order_by(*order_by_query_array) .paginate(page=page, per_page=per_page, error_out=False) ) - results = ProcessInstanceReportService.add_metadata_columns_to_process_instance( + results = cls.add_metadata_columns_to_process_instance( process_instances.items, process_instance_report.report_metadata["columns"] ) + + if report_filter.oldest_open_human_task_fields: + results = cls.add_human_task_fields(results, report_filter.oldest_open_human_task_fields) response_json = { "report": process_instance_report, "results": results, diff --git a/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx b/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx index 81b644d1a..0dcc63989 100644 --- a/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx +++ b/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx @@ -69,6 +69,8 @@ import { Notification } from './Notification'; import useAPIError from '../hooks/UseApiError'; import { usePermissionFetcher } from '../hooks/PermissionService'; import { Can } from '../contexts/Can'; +import TableCellWithTimeAgoInWords from './TableCellWithTimeAgoInWords'; +import UserService from '../services/UserService'; type OwnProps = { filtersEnabled?: boolean; @@ -82,6 +84,8 @@ type OwnProps = { autoReload?: boolean; additionalParams?: string; variant?: string; + canCompleteAllTasks?: boolean; + showActionsColumn?: boolean; }; interface dateParameters { @@ -100,6 +104,8 @@ export default function ProcessInstanceListTable({ paginationClassName, autoReload = false, variant = 'for-me', + canCompleteAllTasks = false, + showActionsColumn = false, }: OwnProps) { let apiPath = '/process-instances/for-me'; if (variant === 'all') { @@ -141,6 +147,9 @@ export default function ProcessInstanceListTable({ const [requiresRefilter, setRequiresRefilter] = useState(false); const [lastColumnFilter, setLastColumnFilter] = useState(''); + const preferredUsername = UserService.getPreferredUsername(); + const userEmail = UserService.getUserEmail(); + const processInstanceListPathPrefix = variant === 'all' ? '/admin/process-instances/all' @@ -1308,9 +1317,11 @@ export default function ProcessInstanceListTable({ return headerLabels[header] ?? header; }; const headers = reportColumns().map((column: any) => { - // return {getHeaderLabel((column as any).Header)}; return getHeaderLabel((column as any).Header); }); + if (showActionsColumn) { + headers.push('Actions'); + } const formatProcessInstanceId = (row: ProcessInstance, id: number) => { return {id}; @@ -1329,12 +1340,34 @@ export default function ProcessInstanceListTable({ return value; }; + const getWaitingForTableCellComponent = (processInstanceTask: any) => { + let fullUsernameString = ''; + let shortUsernameString = ''; + if (processInstanceTask.potential_owner_usernames) { + fullUsernameString = processInstanceTask.potential_owner_usernames; + const usernames = + processInstanceTask.potential_owner_usernames.split(','); + const firstTwoUsernames = usernames.slice(0, 2); + if (usernames.length > 2) { + firstTwoUsernames.push('...'); + } + shortUsernameString = firstTwoUsernames.join(','); + } + if (processInstanceTask.assigned_user_group_identifier) { + fullUsernameString = processInstanceTask.assigned_user_group_identifier; + shortUsernameString = + processInstanceTask.assigned_user_group_identifier; + } + return {shortUsernameString}; + }; + const reportColumnFormatters: Record = { id: formatProcessInstanceId, process_model_identifier: formatProcessModelIdentifier, process_model_display_name: formatProcessModelDisplayName, start_in_seconds: formatSecondsForDisplay, end_in_seconds: formatSecondsForDisplay, + updated_at_in_seconds: formatSecondsForDisplay, }; const formattedColumn = (row: any, column: any) => { const formatter = @@ -1377,6 +1410,16 @@ export default function ProcessInstanceListTable({ ); } + if (column.accessor === 'waiting_for') { + return {getWaitingForTableCellComponent(row)}; + } + if (column.accessor === 'updated_at_in_seconds') { + return ( + + ); + } return ( // eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions { return formattedColumn(row, column); }); + if (showActionsColumn) { + let buttonElement = null; + if (row.task_id) { + const taskUrl = `/tasks/${row.id}/${row.task_id}`; + const regex = new RegExp(`\\b(${preferredUsername}|${userEmail})\\b`); + let hasAccessToCompleteTask = false; + if ( + canCompleteAllTasks || + (row.potential_owner_usernames || '').match(regex) + ) { + hasAccessToCompleteTask = true; + } + buttonElement = ( + + ); + } + + currentRow.push({buttonElement}); + } + const rowStyle = { cursor: 'pointer' }; + return ( {currentRow} diff --git a/spiffworkflow-frontend/src/routes/InProgressInstances.tsx b/spiffworkflow-frontend/src/routes/InProgressInstances.tsx index a66815973..5616796ae 100644 --- a/spiffworkflow-frontend/src/routes/InProgressInstances.tsx +++ b/spiffworkflow-frontend/src/routes/InProgressInstances.tsx @@ -34,6 +34,8 @@ export default function InProgressInstances() { showReports={false} textToShowIfEmpty="This group has no completed instances at this time." additionalParams={`user_group_identifier=${userGroup}`} + canCompleteAllTasks + showActionsColumn autoReload={false} /> @@ -55,6 +57,7 @@ export default function InProgressInstances() { showReports={false} textToShowIfEmpty="There are no open instances you started at this time." paginationClassName="with-large-bottom-margin" + showActionsColumn autoReload={false} />

With tasks I can complete

@@ -69,6 +72,8 @@ export default function InProgressInstances() { showReports={false} textToShowIfEmpty="You have no completed instances at this time." paginationClassName="with-large-bottom-margin" + canCompleteAllTasks + showActionsColumn autoReload={false} /> {groupTableComponents()} From fd25bb64d72bd1d992168829d277aede2808008b Mon Sep 17 00:00:00 2001 From: jasquat Date: Thu, 13 Apr 2023 16:36:46 -0400 Subject: [PATCH 4/7] updated text on home page w/ burnettk --- .../process_instance_report_service.py | 2 +- .../components/ProcessInstanceListTable.tsx | 214 +++++++++--------- spiffworkflow-frontend/src/index.css | 5 +- .../src/routes/CompletedInstances.tsx | 34 +-- .../src/routes/HomePageRoutes.tsx | 1 - .../src/routes/InProgressInstances.tsx | 46 ++-- .../src/routes/TaskShow.tsx | 5 +- 7 files changed, 160 insertions(+), 147 deletions(-) diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_report_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_report_service.py index 673834f85..ab1de98d5 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_report_service.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_report_service.py @@ -422,7 +422,7 @@ class ProcessInstanceReportService: for process_instance_dict in process_instance_dicts: assigned_user = aliased(UserModel) human_task_query = ( - HumanTaskModel.query.filter_by(process_instance_id=process_instance_dict['id']) + HumanTaskModel.query.filter_by(process_instance_id=process_instance_dict['id'], completed=False) .group_by(HumanTaskModel.id) .outerjoin( HumanTaskUserModel, diff --git a/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx b/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx index 0dcc63989..f193f0e8b 100644 --- a/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx +++ b/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx @@ -1303,6 +1303,111 @@ export default function ProcessInstanceListTable({ ); }; + const getWaitingForTableCellComponent = (processInstanceTask: any) => { + let fullUsernameString = ''; + let shortUsernameString = ''; + if (processInstanceTask.potential_owner_usernames) { + fullUsernameString = processInstanceTask.potential_owner_usernames; + const usernames = + processInstanceTask.potential_owner_usernames.split(','); + const firstTwoUsernames = usernames.slice(0, 2); + if (usernames.length > 2) { + firstTwoUsernames.push('...'); + } + shortUsernameString = firstTwoUsernames.join(','); + } + if (processInstanceTask.assigned_user_group_identifier) { + fullUsernameString = processInstanceTask.assigned_user_group_identifier; + shortUsernameString = processInstanceTask.assigned_user_group_identifier; + } + return {shortUsernameString}; + }; + const formatProcessInstanceId = (row: ProcessInstance, id: number) => { + return {id}; + }; + const formatProcessModelIdentifier = (_row: any, identifier: any) => { + return {identifier}; + }; + const formatProcessModelDisplayName = (_row: any, identifier: any) => { + return {identifier}; + }; + + const formatSecondsForDisplay = (_row: any, seconds: any) => { + return convertSecondsToFormattedDateTime(seconds) || '-'; + }; + const defaultFormatter = (_row: any, value: any) => { + return value; + }; + + const formattedColumn = (row: any, column: any) => { + const reportColumnFormatters: Record = { + id: formatProcessInstanceId, + process_model_identifier: formatProcessModelIdentifier, + process_model_display_name: formatProcessModelDisplayName, + start_in_seconds: formatSecondsForDisplay, + end_in_seconds: formatSecondsForDisplay, + updated_at_in_seconds: formatSecondsForDisplay, + }; + const formatter = + reportColumnFormatters[column.accessor] ?? defaultFormatter; + const value = row[column.accessor]; + const modifiedModelId = modifyProcessIdentifierForPathParam( + row.process_model_identifier + ); + const navigateToProcessInstance = () => { + navigate(`${processInstanceShowPathPrefix}/${modifiedModelId}/${row.id}`); + }; + const navigateToProcessModel = () => { + navigate(`/admin/process-models/${modifiedModelId}`); + }; + + if (column.accessor === 'status') { + return ( + // eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions + + {formatter(row, value)} + + ); + } + if (column.accessor === 'process_model_display_name') { + const pmStyle = { background: 'rgba(0, 0, 0, .02)' }; + return ( + // eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions + + {formatter(row, value)} + + ); + } + if (column.accessor === 'waiting_for') { + return {getWaitingForTableCellComponent(row)}; + } + if (column.accessor === 'updated_at_in_seconds') { + return ( + + ); + } + return ( + // eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions + + {formatter(row, value)} + + ); + }; + const buildTable = () => { const headerLabels: Record = { id: 'Id', @@ -1323,115 +1428,6 @@ export default function ProcessInstanceListTable({ headers.push('Actions'); } - const formatProcessInstanceId = (row: ProcessInstance, id: number) => { - return {id}; - }; - const formatProcessModelIdentifier = (_row: any, identifier: any) => { - return {identifier}; - }; - const formatProcessModelDisplayName = (_row: any, identifier: any) => { - return {identifier}; - }; - - const formatSecondsForDisplay = (_row: any, seconds: any) => { - return convertSecondsToFormattedDateTime(seconds) || '-'; - }; - const defaultFormatter = (_row: any, value: any) => { - return value; - }; - - const getWaitingForTableCellComponent = (processInstanceTask: any) => { - let fullUsernameString = ''; - let shortUsernameString = ''; - if (processInstanceTask.potential_owner_usernames) { - fullUsernameString = processInstanceTask.potential_owner_usernames; - const usernames = - processInstanceTask.potential_owner_usernames.split(','); - const firstTwoUsernames = usernames.slice(0, 2); - if (usernames.length > 2) { - firstTwoUsernames.push('...'); - } - shortUsernameString = firstTwoUsernames.join(','); - } - if (processInstanceTask.assigned_user_group_identifier) { - fullUsernameString = processInstanceTask.assigned_user_group_identifier; - shortUsernameString = - processInstanceTask.assigned_user_group_identifier; - } - return {shortUsernameString}; - }; - - const reportColumnFormatters: Record = { - id: formatProcessInstanceId, - process_model_identifier: formatProcessModelIdentifier, - process_model_display_name: formatProcessModelDisplayName, - start_in_seconds: formatSecondsForDisplay, - end_in_seconds: formatSecondsForDisplay, - updated_at_in_seconds: formatSecondsForDisplay, - }; - const formattedColumn = (row: any, column: any) => { - const formatter = - reportColumnFormatters[column.accessor] ?? defaultFormatter; - const value = row[column.accessor]; - const modifiedModelId = modifyProcessIdentifierForPathParam( - row.process_model_identifier - ); - const navigateToProcessInstance = () => { - navigate( - `${processInstanceShowPathPrefix}/${modifiedModelId}/${row.id}` - ); - }; - const navigateToProcessModel = () => { - navigate(`/admin/process-models/${modifiedModelId}`); - }; - - if (column.accessor === 'status') { - return ( - // eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions - - {formatter(row, value)} - - ); - } - if (column.accessor === 'process_model_display_name') { - const pmStyle = { background: 'rgba(0, 0, 0, .02)' }; - return ( - // eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions - - {formatter(row, value)} - - ); - } - if (column.accessor === 'waiting_for') { - return {getWaitingForTableCellComponent(row)}; - } - if (column.accessor === 'updated_at_in_seconds') { - return ( - - ); - } - return ( - // eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions - - {formatter(row, value)} - - ); - }; - const rows = processInstances.map((row: any) => { const currentRow = reportColumns().map((column: any) => { return formattedColumn(row, column); diff --git a/spiffworkflow-frontend/src/index.css b/spiffworkflow-frontend/src/index.css index 077040363..e05c28470 100644 --- a/spiffworkflow-frontend/src/index.css +++ b/spiffworkflow-frontend/src/index.css @@ -332,10 +332,13 @@ td.actions-cell { width: 1em; } +.process-instance-table-header { + margin-bottom: 1em; +} + .no-results-message { font-style: italic; margin-left: 2em; - margin-top: 1em; font-size: 14px; } diff --git a/spiffworkflow-frontend/src/routes/CompletedInstances.tsx b/spiffworkflow-frontend/src/routes/CompletedInstances.tsx index 5c7ce445b..78f73e92f 100644 --- a/spiffworkflow-frontend/src/routes/CompletedInstances.tsx +++ b/spiffworkflow-frontend/src/routes/CompletedInstances.tsx @@ -18,13 +18,12 @@ export default function CompletedInstances() { } return userGroups.map((userGroup: string) => { + const titleText = `This is a list of instances with tasks that were completed by the ${userGroup} group.`; return ( <> -

With tasks completed by group: {userGroup}

-

- This is a list of instances with tasks that were completed by the{' '} - {userGroup} group. -

+

+ With tasks completed by {userGroup} +

-

My completed instances

-

- This is a list of instances you started that are now complete. -

+

+ Started by me +

-

With tasks completed by me

-

- This is a list of instances where you have completed tasks. -

+

+ With tasks completed by me +

{ + const titleText = `This is a list of instances with tasks that are waiting for the ${userGroup} group.`; return ( <> -

With tasks completed by group: {userGroup}

-

- This is a list of instances with tasks that were completed by the{' '} - {userGroup} group. -

+

+ Waiting for {userGroup} +

-

My open instances

-

- This is a list of instances you started that are now complete. -

+

+ Started by me +

-

With tasks I can complete

-

- This is a list of instances that have tasks that you can complete. -

+

+ Waiting for me +

Date: Thu, 13 Apr 2023 16:56:49 -0400 Subject: [PATCH 5/7] pyl and turn back on autoreload for homepage w/ burnettk --- .flake8 | 3 ++ spiffworkflow-backend/.flake8 | 3 ++ .../process_instance_report_service.py | 51 +++++++++++-------- .../src/routes/InProgressInstances.tsx | 6 +-- 4 files changed, 38 insertions(+), 25 deletions(-) diff --git a/.flake8 b/.flake8 index 6e5fa533b..eb9a3a611 100644 --- a/.flake8 +++ b/.flake8 @@ -44,3 +44,6 @@ per-file-ignores = # S607 Starting a process with a partial executable path # S605 Starting a process with a shell: Seems safe, but may be changed in the future, consider rewriting without shell spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_process_api.py:S607,S101,S605,D102,D103,D101 + + # TODO: refactor this service so complexity should be reduced throughout + spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_report_service.py:C901,D100,D101,D102,D103,D107 diff --git a/spiffworkflow-backend/.flake8 b/spiffworkflow-backend/.flake8 index d73f1dba4..36c7a9124 100644 --- a/spiffworkflow-backend/.flake8 +++ b/spiffworkflow-backend/.flake8 @@ -41,3 +41,6 @@ per-file-ignores = src/spiffworkflow_backend/services/logging_service.py:N802,B950 tests/spiffworkflow_backend/integration/test_process_api.py:S607,S101,S605,D102,D103,D101 + + # TODO: refactor this service so complexity should be reduced throughout + src/spiffworkflow_backend/services/process_instance_report_service.py:C901,D100,D101,D102,D103,D107 diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_report_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_report_service.py index ab1de98d5..e354d31ee 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_report_service.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_report_service.py @@ -1,25 +1,25 @@ """Process_instance_report_service.""" import re -from flask import current_app -from sqlalchemy.orm.util import AliasedClass from dataclasses import dataclass from typing import Any from typing import Optional from typing import Type import sqlalchemy +from flask import current_app from sqlalchemy import and_ from sqlalchemy import func from sqlalchemy import or_ from sqlalchemy.orm import aliased from sqlalchemy.orm import selectinload +from sqlalchemy.orm.util import AliasedClass from spiffworkflow_backend.exceptions.api_error import ApiError from spiffworkflow_backend.models.db import SpiffworkflowBaseDBModel from spiffworkflow_backend.models.group import GroupModel from spiffworkflow_backend.models.human_task import HumanTaskModel from spiffworkflow_backend.models.human_task_user import HumanTaskUserModel -from spiffworkflow_backend.models.process_instance import ProcessInstanceModel, ProcessInstanceStatus +from spiffworkflow_backend.models.process_instance import ProcessInstanceModel from spiffworkflow_backend.models.process_instance_metadata import ( ProcessInstanceMetadataModel, ) @@ -167,7 +167,12 @@ class ProcessInstanceReportService: "filter_by": [ {"field_name": "initiated_by_me", "field_value": "true"}, {"field_name": "has_terminal_status", "field_value": "false"}, - {"field_name": "oldest_open_human_task_fields", "field_value": ['task_id', 'task_title', 'task_name', 'potential_owner_usernames', 'assigned_user_group_identifier']}, + { + "field_name": "oldest_open_human_task_fields", + "field_value": ( + "task_id,task_title,task_name,potential_owner_usernames,assigned_user_group_identifier" + ), + }, ], "order_by": ["-start_in_seconds", "-id"], }, @@ -186,7 +191,10 @@ class ProcessInstanceReportService: "filter_by": [ {"field_name": "with_tasks_i_can_complete", "field_value": "true"}, {"field_name": "has_active_status", "field_value": "true"}, - {"field_name": "oldest_open_human_task_fields", "field_value": ['task_id', 'task_title', 'task_name']}, + { + "field_name": "oldest_open_human_task_fields", + "field_value": "task_id,task_title,task_name", + }, ], "order_by": ["-start_in_seconds", "-id"], }, @@ -208,7 +216,10 @@ class ProcessInstanceReportService: "field_value": "true", }, {"field_name": "has_active_status", "field_value": "true"}, - {"field_name": "oldest_open_human_task_fields", "field_value": ['task_id', 'task_title', 'task_name']}, + { + "field_name": "oldest_open_human_task_fields", + "field_value": "task_id,task_title,task_name", + }, ], "order_by": ["-start_in_seconds", "-id"], }, @@ -274,7 +285,7 @@ class ProcessInstanceReportService: if key not in filters: return None # bool returns True if not an empty string so check explicitly for false - if filters[key] in ['false', 'False']: + if filters[key] in ["false", "False"]: return False return bool(filters[key]) @@ -283,12 +294,7 @@ class ProcessInstanceReportService: return int(filters[key]) if key in filters else None def list_value(key: str) -> Optional[list[str]]: - """List_value.""" - if key not in filters: - return None - if isinstance(filters[key], list): - return filters[key] - return filters[key].split(",") + return filters[key].split(",") if key in filters else None process_model_identifier = filters.get("process_model_identifier") user_group_identifier = filters.get("user_group_identifier") @@ -418,11 +424,13 @@ class ProcessInstanceReportService: return results @classmethod - def add_human_task_fields(cls, process_instance_dicts: list[dict], oldest_open_human_task_fields: list) -> list[dict]: + def add_human_task_fields( + cls, process_instance_dicts: list[dict], oldest_open_human_task_fields: list + ) -> list[dict]: for process_instance_dict in process_instance_dicts: assigned_user = aliased(UserModel) human_task_query = ( - HumanTaskModel.query.filter_by(process_instance_id=process_instance_dict['id'], completed=False) + HumanTaskModel.query.filter_by(process_instance_id=process_instance_dict["id"], completed=False) .group_by(HumanTaskModel.id) .outerjoin( HumanTaskUserModel, @@ -438,8 +446,10 @@ class ProcessInstanceReportService: HumanTaskModel.task_name, HumanTaskModel.task_title, func.max(GroupModel.identifier).label("assigned_user_group_identifier"), - potential_owner_usernames_from_group_concat_or_similar - ).order_by(HumanTaskModel.id.asc()).first() # type: ignore + potential_owner_usernames_from_group_concat_or_similar, + ) + .order_by(HumanTaskModel.id.asc()) # type: ignore + .first() ) if human_task is not None: for field in oldest_open_human_task_fields: @@ -602,13 +612,10 @@ class ProcessInstanceReportService: and_( HumanTaskModel.process_instance_id == ProcessInstanceModel.id, HumanTaskModel.lane_assignment_id.is_(None), # type: ignore - ) + ), ).join( HumanTaskUserModel, - and_( - HumanTaskUserModel.human_task_id == HumanTaskModel.id, - HumanTaskUserModel.user_id == user.id - ) + and_(HumanTaskUserModel.human_task_id == HumanTaskModel.id, HumanTaskUserModel.user_id == user.id), ) if report_filter.with_tasks_assigned_to_my_group is True: diff --git a/spiffworkflow-frontend/src/routes/InProgressInstances.tsx b/spiffworkflow-frontend/src/routes/InProgressInstances.tsx index 6bc6b38a4..9ba7cd266 100644 --- a/spiffworkflow-frontend/src/routes/InProgressInstances.tsx +++ b/spiffworkflow-frontend/src/routes/InProgressInstances.tsx @@ -38,7 +38,7 @@ export default function InProgressInstances() { additionalParams={`user_group_identifier=${userGroup}`} canCompleteAllTasks showActionsColumn - autoReload={false} + autoReload /> ); @@ -66,7 +66,7 @@ export default function InProgressInstances() { textToShowIfEmpty="There are no open instances you started at this time." paginationClassName="with-large-bottom-margin" showActionsColumn - autoReload={false} + autoReload />

{groupTableComponents()} From 4a96b56aec000eedb7d6b7c44b18c42c546d5d25 Mon Sep 17 00:00:00 2001 From: jasquat Date: Thu, 13 Apr 2023 17:09:36 -0400 Subject: [PATCH 6/7] stop refreshing homepage if unauthorized w/ burnettk --- .../src/components/ProcessInstanceListTable.tsx | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx b/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx index f193f0e8b..227ae1bc5 100644 --- a/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx +++ b/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx @@ -254,6 +254,8 @@ export default function ProcessInstanceListTable({ // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + const clearRefreshRef = useRef(null); + // eslint-disable-next-line sonarjs/cognitive-complexity useEffect(() => { function setProcessInstancesFromResult(result: any) { @@ -268,6 +270,11 @@ export default function ProcessInstanceListTable({ setProcessInstanceReportSelection(result.report); } } + const stopRefreshing = () => { + if (clearRefreshRef.current) { + clearRefreshRef.current(); + } + }; function getProcessInstances() { // eslint-disable-next-line prefer-const let { page, perPage } = getPageInfoFromSearchParams( @@ -352,6 +359,7 @@ export default function ProcessInstanceListTable({ HttpService.makeCallToBackend({ path: `${apiPath}?${queryParamString}`, successCallback: setProcessInstancesFromResult, + onUnauthorized: stopRefreshing, }); } function processResultForProcessModels(result: any) { @@ -396,11 +404,12 @@ export default function ProcessInstanceListTable({ checkFiltersAndRun(); if (autoReload) { - return refreshAtInterval( + clearRefreshRef.current = refreshAtInterval( REFRESH_INTERVAL_SECONDS, REFRESH_TIMEOUT_SECONDS, checkFiltersAndRun ); + return clearRefreshRef.current; } return undefined; }, [ From a2130ec1d2db13774772601ad2113e0aa0c37d69 Mon Sep 17 00:00:00 2001 From: jasquat Date: Fri, 14 Apr 2023 10:00:06 -0400 Subject: [PATCH 7/7] fix cypress tests --- spiffworkflow-frontend/cypress/e2e/tasks.cy.js | 4 +++- spiffworkflow-frontend/src/components/ReactDiagramEditor.tsx | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/spiffworkflow-frontend/cypress/e2e/tasks.cy.js b/spiffworkflow-frontend/cypress/e2e/tasks.cy.js index a4b4a4ddc..50d24899f 100644 --- a/spiffworkflow-frontend/cypress/e2e/tasks.cy.js +++ b/spiffworkflow-frontend/cypress/e2e/tasks.cy.js @@ -90,7 +90,9 @@ describe('tasks', () => { cy.get('.is-visible .cds--modal-close').click(); cy.navigateToHome(); - cy.contains('Tasks').should('exist'); + + // look for somethig to make sure the homepage has loaded + cy.contains('Waiting for me').should('exist'); // FIXME: this will probably need a better way to link to the proper form that we want cy.contains('Go').click(); diff --git a/spiffworkflow-frontend/src/components/ReactDiagramEditor.tsx b/spiffworkflow-frontend/src/components/ReactDiagramEditor.tsx index 82dddd4ad..9b9307f82 100644 --- a/spiffworkflow-frontend/src/components/ReactDiagramEditor.tsx +++ b/spiffworkflow-frontend/src/components/ReactDiagramEditor.tsx @@ -467,7 +467,9 @@ export default function ReactDiagramEditor({ return; } diagramModelerToUse.importXML(diagramXMLToDisplay).then(() => { - diagramModelerToUse.get('canvas').zoom('fit-viewport'); + if (diagramType === 'bpmn' || diagramType === 'readonly') { + diagramModelerToUse.get('canvas').zoom('fit-viewport'); + } }); alreadyImportedXmlRef.current = true;