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),
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,

View File

@ -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":

View File

@ -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()
)
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()

View File

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

View File

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

View File

@ -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<string, any> = {
id: formatProcessInstanceId,

View File

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

View File

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

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",
"skipLibCheck": true,
"strict": true,
"target": "es2016",
"target": "es2021",
},
"include": ["src/**/*"]
}