diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/api.yml b/spiffworkflow-backend/src/spiffworkflow_backend/api.yml index 963ef12b..ebf27ad8 100755 --- a/spiffworkflow-backend/src/spiffworkflow_backend/api.yml +++ b/spiffworkflow-backend/src/spiffworkflow_backend/api.yml @@ -660,6 +660,18 @@ paths: description: The username of the process initiator schema: type: string + - name: report_columns + in: query + required: false + description: Base64 encoded json of report columns. + schema: + type: string + - name: report_filter_by + in: query + required: false + description: Base64 encoded json of report filter by. + schema: + type: string get: operationId: spiffworkflow_backend.routes.process_instances_controller.process_instance_list_for_me summary: Returns a list of process instances that are associated with me. @@ -779,6 +791,18 @@ paths: description: The username of the process initiator schema: type: string + - name: report_columns + in: query + required: false + description: Base64 encoded json of report columns. + schema: + type: string + - name: report_filter_by + in: query + required: false + description: Base64 encoded json of report filter by. + schema: + type: string get: operationId: spiffworkflow_backend.routes.process_instances_controller.process_instance_list summary: Returns a list of process instances. diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/models/task.py b/spiffworkflow-backend/src/spiffworkflow_backend/models/task.py index 302df25a..b35c8759 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/models/task.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/models/task.py @@ -55,7 +55,7 @@ class TaskModel(SpiffworkflowBaseDBModel): json_data_hash: str = db.Column(db.String(255), nullable=False, index=True) start_in_seconds: float = db.Column(db.DECIMAL(17, 6)) - end_in_seconds: float | None = db.Column(db.DECIMAL(17, 6)) + end_in_seconds: Union[float, None] = db.Column(db.DECIMAL(17, 6)) class Task: diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_instances_controller.py b/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_instances_controller.py index 634bf0ae..9608a705 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_instances_controller.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_instances_controller.py @@ -1,4 +1,5 @@ """APIs for dealing with process groups, process models, and process instances.""" +import base64 import json from uuid import UUID from typing import Any @@ -117,10 +118,10 @@ def process_instance_run( ) processor = ProcessInstanceProcessor(process_instance) - processor.lock_process_instance("Web") if do_engine_steps: try: + processor.lock_process_instance("Web") processor.do_engine_steps(save=True) except ApiError as e: ErrorHandlingService().handle_error(processor, e) @@ -252,6 +253,8 @@ def process_instance_list_for_me( report_id: Optional[int] = None, user_group_identifier: Optional[str] = None, process_initiator_username: Optional[str] = None, + report_columns: Optional[str] = None, + report_filter_by: Optional[str] = None, ) -> flask.wrappers.Response: """Process_instance_list_for_me.""" return process_instance_list( @@ -268,6 +271,8 @@ def process_instance_list_for_me( report_id=report_id, user_group_identifier=user_group_identifier, with_relation_to_me=True, + report_columns=report_columns, + report_filter_by=report_filter_by, ) @@ -286,12 +291,21 @@ def process_instance_list( report_id: Optional[int] = None, user_group_identifier: Optional[str] = None, process_initiator_username: Optional[str] = None, + report_columns: Optional[str] = None, + report_filter_by: Optional[str] = None, ) -> flask.wrappers.Response: """Process_instance_list.""" process_instance_report = ProcessInstanceReportService.report_with_identifier( g.user, report_id, report_identifier ) + report_column_list = None + if report_columns: + report_column_list = json.loads(base64.b64decode(report_columns)) + report_filter_by_list = None + if report_filter_by: + report_filter_by_list = json.loads(base64.b64decode(report_filter_by)) + if user_filter: report_filter = ProcessInstanceReportFilter( process_model_identifier=process_model_identifier, @@ -303,6 +317,8 @@ def process_instance_list( with_relation_to_me=with_relation_to_me, process_status=process_status.split(",") if process_status else None, process_initiator_username=process_initiator_username, + report_column_list=report_column_list, + report_filter_by_list=report_filter_by_list, ) else: report_filter = ( @@ -317,6 +333,8 @@ def process_instance_list( process_status=process_status, with_relation_to_me=with_relation_to_me, process_initiator_username=process_initiator_username, + report_column_list=report_column_list, + report_filter_by_list=report_filter_by_list, ) ) diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/logging_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/logging_service.py index 25f00d63..401d071e 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/logging_service.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/logging_service.py @@ -2,6 +2,7 @@ import json import logging import re +import sys from typing import Any from typing import Optional @@ -171,6 +172,10 @@ def setup_logger(app: Flask) -> None: the_logger.propagate = False the_logger.addHandler(spiff_logger_filehandler) else: + if len(the_logger.handlers) < 1: + # it's very verbose, so only add handlers for the obscure loggers when log level is DEBUG + if upper_log_level_string == "DEBUG": + the_logger.addHandler(logging.StreamHandler(sys.stdout)) for the_handler in the_logger.handlers: the_handler.setFormatter(log_formatter) the_handler.setLevel(log_level) 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 809b6ae1..e2d7ef19 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 @@ -50,6 +50,8 @@ class ProcessInstanceReportFilter: with_tasks_assigned_to_my_group: Optional[bool] = None with_relation_to_me: Optional[bool] = None process_initiator_username: Optional[str] = None + report_column_list: Optional[list] = None + report_filter_by_list: Optional[list] = None def to_dict(self) -> dict[str, str]: """To_dict.""" @@ -85,6 +87,10 @@ class ProcessInstanceReportFilter: d["with_relation_to_me"] = str(self.with_relation_to_me).lower() if self.process_initiator_username is not None: d["process_initiator_username"] = str(self.process_initiator_username) + if self.report_column_list is not None: + 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) return d @@ -229,6 +235,8 @@ class ProcessInstanceReportService: 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") + report_column_list = list_value("report_column_list") + report_filter_by_list = list_value("report_filter_by_list") report_filter = ProcessInstanceReportFilter( process_model_identifier=process_model_identifier, @@ -244,6 +252,8 @@ class ProcessInstanceReportService: 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, + report_column_list=report_column_list, + report_filter_by_list=report_filter_by_list, ) return report_filter @@ -265,6 +275,8 @@ class ProcessInstanceReportService: with_tasks_assigned_to_my_group: Optional[bool] = None, with_relation_to_me: Optional[bool] = None, process_initiator_username: Optional[str] = None, + report_column_list: Optional[list] = None, + report_filter_by_list: Optional[list] = None, ) -> ProcessInstanceReportFilter: """Filter_from_metadata_with_overrides.""" report_filter = cls.filter_from_metadata(process_instance_report) @@ -291,6 +303,10 @@ class ProcessInstanceReportService: report_filter.with_tasks_completed_by_me = with_tasks_completed_by_me if process_initiator_username is not None: report_filter.process_initiator_username = process_initiator_username + if report_column_list is not None: + 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 with_tasks_assigned_to_my_group is not None: report_filter.with_tasks_assigned_to_my_group = ( with_tasks_assigned_to_my_group @@ -483,6 +499,15 @@ class ProcessInstanceReportService: stock_columns = ProcessInstanceReportService.get_column_names_for_model( ProcessInstanceModel ) + if report_filter.report_column_list: + process_instance_report.report_metadata["columns"] = ( + report_filter.report_column_list + ) + if report_filter.report_filter_by_list: + process_instance_report.report_metadata["filter_by"] = ( + report_filter.report_filter_by_list + ) + for column in process_instance_report.report_metadata["columns"]: if column["accessor"] in stock_columns: continue diff --git a/spiffworkflow-frontend/cypress/pilot/pp1.cy.js b/spiffworkflow-frontend/cypress/pilot/pp1.cy.js index aac3dbc8..7f38e37d 100644 --- a/spiffworkflow-frontend/cypress/pilot/pp1.cy.js +++ b/spiffworkflow-frontend/cypress/pilot/pp1.cy.js @@ -10,7 +10,7 @@ const approveWithUser = ( .contains(/^Submit$/) .click(); - cy.contains('Tasks I can complete', { timeout: 20000 }); + cy.contains('Tasks I can complete', { timeout: 30000 }); cy.get('.cds--btn').contains(/^Go$/).click(); // approve! @@ -19,12 +19,12 @@ const approveWithUser = ( .contains(/^Submit$/) .click(); if (expectAdditionalApprovalInfoPage) { - cy.contains(expectAdditionalApprovalInfoPage, { timeout: 20000 }); + cy.contains(expectAdditionalApprovalInfoPage, { timeout: 30000 }); cy.get('button') .contains(/^Continue$/) .click(); } - cy.location({ timeout: 20000 }).should((loc) => { + cy.location({ timeout: 30000 }).should((loc) => { expect(loc.pathname).to.eq('/tasks'); }); cy.logout(); @@ -37,7 +37,7 @@ describe('pp1', () => { cy.contains('Start New +').click(); cy.contains('Raise New Demand Request'); cy.runPrimaryBpmnFile(true); - cy.contains('Please select the type of request to Start the process.'); + cy.contains('Please select the type of request to start the process.'); // wait a second to ensure we can click the radio button cy.wait(1000); cy.get('input#root-procurement').click(); @@ -47,7 +47,7 @@ describe('pp1', () => { .click(); cy.contains( 'Submit a new demand request for the procurement of needed items', - { timeout: 20000 } + { timeout: 30000 } ); cy.url().then((currentUrl) => { @@ -68,7 +68,7 @@ describe('pp1', () => { .contains(/^Submit$/) .click(); - cy.contains('Task: Enter NDR Items', { timeout: 20000 }); + cy.contains('Task: Enter NDR Items', { timeout: 30000 }); cy.get('#root_0_sub_category').select('op_src'); cy.get('#root_0_item').clear().type('spiffworkflow'); cy.get('#root_0_qty').clear().type('1'); @@ -81,13 +81,14 @@ describe('pp1', () => { cy.contains( 'Review and provide any supporting information or files for your request.', - { timeout: 20000 } + { timeout: 30000 } ); cy.contains('Submit the Request').click(); cy.get('input[value="Submit the Request"]').click(); cy.get('button') .contains(/^Submit$/) .click(); + cy.contains('Tasks for my open instances', { timeout: 30000 }); cy.logout(); approveWithUser( diff --git a/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx b/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx index 8fe2404a..76a04440 100644 --- a/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx +++ b/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx @@ -36,6 +36,7 @@ import { convertSecondsToFormattedDateString, convertSecondsToFormattedDateTime, convertSecondsToFormattedTimeHoursMinutes, + encodeBase64, getPageInfoFromSearchParams, getProcessModelFullIdentifierFromSearchParams, modifyProcessIdentifierForPathParam, @@ -268,6 +269,17 @@ export default function ProcessInstanceListTable({ queryParamString += `&report_identifier=${reportIdentifier}`; } + if (searchParams.get('report_columns')) { + queryParamString += `&report_columns=${searchParams.get( + 'report_columns' + )}`; + } + if (searchParams.get('report_filter_by')) { + queryParamString += `&report_filter_by=${searchParams.get( + 'report_filter_by' + )}`; + } + Object.keys(dateParametersToAlwaysFilterBy).forEach( (paramName: string) => { const dateFunctionToCall = @@ -529,6 +541,14 @@ export default function ProcessInstanceListTable({ }; }; + const reportColumns = () => { + return (reportMetadata as any).columns; + }; + + const reportFilterBy = () => { + return (reportMetadata as any).filter_by; + }; + const applyFilter = (event: any) => { event.preventDefault(); const { page, perPage } = getPageInfoFromSearchParams( @@ -578,6 +598,11 @@ export default function ProcessInstanceListTable({ queryParamString += `&process_initiator_username=${processInitiatorSelection.username}`; } + const reportColumnsBase64 = encodeBase64(JSON.stringify(reportColumns())); + queryParamString += `&report_columns=${reportColumnsBase64}`; + const reportFilterByBase64 = encodeBase64(JSON.stringify(reportFilterBy())); + queryParamString += `&report_filter_by=${reportFilterByBase64}`; + removeError(); setProcessInstanceReportJustSaved(null); setProcessInstanceFilters({}); @@ -683,10 +708,6 @@ export default function ProcessInstanceListTable({ navigate(`${processInstanceListPathPrefix}${queryParamString}`); }; - const reportColumns = () => { - return (reportMetadata as any).columns; - }; - const reportColumnAccessors = () => { return reportColumns().map((reportColumn: ReportColumn) => { return reportColumn.accessor; diff --git a/spiffworkflow-frontend/src/helpers.tsx b/spiffworkflow-frontend/src/helpers.tsx index b94e28bc..88ab1522 100644 --- a/spiffworkflow-frontend/src/helpers.tsx +++ b/spiffworkflow-frontend/src/helpers.tsx @@ -1,4 +1,6 @@ import { format } from 'date-fns'; +import { Buffer } from 'buffer'; + import { DATE_TIME_FORMAT, DATE_FORMAT, @@ -260,3 +262,11 @@ export const getBpmnProcessIdentifiers = (rootBpmnElement: any) => { export const isInteger = (str: string | number) => { return /^\d+$/.test(str.toString()); }; + +export const encodeBase64 = (data: string) => { + return Buffer.from(data).toString('base64'); +}; + +export const decodeBase64 = (data: string) => { + return Buffer.from(data, 'base64').toString('ascii'); +}; diff --git a/spiffworkflow-frontend/src/routes/ProcessInstanceShow.tsx b/spiffworkflow-frontend/src/routes/ProcessInstanceShow.tsx index e9da2273..36c06d23 100644 --- a/spiffworkflow-frontend/src/routes/ProcessInstanceShow.tsx +++ b/spiffworkflow-frontend/src/routes/ProcessInstanceShow.tsx @@ -80,7 +80,6 @@ export default function ProcessInstanceShow({ variant }: OwnProps) { const [eventPayload, setEventPayload] = useState('{}'); const [eventTextEditorEnabled, setEventTextEditorEnabled] = useState(false); - const [displayDetails, setDisplayDetails] = useState(false); const [showProcessInstanceMetadata, setShowProcessInstanceMetadata] = useState(false); @@ -304,92 +303,26 @@ export default function ProcessInstanceShow({ variant }: OwnProps) { }); }; - const detailedViewElement = () => { - if (!processInstance) { - return null; - } - - if (displayDetails) { - return ( - <> - - - - - - Updated At:{' '} - - - {convertSecondsToFormattedDateTime( - processInstance.updated_at_in_seconds - )} - - - - - Created At:{' '} - - - {convertSecondsToFormattedDateTime( - processInstance.created_at_in_seconds - )} - - - - - Process model revision:{' '} - - - {processInstance.bpmn_version_control_identifier} ( - {processInstance.bpmn_version_control_type}) - - - - ); - } - return ( - - - - ); - }; - const getInfoTag = () => { if (!processInstance) { return null; } - const currentEndDate = convertSecondsToFormattedDateTime( - processInstance.end_in_seconds || 0 - ); - let currentEndDateTag; - if (currentEndDate) { - currentEndDateTag = ( - - - Completed:{' '} - - - {convertSecondsToFormattedDateTime( - processInstance.end_in_seconds || 0 - ) || 'N/A'} - - - ); + let lastUpdatedTimeLabel = 'Updated At'; + let lastUpdatedTime = processInstance.updated_at_in_seconds; + if (processInstance.end_in_seconds) { + lastUpdatedTimeLabel = 'Completed'; + lastUpdatedTime = processInstance.end_in_seconds; } + const lastUpdatedTimeTag = ( + + + {lastUpdatedTimeLabel}:{' '} + + + {convertSecondsToFormattedDateTime(lastUpdatedTime || 0) || 'N/A'} + + + ); let statusIcon = ; if (processInstance.status === 'suspended') { @@ -433,13 +366,30 @@ export default function ProcessInstanceShow({ variant }: OwnProps) { Started:{' '} - + {convertSecondsToFormattedDateTime( processInstance.start_in_seconds || 0 )} - {currentEndDateTag} + {lastUpdatedTimeTag} + + + Process model revision:{' '} + + + {processInstance.bpmn_version_control_identifier} ( + {processInstance.bpmn_version_control_type}) + + Status:{' '} @@ -450,7 +400,6 @@ export default function ProcessInstanceShow({ variant }: OwnProps) { - {detailedViewElement()}
@@ -493,7 +442,7 @@ export default function ProcessInstanceShow({ variant }: OwnProps) { setShowProcessInstanceMetadata(true); }} > - Metadata + Details ) : null} @@ -1012,7 +961,7 @@ export default function ProcessInstanceShow({ variant }: OwnProps) { return ( setShowProcessInstanceMetadata(false)} > diff --git a/spiffworkflow-frontend/src/routes/TaskShow.tsx b/spiffworkflow-frontend/src/routes/TaskShow.tsx index 53ac8c0c..fbf0bd81 100644 --- a/spiffworkflow-frontend/src/routes/TaskShow.tsx +++ b/spiffworkflow-frontend/src/routes/TaskShow.tsx @@ -19,6 +19,7 @@ import HttpService from '../services/HttpService'; import useAPIError from '../hooks/UseApiError'; import { modifyProcessIdentifierForPathParam } from '../helpers'; import { ProcessInstanceTask } from '../interfaces'; +import ProcessBreadcrumb from '../components/ProcessBreadcrumb'; export default function TaskShow() { const [task, setTask] = useState(null); @@ -85,7 +86,6 @@ export default function TaskShow() { successCallback: processSubmitResult, failureCallback: (error: any) => { addError(error); - setDisabled(false); }, httpMethod: 'PUT', postBody: dataToSubmit, @@ -270,7 +270,7 @@ export default function TaskShow() { return (
{/* - https://www.npmjs.com/package/@uiw/react-md-editor switches to dark mode by default by respecting @media (prefers-color-scheme: dark) + https://www.npmjs.com/package/@uiw/react-md-editor switches to dark mode by default by respecting @media (prefers-color-scheme: dark) This makes it look like our site is broken, so until the rest of the site supports dark mode, turn off dark mode for this component. */}
@@ -288,6 +288,17 @@ export default function TaskShow() { return (
+
{buildTaskNavigation()}

Task: {task.title} ({task.process_model_display_name}){statusString}