sapiopycommons 2025.7.1a575__py3-none-any.whl → 2025.7.2a578__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of sapiopycommons might be problematic. Click here for more details.

Files changed (59) hide show
  1. sapiopycommons/ai/api/fielddefinitions/proto/fields_pb2.py +43 -0
  2. sapiopycommons/ai/api/fielddefinitions/proto/fields_pb2.pyi +31 -0
  3. sapiopycommons/ai/api/fielddefinitions/proto/fields_pb2_grpc.py +24 -0
  4. sapiopycommons/ai/api/fielddefinitions/proto/velox_field_def_pb2.py +123 -0
  5. sapiopycommons/ai/api/fielddefinitions/proto/velox_field_def_pb2.pyi +598 -0
  6. sapiopycommons/ai/api/fielddefinitions/proto/velox_field_def_pb2_grpc.py +24 -0
  7. sapiopycommons/ai/api/plan/proto/step_output_pb2.py +45 -0
  8. sapiopycommons/ai/api/plan/proto/step_output_pb2.pyi +42 -0
  9. sapiopycommons/ai/api/plan/proto/step_output_pb2_grpc.py +24 -0
  10. sapiopycommons/ai/api/plan/proto/step_pb2.py +43 -0
  11. sapiopycommons/ai/api/plan/proto/step_pb2.pyi +43 -0
  12. sapiopycommons/ai/api/plan/proto/step_pb2_grpc.py +24 -0
  13. sapiopycommons/ai/api/plan/script/proto/script_pb2.py +53 -0
  14. sapiopycommons/ai/api/plan/script/proto/script_pb2.pyi +99 -0
  15. sapiopycommons/ai/api/plan/script/proto/script_pb2_grpc.py +153 -0
  16. sapiopycommons/ai/api/plan/tool/proto/entry_pb2.py +57 -0
  17. sapiopycommons/ai/api/plan/tool/proto/entry_pb2.pyi +96 -0
  18. sapiopycommons/ai/api/plan/tool/proto/entry_pb2_grpc.py +24 -0
  19. sapiopycommons/ai/api/plan/tool/proto/tool_pb2.py +67 -0
  20. sapiopycommons/ai/api/plan/tool/proto/tool_pb2.pyi +220 -0
  21. sapiopycommons/ai/api/plan/tool/proto/tool_pb2_grpc.py +154 -0
  22. sapiopycommons/ai/api/session/proto/sapio_conn_info_pb2.py +39 -0
  23. sapiopycommons/ai/api/session/proto/sapio_conn_info_pb2.pyi +32 -0
  24. sapiopycommons/ai/api/session/proto/sapio_conn_info_pb2_grpc.py +24 -0
  25. sapiopycommons/ai/protobuf_utils.py +508 -0
  26. sapiopycommons/ai/test_client.py +251 -0
  27. sapiopycommons/ai/tool_service_base.py +798 -0
  28. sapiopycommons/callbacks/callback_util.py +332 -665
  29. sapiopycommons/callbacks/field_builder.py +0 -2
  30. sapiopycommons/chem/IndigoMolecules.py +1 -29
  31. sapiopycommons/chem/Molecules.py +3 -3
  32. sapiopycommons/customreport/auto_pagers.py +1 -26
  33. sapiopycommons/customreport/term_builder.py +1 -1
  34. sapiopycommons/datatype/pseudo_data_types.py +326 -349
  35. sapiopycommons/eln/experiment_handler.py +767 -408
  36. sapiopycommons/eln/experiment_report_util.py +6 -11
  37. sapiopycommons/eln/plate_designer.py +2 -7
  38. sapiopycommons/files/file_util.py +5 -7
  39. sapiopycommons/general/accession_service.py +2 -2
  40. sapiopycommons/general/aliases.py +1 -3
  41. sapiopycommons/general/audit_log.py +0 -7
  42. sapiopycommons/general/custom_report_util.py +0 -12
  43. sapiopycommons/processtracking/custom_workflow_handler.py +1 -11
  44. sapiopycommons/processtracking/endpoints.py +0 -27
  45. sapiopycommons/recordmodel/record_handler.py +317 -657
  46. sapiopycommons/rules/eln_rule_handler.py +1 -8
  47. sapiopycommons/rules/on_save_rule_handler.py +1 -8
  48. sapiopycommons/webhook/webhook_handlers.py +0 -3
  49. sapiopycommons/webhook/webservice_handlers.py +2 -2
  50. {sapiopycommons-2025.7.1a575.dist-info → sapiopycommons-2025.7.2a578.dist-info}/METADATA +2 -2
  51. sapiopycommons-2025.7.2a578.dist-info/RECORD +92 -0
  52. sapiopycommons/ai/tool_of_tools.py +0 -917
  53. sapiopycommons/eln/experiment_cache.py +0 -188
  54. sapiopycommons/eln/experiment_step_factory.py +0 -476
  55. sapiopycommons/eln/step_creation.py +0 -236
  56. sapiopycommons/general/data_structure_util.py +0 -115
  57. sapiopycommons-2025.7.1a575.dist-info/RECORD +0 -70
  58. {sapiopycommons-2025.7.1a575.dist-info → sapiopycommons-2025.7.2a578.dist-info}/WHEEL +0 -0
  59. {sapiopycommons-2025.7.1a575.dist-info → sapiopycommons-2025.7.2a578.dist-info}/licenses/LICENSE +0 -0
@@ -4,8 +4,7 @@ import io
4
4
  import re
5
5
  import warnings
6
6
  from copy import copy
7
- from enum import Enum
8
- from typing import Iterable, TypeAlias, Any, Callable, Container, Collection
7
+ from typing import Iterable, TypeAlias, Any, Callable
9
8
  from weakref import WeakValueDictionary
10
9
 
11
10
  from requests import ReadTimeout
@@ -39,35 +38,11 @@ from sapiopycommons.general.aliases import FieldMap, SapioRecord, AliasUtil, Rec
39
38
  from sapiopycommons.general.custom_report_util import CustomReportUtil
40
39
  from sapiopycommons.general.exceptions import SapioUserCancelledException, SapioException, SapioUserErrorException, \
41
40
  SapioDialogTimeoutException
42
- from sapiopycommons.general.time_util import TimeUtil
43
41
  from sapiopycommons.recordmodel.record_handler import RecordHandler
44
42
 
45
43
  DataTypeLayoutIdentifier: TypeAlias = DataTypeLayout | str | None
46
44
 
47
45
 
48
- # FR-47690: Added enum to customize blank handling result behavior, instead of using the require_selection/input
49
- # boolean parameter.
50
- class BlankResultHandling(Enum):
51
- """
52
- An enum that controls how blank results are handled in dialogs.
53
- """
54
- DEFAULT = 0
55
- """Used only by dialog functions. If a dialog function parameter is set to this value, then the blank result
56
- handling of the CallbackUtil is used."""
57
- REPEAT = 1
58
- """If the user provides a blank result, repeat the dialog."""
59
- CANCEL = 2
60
- """If the user provides a blank result, throw a cancel exception."""
61
- RETURN = 3
62
- """If the user provides a blank result, return it to the caller."""
63
-
64
-
65
- # CR-47521: Updated various parameter type hints from list or Iterable to more specific type hints.
66
- # If we need to iterate over the parameter, then it is Iterable.
67
- # If we need to see if the parameter contains a value, then it is Container.
68
- # If the length/size of the parameter is needed, then it is Collection.
69
- # If we need to access the parameter by an index, then it is Sequence. (This excludes sets and dictionaries, so it's
70
- # probably better to accept a Collection then cast the parameter to a list if you need to get an element from it.)
71
46
  class CallbackUtil:
72
47
  user: SapioUser
73
48
  callback: ClientCallback
@@ -78,7 +53,6 @@ class CallbackUtil:
78
53
  timeout_seconds: int
79
54
  width_pixels: int | None
80
55
  width_percent: float | None
81
- _default_blank_result_handling: BlankResultHandling
82
56
 
83
57
  __instances: WeakValueDictionary[SapioUser, CallbackUtil] = WeakValueDictionary()
84
58
  __initialized: bool
@@ -116,7 +90,6 @@ class CallbackUtil:
116
90
  self.timeout_seconds = self.user.timeout_seconds
117
91
  self.width_pixels = None
118
92
  self.width_percent = None
119
- self._default_blank_result_handling = BlankResultHandling.CANCEL
120
93
  self.__layouts = {}
121
94
 
122
95
  def set_dialog_width(self, width_pixels: int | None = None, width_percent: float | None = None):
@@ -143,19 +116,6 @@ class CallbackUtil:
143
116
  """
144
117
  self.timeout_seconds = timeout
145
118
 
146
- def set_default_blank_result_handling(self, handling: BlankResultHandling):
147
- """
148
- Set the default handling of blank results provided by the user in certain dialogs. This will only be used
149
- if the dialog functions have their own blank_result_handling parameter set to DEFAULT.
150
-
151
- :param handling: The handling to use for blank results in dialogs.
152
- """
153
- if not isinstance(handling, BlankResultHandling):
154
- raise SapioException("Invalid blank result handling provided.")
155
- if handling == BlankResultHandling.DEFAULT:
156
- raise SapioException("Blank result handling cannot be set to DEFAULT.")
157
- self._default_blank_result_handling = handling
158
-
159
119
  def toaster_popup(self, message: str, title: str = "", popup_type: PopupType = PopupType.Info) -> None:
160
120
  """
161
121
  Display a toaster popup in the bottom right corner of the user's screen.
@@ -217,7 +177,7 @@ class CallbackUtil:
217
177
  # Send the request to the user.
218
178
  request = OptionDialogRequest(title, msg, options, default_option, user_can_cancel,
219
179
  width_in_pixels=self.width_pixels, width_percentage=self.width_percent)
220
- response: int = self.__send_dialog(request, self.callback.show_option_dialog)
180
+ response: int = self.__handle_dialog_request(request, self.callback.show_option_dialog)
221
181
  return options[response]
222
182
 
223
183
  def ok_dialog(self, title: str, msg: str) -> None:
@@ -233,7 +193,7 @@ class CallbackUtil:
233
193
  def ok_cancel_dialog(self, title: str, msg: str, default_ok: bool = True) -> bool:
234
194
  """
235
195
  Create an option dialog where the only options are "OK" and "Cancel". Doesn't allow the user to cancel the
236
- dialog using the X in the top right corner.
196
+ dialog using the X at the top right corner.
237
197
 
238
198
  :param title: The title of the dialog.
239
199
  :param msg: The message to display in the dialog. This can be formatted using HTML elements.
@@ -245,7 +205,7 @@ class CallbackUtil:
245
205
  def yes_no_dialog(self, title: str, msg: str, default_yes: bool = True) -> bool:
246
206
  """
247
207
  Create an option dialog where the only options are "Yes" and "No". Doesn't allow the user to cancel the
248
- dialog using the X in the top right corner.
208
+ dialog using the X at the top right corner.
249
209
 
250
210
  :param title: The title of the dialog.
251
211
  :param msg: The message to display in the dialog. This can be formatted using HTML elements.
@@ -254,46 +214,16 @@ class CallbackUtil:
254
214
  """
255
215
  return self.option_dialog(title, msg, ["Yes", "No"], 0 if default_yes else 1, False) == "Yes"
256
216
 
257
- # FR-47690: Added function.
258
- def accept_decline_dialog(self, title: str, msg: str, default_accept: bool = True) -> bool:
259
- """
260
- Create an option dialog where the only options are "Accept" and "Decline". Doesn't allow the user to cancel the
261
- dialog using the X in the top right corner.
262
-
263
- :param title: The title of the dialog.
264
- :param msg: The message to display in the dialog. This can be formatted using HTML elements.
265
- :param default_accept: If true, "Accept" is the default choice. Otherwise, the default choice is "Decline".
266
- :return: True if the user selected Accept. False if the user selected Decline.
267
- """
268
- return self.option_dialog(title, msg, ["Accept", "Decline"], 0 if default_accept else 1, False) == "Accept"
269
-
270
- # FR-47690: Added function.
271
- def confirm_deny_dialog(self, title: str, msg: str, default_confirm: bool = True) -> bool:
272
- """
273
- Create an option dialog where the only options are "Confirm" and "Deny". Doesn't allow the user to cancel the
274
- dialog using the X in the top right corner.
275
-
276
- :param title: The title of the dialog.
277
- :param msg: The message to display in the dialog. This can be formatted using HTML elements.
278
- :param default_confirm: If true, "Confirm" is the default choice. Otherwise, the default choice is "Deny".
279
- :return: True if the user selected Confirm. False if the user selected Deny.
280
- """
281
- return self.option_dialog(title, msg, ["Confirm", "Deny"], 0 if default_confirm else 1, False) == "Confirm"
282
-
283
217
  # CR-47310: Add a parameter to the list, input, selection, and e-sign dialog functions to control reprompting the
284
218
  # user if no input/selection/valid credentials are provided.
285
- # FR-47690: Added shortcut_single_option parameter. Updated with blank result handling behavior.
286
219
  def list_dialog(self,
287
220
  title: str,
288
221
  options: Iterable[str],
289
222
  multi_select: bool = False,
290
223
  preselected_values: Iterable[str] | None = None,
291
224
  *,
292
- shortcut_single_option: bool = True,
293
- require_selection = None,
294
- blank_result_handling: BlankResultHandling = BlankResultHandling.DEFAULT,
295
- repeat_message: str | None = "Please provide a selection to continue.",
296
- cancel_message: str | None = "No selection was provided. Cancelling dialog.") -> list[str]:
225
+ require_selection: bool = False,
226
+ repeat_message: str | None = "Please provide a selection to continue.") -> list[str]:
297
227
  """
298
228
  Create a list dialog with the given options for the user to choose from.
299
229
 
@@ -302,237 +232,33 @@ class CallbackUtil:
302
232
  :param multi_select: Whether the user is able to select multiple options from the list.
303
233
  :param preselected_values: A list of values that will already be selected when the list dialog is created. The
304
234
  user can unselect these values if they want to.
305
- :param shortcut_single_option: If true, then if the list contains only one option, the dialog will not be shown
306
- and the single option will be returned immediately.
307
- :param require_selection: DEPRECATED. Use blank_result_handling with a value of BlankResultHandling.REPEAT
308
- instead.
309
- :param blank_result_handling: Determine how to handle the result of a callback when the user provides a blank
310
- result.
311
- :param repeat_message: If blank_result_handling is REPEAT and a repeat_message is provided, then that message
312
- appears as toaster text when the user provides a blank result.
313
- :param cancel_message: If blank_result_handling is CANCEL and a cancel_message is provided, then that message
314
- appears as toaster text when the user provides a blank result.
235
+ :param require_selection: If true, the request will be re-sent if the user submits the dialog without making
236
+ a selection.
237
+ :param repeat_message: If require_selection is true and a repeat_message is provided, then that message appears
238
+ as toaster text if the dialog is repeated.
315
239
  :return: The list of options that the user selected.
316
240
  """
317
241
  if not options:
318
242
  raise SapioException("No options provided.")
319
- options = list(options)
320
- if len(options) == 1 and shortcut_single_option:
321
- return [options[0]]
322
243
 
323
244
  # Send the request to the user.
324
- request = ListDialogRequest(title, multi_select, options,
245
+ request = ListDialogRequest(title, multi_select, list(options),
325
246
  list(preselected_values) if preselected_values else None,
326
247
  width_in_pixels=self.width_pixels, width_percentage=self.width_percent)
327
248
 
328
- # Reverse compatibility: If require_selection is true and blank_result_handling is not set, then
329
- # set blank_result_handling to REPEAT.
330
- if require_selection is True and blank_result_handling == BlankResultHandling.DEFAULT:
331
- blank_result_handling = BlankResultHandling.REPEAT
332
- def not_blank_func(r: list[str]) -> bool:
333
- return bool(r)
334
- return self.__send_dialog_blank_results(request, self.callback.show_list_dialog, not_blank_func,
335
- blank_result_handling, repeat_message, cancel_message)
336
-
337
- # FR-47690: Updated with blank result handling behavior.
338
- def input_dialog(self,
339
- title: str,
340
- msg: str,
341
- field: AbstractVeloxFieldDefinition,
342
- *,
343
- require_input = None,
344
- blank_result_handling: BlankResultHandling = BlankResultHandling.DEFAULT,
345
- repeat_message: str | None = "Please provide a value to continue.",
346
- cancel_message: str | None = "No input was provided. Cancelling dialog.") -> FieldValue:
347
- """
348
- Create an input dialog where the user must input data for a singular field.
349
-
350
- :param title: The title of the dialog.
351
- :param msg: The message to display in the dialog. This can be formatted using HTML elements.
352
- :param field: The definition for a field that the user must provide input to.
353
- :param require_input: DEPRECATED. Use blank_result_handling with a value of BlankResultHandling.REPEAT
354
- instead.
355
- :param blank_result_handling: Determine how to handle the result of a callback when the user provides a blank
356
- result.
357
- :param repeat_message: If blank_result_handling is REPEAT and a repeat_message is provided, then that message
358
- appears as toaster text when the user provides a blank result.
359
- :param cancel_message: If blank_result_handling is CANCEL and a cancel_message is provided, then that message
360
- appears as toaster text when the user provides a blank result.
361
- :return: The response value from the user for the given field.
362
- """
363
- # Send the request to the user.
364
- request = InputDialogCriteria(title, msg, field,
365
- width_in_pixels=self.width_pixels, width_percentage=self.width_percent)
366
-
367
- # Reverse compatibility: If require_selection is true and blank_result_handling is not set, then
368
- # set blank_result_handling to REPEAT.
369
- handling = blank_result_handling
370
- if require_input is True and handling == BlankResultHandling.DEFAULT:
371
- handling = BlankResultHandling.REPEAT
372
- if handling == BlankResultHandling.DEFAULT or handling is None:
373
- handling = self._default_blank_result_handling
374
-
249
+ # If require_selection is true, repeat the request if the user didn't make a selection.
375
250
  while True:
376
- try:
377
- self.user.timeout_seconds = self.timeout_seconds
378
- # It's not possible to distinguish between the user cancelling this dialog and submitting the dialog
379
- # with no input if the ClientCallback show_input_dialog function is used, as both cases just return
380
- # None. Therefore, in order to be able to make that distinction, we need to call the endpoint without
381
- # ClientCallback and get the raw response object.
382
- raw_response = self.user.post('/clientcallback/showInputDialog', payload=request.to_json())
383
- # A response status code of 204 is what represents a cancelled dialog.
384
- if raw_response.status_code == 204:
385
- raise SapioUserCancelledException()
386
- self.user.raise_for_status(raw_response)
387
- json_dct: dict | None = self.user.get_json_data_or_none(raw_response)
388
- response: FieldValue = json_dct['result'] if json_dct else None
389
- except ReadTimeout:
390
- raise SapioDialogTimeoutException()
391
- finally:
392
- self.user.timeout_seconds = self._original_timeout
393
-
394
- # String fields that the user didn't provide will return as an empty string instead of a None response.
395
- is_str: bool = isinstance(response, str)
396
- if (is_str and response) or (not is_str and response is not None):
397
- return response
398
-
399
- match handling:
400
- case BlankResultHandling.CANCEL:
401
- # If the user provided no selection, throw an exception.
402
- if cancel_message:
403
- self.toaster_popup(cancel_message, popup_type=PopupType.Warning)
404
- raise SapioUserCancelledException()
405
- case BlankResultHandling.REPEAT:
406
- # If the user provided no selection, repeat the dialog.
407
- # If a repeatMessage is provided, display it as a toaster popup.
408
- if repeat_message:
409
- self.toaster_popup(repeat_message, popup_type=PopupType.Warning)
410
- case BlankResultHandling.RETURN:
411
- # If the user provided no selection, return the blank result.
412
- return response
413
-
414
- def string_input_dialog(self,
415
- title: str,
416
- msg: str,
417
- field_name: str,
418
- default_value: str | None = None,
419
- max_length: int | None = None,
420
- editable: bool = True,
421
- *,
422
- require_input: bool = False,
423
- repeat_message: str | None = "Please provide a value to continue.",
424
- **kwargs) -> str:
425
- """
426
- Create an input dialog where the user must input data for a singular text field.
427
-
428
- :param title: The title of the dialog.
429
- :param msg: The message to display in the dialog. This can be formatted using HTML elements.
430
- :param field_name: The name and display name of the string field.
431
- :param default_value: The default value to place into the string field, if any.
432
- :param max_length: The max length of the string value. If not provided, uses the length of the default value.
433
- If neither this nor a default value are provided, defaults to 100 characters.
434
- :param editable: Whether the field is editable by the user.
435
- :param require_input: If true, the request will be re-sent if the user submits the dialog without making
436
- a selection.
437
- :param repeat_message: If require_input is true and a repeat_message is provided, then that message appears
438
- as toaster text if the dialog is repeated.
439
- :param kwargs: Any additional keyword arguments to pass to the field definition.
440
- :return: The string that the user input into the dialog.
441
- """
442
- # FR-47690: Deprecated in favor of suggesting the use of the FieldBuilder to customize an input_dialog's field.
443
- warnings.warn("Deprecated. Use the base input_dialog function and the FieldBuilder class to construct the "
444
- "input field.", DeprecationWarning)
445
- if max_length is None:
446
- max_length = len(default_value) if default_value else 100
447
- field = VeloxStringFieldDefinition("Input", field_name, field_name, default_value=default_value,
448
- max_length=max_length, editable=editable, **kwargs)
449
- return self.input_dialog(title, msg, field,
450
- require_input=require_input, repeat_message=repeat_message)
451
-
452
- def integer_input_dialog(self,
453
- title: str,
454
- msg: str,
455
- field_name: str,
456
- default_value: int = None,
457
- min_value: int = -10000,
458
- max_value: int = 10000,
459
- editable: bool = True,
460
- *,
461
- require_input: bool = False,
462
- repeat_message: str | None = "Please provide a value to continue.",
463
- **kwargs) -> int:
464
- """
465
- Create an input dialog where the user must input data for a singular integer field.
466
-
467
- :param title: The title of the dialog.
468
- :param msg: The message to display in the dialog. This can be formatted using HTML elements.
469
- :param field_name: The name and display name of the integer field.
470
- :param default_value: The default value to place into the integer field. If not provided, defaults to the 0 or
471
- the minimum value, whichever is higher.
472
- :param min_value: The minimum allowed value of the input.
473
- :param max_value: The maximum allowed value of the input.
474
- :param editable: Whether the field is editable by the user.
475
- :param require_input: If true, the request will be re-sent if the user submits the dialog without making
476
- a selection.
477
- :param repeat_message: If require_input is true and a repeat_message is provided, then that message appears
478
- as toaster text if the dialog is repeated.
479
- :param kwargs: Any additional keyword arguments to pass to the field definition.
480
- :return: The integer that the user input into the dialog.
481
- """
482
- # FR-47690: Deprecated in favor of suggesting the use of the FieldBuilder to customize an input_dialog's field.
483
- warnings.warn("Deprecated. Use the base input_dialog function and the FieldBuilder class to construct the "
484
- "input field.", DeprecationWarning)
485
- if default_value is None:
486
- default_value = max(0, min_value)
487
- field = VeloxIntegerFieldDefinition("Input", field_name, field_name, default_value=default_value,
488
- min_value=min_value, max_value=max_value, editable=editable, **kwargs)
489
- return self.input_dialog(title, msg, field,
490
- require_input=require_input, repeat_message=repeat_message)
491
-
492
- def double_input_dialog(self,
493
- title: str,
494
- msg: str,
495
- field_name: str,
496
- default_value: float = None,
497
- min_value: float = -10000000,
498
- max_value: float = 100000000,
499
- editable: bool = True,
500
- *,
501
- require_input: bool = False,
502
- repeat_message: str | None = "Please provide a value to continue.",
503
- **kwargs) -> float:
504
- """
505
- Create an input dialog where the user must input data for a singular double field.
506
-
507
- :param title: The title of the dialog.
508
- :param msg: The message to display in the dialog. This can be formatted using HTML elements.
509
- :param field_name: The name and display name of the double field.
510
- :param default_value: The default value to place into the double field. If not provided, defaults to the 0 or
511
- the minimum value, whichever is higher.
512
- :param min_value: The minimum allowed value of the input.
513
- :param max_value: The maximum allowed value of the input.
514
- :param editable: Whether the field is editable by the user.
515
- :param require_input: If true, the request will be re-sent if the user submits the dialog without making
516
- a selection.
517
- :param repeat_message: If require_input is true and a repeat_message is provided, then that message appears
518
- as toaster text if the dialog is repeated.
519
- :param kwargs: Any additional keyword arguments to pass to the field definition.
520
- :return: The float that the user input into the dialog.
521
- """
522
- # FR-47690: Deprecated in favor of suggesting the use of the FieldBuilder to customize an input_dialog's field.
523
- warnings.warn("Deprecated. Use the base input_dialog function and the FieldBuilder class to construct the "
524
- "input field.", DeprecationWarning)
525
- if default_value is None:
526
- default_value = max(0., min_value)
527
- field = VeloxDoubleFieldDefinition("Input", field_name, field_name, default_value=default_value,
528
- min_value=min_value, max_value=max_value, editable=editable, **kwargs)
529
- return self.input_dialog(title, msg, field,
530
- require_input=require_input, repeat_message=repeat_message)
251
+ response: list[str] = self.__handle_dialog_request(request, self.callback.show_list_dialog)
252
+ if not require_selection or response:
253
+ break
254
+ if repeat_message:
255
+ self.toaster_popup(repeat_message, popup_type=PopupType.Warning)
256
+ return response
531
257
 
532
258
  def form_dialog(self,
533
259
  title: str,
534
260
  msg: str,
535
- fields: Iterable[AbstractVeloxFieldDefinition],
261
+ fields: list[AbstractVeloxFieldDefinition],
536
262
  values: FieldMap = None,
537
263
  column_positions: dict[str, tuple[int, int]] = None,
538
264
  *,
@@ -547,8 +273,7 @@ class CallbackUtil:
547
273
  :param msg: The message to display at the top of the form. This can be formatted using HTML elements.
548
274
  :param fields: The definitions of the fields to display in the form. Fields will be displayed in the order they
549
275
  are provided in this list.
550
- :param values: Sets the default values of the fields. If a field name from the fields parameter is not
551
- provided in this dictionary, it will be initialized with its default value.
276
+ :param values: Sets the default values of the fields.
552
277
  :param column_positions: If a tuple is provided for a field name, alters that field's column position and column
553
278
  span. (Field order is still determined by the fields list.)
554
279
  :param data_type: The data type name for the temporary data type that will be created for this form.
@@ -562,23 +287,16 @@ class CallbackUtil:
562
287
  # Build a temporary data type for the request.
563
288
  temp_dt = self.__temp_dt_from_field_defs(data_type, display_name, plural_display_name, fields, column_positions)
564
289
 
565
- # FR-47690: Set default values for fields that aren't present.
566
- if values is None:
567
- values = {}
568
- for field in fields:
569
- if field.data_field_name not in values:
570
- values[field.data_field_name] = field.default_value
571
-
572
290
  # Send the request to the user.
573
291
  request = FormEntryDialogRequest(title, msg, temp_dt, values,
574
292
  width_in_pixels=self.width_pixels, width_percentage=self.width_percent)
575
- response: FieldMap = self.__send_dialog(request, self.callback.show_form_entry_dialog)
293
+ response: FieldMap = self.__handle_dialog_request(request, self.callback.show_form_entry_dialog)
576
294
  return response
577
295
 
578
296
  def record_form_dialog(self,
579
297
  title: str,
580
298
  msg: str,
581
- fields: Iterable[FieldIdentifier | FieldFilterCriteria] | DataTypeLayoutIdentifier,
299
+ fields: list[FieldIdentifier | FieldFilterCriteria] | DataTypeLayoutIdentifier,
582
300
  record: SapioRecord,
583
301
  column_positions: dict[str, tuple[int, int]] | None = None,
584
302
  editable=None,
@@ -644,14 +362,14 @@ class CallbackUtil:
644
362
  # Send the request to the user.
645
363
  request = FormEntryDialogRequest(title, msg, temp_dt, values,
646
364
  width_in_pixels=self.width_pixels, width_percentage=self.width_percent)
647
- response: FieldMap = self.__send_dialog(request, self.callback.show_form_entry_dialog)
365
+ response: FieldMap = self.__handle_dialog_request(request, self.callback.show_form_entry_dialog)
648
366
  return response
649
367
 
650
368
  # FR-47314: Create record form and table dialogs for updating or creating records.
651
369
  def set_record_form_dialog(self,
652
370
  title: str,
653
371
  msg: str,
654
- fields: Iterable[FieldIdentifier | FieldFilterCriteria] | DataTypeLayoutIdentifier,
372
+ fields: list[FieldIdentifier | FieldFilterCriteria] | DataTypeLayoutIdentifier,
655
373
  record: SapioRecord,
656
374
  column_positions: dict[str, tuple[int, int]] | None = None,
657
375
  *,
@@ -694,7 +412,7 @@ class CallbackUtil:
694
412
  def create_record_form_dialog(self,
695
413
  title: str,
696
414
  msg: str,
697
- fields: Iterable[FieldIdentifier | FieldFilterCriteria] | DataTypeLayoutIdentifier,
415
+ fields: list[FieldIdentifier | FieldFilterCriteria] | DataTypeLayoutIdentifier,
698
416
  wrapper_type: type[WrappedType] | str,
699
417
  column_positions: dict[str, tuple[int, int]] | None = None,
700
418
  *,
@@ -738,17 +456,176 @@ class CallbackUtil:
738
456
  default_modifier=default_modifier, field_modifiers=field_modifiers)
739
457
  return record
740
458
 
459
+ def input_dialog(self,
460
+ title: str,
461
+ msg: str,
462
+ field: AbstractVeloxFieldDefinition,
463
+ *,
464
+ require_input: bool = False,
465
+ repeat_message: str | None = "Please provide a value to continue.") -> FieldValue:
466
+ """
467
+ Create an input dialog where the user must input data for a singular field.
468
+
469
+ :param title: The title of the dialog.
470
+ :param msg: The message to display in the dialog. This can be formatted using HTML elements.
471
+ :param field: The definition for a field that the user must provide input to.
472
+ :param require_input: If true, the request will be re-sent if the user submits the dialog without providing an
473
+ input field value.
474
+ :param repeat_message: If require_input is true and a repeat_message is provided, then that message appears
475
+ as toaster text if the dialog is repeated.
476
+ :return: The response value from the user for the given field.
477
+ """
478
+ # Send the request to the user.
479
+ request = InputDialogCriteria(title, msg, field,
480
+ width_in_pixels=self.width_pixels, width_percentage=self.width_percent)
481
+
482
+ # If require_input is true, repeat the request if the user didn't provide a field value.
483
+ while True:
484
+ try:
485
+ self.user.timeout_seconds = self.timeout_seconds
486
+ # It's not possible to distinguish between the user cancelling this dialog and submitting the dialog
487
+ # with no input if the ClientCallback show_input_dialog function is used, as both cases just return
488
+ # None. Therefore, in order to be able to make that distinction, we need to call the endpoint without
489
+ # ClientCallback and get the raw response object.
490
+ raw_response = self.user.post('/clientcallback/showInputDialog', payload=request.to_json())
491
+ # A response status code of 204 is what represents a cancelled dialog.
492
+ if raw_response.status_code == 204:
493
+ raise SapioUserCancelledException()
494
+ self.user.raise_for_status(raw_response)
495
+ json_dct: dict | None = self.user.get_json_data_or_none(raw_response)
496
+ response: FieldValue = json_dct['result'] if json_dct else None
497
+ except ReadTimeout:
498
+ raise SapioDialogTimeoutException()
499
+ finally:
500
+ self.user.timeout_seconds = self._original_timeout
501
+ # String fields that the user didn't provide will return as an empty string instead of a None response.
502
+ is_str: bool = isinstance(response, str)
503
+ if not require_input or (is_str and response) or (not is_str and response is not None):
504
+ break
505
+ if repeat_message:
506
+ self.toaster_popup(repeat_message, popup_type=PopupType.Warning)
507
+ return response
508
+
509
+ def string_input_dialog(self,
510
+ title: str,
511
+ msg: str,
512
+ field_name: str,
513
+ default_value: str | None = None,
514
+ max_length: int | None = None,
515
+ editable: bool = True,
516
+ *,
517
+ require_input: bool = False,
518
+ repeat_message: str | None = "Please provide a value to continue.",
519
+ **kwargs) -> str:
520
+ """
521
+ Create an input dialog where the user must input data for a singular text field.
522
+
523
+ :param title: The title of the dialog.
524
+ :param msg: The message to display in the dialog. This can be formatted using HTML elements.
525
+ :param field_name: The name and display name of the string field.
526
+ :param default_value: The default value to place into the string field, if any.
527
+ :param max_length: The max length of the string value. If not provided, uses the length of the default value.
528
+ If neither this nor a default value are provided, defaults to 100 characters.
529
+ :param editable: Whether the field is editable by the user.
530
+ :param require_input: If true, the request will be re-sent if the user submits the dialog without making
531
+ a selection.
532
+ :param repeat_message: If require_input is true and a repeat_message is provided, then that message appears
533
+ as toaster text if the dialog is repeated.
534
+ :param kwargs: Any additional keyword arguments to pass to the field definition.
535
+ :return: The string that the user input into the dialog.
536
+ """
537
+ if max_length is None:
538
+ max_length = len(default_value) if default_value else 100
539
+ field = VeloxStringFieldDefinition("Input", field_name, field_name, default_value=default_value,
540
+ max_length=max_length, editable=editable, **kwargs)
541
+ return self.input_dialog(title, msg, field,
542
+ require_input=require_input, repeat_message=repeat_message)
543
+
544
+ def integer_input_dialog(self,
545
+ title: str,
546
+ msg: str,
547
+ field_name: str,
548
+ default_value: int = None,
549
+ min_value: int = -10000,
550
+ max_value: int = 10000,
551
+ editable: bool = True,
552
+ *,
553
+ require_input: bool = False,
554
+ repeat_message: str | None = "Please provide a value to continue.",
555
+ **kwargs) -> int:
556
+ """
557
+ Create an input dialog where the user must input data for a singular integer field.
558
+
559
+ :param title: The title of the dialog.
560
+ :param msg: The message to display in the dialog. This can be formatted using HTML elements.
561
+ :param field_name: The name and display name of the integer field.
562
+ :param default_value: The default value to place into the integer field. If not provided, defaults to the 0 or
563
+ the minimum value, whichever is higher.
564
+ :param min_value: The minimum allowed value of the input.
565
+ :param max_value: The maximum allowed value of the input.
566
+ :param editable: Whether the field is editable by the user.
567
+ :param require_input: If true, the request will be re-sent if the user submits the dialog without making
568
+ a selection.
569
+ :param repeat_message: If require_input is true and a repeat_message is provided, then that message appears
570
+ as toaster text if the dialog is repeated.
571
+ :param kwargs: Any additional keyword arguments to pass to the field definition.
572
+ :return: The integer that the user input into the dialog.
573
+ """
574
+ if default_value is None:
575
+ default_value = max(0, min_value)
576
+ field = VeloxIntegerFieldDefinition("Input", field_name, field_name, default_value=default_value,
577
+ min_value=min_value, max_value=max_value, editable=editable, **kwargs)
578
+ return self.input_dialog(title, msg, field,
579
+ require_input=require_input, repeat_message=repeat_message)
580
+
581
+ def double_input_dialog(self,
582
+ title: str,
583
+ msg: str,
584
+ field_name: str,
585
+ default_value: float = None,
586
+ min_value: float = -10000000,
587
+ max_value: float = 100000000,
588
+ editable: bool = True,
589
+ *,
590
+ require_input: bool = False,
591
+ repeat_message: str | None = "Please provide a value to continue.",
592
+ **kwargs) -> float:
593
+ """
594
+ Create an input dialog where the user must input data for a singular double field.
595
+
596
+ :param title: The title of the dialog.
597
+ :param msg: The message to display in the dialog. This can be formatted using HTML elements.
598
+ :param field_name: The name and display name of the double field.
599
+ :param default_value: The default value to place into the double field. If not provided, defaults to the 0 or
600
+ the minimum value, whichever is higher.
601
+ :param min_value: The minimum allowed value of the input.
602
+ :param max_value: The maximum allowed value of the input.
603
+ :param editable: Whether the field is editable by the user.
604
+ :param require_input: If true, the request will be re-sent if the user submits the dialog without making
605
+ a selection.
606
+ :param repeat_message: If require_input is true and a repeat_message is provided, then that message appears
607
+ as toaster text if the dialog is repeated.
608
+ :param kwargs: Any additional keyword arguments to pass to the field definition.
609
+ :return: The float that the user input into the dialog.
610
+ """
611
+ if default_value is None:
612
+ default_value = max(0., min_value)
613
+ field = VeloxDoubleFieldDefinition("Input", field_name, field_name, default_value=default_value,
614
+ min_value=min_value, max_value=max_value, editable=editable, **kwargs)
615
+ return self.input_dialog(title, msg, field,
616
+ require_input=require_input, repeat_message=repeat_message)
617
+
741
618
  def table_dialog(self,
742
619
  title: str,
743
620
  msg: str,
744
- fields: Iterable[AbstractVeloxFieldDefinition],
745
- values: Iterable[FieldMap] | int,
621
+ fields: list[AbstractVeloxFieldDefinition],
622
+ values: list[FieldMap],
746
623
  *,
747
624
  data_type: DataTypeIdentifier = "Default",
748
625
  display_name: str | None = None,
749
626
  plural_display_name: str | None = None,
750
627
  group_by: FieldIdentifier | None = None,
751
- image_data: Iterable[bytes] | None = None) -> list[FieldMap]:
628
+ image_data: list[bytes] | None = None) -> list[FieldMap]:
752
629
  """
753
630
  Create a table dialog where the user may input data into the fields of the table. Requires that the caller
754
631
  provide the definitions of every field in the table.
@@ -757,8 +634,7 @@ class CallbackUtil:
757
634
  :param msg: The message to display at the top of the form. This can be formatted using HTML elements.
758
635
  :param fields: The definitions of the fields to display as table columns. Fields will be displayed in the order
759
636
  they are provided in this list.
760
- :param values: The values to set for each row of the table. If an integer is provided, it is treated as the
761
- number of rows to create in the table, with each row using the default values of the field definitions.
637
+ :param values: The values to set for each row of the table.
762
638
  :param group_by: If provided, the created table dialog will be grouped by the field with this name by default.
763
639
  The user may remove this grouping if they want to.
764
640
  :param image_data: The bytes to the images that should be displayed in the rows of the table. Each element in
@@ -771,18 +647,9 @@ class CallbackUtil:
771
647
  :return: A list of dictionaries mapping the data field names of the given field definitions to the response
772
648
  value from the user for that field for each row.
773
649
  """
774
- # FR-47690: Accept an integer as the values parameter to create a table with that many rows.
775
- if isinstance(values, int):
776
- values: list[dict[str, Any]] = [{} for _ in range(values)]
777
650
  if not values:
778
651
  raise SapioException("No values provided.")
779
652
 
780
- # FR-47690: Set default values for fields that aren't present.
781
- for row in values:
782
- for field in fields:
783
- if field.data_field_name not in row:
784
- row[field.data_field_name] = field.default_value
785
-
786
653
  # Convert the group_by parameter to a field name.
787
654
  if group_by is not None:
788
655
  group_by: str = AliasUtil.to_data_field_name(group_by)
@@ -793,24 +660,23 @@ class CallbackUtil:
793
660
  temp_dt.record_image_assignable = bool(image_data)
794
661
 
795
662
  # Send the request to the user.
796
- request = TableEntryDialogRequest(title, msg, temp_dt, list(values),
663
+ request = TableEntryDialogRequest(title, msg, temp_dt, values,
797
664
  record_image_data_list=image_data, group_by_field=group_by,
798
665
  width_in_pixels=self.width_pixels, width_percentage=self.width_percent)
799
- response: list[FieldMap] = self.__send_dialog(request, self.callback.show_table_entry_dialog)
666
+ response: list[FieldMap] = self.__handle_dialog_request(request, self.callback.show_table_entry_dialog)
800
667
  return response
801
668
 
802
669
  def record_table_dialog(self,
803
670
  title: str,
804
671
  msg: str,
805
- fields: Iterable[FieldIdentifier | FieldFilterCriteria] | DataTypeLayoutIdentifier,
806
- records: Iterable[SapioRecord],
672
+ fields: list[FieldIdentifier | FieldFilterCriteria] | DataTypeLayoutIdentifier,
673
+ records: list[SapioRecord],
807
674
  editable=None,
808
675
  *,
809
676
  default_modifier: FieldModifier | None = None,
810
677
  field_modifiers: dict[FieldIdentifier, FieldModifier] | None = None,
811
678
  group_by: FieldIdentifier | None = None,
812
- image_data: Iterable[bytes] | None = None,
813
- index_field: str | None = None) -> list[FieldMap]:
679
+ image_data: list[bytes] | None = None) -> list[FieldMap]:
814
680
  """
815
681
  Create a table dialog where the user may input data into the fields of the table. The table is constructed from
816
682
  a given list of records of a singular type.
@@ -840,12 +706,6 @@ class CallbackUtil:
840
706
  The user may remove this grouping if they want to.
841
707
  :param image_data: The bytes to the images that should be displayed in the rows of the table. Each element in
842
708
  the image data list corresponds to the element at the same index in the records list.
843
- :param index_field: If provided, the returned field maps will contain a field with this name that is equal to
844
- the record ID of the record at the same index in the records list. This can be used to map the results
845
- back to the original records. This is used instead of using a RecordId field, as the RecordId field has
846
- special behavior in the system that can cause issues if the given records are uncommitted record models
847
- with negative record IDs, meaning we don't want to have a RecordId field in the field maps provided to the
848
- system.
849
709
  :return: A list of dictionaries mapping the data field names of the given field definitions to the response
850
710
  value from the user for that field for each row.
851
711
  """
@@ -857,10 +717,7 @@ class CallbackUtil:
857
717
  if not records:
858
718
  raise SapioException("No records provided.")
859
719
  data_type: str = AliasUtil.to_singular_data_type_name(records)
860
- if index_field is not None:
861
- field_map_list: list[FieldMap] = self.__get_indexed_field_maps(records, index_field)
862
- else:
863
- field_map_list: list[FieldMap] = AliasUtil.to_field_map_list(records)
720
+ field_map_list: list[FieldMap] = AliasUtil.to_field_map_list(records)
864
721
 
865
722
  # Convert the group_by parameter to a field name.
866
723
  if group_by is not None:
@@ -886,20 +743,20 @@ class CallbackUtil:
886
743
  request = TableEntryDialogRequest(title, msg, temp_dt, field_map_list,
887
744
  record_image_data_list=image_data, group_by_field=group_by,
888
745
  width_in_pixels=self.width_pixels, width_percentage=self.width_percent)
889
- response: list[FieldMap] = self.__send_dialog(request, self.callback.show_table_entry_dialog)
746
+ response: list[FieldMap] = self.__handle_dialog_request(request, self.callback.show_table_entry_dialog)
890
747
  return response
891
748
 
892
749
  # FR-47314: Create record form and table dialogs for updating or creating records.
893
750
  def set_record_table_dialog(self,
894
751
  title: str,
895
752
  msg: str,
896
- fields: Iterable[FieldValue] | DataTypeLayoutIdentifier,
897
- records: Iterable[SapioRecord],
753
+ fields: list[FieldValue] | DataTypeLayoutIdentifier,
754
+ records: list[SapioRecord],
898
755
  *,
899
756
  default_modifier: FieldModifier | None = None,
900
757
  field_modifiers: dict[FieldIdentifier, FieldModifier] | None = None,
901
758
  group_by: FieldIdentifier | None = None,
902
- image_data: Iterable[bytes] | None = None):
759
+ image_data: list[bytes] | None = None):
903
760
  """
904
761
  Create a table dialog where the user may input data into the fields of the table. The table is constructed from
905
762
  a given list of records of a singular type. After the user submits this dialog, the values that the user
@@ -930,35 +787,27 @@ class CallbackUtil:
930
787
  :param image_data: The bytes to the images that should be displayed in the rows of the table. Each element in
931
788
  the image data list corresponds to the element at the same index in the records list.
932
789
  """
933
- # Index the records with a field name that is the current time in milliseconds. This is done to avoid
934
- # collisions with any existing field names.
935
- index_field: str = f"_{TimeUtil.now_in_millis()}"
936
790
  results: list[FieldMap] = self.record_table_dialog(title, msg, fields, records,
937
791
  default_modifier=default_modifier,
938
792
  field_modifiers=field_modifiers,
939
- group_by=group_by, image_data=image_data,
940
- index_field=index_field)
793
+ group_by=group_by, image_data=image_data)
941
794
  records_by_id: dict[int, SapioRecord] = self.rec_handler.map_by_id(records)
942
795
  for result in results:
943
- index: int = result.pop(index_field)
944
- records_by_id[index].set_field_values(result)
796
+ records_by_id[result["RecordId"]].set_field_values(result)
945
797
 
946
- # FR-47690: Updated with blank result handling behavior.
947
798
  def create_record_table_dialog(self,
948
799
  title: str,
949
800
  msg: str,
950
- fields: Iterable[FieldValue] | DataTypeLayoutIdentifier,
801
+ fields: list[FieldValue] | DataTypeLayoutIdentifier,
951
802
  wrapper_type: type[WrappedType] | str,
952
803
  count: int | tuple[int, int],
953
804
  *,
954
805
  default_modifier: FieldModifier | None = None,
955
806
  field_modifiers: dict[FieldIdentifier, FieldModifier] | None = None,
956
807
  group_by: FieldIdentifier | None = None,
957
- image_data: Iterable[bytes] | None = None,
958
- require_input = None,
959
- blank_result_handling: BlankResultHandling = BlankResultHandling.DEFAULT,
960
- repeat_message: str | None = "Please provide a value to continue.",
961
- cancel_message: str | None = "No value was provided. Cancelling dialog.") \
808
+ image_data: list[bytes] | None = None,
809
+ require_input: bool = False,
810
+ repeat_message: str | None = "Please provide a value to continue.") \
962
811
  -> list[WrappedType] | list[PyRecordModel]:
963
812
  """
964
813
  Create a table dialog where the user may input data into the fields of the table. The table is constructed from
@@ -992,18 +841,13 @@ class CallbackUtil:
992
841
  The user may remove this grouping if they want to.
993
842
  :param image_data: The bytes to the images that should be displayed in the rows of the table. Each element in
994
843
  the image data list corresponds to the element at the same index in the records list.
995
- :param require_input: DEPRECATED. Use blank_result_handling with a value of BlankResultHandling.REPEAT
996
- instead.
997
- :param blank_result_handling: Determine how to handle the result of a callback when the user provides a blank
998
- result.
999
- :param repeat_message: If blank_result_handling is REPEAT and a repeat_message is provided, then that message
1000
- appears as toaster text when the user provides a blank result.
1001
- :param cancel_message: If blank_result_handling is CANCEL and a cancel_message is provided, then that message
1002
- appears as toaster text when the user provides a blank result.
844
+ :param require_input: If true and the user is prompted to input the number of records to create, the request
845
+ will be re-sent if the user submits the dialog without making a selection.
846
+ :param repeat_message: If require_input is true and a repeat_message is provided, then that message appears
847
+ as toaster text if the record count dialog is repeated.
1003
848
  :return: A list of the newly created records.
1004
849
  """
1005
- count: int = self.__prompt_for_count(count, wrapper_type, require_input, blank_result_handling, repeat_message,
1006
- cancel_message)
850
+ count: int = self.__prompt_for_count(count, wrapper_type, require_input, repeat_message)
1007
851
  if count <= 0:
1008
852
  return []
1009
853
  records: list[WrappedType] | list[PyRecordModel] = self.rec_handler.add_models(wrapper_type, count)
@@ -1016,15 +860,14 @@ class CallbackUtil:
1016
860
  def record_adaptive_dialog(self,
1017
861
  title: str,
1018
862
  msg: str,
1019
- fields: Iterable[FieldIdentifier | FieldFilterCriteria] | DataTypeLayoutIdentifier,
1020
- records: Collection[SapioRecord],
863
+ fields: list[FieldIdentifier | FieldFilterCriteria] | DataTypeLayoutIdentifier,
864
+ records: list[SapioRecord],
1021
865
  *,
1022
866
  default_modifier: FieldModifier | None = None,
1023
867
  field_modifiers: dict[FieldIdentifier, FieldModifier] | None = None,
1024
868
  column_positions: dict[str, tuple[int, int]] | None = None,
1025
869
  group_by: FieldIdentifier | None = None,
1026
- image_data: Iterable[bytes] | None = None,
1027
- index_field: str | None = None) -> list[FieldMap]:
870
+ image_data: list[bytes] | None = None) -> list[FieldMap]:
1028
871
  """
1029
872
  Create a dialog where the user may input data into the specified fields. The dialog is constructed from
1030
873
  a given list of records of a singular type.
@@ -1060,12 +903,6 @@ class CallbackUtil:
1060
903
  :param image_data: The bytes to the images that should be displayed in the rows of the table. Each element in
1061
904
  the image data list corresponds to the element at the same index in the records list. Only used if the
1062
905
  adaptive dialog becomes a table.
1063
- :param index_field: If provided, the returned field maps will contain a field with this name that is equal to
1064
- the record ID of the record at the same index in the records list. This can be used to map the results
1065
- back to the original records. This is used instead of using a RecordId field, as the RecordId field has
1066
- special behavior in the system that can cause issues if the given records are uncommitted record models
1067
- with negative record IDs, meaning we don't want to have a RecordId field in the field maps provided to the
1068
- system. Only used if the adaptive dialog becomes a table.
1069
906
  :return: A list of dictionaries mapping the data field names of the given field definitions to the response
1070
907
  value from the user for that field for each row. Even if a form was displayed, the field values will still
1071
908
  be returned in a list.
@@ -1074,23 +911,23 @@ class CallbackUtil:
1074
911
  if not count:
1075
912
  raise SapioException("No records provided.")
1076
913
  if count == 1:
1077
- return [self.record_form_dialog(title, msg, fields, list(records)[0], column_positions,
914
+ return [self.record_form_dialog(title, msg, fields, records[0], column_positions,
1078
915
  default_modifier=default_modifier, field_modifiers=field_modifiers)]
1079
916
  return self.record_table_dialog(title, msg, fields, records,
1080
917
  default_modifier=default_modifier, field_modifiers=field_modifiers,
1081
- group_by=group_by, image_data=image_data, index_field=index_field)
918
+ group_by=group_by, image_data=image_data)
1082
919
 
1083
920
  def set_record_adaptive_dialog(self,
1084
921
  title: str,
1085
922
  msg: str,
1086
- fields: Iterable[FieldIdentifier | FieldFilterCriteria] | DataTypeLayoutIdentifier,
1087
- records: Collection[SapioRecord],
923
+ fields: list[FieldIdentifier | FieldFilterCriteria] | DataTypeLayoutIdentifier,
924
+ records: list[SapioRecord],
1088
925
  *,
1089
926
  default_modifier: FieldModifier | None = None,
1090
927
  field_modifiers: dict[FieldIdentifier, FieldModifier] | None = None,
1091
928
  column_positions: dict[str, tuple[int, int]] | None = None,
1092
929
  group_by: FieldIdentifier | None = None,
1093
- image_data: Iterable[bytes] | None = None) -> None:
930
+ image_data: list[bytes] | None = None) -> None:
1094
931
  """
1095
932
  Create a dialog where the user may input data into the fields of the dialog. The dialog is constructed from
1096
933
  a given list of records of a singular type. After the user submits this dialog, the values that the user
@@ -1132,18 +969,17 @@ class CallbackUtil:
1132
969
  if not count:
1133
970
  raise SapioException("No records provided.")
1134
971
  if count == 1:
1135
- self.set_record_form_dialog(title, msg, fields, list(records)[0], column_positions,
972
+ self.set_record_form_dialog(title, msg, fields, records[0], column_positions,
1136
973
  default_modifier=default_modifier, field_modifiers=field_modifiers)
1137
974
  else:
1138
975
  self.set_record_table_dialog(title, msg, fields, records,
1139
976
  default_modifier=default_modifier, field_modifiers=field_modifiers,
1140
977
  group_by=group_by, image_data=image_data)
1141
978
 
1142
- # FR-47690: Updated with blank result handling behavior.
1143
979
  def create_record_adaptive_dialog(self,
1144
980
  title: str,
1145
981
  msg: str,
1146
- fields: Iterable[FieldValue] | DataTypeLayoutIdentifier,
982
+ fields: list[FieldValue] | DataTypeLayoutIdentifier,
1147
983
  wrapper_type: type[WrappedType] | str,
1148
984
  count: int | tuple[int, int],
1149
985
  *,
@@ -1151,11 +987,9 @@ class CallbackUtil:
1151
987
  field_modifiers: dict[FieldIdentifier, FieldModifier] | None = None,
1152
988
  column_positions: dict[str, tuple[int, int]] | None = None,
1153
989
  group_by: FieldIdentifier | None = None,
1154
- image_data: Iterable[bytes] | None = None,
1155
- require_input = None,
1156
- blank_result_handling: BlankResultHandling = BlankResultHandling.DEFAULT,
1157
- repeat_message: str | None = "Please provide a value to continue.",
1158
- cancel_message: str | None = "No value was provided. Cancelling dialog.") \
990
+ image_data: list[bytes] | None = None,
991
+ require_input: bool = False,
992
+ repeat_message: str | None = "Please provide a value to continue.") \
1159
993
  -> list[WrappedType]:
1160
994
  """
1161
995
  Create a dialog where the user may input data into the specified fields. The dialog is constructed from
@@ -1196,19 +1030,14 @@ class CallbackUtil:
1196
1030
  :param image_data: The bytes to the images that should be displayed in the rows of the table. Each element in
1197
1031
  the image data list corresponds to the element at the same index in the records list. Only used if the
1198
1032
  adaptive dialog becomes a table.
1199
- :param require_input: DEPRECATED. Use blank_result_handling with a value of BlankResultHandling.REPEAT
1200
- instead.
1201
- :param blank_result_handling: Determine how to handle the result of a callback when the user provides a blank
1202
- result.
1203
- :param repeat_message: If blank_result_handling is REPEAT and a repeat_message is provided, then that message
1204
- appears as toaster text when the user provides a blank result.
1205
- :param cancel_message: If blank_result_handling is CANCEL and a cancel_message is provided, then that message
1206
- appears as toaster text when the user provides a blank result.
1033
+ :param require_input: If true and the user is prompted to input the number of records to create, the request
1034
+ will be re-sent if the user submits the dialog without making a selection.
1035
+ :param repeat_message: If require_input is true and a repeat_message is provided, then that message appears
1036
+ as toaster text if the record count dialog is repeated.
1207
1037
  :return: A list of the newly created records. Even if a form was displayed, the created record will still be
1208
1038
  returned in a list.
1209
1039
  """
1210
- count: int = self.__prompt_for_count(count, wrapper_type, require_input, blank_result_handling, repeat_message,
1211
- cancel_message)
1040
+ count: int = self.__prompt_for_count(count, wrapper_type, require_input, repeat_message)
1212
1041
  if count <= 0:
1213
1042
  return []
1214
1043
  if count == 1:
@@ -1218,20 +1047,17 @@ class CallbackUtil:
1218
1047
  default_modifier=default_modifier, field_modifiers=field_modifiers,
1219
1048
  group_by=group_by, image_data=image_data)
1220
1049
 
1221
- # FR-47690: Add group_by and image_data parameters.
1222
1050
  def multi_type_table_dialog(self,
1223
1051
  title: str,
1224
1052
  msg: str,
1225
- fields: Iterable[tuple[DataTypeIdentifier, FieldIdentifier] | AbstractVeloxFieldDefinition],
1226
- row_contents: Iterable[Iterable[SapioRecord | FieldMap]],
1053
+ fields: list[tuple[DataTypeIdentifier, FieldIdentifier] | AbstractVeloxFieldDefinition],
1054
+ row_contents: list[list[SapioRecord | FieldMap]],
1227
1055
  *,
1228
1056
  default_modifier: FieldModifier | None = None,
1229
1057
  field_modifiers: dict[FieldIdentifier, FieldModifier] | None = None,
1230
1058
  data_type: DataTypeIdentifier = "Default",
1231
1059
  display_name: str | None = None,
1232
- plural_display_name: str | None = None,
1233
- group_by: FieldIdentifier | None = None,
1234
- image_data: list[bytes] | None = None) -> list[FieldMap]:
1060
+ plural_display_name: str | None = None) -> list[FieldMap]:
1235
1061
  """
1236
1062
  Create a table dialog where the user may input data into the fields of the table. The table is constructed from
1237
1063
  a given list of records of multiple data types or field maps. Provided field names must match with field names
@@ -1285,10 +1111,6 @@ class CallbackUtil:
1285
1111
  name.
1286
1112
  :param plural_display_name: The plural display name for the temporary data type. If not provided, defaults to
1287
1113
  the display name + "s".
1288
- :param group_by: If provided, the created table dialog will be grouped by the field with this name by default.
1289
- The user may remove this grouping if they want to.
1290
- :param image_data: The bytes to the images that should be displayed in the rows of the table. Each element in
1291
- the image data list corresponds to the element at the same index in the values list.
1292
1114
  :return: A list of dictionaries mapping the data field names of the given field definitions to the response
1293
1115
  value from the user for that field for each row.
1294
1116
  """
@@ -1353,49 +1175,31 @@ class CallbackUtil:
1353
1175
  field_names.add(name)
1354
1176
 
1355
1177
  # Get the values for each row.
1356
- # FR-47690: Updated this for loop to better match the Java implementation.
1357
1178
  values: list[dict[str, FieldValue]] = []
1358
1179
  for row in row_contents:
1359
1180
  # The final values for this row:
1360
1181
  row_values: dict[str, FieldValue] = {}
1361
1182
 
1362
- # Map the records for this row by their data type. If a field map is provided, save it separately to
1363
- # the temp_values dict.
1364
- row_records: dict[str, SapioRecord] = {}
1365
- temp_values: FieldMap = {}
1183
+ # Map the records for this row by their data type. If a field map is provided, its data type is Default.
1184
+ row_records: dict[str, SapioRecord | FieldMap] = {}
1366
1185
  for rec in row:
1367
1186
  # Toss out null elements.
1368
1187
  if rec is None:
1369
1188
  continue
1370
1189
  # Map records to their data type name. Map field maps to Default.
1371
- dt: str = "Default" if isinstance(rec, dict) else AliasUtil.to_data_type_name(rec)
1372
- if dt == "Default":
1373
- temp_values.update(rec)
1374
- else:
1375
- # Warn if the same data type name appears more than once.
1376
- if dt in row_records:
1377
- raise SapioException(f"The data type \"{dt}\" appears more than once in the given row contents.")
1378
- row_records[dt] = rec
1190
+ dt: str = "Default" if isinstance(rec, dict) else AliasUtil.to_data_type_names(rec)
1191
+ # Warn if the same data type name appears more than once.
1192
+ if dt in row_records:
1193
+ raise SapioException(f"The data type \"{dt}\" appears more than once in the given row contents.")
1194
+ row_records[dt] = rec
1379
1195
 
1380
1196
  # Get the field values from the above records.
1381
1197
  for field in final_fields:
1382
- value: Any | None = None
1383
-
1384
1198
  # Find the object that corresponds to this field given its data type name.
1385
- dt: str = field.data_type_name
1386
- fd: str = field.data_field_name
1387
- if dt == "Default":
1388
- # If the field map is provided, get the value from it.
1389
- # FR-47690: If a value is not provided, then use the default value of the field definition.
1390
- if fd in temp_values:
1391
- value = temp_values.get(fd)
1392
- else:
1393
- value = field.default_value
1394
- elif dt in row_records:
1395
- record: SapioRecord = row_records[dt]
1396
- # If the record is not null, get the value from the record.
1397
- if record is not None:
1398
- value = record.get_field_value(fd)
1199
+ record: SapioRecord | FieldMap | None = row_records.get(field.data_type_name)
1200
+ # This could be either a record, a field map, or null. Convert any records to field maps.
1201
+ if not isinstance(record, dict) and record is not None:
1202
+ record: FieldMap | None = AliasUtil.to_field_map(record)
1399
1203
 
1400
1204
  # Find out if this field had its data type prepended to it. If this is the case, then we need to find
1401
1205
  # the true data field name before retrieving the value from the field map.
@@ -1404,22 +1208,16 @@ class CallbackUtil:
1404
1208
  name = name.split(".")[1]
1405
1209
 
1406
1210
  # Set the value for this particular field.
1407
- row_values[name] = value
1211
+ row_values[field.data_field_name] = record.get(name) if record else None
1408
1212
  values.append(row_values)
1409
1213
 
1410
1214
  # Build a temporary data type for the request.
1411
1215
  temp_dt = self.__temp_dt_from_field_defs(data_type, display_name, plural_display_name, final_fields, None)
1412
- temp_dt.record_image_assignable = bool(image_data)
1413
-
1414
- # Convert the group_by parameter to a field name.
1415
- if group_by is not None:
1416
- group_by: str = AliasUtil.to_data_field_name(group_by)
1417
1216
 
1418
1217
  # Send the request to the user.
1419
1218
  request = TableEntryDialogRequest(title, msg, temp_dt, values,
1420
- record_image_data_list=image_data, group_by_field=group_by,
1421
1219
  width_in_pixels=self.width_pixels, width_percentage=self.width_percent)
1422
- response: list[FieldMap] = self.__send_dialog(request, self.callback.show_table_entry_dialog)
1220
+ response: list[FieldMap] = self.__handle_dialog_request(request, self.callback.show_table_entry_dialog)
1423
1221
  return response
1424
1222
 
1425
1223
  def record_view_dialog(self,
@@ -1428,7 +1226,7 @@ class CallbackUtil:
1428
1226
  layout: DataTypeLayoutIdentifier = None,
1429
1227
  minimized: bool = False,
1430
1228
  access_level: FormAccessLevel | None = None,
1431
- plugin_path_list: Iterable[str] | None = None) -> None:
1229
+ plugin_path_list: list[str] | None = None) -> None:
1432
1230
  """
1433
1231
  Create an IDV dialog for the given record. This IDV may use an existing layout already defined in the system,
1434
1232
  and can be created to allow the user to edit the field in the IDV, or to be read-only for the user to review.
@@ -1456,30 +1254,26 @@ class CallbackUtil:
1456
1254
  # Send the request to the user.
1457
1255
  request = DataRecordDialogRequest(title, record, layout, minimized, access_level, plugin_path_list,
1458
1256
  width_in_pixels=self.width_pixels, width_percentage=self.width_percent)
1459
- response: bool = self.__send_dialog(request, self.callback.data_record_form_view_dialog)
1257
+ response: bool = self.__handle_dialog_request(request, self.callback.data_record_form_view_dialog)
1460
1258
  # The __handle_dialog_request function only throws a cancelled exception if the response is None, but in
1461
1259
  # this case we also want to throw if the response is False.
1462
1260
  if not response:
1463
1261
  raise SapioUserCancelledException()
1464
1262
 
1465
1263
  # CR-47326: Allow the selection dialog functions to preselect rows/records in the table.
1466
- # FR-47690: Added shortcut_single_option parameter. Updated with blank result handling behavior.
1467
1264
  def selection_dialog(self,
1468
1265
  msg: str,
1469
- fields: Iterable[AbstractVeloxFieldDefinition],
1470
- values: Iterable[FieldMap],
1266
+ fields: list[AbstractVeloxFieldDefinition],
1267
+ values: list[FieldMap],
1471
1268
  multi_select: bool = True,
1472
- preselected_rows: Iterable[FieldMap | RecordIdentifier] | None = None,
1269
+ preselected_rows: list[FieldMap | RecordIdentifier] | None = None,
1473
1270
  *,
1474
1271
  data_type: DataTypeIdentifier = "Default",
1475
1272
  display_name: str | None = None,
1476
1273
  plural_display_name: str | None = None,
1477
- image_data: Iterable[bytes] | None = None,
1478
- shortcut_single_option: bool = True,
1479
- require_selection = None,
1480
- blank_result_handling: BlankResultHandling = BlankResultHandling.DEFAULT,
1481
- repeat_message: str | None = "Please provide a selection to continue.",
1482
- cancel_message: str | None = "No selection was made. Cancelling dialog.") -> list[FieldMap]:
1274
+ image_data: list[bytes] | None = None,
1275
+ require_selection: bool = False,
1276
+ repeat_message: str | None = "Please provide a selection to continue.") -> list[FieldMap]:
1483
1277
  """
1484
1278
  Create a selection dialog for a list of field maps for the user to choose from. Requires that the caller
1485
1279
  provide the definitions of every field in the table.
@@ -1502,23 +1296,14 @@ class CallbackUtil:
1502
1296
  the display name + "s".
1503
1297
  :param image_data: The bytes to the images that should be displayed in the rows of the table. Each element in
1504
1298
  the image data list corresponds to the element at the same index in the values list.
1505
- :param shortcut_single_option: If true, then if the list contains only one option, the dialog will not be shown
1506
- and the single option will be returned immediately.
1507
- :param require_selection: DEPRECATED. Use blank_result_handling with a value of BlankResultHandling.REPEAT
1508
- instead.
1509
- :param blank_result_handling: Determine how to handle the result of a callback when the user provides a blank
1510
- result.
1511
- :param repeat_message: If blank_result_handling is REPEAT and a repeat_message is provided, then that message
1512
- appears as toaster text when the user provides a blank result.
1513
- :param cancel_message: If blank_result_handling is CANCEL and a cancel_message is provided, then that message
1514
- appears as toaster text when the user provides a blank result.
1299
+ :param require_selection: If true, the request will be re-sent if the user submits the dialog without making
1300
+ a selection.
1301
+ :param repeat_message: If require_selection is true and a repeat_message is provided, then that message appears
1302
+ as toaster text if the dialog is repeated.
1515
1303
  :return: A list of field maps corresponding to the chosen input field maps.
1516
1304
  """
1517
1305
  if not values:
1518
1306
  raise SapioException("No values provided.")
1519
- values = list(values)
1520
- if len(values) == 1 and shortcut_single_option:
1521
- return [values[0]]
1522
1307
 
1523
1308
  if preselected_rows:
1524
1309
  # Confirm that the provided field maps are validly configured to allow the use of preselected rows.
@@ -1544,7 +1329,6 @@ class CallbackUtil:
1544
1329
 
1545
1330
  # Add a RecordId definition to the fields if one is not already present. This is necessary for the
1546
1331
  # pre-selected records parameter to function.
1547
- fields = list(fields)
1548
1332
  if "RecordId" not in [x.data_field_name for x in fields]:
1549
1333
  builder = FieldBuilder(data_type)
1550
1334
  fields.append(builder.long_field("RecordId", abstract_info=AnyFieldInfo(visible=False)))
@@ -1554,31 +1338,27 @@ class CallbackUtil:
1554
1338
  temp_dt.record_image_assignable = bool(image_data)
1555
1339
 
1556
1340
  # Send the request to the user.
1557
- request = TempTableSelectionRequest(temp_dt, msg, list(values), image_data, preselected_rows, multi_select)
1558
-
1559
- # Reverse compatibility: If require_selection is true and blank_result_handling is not set, then
1560
- # set blank_result_handling to REPEAT.
1561
- if require_selection is True and blank_result_handling == BlankResultHandling.DEFAULT:
1562
- blank_result_handling = BlankResultHandling.REPEAT
1563
- def not_blank_func(r: list[FieldMap]) -> bool:
1564
- return bool(r)
1565
- return self.__send_dialog_blank_results(request, self.callback.show_temp_table_selection_dialog, not_blank_func,
1566
- blank_result_handling, repeat_message, cancel_message)
1567
-
1568
- # FR-47690: Added shortcut_single_option parameter. Updated with blank result handling behavior.
1341
+ request = TempTableSelectionRequest(temp_dt, msg, values, image_data, preselected_rows, multi_select)
1342
+ # If require_selection is true, repeat the request if the user didn't make a selection.
1343
+ while True:
1344
+ response: list[FieldMap] = self.__handle_dialog_request(request,
1345
+ self.callback.show_temp_table_selection_dialog)
1346
+ if not require_selection or response:
1347
+ break
1348
+ if repeat_message:
1349
+ self.toaster_popup(repeat_message, popup_type=PopupType.Warning)
1350
+ return response
1351
+
1569
1352
  def record_selection_dialog(self,
1570
1353
  msg: str,
1571
- fields: Iterable[FieldIdentifier | FieldFilterCriteria] | DataTypeLayoutIdentifier,
1572
- records: Iterable[SapioRecord],
1354
+ fields: list[FieldIdentifier | FieldFilterCriteria] | DataTypeLayoutIdentifier,
1355
+ records: list[SapioRecord],
1573
1356
  multi_select: bool = True,
1574
- preselected_records: Iterable[RecordIdentifier] | None = None,
1357
+ preselected_records: list[RecordIdentifier] | None = None,
1575
1358
  *,
1576
- image_data: Iterable[bytes] | None = None,
1577
- shortcut_single_option: bool = True,
1578
- require_selection = None,
1579
- blank_result_handling: BlankResultHandling = BlankResultHandling.DEFAULT,
1580
- repeat_message: str | None = "Please provide a selection to continue.",
1581
- cancel_message: str | None = "No selection was made. Cancelling dialog.") \
1359
+ image_data: list[bytes] | None = None,
1360
+ require_selection: bool = False,
1361
+ repeat_message: str | None = "Please provide a selection to continue.") \
1582
1362
  -> list[SapioRecord]:
1583
1363
  """
1584
1364
  Create a record selection dialog for a list of records for the user to choose from. Provided field names must
@@ -1600,24 +1380,15 @@ class CallbackUtil:
1600
1380
  record IDs are provided, the dialog will automatically allow multi-selection of records.
1601
1381
  :param image_data: The bytes to the images that should be displayed in the rows of the table. Each element in
1602
1382
  the image data list corresponds to the element at the same index in the records list.
1603
- :param shortcut_single_option: If true, then if the list contains only one option, the dialog will not be shown
1604
- and the single option will be returned immediately.
1605
- :param require_selection: DEPRECATED. Use blank_result_handling with a value of BlankResultHandling.REPEAT
1606
- instead.
1607
- :param blank_result_handling: Determine how to handle the result of a callback when the user provides a blank
1608
- result.
1609
- :param repeat_message: If blank_result_handling is REPEAT and a repeat_message is provided, then that message
1610
- appears as toaster text when the user provides a blank result.
1611
- :param cancel_message: If blank_result_handling is CANCEL and a cancel_message is provided, then that message
1612
- appears as toaster text when the user provides a blank result.
1383
+ :param require_selection: If true, the request will be re-sent if the user submits the dialog without making
1384
+ a selection.
1385
+ :param repeat_message: If require_selection is true and a repeat_message is provided, then that message appears
1386
+ as toaster text if the dialog is repeated.
1613
1387
  :return: A list of the selected records.
1614
1388
  """
1615
1389
  # Get the data type name and field values from the provided records.
1616
1390
  if not records:
1617
1391
  raise SapioException("No records provided.")
1618
- records = list(records)
1619
- if len(records) == 1 and shortcut_single_option:
1620
- return [records[0]]
1621
1392
  data_type: str = AliasUtil.to_singular_data_type_name(records)
1622
1393
  field_map_list: list[FieldMap] = AliasUtil.to_field_map_list(records, include_record_id=True)
1623
1394
 
@@ -1644,17 +1415,14 @@ class CallbackUtil:
1644
1415
 
1645
1416
  # Send the request to the user.
1646
1417
  request = TempTableSelectionRequest(temp_dt, msg, field_map_list, image_data, preselected_records, multi_select)
1647
-
1648
- # Reverse compatibility: If require_selection is true and blank_result_handling is not set, then
1649
- # set blank_result_handling to REPEAT.
1650
- if require_selection is True and blank_result_handling == BlankResultHandling.DEFAULT:
1651
- blank_result_handling = BlankResultHandling.REPEAT
1652
- def not_blank_func(r: list[FieldMap]) -> bool:
1653
- return bool(r)
1654
- response: list[FieldMap] = self.__send_dialog_blank_results(request,
1655
- self.callback.show_temp_table_selection_dialog,
1656
- not_blank_func, blank_result_handling,
1657
- repeat_message, cancel_message)
1418
+ # If require_selection is true, repeat the request if the user didn't make a selection.
1419
+ while True:
1420
+ response: list[FieldMap] = self.__handle_dialog_request(request,
1421
+ self.callback.show_temp_table_selection_dialog)
1422
+ if not require_selection or response:
1423
+ break
1424
+ if repeat_message:
1425
+ self.toaster_popup(repeat_message, popup_type=PopupType.Warning)
1658
1426
 
1659
1427
  # Map the field maps in the response back to the record they come from, returning the chosen record instead of
1660
1428
  # the chosen field map.
@@ -1665,25 +1433,22 @@ class CallbackUtil:
1665
1433
  return ret_list
1666
1434
 
1667
1435
  # CR-47377: Add allow_creation and default_creation_number to cover new parameters of this request type from 24.12.
1668
- # FR-47690: Updated with blank result handling behavior.
1669
1436
  def input_selection_dialog(self,
1670
1437
  wrapper_type: type[WrappedType] | str,
1671
1438
  msg: str,
1672
1439
  multi_select: bool = True,
1673
1440
  only_key_fields: bool = False,
1674
- search_types: Iterable[SearchType] | None = None,
1441
+ search_types: list[SearchType] | None = None,
1675
1442
  scan_criteria: ScanToSelectCriteria | None = None,
1676
1443
  custom_search: CustomReport | CustomReportCriteria | str | None = None,
1677
- preselected_records: Iterable[RecordIdentifier] | None = None,
1678
- record_blacklist: Iterable[RecordIdentifier] | None = None,
1679
- record_whitelist: Iterable[RecordIdentifier] | None = None,
1444
+ preselected_records: list[RecordIdentifier] | None = None,
1445
+ record_blacklist: list[RecordIdentifier] | None = None,
1446
+ record_whitelist: list[RecordIdentifier] | None = None,
1680
1447
  allow_creation: bool = False,
1681
1448
  default_creation_number: int = 1,
1682
1449
  *,
1683
- require_selection = None,
1684
- blank_result_handling: BlankResultHandling = BlankResultHandling.DEFAULT,
1685
- repeat_message: str | None = "Please provide a selection to continue.",
1686
- cancel_message: str | None = "No selection was made. Cancelling dialog.") \
1450
+ require_selection: bool = False,
1451
+ repeat_message: str | None = "Please provide a selection to continue.") \
1687
1452
  -> list[WrappedType] | list[PyRecordModel]:
1688
1453
  """
1689
1454
  Display a table of records that exist in the system matching the given data type and filter criteria and have
@@ -1725,14 +1490,10 @@ class CallbackUtil:
1725
1490
  than 1, then multi-selection must be true. The data type definition of the records being created must have
1726
1491
  "Prompt for Number to Add" set to true in order to allow the user to select how many records to create, as
1727
1492
  otherwise user will only ever be able to create one record at a time.
1728
- :param require_selection: DEPRECATED. Use blank_result_handling with a value of BlankResultHandling.REPEAT
1729
- instead.
1730
- :param blank_result_handling: Determine how to handle the result of a callback when the user provides a blank
1731
- result.
1732
- :param repeat_message: If blank_result_handling is REPEAT and a repeat_message is provided, then that message
1733
- appears as toaster text when the user provides a blank result.
1734
- :param cancel_message: If blank_result_handling is CANCEL and a cancel_message is provided, then that message
1735
- appears as toaster text when the user provides a blank result.
1493
+ :param require_selection: If true, the request will be re-sent if the user submits the dialog without making
1494
+ a selection.
1495
+ :param repeat_message: If require_selection is true and a repeat_message is provided, then that message appears
1496
+ as toaster text if the dialog is repeated.
1736
1497
  :return: A list of the records selected by the user in the dialog, wrapped as record models using the provided
1737
1498
  wrapper.
1738
1499
  """
@@ -1758,25 +1519,23 @@ class CallbackUtil:
1758
1519
  request = InputSelectionRequest(data_type, msg, search_types, only_key_fields, record_blacklist,
1759
1520
  record_whitelist, preselected_records, custom_search, scan_criteria,
1760
1521
  multi_select, allow_creation, default_creation_number)
1522
+ # If require_selection is true, repeat the request if the user didn't make a selection.
1523
+ while True:
1524
+ response: list[DataRecord] = self.__handle_dialog_request(request,
1525
+ self.callback.show_input_selection_dialog)
1526
+ if not require_selection or response:
1527
+ break
1528
+ if repeat_message:
1529
+ self.toaster_popup(repeat_message, popup_type=PopupType.Warning)
1530
+ return self.rec_handler.wrap_models(response, wrapper_type)
1761
1531
 
1762
- # Reverse compatibility: If require_selection is true and blank_result_handling is not set, then
1763
- # set blank_result_handling to REPEAT.
1764
- if require_selection is True and blank_result_handling == BlankResultHandling.DEFAULT:
1765
- blank_result_handling = BlankResultHandling.REPEAT
1766
- def not_blank_func(r: list[DataRecord]) -> bool:
1767
- return bool(r)
1768
- return self.__send_dialog_blank_results(request, self.callback.show_input_selection_dialog, not_blank_func,
1769
- blank_result_handling, repeat_message, cancel_message)
1770
-
1771
- # FR-47690: Deprecated the require_authentication parameter.
1772
- # noinspection PyUnusedLocal
1773
1532
  def esign_dialog(self,
1774
1533
  title: str,
1775
1534
  msg: str,
1776
1535
  show_comment: bool = True,
1777
- additional_fields: Iterable[AbstractVeloxFieldDefinition] | None = None,
1536
+ additional_fields: list[AbstractVeloxFieldDefinition] | None = None,
1778
1537
  *,
1779
- require_authentication = None) -> ESigningResponsePojo:
1538
+ require_authentication: bool = False) -> ESigningResponsePojo:
1780
1539
  """
1781
1540
  Create an e-sign dialog for the user to interact with.
1782
1541
 
@@ -1786,7 +1545,8 @@ class CallbackUtil:
1786
1545
  user is required to provide an action.
1787
1546
  :param additional_fields: Field definitions for additional fields to display in the dialog, for if there is
1788
1547
  other information you wish to gather from the user alongside the e-sign.
1789
- :param require_authentication: DEPRECATED. Authentication is always required when using this function.
1548
+ :param require_authentication: If true, the request will be re-sent if the user submits the dialog with invalid
1549
+ credentials.
1790
1550
  :return: An e-sign response object containing information about the e-sign attempt.
1791
1551
  """
1792
1552
  # Construct a temporary data type if any additional fields are provided.
@@ -1800,9 +1560,10 @@ class CallbackUtil:
1800
1560
  # Send the request to the user.
1801
1561
  request = ESigningRequestPojo(title, msg, show_comment, temp_dt,
1802
1562
  width_in_pixels=self.width_pixels, width_percentage=self.width_percent)
1563
+ # If require_authentication is true, repeat the request if the user didn't provide valid credentials.
1803
1564
  while True:
1804
- response: ESigningResponsePojo = self.__send_dialog(request, self.callback.show_esign_dialog)
1805
- if response.authenticated:
1565
+ response: ESigningResponsePojo = self.__handle_dialog_request(request, self.callback.show_esign_dialog)
1566
+ if not require_authentication or response.authenticated:
1806
1567
  break
1807
1568
  # This matches the OOB behavior.
1808
1569
  self.toaster_popup("Incorrect username/password", popup_type=PopupType.Error)
@@ -1811,7 +1572,7 @@ class CallbackUtil:
1811
1572
  popup_type=PopupType.Error)
1812
1573
  return response
1813
1574
 
1814
- def request_file(self, title: str, exts: Iterable[str] | None = None,
1575
+ def request_file(self, title: str, exts: list[str] | None = None,
1815
1576
  show_image_editor: bool = False, show_camera_button: bool = False) -> tuple[str, bytes]:
1816
1577
  """
1817
1578
  Request a single file from the user.
@@ -1838,13 +1599,13 @@ class CallbackUtil:
1838
1599
 
1839
1600
  # Send the request to the user.
1840
1601
  request = FilePromptRequest(title, show_image_editor, ",".join(exts), show_camera_button)
1841
- file_path: str = self.__send_dialog(request, self.callback.show_file_dialog, data_sink=do_consume)
1602
+ file_path: str = self.__handle_dialog_request(request, self.callback.show_file_dialog, data_sink=do_consume)
1842
1603
 
1843
1604
  # Verify that each of the file given matches the expected extension(s).
1844
1605
  self.__verify_file(file_path, sink.data, exts)
1845
1606
  return file_path, sink.data
1846
1607
 
1847
- def request_files(self, title: str, exts: Iterable[str] | None = None,
1608
+ def request_files(self, title: str, exts: list[str] | None = None,
1848
1609
  show_image_editor: bool = False, show_camera_button: bool = False) -> dict[str, bytes]:
1849
1610
  """
1850
1611
  Request multiple files from the user.
@@ -1863,7 +1624,7 @@ class CallbackUtil:
1863
1624
 
1864
1625
  # Send the request to the user.
1865
1626
  request = MultiFilePromptRequest(title, show_image_editor, ",".join(exts), show_camera_button)
1866
- file_paths: list[str] = self.__send_dialog(request, self.callback.show_multi_file_dialog)
1627
+ file_paths: list[str] = self.__handle_dialog_request(request, self.callback.show_multi_file_dialog)
1867
1628
 
1868
1629
  # Verify that each of the files given match the expected extension(s).
1869
1630
  ret_dict: dict[str, bytes] = {}
@@ -1876,7 +1637,7 @@ class CallbackUtil:
1876
1637
  return ret_dict
1877
1638
 
1878
1639
  @staticmethod
1879
- def __verify_file(file_path: str, file_bytes: bytes, allowed_extensions: Iterable[str]) -> None:
1640
+ def __verify_file(file_path: str, file_bytes: bytes, allowed_extensions: list[str]) -> None:
1880
1641
  """
1881
1642
  Verify that the provided file was read (i.e. the file path and file bytes aren't None or empty) and that it
1882
1643
  has the correct file extension. Raises a user error exception if something about the file is incorrect.
@@ -1887,11 +1648,10 @@ class CallbackUtil:
1887
1648
  """
1888
1649
  if file_path is None or len(file_path) == 0 or file_bytes is None or len(file_bytes) == 0:
1889
1650
  raise SapioUserErrorException("Empty file provided or file unable to be read.")
1890
- if allowed_extensions:
1651
+ if len(allowed_extensions) != 0:
1891
1652
  matches: bool = False
1892
1653
  for ext in allowed_extensions:
1893
- # FR-47690: Changed to a case-insensitive match.
1894
- if file_path.casefold().endswith("." + ext.lstrip(".").casefold()):
1654
+ if file_path.endswith("." + ext.lstrip(".")):
1895
1655
  matches = True
1896
1656
  break
1897
1657
  if matches is False:
@@ -1905,8 +1665,8 @@ class CallbackUtil:
1905
1665
  :param file_name: The name of the file.
1906
1666
  :param file_data: The data of the file, provided as either a string or as a bytes array.
1907
1667
  """
1908
- with io.BytesIO(file_data.encode() if isinstance(file_data, str) else file_data) as data:
1909
- self.callback.send_file(file_name, False, data)
1668
+ data = io.BytesIO(file_data.encode() if isinstance(file_data, str) else file_data)
1669
+ self.callback.send_file(file_name, False, data)
1910
1670
 
1911
1671
  def write_zip_file(self, zip_name: str, files: dict[str, str | bytes]) -> None:
1912
1672
  """
@@ -1915,38 +1675,16 @@ class CallbackUtil:
1915
1675
  :param zip_name: The name of the zip file.
1916
1676
  :param files: A dictionary of the files to add to the zip file.
1917
1677
  """
1918
- self.write_file(zip_name, FileUtil.zip_files(files))
1919
-
1920
- @staticmethod
1921
- def __get_indexed_field_maps(records: Iterable[SapioRecord], index_field: str) -> list[FieldMap]:
1922
- """
1923
- For dialogs that accept multiple records, we may want to be able to match the returned results back to the
1924
- records that they're for. In this case, we need to add an index to each record so that we can match them back
1925
- to the original records. We can't use the RecordId field, as new record models have negative record IDs that
1926
- cause the callback dialogs to bug out if the RecordId field is present and negative.
1927
-
1928
- :param records: The records to return indexed field maps of.
1929
- :param index_field: The name of the field to use as the index. Make sure that this field doesn't exist on the
1930
- records, as then it will overwrite the existing value.
1931
- :return: A list of field maps for the records, with an index field added to each. The value of the index on
1932
- each field map is the record's record ID (even if it's a record model with a negative ID).
1933
- """
1934
- ret_val: list[FieldMap] = []
1935
- for record in records:
1936
- field_map: FieldMap = AliasUtil.to_field_map(record)
1937
- field_map[index_field] = AliasUtil.to_record_id(record)
1938
- ret_val.append(field_map)
1939
- return ret_val
1678
+ data = io.BytesIO(FileUtil.zip_files(files))
1679
+ self.callback.send_file(zip_name, False, data)
1940
1680
 
1941
1681
  @staticmethod
1942
1682
  def __temp_dt_from_field_defs(data_type: DataTypeIdentifier, display_name: str | None,
1943
- plural_display_name: str | None, fields: Iterable[AbstractVeloxFieldDefinition],
1683
+ plural_display_name: str | None, fields: list[AbstractVeloxFieldDefinition],
1944
1684
  column_positions: dict[str, tuple[int, int]] | None) -> TemporaryDataType:
1945
1685
  """
1946
1686
  Construct a Temporary Data Type definition from a provided list of field definitions for use in a callback.
1947
1687
  """
1948
- if not fields:
1949
- raise SapioException("No fields provided to create a temporary data type.")
1950
1688
  # Get the data type name as a string from the parameters, and set the display name and plural display name if
1951
1689
  # they haven't been set.
1952
1690
  data_type: str = AliasUtil.to_data_type_name(data_type)
@@ -1976,7 +1714,7 @@ class CallbackUtil:
1976
1714
  builder.add_field(field_def, column, span)
1977
1715
  return builder.get_temporary_data_type()
1978
1716
 
1979
- def __temp_dt_from_field_names(self, data_type: str, fields: Iterable[FieldIdentifier | FieldFilterCriteria],
1717
+ def __temp_dt_from_field_names(self, data_type: str, fields: list[FieldIdentifier | FieldFilterCriteria],
1980
1718
  column_positions: dict[str, tuple[int, int]] | None,
1981
1719
  default_modifier: FieldModifier, field_modifiers: dict[str, FieldModifier]) \
1982
1720
  -> TemporaryDataType:
@@ -1993,7 +1731,6 @@ class CallbackUtil:
1993
1731
 
1994
1732
  # Determine if any FieldFilterCriteria were provided. If so, remove them from the fields list so that it
1995
1733
  # contains only field identifiers.
1996
- fields = list(fields)
1997
1734
  filter_criteria: list[FieldFilterCriteria] = [x for x in fields if isinstance(x, FieldFilterCriteria)]
1998
1735
  for criteria in filter_criteria:
1999
1736
  fields.remove(criteria)
@@ -2066,10 +1803,8 @@ class CallbackUtil:
2066
1803
  temp_dt.set_field_definition(modifier.modify_field(field_def))
2067
1804
  return temp_dt
2068
1805
 
2069
- # FR-47690: Updated with blank result handling behavior.
2070
1806
  def __prompt_for_count(self, count: tuple[int, int] | int, wrapper_type: type[WrappedType] | str,
2071
- require_input: bool, blank_result_handling: BlankResultHandling, repeat_message: str,
2072
- cancel_message: str) -> int:
1807
+ require_input: bool, repeat_message: str) -> int:
2073
1808
  """
2074
1809
  Given a count value, if it is a tuple representing an allowable range of values for a number of records to
2075
1810
  create, prompt the user to input the exact count to use. If the count is already a single integer, simply
@@ -2082,14 +1817,8 @@ class CallbackUtil:
2082
1817
  plural: str = self.dt_cache.get_plural_display_name(AliasUtil.to_data_type_name(wrapper_type))
2083
1818
  min_val, max_val = count
2084
1819
  msg: str = f"How many {plural} should be created? ({min_val} to {max_val})"
2085
- count_field: VeloxIntegerFieldDefinition = FieldBuilder().int_field("Count", min_value=min_val,
2086
- max_value=max_val,
2087
- default_value=min_val)
2088
- count: int = self.input_dialog(f"Create {plural}", msg, count_field,
2089
- require_input=require_input, blank_result_handling=blank_result_handling,
2090
- repeat_message=repeat_message, cancel_message=cancel_message)
2091
- if count is None:
2092
- count = 0
1820
+ count: int = self.integer_input_dialog(f"Create {plural}", msg, "Count", min_val, min_val, max_val,
1821
+ require_input=require_input, repeat_message=repeat_message)
2093
1822
  return count
2094
1823
 
2095
1824
  def __to_layout(self, data_type: str, layout: DataTypeLayoutIdentifier) -> DataTypeLayout | None:
@@ -2154,18 +1883,19 @@ class CallbackUtil:
2154
1883
  raise SapioException(f"No field of name \"{field_name}\" in field definitions of type \"{data_type}\"")
2155
1884
  return field_def
2156
1885
 
2157
- def __handle_timeout(self, func: Callable, request: Any, **kwargs) -> Any:
1886
+ def __handle_dialog_request(self, request: Any, func: Callable, **kwargs) -> Any:
2158
1887
  """
2159
1888
  Send a client callback request to the user that creates a dialog.
2160
1889
 
2161
1890
  This function handles updating the user object's request timeout to match the request timeout of this
2162
1891
  CallbackUtil for the duration of the dialog.
2163
1892
  If the dialog times out then a SapioDialogTimeoutException is thrown.
1893
+ If the user cancels the dialog then a SapioUserCancelledException is thrown.
2164
1894
 
2165
1895
  :param request: The client callback request to send to the user.
2166
1896
  :param func: The ClientCallback function to call with the given request as input.
2167
1897
  :param kwargs: Additional keywords for the provided function call.
2168
- :return: The response from the client callback.
1898
+ :return: The response from the client callback, if one was received.
2169
1899
  """
2170
1900
  try:
2171
1901
  self.user.timeout_seconds = self.timeout_seconds
@@ -2174,73 +1904,10 @@ class CallbackUtil:
2174
1904
  raise SapioDialogTimeoutException()
2175
1905
  finally:
2176
1906
  self.user.timeout_seconds = self._original_timeout
2177
- return response
2178
-
2179
- def __send_dialog(self, request: Any, func: Callable, **kwargs) -> Any:
2180
- """
2181
- Send a client callback request to the user that creates a dialog.
2182
-
2183
- This function handles updating the user object's request timeout to match the request timeout of this
2184
- CallbackUtil for the duration of the dialog.
2185
- If the dialog times out then a SapioDialogTimeoutException is thrown.
2186
- If the user cancels the dialog then a SapioUserCancelledException is thrown.
2187
-
2188
- :param request: The client callback request to send to the user.
2189
- :param func: The ClientCallback function to call with the given request as input.
2190
- :param kwargs: Additional keywords for the provided function call.
2191
- :return: The response from the client callback, if one was received.
2192
- """
2193
- response: Any | None = self.__handle_timeout(func, request, **kwargs)
2194
1907
  if response is None:
2195
1908
  raise SapioUserCancelledException()
2196
1909
  return response
2197
1910
 
2198
- def __send_dialog_blank_results(self, request: Any, func: Callable, not_blank_func: Callable,
2199
- handling: BlankResultHandling,
2200
- repeat_message: str | None, cancel_message: str | None, **kwargs):
2201
- """
2202
- Send a client callback request to the user that creates a dialog.
2203
-
2204
- This function handles updating the user object's request timeout to match the request timeout of this
2205
- CallbackUtil for the duration of the dialog.
2206
- If the dialog times out then a SapioDialogTimeoutException is thrown.
2207
- If the user cancels the dialog then a SapioUserCancelledException is thrown.
2208
- If the user provides a blank result, then the handling is used to determine what to do with that result.
2209
-
2210
- :param request: The client callback request to send to the user.
2211
- :param func: The ClientCallback function to call with the given request as input.
2212
- :param not_blank_func: The function to determine whether the provided result is blank or not.
2213
- :param handling: The handling to use for blank results.
2214
- :param repeat_message: If handling is REPEAT and a repeat_message is provided, then that message appears as
2215
- toaster text when the user provides a blank result.
2216
- :param cancel_message: If handling is CANCEL and a cancel_message is provided, then that message appears as
2217
- toaster text when the user provides a blank result.
2218
- :param kwargs: Additional keywords for the provided function call.
2219
- :return: The response from the client callback, if one was received.
2220
- """
2221
- if handling == BlankResultHandling.DEFAULT or handling is None:
2222
- handling = self._default_blank_result_handling
2223
- while True:
2224
- response: Any | None = self.__handle_timeout(func, request, **kwargs)
2225
- if response is None:
2226
- raise SapioUserCancelledException()
2227
- if not_blank_func(response):
2228
- return response
2229
- match handling:
2230
- case BlankResultHandling.CANCEL:
2231
- # If the user provided no selection, throw an exception.
2232
- if cancel_message:
2233
- self.toaster_popup(cancel_message, popup_type=PopupType.Warning)
2234
- raise SapioUserCancelledException()
2235
- case BlankResultHandling.REPEAT:
2236
- # If the user provided no selection, repeat the dialog.
2237
- # If a repeatMessage is provided, display it as a toaster popup.
2238
- if repeat_message:
2239
- self.toaster_popup(repeat_message, popup_type=PopupType.Warning)
2240
- case BlankResultHandling.RETURN:
2241
- # If the user provided no selection, return the blank result.
2242
- return response
2243
-
2244
1911
 
2245
1912
  class FieldModifier:
2246
1913
  """
@@ -2316,15 +1983,15 @@ class FieldFilterCriteria:
2316
1983
  key_field: bool | None
2317
1984
  identifier: bool | None
2318
1985
  system_field: bool | None
2319
- field_types: Container[FieldType] | None
2320
- not_field_types: Container[FieldType] | None
1986
+ field_types: list[FieldType] | None
1987
+ not_field_types: list[FieldType] | None
2321
1988
  matches_tag: str | None
2322
1989
  contains_tag: str | None
2323
1990
  regex_tag: str | re.Pattern[str] | None
2324
1991
 
2325
1992
  def __init__(self, *, required: bool | None = None, editable: bool | None = None, key_field: bool | None = None,
2326
1993
  identifier: bool | None = None, system_field: bool | None = None,
2327
- field_types: Container[FieldType] | None = None, not_field_types: Container[FieldType] | None = None,
1994
+ field_types: list[FieldType] | None = None, not_field_types: list[FieldType] | None = None,
2328
1995
  matches_tag: str | None = None, contains_tag: str | None = None,
2329
1996
  regex_tag: str | re.Pattern[str] | None = None):
2330
1997
  """