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 <jasquat@users.noreply.github.com>
This commit is contained in:
jasquat 2023-10-20 15:19:26 -04:00 committed by GitHub
parent 6664b52985
commit 54b7c5c3ec
10 changed files with 172 additions and 26 deletions

View File

@ -153,11 +153,12 @@ def _run_extension(
script_engine=CustomBpmnScriptEngine(use_restricted_script_engine=False), script_engine=CustomBpmnScriptEngine(use_restricted_script_engine=False),
process_id_to_run=process_id_to_run, process_id_to_run=process_id_to_run,
) )
save_to_db = process_instance.persistence_level != "none"
if body and "extension_input" in body: 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 = processor.next_task()
next_task.update_data(body["extension_input"]) 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 ( except (
ApiError, ApiError,
ProcessInstanceIsNotEnqueuedError, ProcessInstanceIsNotEnqueuedError,

View File

@ -643,6 +643,10 @@ class ProcessInstanceReportService:
join_conditions.append(instance_metadata_alias.value == filter_for_column["field_value"]) join_conditions.append(instance_metadata_alias.value == filter_for_column["field_value"])
elif filter_for_column["operator"] == "not_equals": elif filter_for_column["operator"] == "not_equals":
join_conditions.append(instance_metadata_alias.value != filter_for_column["field_value"]) 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": elif filter_for_column["operator"] == "contains":
join_conditions.append(instance_metadata_alias.value.like(f"%{filter_for_column['field_value']}%")) join_conditions.append(instance_metadata_alias.value.like(f"%{filter_for_column['field_value']}%"))
elif filter_for_column["operator"] == "is_empty": elif filter_for_column["operator"] == "is_empty":

View File

@ -488,6 +488,7 @@ class BaseTest:
process_instance: ProcessInstanceModel, process_instance: ProcessInstanceModel,
operator: str, operator: str,
filter_field_value: str = "", filter_field_value: str = "",
expect_to_find_instance: bool = True,
) -> None: ) -> None:
report_metadata: ReportMetadata = { report_metadata: ReportMetadata = {
"columns": [ "columns": [
@ -506,8 +507,17 @@ class BaseTest:
response = self.post_to_process_instance_list( response = self.post_to_process_instance_list(
client, user, report_metadata=process_instance_report.get_report_metadata() 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.delete(process_instance_report)
db.session.commit() db.session.commit()

View File

@ -2937,10 +2937,102 @@ class TestProcessApi(BaseTest):
operator="not_equals", operator="not_equals",
filter_field_value="hey", 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( self.assert_report_with_process_metadata_operator_includes_instance(
client=client, user=with_super_admin_user, process_instance=process_instance_two, operator="is_empty" 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( def test_can_get_process_instance_list_with_report_metadata_and_process_initiator(
self, self,
app: Flask, app: Flask,

View File

@ -2,6 +2,7 @@ import React, { useEffect, useState } from 'react';
// @ts-ignore // @ts-ignore
import MDEditor from '@uiw/react-md-editor'; import MDEditor from '@uiw/react-md-editor';
import { Toggle } from '@carbon/react'; import { Toggle } from '@carbon/react';
import FormattingService from '../services/FormattingService';
type OwnProps = { type OwnProps = {
task: any; task: any;
@ -25,6 +26,7 @@ export default function InstructionsForEndUser({
if (instructionsForEndUser) { if (instructionsForEndUser) {
instructions = instructionsForEndUser; instructions = instructionsForEndUser;
} }
instructions = FormattingService.checkForSpiffFormats(instructions);
const maxLineCount: number = 8; const maxLineCount: number = 8;
const maxWordCount: number = 75; const maxWordCount: number = 75;

View File

@ -50,8 +50,8 @@ import {
REFRESH_TIMEOUT_SECONDS, REFRESH_TIMEOUT_SECONDS,
titleizeString, titleizeString,
truncateString, truncateString,
isANumber,
formatDurationForDisplay, formatDurationForDisplay,
formatDateTime,
} from '../helpers'; } from '../helpers';
import { useUriListForPermissions } from '../hooks/UriListForPermissions'; import { useUriListForPermissions } from '../hooks/UriListForPermissions';
@ -1697,24 +1697,6 @@ export default function ProcessInstanceListTable({
return value; 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 formattedColumn = (row: ProcessInstance, column: ReportColumn) => {
const reportColumnFormatters: Record<string, any> = { const reportColumnFormatters: Record<string, any> = {
id: formatProcessInstanceId, id: formatProcessInstanceId,

View File

@ -442,3 +442,21 @@ export const formatDurationForDisplay = (_row: any, value: any) => {
} }
return durationTimes.join(' '); 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;
};

View File

@ -16,6 +16,7 @@ import {
UiSchemaPageDefinition, UiSchemaPageDefinition,
} from '../extension_ui_schema_interfaces'; } from '../extension_ui_schema_interfaces';
import ErrorDisplay from '../components/ErrorDisplay'; import ErrorDisplay from '../components/ErrorDisplay';
import FormattingService from '../services/FormattingService';
// eslint-disable-next-line sonarjs/cognitive-complexity // eslint-disable-next-line sonarjs/cognitive-complexity
export default function Extension() { export default function Extension() {
@ -47,7 +48,10 @@ export default function Extension() {
const processLoadResult = (result: any) => { const processLoadResult = (result: any) => {
setFormData(result.task_data); setFormData(result.task_data);
if (result.rendered_results_markdown) { 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 { } else {
setProcessedTaskData(result.task_data); setProcessedTaskData(result.task_data);
if (result.rendered_results_markdown) { if (result.rendered_results_markdown) {
setMarkdownToRenderOnSubmit(result.rendered_results_markdown); const newMarkdown = FormattingService.checkForSpiffFormats(
result.rendered_results_markdown
);
setMarkdownToRenderOnSubmit(newMarkdown);
} }
setFormButtonsDisabled(false); setFormButtonsDisabled(false);
} }

View File

@ -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;

View File

@ -6,7 +6,7 @@
"module": "commonjs", "module": "commonjs",
"skipLibCheck": true, "skipLibCheck": true,
"strict": true, "strict": true,
"target": "es2016", "target": "es2021",
}, },
"include": ["src/**/*"] "include": ["src/**/*"]
} }