sapiopycommons 2025.6.11a561__py3-none-any.whl → 2025.6.16a562__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.
- sapiopycommons/callbacks/callback_util.py +551 -270
- {sapiopycommons-2025.6.11a561.dist-info → sapiopycommons-2025.6.16a562.dist-info}/METADATA +1 -1
- {sapiopycommons-2025.6.11a561.dist-info → sapiopycommons-2025.6.16a562.dist-info}/RECORD +5 -5
- {sapiopycommons-2025.6.11a561.dist-info → sapiopycommons-2025.6.16a562.dist-info}/WHEEL +0 -0
- {sapiopycommons-2025.6.11a561.dist-info → sapiopycommons-2025.6.16a562.dist-info}/licenses/LICENSE +0 -0
|
@@ -4,6 +4,7 @@ import io
|
|
|
4
4
|
import re
|
|
5
5
|
import warnings
|
|
6
6
|
from copy import copy
|
|
7
|
+
from enum import Enum
|
|
7
8
|
from typing import Iterable, TypeAlias, Any, Callable, Container, Collection
|
|
8
9
|
from weakref import WeakValueDictionary
|
|
9
10
|
|
|
@@ -44,6 +45,23 @@ from sapiopycommons.recordmodel.record_handler import RecordHandler
|
|
|
44
45
|
DataTypeLayoutIdentifier: TypeAlias = DataTypeLayout | str | None
|
|
45
46
|
|
|
46
47
|
|
|
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
|
+
|
|
47
65
|
# CR-47521: Updated various parameter type hints from list or Iterable to more specific type hints.
|
|
48
66
|
# If we need to iterate over the parameter, then it is Iterable.
|
|
49
67
|
# If we need to see if the parameter contains a value, then it is Container.
|
|
@@ -60,6 +78,7 @@ class CallbackUtil:
|
|
|
60
78
|
timeout_seconds: int
|
|
61
79
|
width_pixels: int | None
|
|
62
80
|
width_percent: float | None
|
|
81
|
+
_default_blank_result_handling: BlankResultHandling
|
|
63
82
|
|
|
64
83
|
__instances: WeakValueDictionary[SapioUser, CallbackUtil] = WeakValueDictionary()
|
|
65
84
|
__initialized: bool
|
|
@@ -97,6 +116,7 @@ class CallbackUtil:
|
|
|
97
116
|
self.timeout_seconds = self.user.timeout_seconds
|
|
98
117
|
self.width_pixels = None
|
|
99
118
|
self.width_percent = None
|
|
119
|
+
self._default_blank_result_handling = BlankResultHandling.CANCEL
|
|
100
120
|
self.__layouts = {}
|
|
101
121
|
|
|
102
122
|
def set_dialog_width(self, width_pixels: int | None = None, width_percent: float | None = None):
|
|
@@ -123,6 +143,19 @@ class CallbackUtil:
|
|
|
123
143
|
"""
|
|
124
144
|
self.timeout_seconds = timeout
|
|
125
145
|
|
|
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
|
+
|
|
126
159
|
def toaster_popup(self, message: str, title: str = "", popup_type: PopupType = PopupType.Info) -> None:
|
|
127
160
|
"""
|
|
128
161
|
Display a toaster popup in the bottom right corner of the user's screen.
|
|
@@ -184,7 +217,7 @@ class CallbackUtil:
|
|
|
184
217
|
# Send the request to the user.
|
|
185
218
|
request = OptionDialogRequest(title, msg, options, default_option, user_can_cancel,
|
|
186
219
|
width_in_pixels=self.width_pixels, width_percentage=self.width_percent)
|
|
187
|
-
response: int = self.
|
|
220
|
+
response: int = self.__send_dialog(request, self.callback.show_option_dialog)
|
|
188
221
|
return options[response]
|
|
189
222
|
|
|
190
223
|
def ok_dialog(self, title: str, msg: str) -> None:
|
|
@@ -221,16 +254,46 @@ class CallbackUtil:
|
|
|
221
254
|
"""
|
|
222
255
|
return self.option_dialog(title, msg, ["Yes", "No"], 0 if default_yes else 1, False) == "Yes"
|
|
223
256
|
|
|
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
|
+
|
|
224
283
|
# CR-47310: Add a parameter to the list, input, selection, and e-sign dialog functions to control reprompting the
|
|
225
284
|
# user if no input/selection/valid credentials are provided.
|
|
285
|
+
# FR-47690: Added shortcut_single_option parameter. Updated with blank result handling behavior.
|
|
226
286
|
def list_dialog(self,
|
|
227
287
|
title: str,
|
|
228
288
|
options: Iterable[str],
|
|
229
289
|
multi_select: bool = False,
|
|
230
290
|
preselected_values: Iterable[str] | None = None,
|
|
231
291
|
*,
|
|
232
|
-
|
|
233
|
-
|
|
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]:
|
|
234
297
|
"""
|
|
235
298
|
Create a list dialog with the given options for the user to choose from.
|
|
236
299
|
|
|
@@ -239,28 +302,232 @@ class CallbackUtil:
|
|
|
239
302
|
:param multi_select: Whether the user is able to select multiple options from the list.
|
|
240
303
|
:param preselected_values: A list of values that will already be selected when the list dialog is created. The
|
|
241
304
|
user can unselect these values if they want to.
|
|
242
|
-
:param
|
|
243
|
-
|
|
244
|
-
:param
|
|
245
|
-
|
|
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.
|
|
246
315
|
:return: The list of options that the user selected.
|
|
247
316
|
"""
|
|
248
317
|
if not options:
|
|
249
318
|
raise SapioException("No options provided.")
|
|
319
|
+
options = list(options)
|
|
320
|
+
if len(options) == 1 and shortcut_single_option:
|
|
321
|
+
return [options[0]]
|
|
250
322
|
|
|
251
323
|
# Send the request to the user.
|
|
252
|
-
request = ListDialogRequest(title, multi_select,
|
|
324
|
+
request = ListDialogRequest(title, multi_select, options,
|
|
253
325
|
list(preselected_values) if preselected_values else None,
|
|
254
326
|
width_in_pixels=self.width_pixels, width_percentage=self.width_percent)
|
|
255
327
|
|
|
256
|
-
# If require_selection is true
|
|
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
|
+
|
|
257
375
|
while True:
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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)
|
|
264
531
|
|
|
265
532
|
def form_dialog(self,
|
|
266
533
|
title: str,
|
|
@@ -280,7 +547,8 @@ class CallbackUtil:
|
|
|
280
547
|
:param msg: The message to display at the top of the form. This can be formatted using HTML elements.
|
|
281
548
|
:param fields: The definitions of the fields to display in the form. Fields will be displayed in the order they
|
|
282
549
|
are provided in this list.
|
|
283
|
-
:param values: Sets the default values of the fields.
|
|
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.
|
|
284
552
|
:param column_positions: If a tuple is provided for a field name, alters that field's column position and column
|
|
285
553
|
span. (Field order is still determined by the fields list.)
|
|
286
554
|
:param data_type: The data type name for the temporary data type that will be created for this form.
|
|
@@ -294,10 +562,17 @@ class CallbackUtil:
|
|
|
294
562
|
# Build a temporary data type for the request.
|
|
295
563
|
temp_dt = self.__temp_dt_from_field_defs(data_type, display_name, plural_display_name, fields, column_positions)
|
|
296
564
|
|
|
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
|
+
|
|
297
572
|
# Send the request to the user.
|
|
298
573
|
request = FormEntryDialogRequest(title, msg, temp_dt, values,
|
|
299
574
|
width_in_pixels=self.width_pixels, width_percentage=self.width_percent)
|
|
300
|
-
response: FieldMap = self.
|
|
575
|
+
response: FieldMap = self.__send_dialog(request, self.callback.show_form_entry_dialog)
|
|
301
576
|
return response
|
|
302
577
|
|
|
303
578
|
def record_form_dialog(self,
|
|
@@ -369,7 +644,7 @@ class CallbackUtil:
|
|
|
369
644
|
# Send the request to the user.
|
|
370
645
|
request = FormEntryDialogRequest(title, msg, temp_dt, values,
|
|
371
646
|
width_in_pixels=self.width_pixels, width_percentage=self.width_percent)
|
|
372
|
-
response: FieldMap = self.
|
|
647
|
+
response: FieldMap = self.__send_dialog(request, self.callback.show_form_entry_dialog)
|
|
373
648
|
return response
|
|
374
649
|
|
|
375
650
|
# FR-47314: Create record form and table dialogs for updating or creating records.
|
|
@@ -463,170 +738,11 @@ class CallbackUtil:
|
|
|
463
738
|
default_modifier=default_modifier, field_modifiers=field_modifiers)
|
|
464
739
|
return record
|
|
465
740
|
|
|
466
|
-
def input_dialog(self,
|
|
467
|
-
title: str,
|
|
468
|
-
msg: str,
|
|
469
|
-
field: AbstractVeloxFieldDefinition,
|
|
470
|
-
*,
|
|
471
|
-
require_input: bool = False,
|
|
472
|
-
repeat_message: str | None = "Please provide a value to continue.") -> FieldValue:
|
|
473
|
-
"""
|
|
474
|
-
Create an input dialog where the user must input data for a singular field.
|
|
475
|
-
|
|
476
|
-
:param title: The title of the dialog.
|
|
477
|
-
:param msg: The message to display in the dialog. This can be formatted using HTML elements.
|
|
478
|
-
:param field: The definition for a field that the user must provide input to.
|
|
479
|
-
:param require_input: If true, the request will be re-sent if the user submits the dialog without providing an
|
|
480
|
-
input field value.
|
|
481
|
-
:param repeat_message: If require_input is true and a repeat_message is provided, then that message appears
|
|
482
|
-
as toaster text if the dialog is repeated.
|
|
483
|
-
:return: The response value from the user for the given field.
|
|
484
|
-
"""
|
|
485
|
-
# Send the request to the user.
|
|
486
|
-
request = InputDialogCriteria(title, msg, field,
|
|
487
|
-
width_in_pixels=self.width_pixels, width_percentage=self.width_percent)
|
|
488
|
-
|
|
489
|
-
# If require_input is true, repeat the request if the user didn't provide a field value.
|
|
490
|
-
while True:
|
|
491
|
-
try:
|
|
492
|
-
self.user.timeout_seconds = self.timeout_seconds
|
|
493
|
-
# It's not possible to distinguish between the user cancelling this dialog and submitting the dialog
|
|
494
|
-
# with no input if the ClientCallback show_input_dialog function is used, as both cases just return
|
|
495
|
-
# None. Therefore, in order to be able to make that distinction, we need to call the endpoint without
|
|
496
|
-
# ClientCallback and get the raw response object.
|
|
497
|
-
raw_response = self.user.post('/clientcallback/showInputDialog', payload=request.to_json())
|
|
498
|
-
# A response status code of 204 is what represents a cancelled dialog.
|
|
499
|
-
if raw_response.status_code == 204:
|
|
500
|
-
raise SapioUserCancelledException()
|
|
501
|
-
self.user.raise_for_status(raw_response)
|
|
502
|
-
json_dct: dict | None = self.user.get_json_data_or_none(raw_response)
|
|
503
|
-
response: FieldValue = json_dct['result'] if json_dct else None
|
|
504
|
-
except ReadTimeout:
|
|
505
|
-
raise SapioDialogTimeoutException()
|
|
506
|
-
finally:
|
|
507
|
-
self.user.timeout_seconds = self._original_timeout
|
|
508
|
-
# String fields that the user didn't provide will return as an empty string instead of a None response.
|
|
509
|
-
is_str: bool = isinstance(response, str)
|
|
510
|
-
if not require_input or (is_str and response) or (not is_str and response is not None):
|
|
511
|
-
break
|
|
512
|
-
if repeat_message:
|
|
513
|
-
self.toaster_popup(repeat_message, popup_type=PopupType.Warning)
|
|
514
|
-
return response
|
|
515
|
-
|
|
516
|
-
def string_input_dialog(self,
|
|
517
|
-
title: str,
|
|
518
|
-
msg: str,
|
|
519
|
-
field_name: str,
|
|
520
|
-
default_value: str | None = None,
|
|
521
|
-
max_length: int | None = None,
|
|
522
|
-
editable: bool = True,
|
|
523
|
-
*,
|
|
524
|
-
require_input: bool = False,
|
|
525
|
-
repeat_message: str | None = "Please provide a value to continue.",
|
|
526
|
-
**kwargs) -> str:
|
|
527
|
-
"""
|
|
528
|
-
Create an input dialog where the user must input data for a singular text field.
|
|
529
|
-
|
|
530
|
-
:param title: The title of the dialog.
|
|
531
|
-
:param msg: The message to display in the dialog. This can be formatted using HTML elements.
|
|
532
|
-
:param field_name: The name and display name of the string field.
|
|
533
|
-
:param default_value: The default value to place into the string field, if any.
|
|
534
|
-
:param max_length: The max length of the string value. If not provided, uses the length of the default value.
|
|
535
|
-
If neither this nor a default value are provided, defaults to 100 characters.
|
|
536
|
-
:param editable: Whether the field is editable by the user.
|
|
537
|
-
:param require_input: If true, the request will be re-sent if the user submits the dialog without making
|
|
538
|
-
a selection.
|
|
539
|
-
:param repeat_message: If require_input is true and a repeat_message is provided, then that message appears
|
|
540
|
-
as toaster text if the dialog is repeated.
|
|
541
|
-
:param kwargs: Any additional keyword arguments to pass to the field definition.
|
|
542
|
-
:return: The string that the user input into the dialog.
|
|
543
|
-
"""
|
|
544
|
-
if max_length is None:
|
|
545
|
-
max_length = len(default_value) if default_value else 100
|
|
546
|
-
field = VeloxStringFieldDefinition("Input", field_name, field_name, default_value=default_value,
|
|
547
|
-
max_length=max_length, editable=editable, **kwargs)
|
|
548
|
-
return self.input_dialog(title, msg, field,
|
|
549
|
-
require_input=require_input, repeat_message=repeat_message)
|
|
550
|
-
|
|
551
|
-
def integer_input_dialog(self,
|
|
552
|
-
title: str,
|
|
553
|
-
msg: str,
|
|
554
|
-
field_name: str,
|
|
555
|
-
default_value: int = None,
|
|
556
|
-
min_value: int = -10000,
|
|
557
|
-
max_value: int = 10000,
|
|
558
|
-
editable: bool = True,
|
|
559
|
-
*,
|
|
560
|
-
require_input: bool = False,
|
|
561
|
-
repeat_message: str | None = "Please provide a value to continue.",
|
|
562
|
-
**kwargs) -> int:
|
|
563
|
-
"""
|
|
564
|
-
Create an input dialog where the user must input data for a singular integer field.
|
|
565
|
-
|
|
566
|
-
:param title: The title of the dialog.
|
|
567
|
-
:param msg: The message to display in the dialog. This can be formatted using HTML elements.
|
|
568
|
-
:param field_name: The name and display name of the integer field.
|
|
569
|
-
:param default_value: The default value to place into the integer field. If not provided, defaults to the 0 or
|
|
570
|
-
the minimum value, whichever is higher.
|
|
571
|
-
:param min_value: The minimum allowed value of the input.
|
|
572
|
-
:param max_value: The maximum allowed value of the input.
|
|
573
|
-
:param editable: Whether the field is editable by the user.
|
|
574
|
-
:param require_input: If true, the request will be re-sent if the user submits the dialog without making
|
|
575
|
-
a selection.
|
|
576
|
-
:param repeat_message: If require_input is true and a repeat_message is provided, then that message appears
|
|
577
|
-
as toaster text if the dialog is repeated.
|
|
578
|
-
:param kwargs: Any additional keyword arguments to pass to the field definition.
|
|
579
|
-
:return: The integer that the user input into the dialog.
|
|
580
|
-
"""
|
|
581
|
-
if default_value is None:
|
|
582
|
-
default_value = max(0, min_value)
|
|
583
|
-
field = VeloxIntegerFieldDefinition("Input", field_name, field_name, default_value=default_value,
|
|
584
|
-
min_value=min_value, max_value=max_value, editable=editable, **kwargs)
|
|
585
|
-
return self.input_dialog(title, msg, field,
|
|
586
|
-
require_input=require_input, repeat_message=repeat_message)
|
|
587
|
-
|
|
588
|
-
def double_input_dialog(self,
|
|
589
|
-
title: str,
|
|
590
|
-
msg: str,
|
|
591
|
-
field_name: str,
|
|
592
|
-
default_value: float = None,
|
|
593
|
-
min_value: float = -10000000,
|
|
594
|
-
max_value: float = 100000000,
|
|
595
|
-
editable: bool = True,
|
|
596
|
-
*,
|
|
597
|
-
require_input: bool = False,
|
|
598
|
-
repeat_message: str | None = "Please provide a value to continue.",
|
|
599
|
-
**kwargs) -> float:
|
|
600
|
-
"""
|
|
601
|
-
Create an input dialog where the user must input data for a singular double field.
|
|
602
|
-
|
|
603
|
-
:param title: The title of the dialog.
|
|
604
|
-
:param msg: The message to display in the dialog. This can be formatted using HTML elements.
|
|
605
|
-
:param field_name: The name and display name of the double field.
|
|
606
|
-
:param default_value: The default value to place into the double field. If not provided, defaults to the 0 or
|
|
607
|
-
the minimum value, whichever is higher.
|
|
608
|
-
:param min_value: The minimum allowed value of the input.
|
|
609
|
-
:param max_value: The maximum allowed value of the input.
|
|
610
|
-
:param editable: Whether the field is editable by the user.
|
|
611
|
-
:param require_input: If true, the request will be re-sent if the user submits the dialog without making
|
|
612
|
-
a selection.
|
|
613
|
-
:param repeat_message: If require_input is true and a repeat_message is provided, then that message appears
|
|
614
|
-
as toaster text if the dialog is repeated.
|
|
615
|
-
:param kwargs: Any additional keyword arguments to pass to the field definition.
|
|
616
|
-
:return: The float that the user input into the dialog.
|
|
617
|
-
"""
|
|
618
|
-
if default_value is None:
|
|
619
|
-
default_value = max(0., min_value)
|
|
620
|
-
field = VeloxDoubleFieldDefinition("Input", field_name, field_name, default_value=default_value,
|
|
621
|
-
min_value=min_value, max_value=max_value, editable=editable, **kwargs)
|
|
622
|
-
return self.input_dialog(title, msg, field,
|
|
623
|
-
require_input=require_input, repeat_message=repeat_message)
|
|
624
|
-
|
|
625
741
|
def table_dialog(self,
|
|
626
742
|
title: str,
|
|
627
743
|
msg: str,
|
|
628
744
|
fields: Iterable[AbstractVeloxFieldDefinition],
|
|
629
|
-
values: Iterable[FieldMap],
|
|
745
|
+
values: Iterable[FieldMap] | int,
|
|
630
746
|
*,
|
|
631
747
|
data_type: DataTypeIdentifier = "Default",
|
|
632
748
|
display_name: str | None = None,
|
|
@@ -641,7 +757,8 @@ class CallbackUtil:
|
|
|
641
757
|
:param msg: The message to display at the top of the form. This can be formatted using HTML elements.
|
|
642
758
|
:param fields: The definitions of the fields to display as table columns. Fields will be displayed in the order
|
|
643
759
|
they are provided in this list.
|
|
644
|
-
:param values: The values to set for each row of the table.
|
|
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.
|
|
645
762
|
:param group_by: If provided, the created table dialog will be grouped by the field with this name by default.
|
|
646
763
|
The user may remove this grouping if they want to.
|
|
647
764
|
:param image_data: The bytes to the images that should be displayed in the rows of the table. Each element in
|
|
@@ -654,9 +771,18 @@ class CallbackUtil:
|
|
|
654
771
|
:return: A list of dictionaries mapping the data field names of the given field definitions to the response
|
|
655
772
|
value from the user for that field for each row.
|
|
656
773
|
"""
|
|
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)]
|
|
657
777
|
if not values:
|
|
658
778
|
raise SapioException("No values provided.")
|
|
659
779
|
|
|
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 values:
|
|
784
|
+
row[field.data_field_name] = field.default_value
|
|
785
|
+
|
|
660
786
|
# Convert the group_by parameter to a field name.
|
|
661
787
|
if group_by is not None:
|
|
662
788
|
group_by: str = AliasUtil.to_data_field_name(group_by)
|
|
@@ -670,7 +796,7 @@ class CallbackUtil:
|
|
|
670
796
|
request = TableEntryDialogRequest(title, msg, temp_dt, list(values),
|
|
671
797
|
record_image_data_list=image_data, group_by_field=group_by,
|
|
672
798
|
width_in_pixels=self.width_pixels, width_percentage=self.width_percent)
|
|
673
|
-
response: list[FieldMap] = self.
|
|
799
|
+
response: list[FieldMap] = self.__send_dialog(request, self.callback.show_table_entry_dialog)
|
|
674
800
|
return response
|
|
675
801
|
|
|
676
802
|
def record_table_dialog(self,
|
|
@@ -760,7 +886,7 @@ class CallbackUtil:
|
|
|
760
886
|
request = TableEntryDialogRequest(title, msg, temp_dt, field_map_list,
|
|
761
887
|
record_image_data_list=image_data, group_by_field=group_by,
|
|
762
888
|
width_in_pixels=self.width_pixels, width_percentage=self.width_percent)
|
|
763
|
-
response: list[FieldMap] = self.
|
|
889
|
+
response: list[FieldMap] = self.__send_dialog(request, self.callback.show_table_entry_dialog)
|
|
764
890
|
return response
|
|
765
891
|
|
|
766
892
|
# FR-47314: Create record form and table dialogs for updating or creating records.
|
|
@@ -817,6 +943,7 @@ class CallbackUtil:
|
|
|
817
943
|
index: int = result.pop(index_field)
|
|
818
944
|
records_by_id[index].set_field_values(result)
|
|
819
945
|
|
|
946
|
+
# FR-47690: Updated with blank result handling behavior.
|
|
820
947
|
def create_record_table_dialog(self,
|
|
821
948
|
title: str,
|
|
822
949
|
msg: str,
|
|
@@ -828,8 +955,10 @@ class CallbackUtil:
|
|
|
828
955
|
field_modifiers: dict[FieldIdentifier, FieldModifier] | None = None,
|
|
829
956
|
group_by: FieldIdentifier | None = None,
|
|
830
957
|
image_data: Iterable[bytes] | None = None,
|
|
831
|
-
require_input
|
|
832
|
-
|
|
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.") \
|
|
833
962
|
-> list[WrappedType] | list[PyRecordModel]:
|
|
834
963
|
"""
|
|
835
964
|
Create a table dialog where the user may input data into the fields of the table. The table is constructed from
|
|
@@ -863,13 +992,18 @@ class CallbackUtil:
|
|
|
863
992
|
The user may remove this grouping if they want to.
|
|
864
993
|
:param image_data: The bytes to the images that should be displayed in the rows of the table. Each element in
|
|
865
994
|
the image data list corresponds to the element at the same index in the records list.
|
|
866
|
-
:param require_input:
|
|
867
|
-
|
|
868
|
-
:param
|
|
869
|
-
|
|
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.
|
|
870
1003
|
:return: A list of the newly created records.
|
|
871
1004
|
"""
|
|
872
|
-
count: int = self.__prompt_for_count(count, wrapper_type, require_input, repeat_message
|
|
1005
|
+
count: int = self.__prompt_for_count(count, wrapper_type, require_input, blank_result_handling, repeat_message,
|
|
1006
|
+
cancel_message)
|
|
873
1007
|
if count <= 0:
|
|
874
1008
|
return []
|
|
875
1009
|
records: list[WrappedType] | list[PyRecordModel] = self.rec_handler.add_models(wrapper_type, count)
|
|
@@ -1005,6 +1139,7 @@ class CallbackUtil:
|
|
|
1005
1139
|
default_modifier=default_modifier, field_modifiers=field_modifiers,
|
|
1006
1140
|
group_by=group_by, image_data=image_data)
|
|
1007
1141
|
|
|
1142
|
+
# FR-47690: Updated with blank result handling behavior.
|
|
1008
1143
|
def create_record_adaptive_dialog(self,
|
|
1009
1144
|
title: str,
|
|
1010
1145
|
msg: str,
|
|
@@ -1017,8 +1152,10 @@ class CallbackUtil:
|
|
|
1017
1152
|
column_positions: dict[str, tuple[int, int]] | None = None,
|
|
1018
1153
|
group_by: FieldIdentifier | None = None,
|
|
1019
1154
|
image_data: Iterable[bytes] | None = None,
|
|
1020
|
-
require_input
|
|
1021
|
-
|
|
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.") \
|
|
1022
1159
|
-> list[WrappedType]:
|
|
1023
1160
|
"""
|
|
1024
1161
|
Create a dialog where the user may input data into the specified fields. The dialog is constructed from
|
|
@@ -1059,14 +1196,19 @@ class CallbackUtil:
|
|
|
1059
1196
|
:param image_data: The bytes to the images that should be displayed in the rows of the table. Each element in
|
|
1060
1197
|
the image data list corresponds to the element at the same index in the records list. Only used if the
|
|
1061
1198
|
adaptive dialog becomes a table.
|
|
1062
|
-
:param require_input:
|
|
1063
|
-
|
|
1064
|
-
:param
|
|
1065
|
-
|
|
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.
|
|
1066
1207
|
:return: A list of the newly created records. Even if a form was displayed, the created record will still be
|
|
1067
1208
|
returned in a list.
|
|
1068
1209
|
"""
|
|
1069
|
-
count: int = self.__prompt_for_count(count, wrapper_type, require_input, repeat_message
|
|
1210
|
+
count: int = self.__prompt_for_count(count, wrapper_type, require_input, blank_result_handling, repeat_message,
|
|
1211
|
+
cancel_message)
|
|
1070
1212
|
if count <= 0:
|
|
1071
1213
|
return []
|
|
1072
1214
|
if count == 1:
|
|
@@ -1076,6 +1218,7 @@ class CallbackUtil:
|
|
|
1076
1218
|
default_modifier=default_modifier, field_modifiers=field_modifiers,
|
|
1077
1219
|
group_by=group_by, image_data=image_data)
|
|
1078
1220
|
|
|
1221
|
+
# FR-47690: Add group_by and image_data parameters.
|
|
1079
1222
|
def multi_type_table_dialog(self,
|
|
1080
1223
|
title: str,
|
|
1081
1224
|
msg: str,
|
|
@@ -1086,7 +1229,9 @@ class CallbackUtil:
|
|
|
1086
1229
|
field_modifiers: dict[FieldIdentifier, FieldModifier] | None = None,
|
|
1087
1230
|
data_type: DataTypeIdentifier = "Default",
|
|
1088
1231
|
display_name: str | None = None,
|
|
1089
|
-
plural_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]:
|
|
1090
1235
|
"""
|
|
1091
1236
|
Create a table dialog where the user may input data into the fields of the table. The table is constructed from
|
|
1092
1237
|
a given list of records of multiple data types or field maps. Provided field names must match with field names
|
|
@@ -1140,6 +1285,10 @@ class CallbackUtil:
|
|
|
1140
1285
|
name.
|
|
1141
1286
|
:param plural_display_name: The plural display name for the temporary data type. If not provided, defaults to
|
|
1142
1287
|
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.
|
|
1143
1292
|
:return: A list of dictionaries mapping the data field names of the given field definitions to the response
|
|
1144
1293
|
value from the user for that field for each row.
|
|
1145
1294
|
"""
|
|
@@ -1204,31 +1353,49 @@ class CallbackUtil:
|
|
|
1204
1353
|
field_names.add(name)
|
|
1205
1354
|
|
|
1206
1355
|
# Get the values for each row.
|
|
1356
|
+
# FR-47690: Updated this for loop to better match the Java implementation.
|
|
1207
1357
|
values: list[dict[str, FieldValue]] = []
|
|
1208
1358
|
for row in row_contents:
|
|
1209
1359
|
# The final values for this row:
|
|
1210
1360
|
row_values: dict[str, FieldValue] = {}
|
|
1211
1361
|
|
|
1212
|
-
# Map the records for this row by their data type. If a field map is provided,
|
|
1213
|
-
|
|
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 = {}
|
|
1214
1366
|
for rec in row:
|
|
1215
1367
|
# Toss out null elements.
|
|
1216
1368
|
if rec is None:
|
|
1217
1369
|
continue
|
|
1218
1370
|
# Map records to their data type name. Map field maps to Default.
|
|
1219
1371
|
dt: str = "Default" if isinstance(rec, dict) else AliasUtil.to_data_type_name(rec)
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
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
|
|
1224
1379
|
|
|
1225
1380
|
# Get the field values from the above records.
|
|
1226
1381
|
for field in final_fields:
|
|
1382
|
+
value: Any | None = None
|
|
1383
|
+
|
|
1227
1384
|
# Find the object that corresponds to this field given its data type name.
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
if
|
|
1231
|
-
|
|
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)
|
|
1232
1399
|
|
|
1233
1400
|
# Find out if this field had its data type prepended to it. If this is the case, then we need to find
|
|
1234
1401
|
# the true data field name before retrieving the value from the field map.
|
|
@@ -1237,16 +1404,22 @@ class CallbackUtil:
|
|
|
1237
1404
|
name = name.split(".")[1]
|
|
1238
1405
|
|
|
1239
1406
|
# Set the value for this particular field.
|
|
1240
|
-
row_values[
|
|
1407
|
+
row_values[name] = value
|
|
1241
1408
|
values.append(row_values)
|
|
1242
1409
|
|
|
1243
1410
|
# Build a temporary data type for the request.
|
|
1244
1411
|
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)
|
|
1245
1417
|
|
|
1246
1418
|
# Send the request to the user.
|
|
1247
1419
|
request = TableEntryDialogRequest(title, msg, temp_dt, values,
|
|
1420
|
+
record_image_data_list=image_data, group_by_field=group_by,
|
|
1248
1421
|
width_in_pixels=self.width_pixels, width_percentage=self.width_percent)
|
|
1249
|
-
response: list[FieldMap] = self.
|
|
1422
|
+
response: list[FieldMap] = self.__send_dialog(request, self.callback.show_table_entry_dialog)
|
|
1250
1423
|
return response
|
|
1251
1424
|
|
|
1252
1425
|
def record_view_dialog(self,
|
|
@@ -1283,13 +1456,14 @@ class CallbackUtil:
|
|
|
1283
1456
|
# Send the request to the user.
|
|
1284
1457
|
request = DataRecordDialogRequest(title, record, layout, minimized, access_level, plugin_path_list,
|
|
1285
1458
|
width_in_pixels=self.width_pixels, width_percentage=self.width_percent)
|
|
1286
|
-
response: bool = self.
|
|
1459
|
+
response: bool = self.__send_dialog(request, self.callback.data_record_form_view_dialog)
|
|
1287
1460
|
# The __handle_dialog_request function only throws a cancelled exception if the response is None, but in
|
|
1288
1461
|
# this case we also want to throw if the response is False.
|
|
1289
1462
|
if not response:
|
|
1290
1463
|
raise SapioUserCancelledException()
|
|
1291
1464
|
|
|
1292
1465
|
# 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.
|
|
1293
1467
|
def selection_dialog(self,
|
|
1294
1468
|
msg: str,
|
|
1295
1469
|
fields: Iterable[AbstractVeloxFieldDefinition],
|
|
@@ -1301,8 +1475,11 @@ class CallbackUtil:
|
|
|
1301
1475
|
display_name: str | None = None,
|
|
1302
1476
|
plural_display_name: str | None = None,
|
|
1303
1477
|
image_data: Iterable[bytes] | None = None,
|
|
1304
|
-
|
|
1305
|
-
|
|
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]:
|
|
1306
1483
|
"""
|
|
1307
1484
|
Create a selection dialog for a list of field maps for the user to choose from. Requires that the caller
|
|
1308
1485
|
provide the definitions of every field in the table.
|
|
@@ -1325,14 +1502,23 @@ class CallbackUtil:
|
|
|
1325
1502
|
the display name + "s".
|
|
1326
1503
|
:param image_data: The bytes to the images that should be displayed in the rows of the table. Each element in
|
|
1327
1504
|
the image data list corresponds to the element at the same index in the values list.
|
|
1328
|
-
:param
|
|
1329
|
-
|
|
1330
|
-
:param
|
|
1331
|
-
|
|
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.
|
|
1332
1515
|
:return: A list of field maps corresponding to the chosen input field maps.
|
|
1333
1516
|
"""
|
|
1334
1517
|
if not values:
|
|
1335
1518
|
raise SapioException("No values provided.")
|
|
1519
|
+
values = list(values)
|
|
1520
|
+
if len(values) == 1 and shortcut_single_option:
|
|
1521
|
+
return [values[0]]
|
|
1336
1522
|
|
|
1337
1523
|
if preselected_rows:
|
|
1338
1524
|
# Confirm that the provided field maps are validly configured to allow the use of preselected rows.
|
|
@@ -1369,16 +1555,17 @@ class CallbackUtil:
|
|
|
1369
1555
|
|
|
1370
1556
|
# Send the request to the user.
|
|
1371
1557
|
request = TempTableSelectionRequest(temp_dt, msg, list(values), image_data, preselected_rows, multi_select)
|
|
1372
|
-
# If require_selection is true, repeat the request if the user didn't make a selection.
|
|
1373
|
-
while True:
|
|
1374
|
-
response: list[FieldMap] = self.__handle_dialog_request(request,
|
|
1375
|
-
self.callback.show_temp_table_selection_dialog)
|
|
1376
|
-
if not require_selection or response:
|
|
1377
|
-
break
|
|
1378
|
-
if repeat_message:
|
|
1379
|
-
self.toaster_popup(repeat_message, popup_type=PopupType.Warning)
|
|
1380
|
-
return response
|
|
1381
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.
|
|
1382
1569
|
def record_selection_dialog(self,
|
|
1383
1570
|
msg: str,
|
|
1384
1571
|
fields: Iterable[FieldIdentifier | FieldFilterCriteria] | DataTypeLayoutIdentifier,
|
|
@@ -1387,8 +1574,11 @@ class CallbackUtil:
|
|
|
1387
1574
|
preselected_records: Iterable[RecordIdentifier] | None = None,
|
|
1388
1575
|
*,
|
|
1389
1576
|
image_data: Iterable[bytes] | None = None,
|
|
1390
|
-
|
|
1391
|
-
|
|
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.") \
|
|
1392
1582
|
-> list[SapioRecord]:
|
|
1393
1583
|
"""
|
|
1394
1584
|
Create a record selection dialog for a list of records for the user to choose from. Provided field names must
|
|
@@ -1410,15 +1600,24 @@ class CallbackUtil:
|
|
|
1410
1600
|
record IDs are provided, the dialog will automatically allow multi-selection of records.
|
|
1411
1601
|
:param image_data: The bytes to the images that should be displayed in the rows of the table. Each element in
|
|
1412
1602
|
the image data list corresponds to the element at the same index in the records list.
|
|
1413
|
-
:param
|
|
1414
|
-
|
|
1415
|
-
:param
|
|
1416
|
-
|
|
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.
|
|
1417
1613
|
:return: A list of the selected records.
|
|
1418
1614
|
"""
|
|
1419
1615
|
# Get the data type name and field values from the provided records.
|
|
1420
1616
|
if not records:
|
|
1421
1617
|
raise SapioException("No records provided.")
|
|
1618
|
+
records = list(records)
|
|
1619
|
+
if len(records) == 1 and shortcut_single_option:
|
|
1620
|
+
return [records[0]]
|
|
1422
1621
|
data_type: str = AliasUtil.to_singular_data_type_name(records)
|
|
1423
1622
|
field_map_list: list[FieldMap] = AliasUtil.to_field_map_list(records, include_record_id=True)
|
|
1424
1623
|
|
|
@@ -1445,14 +1644,17 @@ class CallbackUtil:
|
|
|
1445
1644
|
|
|
1446
1645
|
# Send the request to the user.
|
|
1447
1646
|
request = TempTableSelectionRequest(temp_dt, msg, field_map_list, image_data, preselected_records, multi_select)
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
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)
|
|
1456
1658
|
|
|
1457
1659
|
# Map the field maps in the response back to the record they come from, returning the chosen record instead of
|
|
1458
1660
|
# the chosen field map.
|
|
@@ -1463,6 +1665,7 @@ class CallbackUtil:
|
|
|
1463
1665
|
return ret_list
|
|
1464
1666
|
|
|
1465
1667
|
# 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.
|
|
1466
1669
|
def input_selection_dialog(self,
|
|
1467
1670
|
wrapper_type: type[WrappedType] | str,
|
|
1468
1671
|
msg: str,
|
|
@@ -1477,8 +1680,10 @@ class CallbackUtil:
|
|
|
1477
1680
|
allow_creation: bool = False,
|
|
1478
1681
|
default_creation_number: int = 1,
|
|
1479
1682
|
*,
|
|
1480
|
-
require_selection
|
|
1481
|
-
|
|
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.") \
|
|
1482
1687
|
-> list[WrappedType] | list[PyRecordModel]:
|
|
1483
1688
|
"""
|
|
1484
1689
|
Display a table of records that exist in the system matching the given data type and filter criteria and have
|
|
@@ -1520,10 +1725,14 @@ class CallbackUtil:
|
|
|
1520
1725
|
than 1, then multi-selection must be true. The data type definition of the records being created must have
|
|
1521
1726
|
"Prompt for Number to Add" set to true in order to allow the user to select how many records to create, as
|
|
1522
1727
|
otherwise user will only ever be able to create one record at a time.
|
|
1523
|
-
:param require_selection:
|
|
1524
|
-
|
|
1525
|
-
:param
|
|
1526
|
-
|
|
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.
|
|
1527
1736
|
:return: A list of the records selected by the user in the dialog, wrapped as record models using the provided
|
|
1528
1737
|
wrapper.
|
|
1529
1738
|
"""
|
|
@@ -1549,23 +1758,25 @@ class CallbackUtil:
|
|
|
1549
1758
|
request = InputSelectionRequest(data_type, msg, search_types, only_key_fields, record_blacklist,
|
|
1550
1759
|
record_whitelist, preselected_records, custom_search, scan_criteria,
|
|
1551
1760
|
multi_select, allow_creation, default_creation_number)
|
|
1552
|
-
# If require_selection is true, repeat the request if the user didn't make a selection.
|
|
1553
|
-
while True:
|
|
1554
|
-
response: list[DataRecord] = self.__handle_dialog_request(request,
|
|
1555
|
-
self.callback.show_input_selection_dialog)
|
|
1556
|
-
if not require_selection or response:
|
|
1557
|
-
break
|
|
1558
|
-
if repeat_message:
|
|
1559
|
-
self.toaster_popup(repeat_message, popup_type=PopupType.Warning)
|
|
1560
|
-
return self.rec_handler.wrap_models(response, wrapper_type)
|
|
1561
1761
|
|
|
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
|
|
1562
1773
|
def esign_dialog(self,
|
|
1563
1774
|
title: str,
|
|
1564
1775
|
msg: str,
|
|
1565
1776
|
show_comment: bool = True,
|
|
1566
1777
|
additional_fields: Iterable[AbstractVeloxFieldDefinition] | None = None,
|
|
1567
1778
|
*,
|
|
1568
|
-
require_authentication
|
|
1779
|
+
require_authentication = None) -> ESigningResponsePojo:
|
|
1569
1780
|
"""
|
|
1570
1781
|
Create an e-sign dialog for the user to interact with.
|
|
1571
1782
|
|
|
@@ -1575,8 +1786,7 @@ class CallbackUtil:
|
|
|
1575
1786
|
user is required to provide an action.
|
|
1576
1787
|
:param additional_fields: Field definitions for additional fields to display in the dialog, for if there is
|
|
1577
1788
|
other information you wish to gather from the user alongside the e-sign.
|
|
1578
|
-
:param require_authentication:
|
|
1579
|
-
credentials.
|
|
1789
|
+
:param require_authentication: DEPRECATED. Authentication is always required when using this function.
|
|
1580
1790
|
:return: An e-sign response object containing information about the e-sign attempt.
|
|
1581
1791
|
"""
|
|
1582
1792
|
# Construct a temporary data type if any additional fields are provided.
|
|
@@ -1590,10 +1800,9 @@ class CallbackUtil:
|
|
|
1590
1800
|
# Send the request to the user.
|
|
1591
1801
|
request = ESigningRequestPojo(title, msg, show_comment, temp_dt,
|
|
1592
1802
|
width_in_pixels=self.width_pixels, width_percentage=self.width_percent)
|
|
1593
|
-
# If require_authentication is true, repeat the request if the user didn't provide valid credentials.
|
|
1594
1803
|
while True:
|
|
1595
|
-
response: ESigningResponsePojo = self.
|
|
1596
|
-
if
|
|
1804
|
+
response: ESigningResponsePojo = self.__send_dialog(request, self.callback.show_esign_dialog)
|
|
1805
|
+
if response.authenticated:
|
|
1597
1806
|
break
|
|
1598
1807
|
# This matches the OOB behavior.
|
|
1599
1808
|
self.toaster_popup("Incorrect username/password", popup_type=PopupType.Error)
|
|
@@ -1629,7 +1838,7 @@ class CallbackUtil:
|
|
|
1629
1838
|
|
|
1630
1839
|
# Send the request to the user.
|
|
1631
1840
|
request = FilePromptRequest(title, show_image_editor, ",".join(exts), show_camera_button)
|
|
1632
|
-
file_path: str = self.
|
|
1841
|
+
file_path: str = self.__send_dialog(request, self.callback.show_file_dialog, data_sink=do_consume)
|
|
1633
1842
|
|
|
1634
1843
|
# Verify that each of the file given matches the expected extension(s).
|
|
1635
1844
|
self.__verify_file(file_path, sink.data, exts)
|
|
@@ -1654,7 +1863,7 @@ class CallbackUtil:
|
|
|
1654
1863
|
|
|
1655
1864
|
# Send the request to the user.
|
|
1656
1865
|
request = MultiFilePromptRequest(title, show_image_editor, ",".join(exts), show_camera_button)
|
|
1657
|
-
file_paths: list[str] = self.
|
|
1866
|
+
file_paths: list[str] = self.__send_dialog(request, self.callback.show_multi_file_dialog)
|
|
1658
1867
|
|
|
1659
1868
|
# Verify that each of the files given match the expected extension(s).
|
|
1660
1869
|
ret_dict: dict[str, bytes] = {}
|
|
@@ -1681,7 +1890,8 @@ class CallbackUtil:
|
|
|
1681
1890
|
if allowed_extensions:
|
|
1682
1891
|
matches: bool = False
|
|
1683
1892
|
for ext in allowed_extensions:
|
|
1684
|
-
|
|
1893
|
+
# FR-47690: Changed to a case-insensitive match.
|
|
1894
|
+
if file_path.casefold().endswith("." + ext.lstrip(".").casefold()):
|
|
1685
1895
|
matches = True
|
|
1686
1896
|
break
|
|
1687
1897
|
if matches is False:
|
|
@@ -1705,8 +1915,7 @@ class CallbackUtil:
|
|
|
1705
1915
|
:param zip_name: The name of the zip file.
|
|
1706
1916
|
:param files: A dictionary of the files to add to the zip file.
|
|
1707
1917
|
"""
|
|
1708
|
-
|
|
1709
|
-
self.callback.send_file(zip_name, False, data)
|
|
1918
|
+
self.write_file(zip_name, FileUtil.zip_files(files))
|
|
1710
1919
|
|
|
1711
1920
|
@staticmethod
|
|
1712
1921
|
def __get_indexed_field_maps(records: Iterable[SapioRecord], index_field: str) -> list[FieldMap]:
|
|
@@ -1736,6 +1945,8 @@ class CallbackUtil:
|
|
|
1736
1945
|
"""
|
|
1737
1946
|
Construct a Temporary Data Type definition from a provided list of field definitions for use in a callback.
|
|
1738
1947
|
"""
|
|
1948
|
+
if not fields:
|
|
1949
|
+
raise SapioException("No fields provided to create a temporary data type.")
|
|
1739
1950
|
# Get the data type name as a string from the parameters, and set the display name and plural display name if
|
|
1740
1951
|
# they haven't been set.
|
|
1741
1952
|
data_type: str = AliasUtil.to_data_type_name(data_type)
|
|
@@ -1855,8 +2066,10 @@ class CallbackUtil:
|
|
|
1855
2066
|
temp_dt.set_field_definition(modifier.modify_field(field_def))
|
|
1856
2067
|
return temp_dt
|
|
1857
2068
|
|
|
2069
|
+
# FR-47690: Updated with blank result handling behavior.
|
|
1858
2070
|
def __prompt_for_count(self, count: tuple[int, int] | int, wrapper_type: type[WrappedType] | str,
|
|
1859
|
-
require_input: bool, repeat_message: str
|
|
2071
|
+
require_input: bool, blank_result_handling: BlankResultHandling, repeat_message: str,
|
|
2072
|
+
cancel_message: str) -> int:
|
|
1860
2073
|
"""
|
|
1861
2074
|
Given a count value, if it is a tuple representing an allowable range of values for a number of records to
|
|
1862
2075
|
create, prompt the user to input the exact count to use. If the count is already a single integer, simply
|
|
@@ -1869,8 +2082,14 @@ class CallbackUtil:
|
|
|
1869
2082
|
plural: str = self.dt_cache.get_plural_display_name(AliasUtil.to_data_type_name(wrapper_type))
|
|
1870
2083
|
min_val, max_val = count
|
|
1871
2084
|
msg: str = f"How many {plural} should be created? ({min_val} to {max_val})"
|
|
1872
|
-
|
|
1873
|
-
|
|
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
|
|
1874
2093
|
return count
|
|
1875
2094
|
|
|
1876
2095
|
def __to_layout(self, data_type: str, layout: DataTypeLayoutIdentifier) -> DataTypeLayout | None:
|
|
@@ -1935,19 +2154,18 @@ class CallbackUtil:
|
|
|
1935
2154
|
raise SapioException(f"No field of name \"{field_name}\" in field definitions of type \"{data_type}\"")
|
|
1936
2155
|
return field_def
|
|
1937
2156
|
|
|
1938
|
-
def
|
|
2157
|
+
def __handle_timeout(self, func: Callable, request: Any, **kwargs) -> Any:
|
|
1939
2158
|
"""
|
|
1940
2159
|
Send a client callback request to the user that creates a dialog.
|
|
1941
2160
|
|
|
1942
2161
|
This function handles updating the user object's request timeout to match the request timeout of this
|
|
1943
2162
|
CallbackUtil for the duration of the dialog.
|
|
1944
2163
|
If the dialog times out then a SapioDialogTimeoutException is thrown.
|
|
1945
|
-
If the user cancels the dialog then a SapioUserCancelledException is thrown.
|
|
1946
2164
|
|
|
1947
2165
|
:param request: The client callback request to send to the user.
|
|
1948
2166
|
:param func: The ClientCallback function to call with the given request as input.
|
|
1949
2167
|
:param kwargs: Additional keywords for the provided function call.
|
|
1950
|
-
:return: The response from the client callback
|
|
2168
|
+
:return: The response from the client callback.
|
|
1951
2169
|
"""
|
|
1952
2170
|
try:
|
|
1953
2171
|
self.user.timeout_seconds = self.timeout_seconds
|
|
@@ -1956,10 +2174,73 @@ class CallbackUtil:
|
|
|
1956
2174
|
raise SapioDialogTimeoutException()
|
|
1957
2175
|
finally:
|
|
1958
2176
|
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)
|
|
1959
2194
|
if response is None:
|
|
1960
2195
|
raise SapioUserCancelledException()
|
|
1961
2196
|
return response
|
|
1962
2197
|
|
|
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
|
+
|
|
1963
2244
|
|
|
1964
2245
|
class FieldModifier:
|
|
1965
2246
|
"""
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: sapiopycommons
|
|
3
|
-
Version: 2025.6.
|
|
3
|
+
Version: 2025.6.16a562
|
|
4
4
|
Summary: Official Sapio Python API Utilities Package
|
|
5
5
|
Project-URL: Homepage, https://github.com/sapiosciences
|
|
6
6
|
Author-email: Jonathan Steck <jsteck@sapiosciences.com>, Yechen Qiao <yqiao@sapiosciences.com>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
sapiopycommons/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
2
|
sapiopycommons/callbacks/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
-
sapiopycommons/callbacks/callback_util.py,sha256=
|
|
3
|
+
sapiopycommons/callbacks/callback_util.py,sha256=rps6RA6lmzCOwiBqPQAe2Mkf0CIF4RjHPQTYgduMAgE,153011
|
|
4
4
|
sapiopycommons/callbacks/field_builder.py,sha256=rnIP-RJafk3mZlAx1eJ8a0eSW9Ps_L6_WadCmusnENw,38772
|
|
5
5
|
sapiopycommons/chem/IndigoMolecules.py,sha256=7ucCaRMLu1zfH2uPIvXwRTSdpNcS03O1P9p_O-5B4xQ,5110
|
|
6
6
|
sapiopycommons/chem/Molecules.py,sha256=mVqPn32MPMjF0iZas-5MFkS-upIdoW5OB72KKZmJRJA,12523
|
|
@@ -62,7 +62,7 @@ sapiopycommons/webhook/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3
|
|
|
62
62
|
sapiopycommons/webhook/webhook_context.py,sha256=D793uLsb1691SalaPnBUk3rOSxn_hYLhdvkaIxjNXss,1909
|
|
63
63
|
sapiopycommons/webhook/webhook_handlers.py,sha256=tUVNCw05CDGu1gFDm2g558hX_O203WVm_n__ojjoRRM,39841
|
|
64
64
|
sapiopycommons/webhook/webservice_handlers.py,sha256=tyaYGG1-v_JJrJHZ6cy5mGCxX9z1foLw7pM4MDJlFxs,14297
|
|
65
|
-
sapiopycommons-2025.6.
|
|
66
|
-
sapiopycommons-2025.6.
|
|
67
|
-
sapiopycommons-2025.6.
|
|
68
|
-
sapiopycommons-2025.6.
|
|
65
|
+
sapiopycommons-2025.6.16a562.dist-info/METADATA,sha256=uPyB0-P-hjRawjFs78zjP8QjsY1kjpGhU6zsDcx7tBY,3143
|
|
66
|
+
sapiopycommons-2025.6.16a562.dist-info/WHEEL,sha256=C2FUgwZgiLbznR-k0b_5k3Ai_1aASOXDss3lzCUsUug,87
|
|
67
|
+
sapiopycommons-2025.6.16a562.dist-info/licenses/LICENSE,sha256=HyVuytGSiAUQ6ErWBHTqt1iSGHhLmlC8fO7jTCuR8dU,16725
|
|
68
|
+
sapiopycommons-2025.6.16a562.dist-info/RECORD,,
|
|
File without changes
|
{sapiopycommons-2025.6.11a561.dist-info → sapiopycommons-2025.6.16a562.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|