From 54b7c5c3ec2b977c86be3b7db64bafbd0acec041 Mon Sep 17 00:00:00 2001 From: jasquat <2487833+jasquat@users.noreply.github.com> Date: Fri, 20 Oct 2023 15:19:26 -0400 Subject: [PATCH] feature/formatting-in-extension-md (#559) * support formatting data client side in markdown and support greater than and less than for metadata column filters w/ burnettk * moved spiff conversion functions to FormattingService and use it in InstructionsForEndUser w/ burnettk * added tests for greater than and less than metadata operators and added negative tests w/ burnettk * removed unneeded useEffect w/ burnettk --------- Co-authored-by: jasquat --- .../routes/extensions_controller.py | 5 +- .../process_instance_report_service.py | 4 + .../helpers/base_test.py | 14 ++- .../integration/test_process_api.py | 92 +++++++++++++++++++ .../src/components/InstructionsForEndUser.tsx | 2 + .../components/ProcessInstanceListTable.tsx | 20 +--- spiffworkflow-frontend/src/helpers.tsx | 18 ++++ .../src/routes/Extension.tsx | 11 ++- .../src/services/FormattingService.tsx | 30 ++++++ spiffworkflow-frontend/tsconfig.json | 2 +- 10 files changed, 172 insertions(+), 26 deletions(-) create mode 100644 spiffworkflow-frontend/src/services/FormattingService.tsx diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/routes/extensions_controller.py b/spiffworkflow-backend/src/spiffworkflow_backend/routes/extensions_controller.py index 999910d1..ddf57811 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/routes/extensions_controller.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/routes/extensions_controller.py @@ -153,11 +153,12 @@ def _run_extension( script_engine=CustomBpmnScriptEngine(use_restricted_script_engine=False), process_id_to_run=process_id_to_run, ) + save_to_db = process_instance.persistence_level != "none" if body and "extension_input" in body: - processor.do_engine_steps(save=False, execution_strategy_name="run_current_ready_tasks") + processor.do_engine_steps(save=save_to_db, execution_strategy_name="run_current_ready_tasks") next_task = processor.next_task() next_task.update_data(body["extension_input"]) - processor.do_engine_steps(save=False, execution_strategy_name="greedy") + processor.do_engine_steps(save=save_to_db, execution_strategy_name="greedy") except ( ApiError, ProcessInstanceIsNotEnqueuedError, 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 fc72b2a6..8d39b2d3 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 @@ -643,6 +643,10 @@ class ProcessInstanceReportService: join_conditions.append(instance_metadata_alias.value == filter_for_column["field_value"]) elif filter_for_column["operator"] == "not_equals": join_conditions.append(instance_metadata_alias.value != filter_for_column["field_value"]) + elif filter_for_column["operator"] == "greater_than_or_equal_to": + join_conditions.append(instance_metadata_alias.value >= filter_for_column["field_value"]) + elif filter_for_column["operator"] == "less_than": + join_conditions.append(instance_metadata_alias.value < filter_for_column["field_value"]) elif filter_for_column["operator"] == "contains": join_conditions.append(instance_metadata_alias.value.like(f"%{filter_for_column['field_value']}%")) elif filter_for_column["operator"] == "is_empty": diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/helpers/base_test.py b/spiffworkflow-backend/tests/spiffworkflow_backend/helpers/base_test.py index c440e8c6..b004748a 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/helpers/base_test.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/helpers/base_test.py @@ -488,6 +488,7 @@ class BaseTest: process_instance: ProcessInstanceModel, operator: str, filter_field_value: str = "", + expect_to_find_instance: bool = True, ) -> None: report_metadata: ReportMetadata = { "columns": [ @@ -506,8 +507,17 @@ class BaseTest: response = self.post_to_process_instance_list( client, user, report_metadata=process_instance_report.get_report_metadata() ) - assert len(response.json["results"]) == 1 - assert response.json["results"][0]["id"] == process_instance.id + + if expect_to_find_instance is True: + assert len(response.json["results"]) == 1 + assert response.json["results"][0]["id"] == process_instance.id + else: + if len(response.json["results"]) == 1: + assert ( + response.json["results"][0]["id"] != process_instance.id + ), "expected not to find a specific process instance, but we found it" + else: + assert len(response.json["results"]) == 0 db.session.delete(process_instance_report) db.session.commit() diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_process_api.py b/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_process_api.py index bf94180d..9f1cbbef 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_process_api.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_process_api.py @@ -2937,10 +2937,102 @@ class TestProcessApi(BaseTest): operator="not_equals", filter_field_value="hey", ) + self.assert_report_with_process_metadata_operator_includes_instance( + client=client, + user=with_super_admin_user, + process_instance=process_instance_one, + operator="less_than", + filter_field_value="value4", + ) + self.assert_report_with_process_metadata_operator_includes_instance( + client=client, + user=with_super_admin_user, + process_instance=process_instance_one, + operator="greater_than_or_equal_to", + filter_field_value="value1", + ) self.assert_report_with_process_metadata_operator_includes_instance( client=client, user=with_super_admin_user, process_instance=process_instance_two, operator="is_empty" ) + def test_can_get_process_instance_list_with_report_metadata_using_different_operators_when_no_matches( + self, + app: Flask, + client: FlaskClient, + with_db_and_bpmn_file_cleanup: None, + with_super_admin_user: UserModel, + ) -> None: + 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_one_metadata = {"key1": "value1"} + process_instance_one = self.create_process_instance_with_synthetic_metadata( + process_model=process_model, process_instance_metadata_dict=process_instance_one_metadata + ) + + process_instance_two_metadata = {"key2": "value2"} + process_instance_two = self.create_process_instance_with_synthetic_metadata( + process_model=process_model, process_instance_metadata_dict=process_instance_two_metadata + ) + + self.assert_report_with_process_metadata_operator_includes_instance( + client=client, + user=with_super_admin_user, + process_instance=process_instance_two, + operator="is_not_empty", + expect_to_find_instance=False, + ) + self.assert_report_with_process_metadata_operator_includes_instance( + client=client, + user=with_super_admin_user, + process_instance=process_instance_one, + operator="equals", + filter_field_value="value2", + expect_to_find_instance=False, + ) + self.assert_report_with_process_metadata_operator_includes_instance( + client=client, + user=with_super_admin_user, + process_instance=process_instance_one, + operator="contains", + filter_field_value="alunooo", + expect_to_find_instance=False, + ) + self.assert_report_with_process_metadata_operator_includes_instance( + client=client, + user=with_super_admin_user, + process_instance=process_instance_one, + operator="not_equals", + filter_field_value="value1", + expect_to_find_instance=False, + ) + self.assert_report_with_process_metadata_operator_includes_instance( + client=client, + user=with_super_admin_user, + process_instance=process_instance_one, + operator="less_than", + filter_field_value="value1", + expect_to_find_instance=False, + ) + self.assert_report_with_process_metadata_operator_includes_instance( + client=client, + user=with_super_admin_user, + process_instance=process_instance_one, + operator="greater_than_or_equal_to", + filter_field_value="value2", + expect_to_find_instance=False, + ) + self.assert_report_with_process_metadata_operator_includes_instance( + client=client, + user=with_super_admin_user, + process_instance=process_instance_one, + operator="is_empty", + expect_to_find_instance=False, + ) + def test_can_get_process_instance_list_with_report_metadata_and_process_initiator( self, app: Flask, diff --git a/spiffworkflow-frontend/src/components/InstructionsForEndUser.tsx b/spiffworkflow-frontend/src/components/InstructionsForEndUser.tsx index be21807d..b0484bec 100644 --- a/spiffworkflow-frontend/src/components/InstructionsForEndUser.tsx +++ b/spiffworkflow-frontend/src/components/InstructionsForEndUser.tsx @@ -2,6 +2,7 @@ import React, { useEffect, useState } from 'react'; // @ts-ignore import MDEditor from '@uiw/react-md-editor'; import { Toggle } from '@carbon/react'; +import FormattingService from '../services/FormattingService'; type OwnProps = { task: any; @@ -25,6 +26,7 @@ export default function InstructionsForEndUser({ if (instructionsForEndUser) { instructions = instructionsForEndUser; } + instructions = FormattingService.checkForSpiffFormats(instructions); const maxLineCount: number = 8; const maxWordCount: number = 75; diff --git a/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx b/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx index 410cff3b..c6e02a8b 100644 --- a/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx +++ b/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx @@ -50,8 +50,8 @@ import { REFRESH_TIMEOUT_SECONDS, titleizeString, truncateString, - isANumber, formatDurationForDisplay, + formatDateTime, } from '../helpers'; import { useUriListForPermissions } from '../hooks/UriListForPermissions'; @@ -1697,24 +1697,6 @@ export default function ProcessInstanceListTable({ return value; }; - const formatDateTime = (_row: ProcessInstance, value: any) => { - if (value === undefined || value === null) { - return value; - } - let dateInSeconds = value; - if (!isANumber(value)) { - const timeArgs = value.split('T'); - dateInSeconds = convertDateAndTimeStringsToSeconds( - timeArgs[0], - timeArgs[1] - ); - } - if (dateInSeconds) { - return convertSecondsToFormattedDateTime(dateInSeconds); - } - return null; - }; - const formattedColumn = (row: ProcessInstance, column: ReportColumn) => { const reportColumnFormatters: Record = { id: formatProcessInstanceId, diff --git a/spiffworkflow-frontend/src/helpers.tsx b/spiffworkflow-frontend/src/helpers.tsx index e51f3c82..fbcdcd2b 100644 --- a/spiffworkflow-frontend/src/helpers.tsx +++ b/spiffworkflow-frontend/src/helpers.tsx @@ -442,3 +442,21 @@ export const formatDurationForDisplay = (_row: any, value: any) => { } return durationTimes.join(' '); }; + +export const formatDateTime = (_row: any, value: any) => { + if (value === undefined || value === null) { + return value; + } + let dateInSeconds = value; + if (!isANumber(value)) { + const timeArgs = value.split('T'); + dateInSeconds = convertDateAndTimeStringsToSeconds( + timeArgs[0], + timeArgs[1] + ); + } + if (dateInSeconds) { + return convertSecondsToFormattedDateTime(dateInSeconds); + } + return null; +}; diff --git a/spiffworkflow-frontend/src/routes/Extension.tsx b/spiffworkflow-frontend/src/routes/Extension.tsx index 573b6eb5..2d1c3f71 100644 --- a/spiffworkflow-frontend/src/routes/Extension.tsx +++ b/spiffworkflow-frontend/src/routes/Extension.tsx @@ -16,6 +16,7 @@ import { UiSchemaPageDefinition, } from '../extension_ui_schema_interfaces'; import ErrorDisplay from '../components/ErrorDisplay'; +import FormattingService from '../services/FormattingService'; // eslint-disable-next-line sonarjs/cognitive-complexity export default function Extension() { @@ -47,7 +48,10 @@ export default function Extension() { const processLoadResult = (result: any) => { setFormData(result.task_data); if (result.rendered_results_markdown) { - setMarkdownToRenderOnLoad(result.rendered_results_markdown); + const newMarkdown = FormattingService.checkForSpiffFormats( + result.rendered_results_markdown + ); + setMarkdownToRenderOnLoad(newMarkdown); } }; @@ -162,7 +166,10 @@ export default function Extension() { } else { setProcessedTaskData(result.task_data); if (result.rendered_results_markdown) { - setMarkdownToRenderOnSubmit(result.rendered_results_markdown); + const newMarkdown = FormattingService.checkForSpiffFormats( + result.rendered_results_markdown + ); + setMarkdownToRenderOnSubmit(newMarkdown); } setFormButtonsDisabled(false); } diff --git a/spiffworkflow-frontend/src/services/FormattingService.tsx b/spiffworkflow-frontend/src/services/FormattingService.tsx new file mode 100644 index 00000000..4ae7b735 --- /dev/null +++ b/spiffworkflow-frontend/src/services/FormattingService.tsx @@ -0,0 +1,30 @@ +import { formatDateTime, formatDurationForDisplay } from '../helpers'; + +const spiffFormatFunctions: { [key: string]: Function } = { + convert_seconds_to_date_time_for_display: formatDateTime, + convert_seconds_to_duration_for_display: formatDurationForDisplay, +}; + +const checkForSpiffFormats = (markdown: string) => { + const replacer = ( + match: string, + spiffFormat: string, + originalValue: string + ) => { + if (spiffFormat in spiffFormatFunctions) { + return spiffFormatFunctions[spiffFormat](undefined, originalValue); + } + console.warn( + `attempted: ${match}, but ${spiffFormat} is not a valid conversion function` + ); + + return match; + }; + return markdown.replaceAll(/SPIFF_FORMAT:::(\w+)\(([^)]+)\)/g, replacer); +}; + +const FormattingService = { + checkForSpiffFormats, +}; + +export default FormattingService; diff --git a/spiffworkflow-frontend/tsconfig.json b/spiffworkflow-frontend/tsconfig.json index 87997aba..ed6b17ab 100644 --- a/spiffworkflow-frontend/tsconfig.json +++ b/spiffworkflow-frontend/tsconfig.json @@ -6,7 +6,7 @@ "module": "commonjs", "skipLibCheck": true, "strict": true, - "target": "es2016", + "target": "es2021", }, "include": ["src/**/*"] }