ansys-solutions-dash-super-components 0.2.dev7__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.
Files changed (26) hide show
  1. ansys/solutions/dash_super_components/__init__.py +44 -0
  2. ansys/solutions/dash_super_components/assets/__init__.py +20 -0
  3. ansys/solutions/dash_super_components/assets/dashAgGridComponentFunctions.js +71 -0
  4. ansys/solutions/dash_super_components/authenticator.py +277 -0
  5. ansys/solutions/dash_super_components/dual_input_range_slider.py +397 -0
  6. ansys/solutions/dash_super_components/folder_selector.py +830 -0
  7. ansys/solutions/dash_super_components/input_form.py +908 -0
  8. ansys/solutions/dash_super_components/input_row_array.py +368 -0
  9. ansys/solutions/dash_super_components/logs_supervisor.py +887 -0
  10. ansys/solutions/dash_super_components/transaction_method_status_badge.py +366 -0
  11. ansys/solutions/dash_super_components/transaction_supervisor.py +587 -0
  12. ansys/solutions/dash_super_components/tree.py +396 -0
  13. ansys/solutions/dash_super_components/utils/__init__.py +18 -0
  14. ansys/solutions/dash_super_components/utils/aio_ids.py +83 -0
  15. ansys/solutions/dash_super_components/utils/api_requests.py +69 -0
  16. ansys/solutions/dash_super_components/utils/assets_server.py +52 -0
  17. ansys/solutions/dash_super_components/utils/config.py +77 -0
  18. ansys/solutions/dash_super_components/utils/logs_parser.py +151 -0
  19. ansys/solutions/dash_super_components/utils/path_validator.py +163 -0
  20. ansys/solutions/dash_super_components/utils/status_badge_properties.py +60 -0
  21. ansys/solutions/dash_super_components/utils/svg_icons.py +395 -0
  22. ansys_solutions_dash_super_components-0.2.dev7.dist-info/METADATA +169 -0
  23. ansys_solutions_dash_super_components-0.2.dev7.dist-info/RECORD +26 -0
  24. ansys_solutions_dash_super_components-0.2.dev7.dist-info/WHEEL +4 -0
  25. ansys_solutions_dash_super_components-0.2.dev7.dist-info/licenses/AUTHORS +12 -0
  26. ansys_solutions_dash_super_components-0.2.dev7.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,908 @@
1
+ # Copyright (C) 2026 ANSYS, Inc. and/or its affiliates.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ #
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+
18
+ """Provide a Dash component for quickly crafting input forms."""
19
+
20
+ from typing import Any
21
+ import uuid
22
+
23
+ try:
24
+ # dash >=3.2.0
25
+ from dash import NoUpdate
26
+ except ImportError:
27
+ # dash >=2.18.2, <3.2.0
28
+ from dash._callback import NoUpdate # pyright: ignore[reportPrivateImportUsage]
29
+ from dash.exceptions import PreventUpdate
30
+
31
+ try:
32
+ # dash-extensions < 2.0.5
33
+ from dash_extensions.enrich import _Wildcard # pyright: ignore[reportAttributeAccessIssue]
34
+ except ImportError:
35
+ # dash-extensions >= 2.0.5
36
+ from dash_extensions.enrich import (
37
+ Wildcard as _Wildcard, # pyright: ignore[reportAttributeAccessIssue]
38
+ )
39
+ from dash_extensions.enrich import (
40
+ MATCH,
41
+ Input,
42
+ Output,
43
+ State,
44
+ callback,
45
+ dcc,
46
+ html,
47
+ no_update,
48
+ )
49
+ import dash_mantine_components as dmc
50
+
51
+ from ansys.solutions.dash_super_components.utils.aio_ids import AIOIds
52
+ from ansys.solutions.dash_super_components.utils.svg_icons import IconNames, create_base64_svg_src
53
+
54
+ DEFAULT_COLUMNS_AND_WIDTHS = {
55
+ "label": 2,
56
+ "fields": 5,
57
+ "unit": 1,
58
+ "description": 3,
59
+ "help": 1,
60
+ }
61
+
62
+ COLUMN_STYLES = {
63
+ "label": {
64
+ "display": "flex",
65
+ "alignItems": "center",
66
+ },
67
+ "unit": {
68
+ "display": "flex",
69
+ "alignItems": "center",
70
+ "justifyContent": "center",
71
+ },
72
+ "description": {
73
+ "display": "flex",
74
+ "alignItems": "center",
75
+ },
76
+ "help": {
77
+ "display": "flex",
78
+ "alignItems": "center",
79
+ "justifyContent": "center",
80
+ },
81
+ "fields": {},
82
+ }
83
+
84
+ SUPPORTED_FIELD_TYPES = [
85
+ "NumberInput",
86
+ "TextInput",
87
+ "PasswordInput",
88
+ "Checkbox",
89
+ "Select",
90
+ "MultiSelect",
91
+ "Switch",
92
+ "Slider",
93
+ ]
94
+
95
+
96
+ def make_form(
97
+ aio_id: str,
98
+ items: list[dict[str, Any]],
99
+ columns: list[str],
100
+ column_names: list[str],
101
+ custom_column_widths: dict[str, int],
102
+ persistence: bool,
103
+ persistence_type: str,
104
+ ) -> html.Div:
105
+ """Create the form layout for an :class:`InputForm` component.
106
+
107
+ Parameters
108
+ ----------
109
+ aio_id : str
110
+ The unique identifier for the parent ``InputForm`` instance.
111
+ items : list of dict
112
+ Row definitions passed to the ``InputForm``.
113
+ columns : list of str
114
+ Ordered list of column names to include in each row.
115
+ column_names : list of str
116
+ Header labels for each column; pass an empty list to omit headers.
117
+ custom_column_widths : dict[str, int]
118
+ Mapping of column names to custom widths. Columns not present use
119
+ the default widths from :data:`DEFAULT_COLUMNS_AND_WIDTHS`.
120
+ persistence : bool
121
+ Whether to enable persistence for the form fields.
122
+ persistence_type : str
123
+ The type of persistence to use.
124
+
125
+ Returns
126
+ -------
127
+ html.Div
128
+ A ``Div`` containing the assembled grid and any help modals.
129
+ """
130
+ column_widths = compute_final_column_widths(columns, custom_column_widths)
131
+ total_column_width = sum(column_widths.values())
132
+
133
+ cells: list[dmc.GridCol] = []
134
+ modals: list[dmc.Modal] = []
135
+
136
+ # add header row if column names are provided
137
+ if column_names:
138
+ cells.extend(_create_header_cells(columns, column_names, column_widths))
139
+
140
+ for row_counter, item in enumerate(items):
141
+ # if the row has an explicit ID, use it as row_index, otherwise use the row_counter:
142
+ row_index = item.get("id", row_counter)
143
+ row_cells, modal = make_row(
144
+ aio_id,
145
+ item,
146
+ columns,
147
+ column_widths,
148
+ row_index,
149
+ persistence,
150
+ persistence_type,
151
+ )
152
+ cells.extend(row_cells)
153
+ modals.append(modal)
154
+
155
+ grid = dmc.Grid(cells, gutter="xs", grow=False, columns=total_column_width)
156
+
157
+ content = [grid, *modals]
158
+
159
+ return html.Div(content)
160
+
161
+
162
+ def _create_header_cells(
163
+ columns: list[str],
164
+ column_names: list[str],
165
+ column_widths: dict[str, int],
166
+ ) -> list[dmc.GridCol]:
167
+ _validate_column_names(column_names, columns)
168
+
169
+ header_cells: list[dmc.GridCol] = []
170
+ for column, column_name in zip(columns, column_names, strict=False):
171
+ header_cells.append(
172
+ dmc.GridCol(
173
+ dmc.Text(column_name, fw=700), # type: ignore - Dash Mantine Components typing issue
174
+ span=column_widths[column],
175
+ ),
176
+ )
177
+
178
+ return header_cells
179
+
180
+
181
+ def make_row(
182
+ aio_id: str,
183
+ item: dict[str, Any],
184
+ columns: list[str],
185
+ column_widths: dict[str, int],
186
+ row_index: int | str,
187
+ persistence: bool,
188
+ persistence_type: str,
189
+ ) -> tuple[list[dmc.GridCol], dmc.Modal]:
190
+ """Create the grid cells and help modal for a single form row.
191
+
192
+ Parameters
193
+ ----------
194
+ aio_id : str
195
+ The unique identifier for the parent ``InputForm`` instance.
196
+ item : dict
197
+ Row definition dictionary with optional keys: ``label``, ``unit``,
198
+ ``description``, ``help``, and required key ``fields``.
199
+ columns : list of str
200
+ Ordered list of column names to render for this row.
201
+ column_widths : dict[str, int]
202
+ Mapping of column names to grid span widths.
203
+ row_index : int or str
204
+ Index used to generate unique subcomponent IDs for this row.
205
+ persistence : bool
206
+ Whether to enable persistence for the form fields in this row.
207
+ persistence_type : str
208
+ The type of persistence to use for the form fields in this row.
209
+
210
+ Returns
211
+ -------
212
+ tuple[list of dmc.GridCol, dmc.Modal]
213
+ A tuple of ``(cells, modal)`` where *cells* is the list of grid
214
+ columns for the row and *modal* is the associated help modal.
215
+ """
216
+ cells: list[dmc.GridCol] = []
217
+ for column in columns:
218
+ if column == "label":
219
+ column_content = _create_label_column(aio_id, item, row_index)
220
+ elif column == "unit":
221
+ column_content = _create_unit_column(aio_id, item, row_index)
222
+ elif column == "description":
223
+ column_content = _create_description_column(aio_id, item, row_index)
224
+ elif column == "help":
225
+ column_content = _create_help_column(aio_id, item, row_index)
226
+ elif column == "fields":
227
+ column_content = _create_input_fields_column(
228
+ aio_id, item, row_index, persistence, persistence_type
229
+ )
230
+ else:
231
+ raise ValueError(f"Unknown column: {column}")
232
+
233
+ cells.append(
234
+ dmc.GridCol(
235
+ column_content,
236
+ span=column_widths[column],
237
+ style=COLUMN_STYLES[column],
238
+ ),
239
+ )
240
+
241
+ modal = _create_modal(aio_id, item, row_index)
242
+
243
+ return cells, modal
244
+
245
+
246
+ def _create_label_column(
247
+ aio_id: str,
248
+ item: dict[str, Any],
249
+ row_index: str | int,
250
+ ) -> dmc.Text:
251
+ return dmc.Text(item.get("label"), id=InputForm.ids.label(aio_id, row_index))
252
+
253
+
254
+ def _create_unit_column(
255
+ aio_id: str,
256
+ item: dict[str, Any],
257
+ row_index: str | int,
258
+ ) -> dmc.Text:
259
+ return dmc.Text(
260
+ (
261
+ dcc.Markdown(
262
+ f"${item['unit']}$",
263
+ mathjax=True,
264
+ )
265
+ if "unit" in item
266
+ else None
267
+ ),
268
+ id=InputForm.ids.unit(aio_id, row_index),
269
+ style={
270
+ "display": "flex",
271
+ "justifyContent": "center",
272
+ "alignItems": "center",
273
+ },
274
+ )
275
+
276
+
277
+ def _create_description_column(
278
+ aio_id: str,
279
+ item: dict[str, Any],
280
+ row_index: str | int,
281
+ ) -> dmc.Text:
282
+ return dmc.Text(item.get("description"), id=InputForm.ids.description(aio_id, row_index))
283
+
284
+
285
+ def _create_help_column(
286
+ aio_id: str,
287
+ item: dict[str, Any],
288
+ row_index: int | str,
289
+ ) -> dmc.ActionIcon:
290
+ return dmc.ActionIcon(
291
+ html.Img(src=create_base64_svg_src(IconNames.MATERIAL_HELP, color="#fab005")),
292
+ size="xl",
293
+ variant="subtle",
294
+ color="yellow",
295
+ id=InputForm.ids.help(aio_id, row_index),
296
+ style={
297
+ "display": "flex" if item.get("help") else "none",
298
+ "textAlign": "center",
299
+ },
300
+ )
301
+
302
+
303
+ def _create_modal(aio_id: str, item: dict[str, Any], row_index: int | str) -> dmc.Modal:
304
+ return dmc.Modal(
305
+ id=InputForm.ids.modal(aio_id, row_index),
306
+ title=item.get("label"),
307
+ zIndex=10000,
308
+ children=item.get("help"),
309
+ size="50%", # type: ignore - Dash Mantine Components typing issue
310
+ centered=True,
311
+ )
312
+
313
+
314
+ def _create_input_fields_column(
315
+ aio_id: str,
316
+ item: dict[str, Any],
317
+ row_index: int | str,
318
+ persistence: bool,
319
+ persistence_type: str,
320
+ ) -> html.Div:
321
+ input_fields: list[html.Div] = []
322
+
323
+ for field in item["fields"]:
324
+ field_type = field["type"]
325
+ identifier = field["id"]
326
+ div_id = InputForm.ids.field_div(aio_id, field_type, identifier, row_index)
327
+ field_id = InputForm.ids.field(aio_id, field_type, identifier, row_index)
328
+
329
+ div_style = {"width": "100%"}
330
+ if "hidden" in field and field["hidden"]:
331
+ div_style["display"] = "none"
332
+
333
+ field_properties = {k: v for k, v in field.items() if k not in ["hidden", "type", "id"]}
334
+
335
+ if "persistence" not in field_properties:
336
+ field_properties["persistence"] = persistence
337
+ if "persistence_type" not in field_properties:
338
+ field_properties["persistence_type"] = persistence_type
339
+
340
+ if "style" in field_properties:
341
+ if "width" not in field_properties["style"]:
342
+ field_properties["style"]["width"] = "100%"
343
+ else:
344
+ field_properties["style"] = {"width": "100%"}
345
+
346
+ input_fields.append(
347
+ _create_field(field_id, field_type, field_properties, div_id, div_style)
348
+ )
349
+
350
+ return html.Div(
351
+ input_fields,
352
+ style={
353
+ "display": "flex",
354
+ "flexDirection": "row",
355
+ "justifyContent": "space-between",
356
+ "alignItems": "center",
357
+ "gap": "10px",
358
+ },
359
+ )
360
+
361
+
362
+ def _create_field(
363
+ field_id: dict[str, Any],
364
+ field_type: str,
365
+ field_properties: dict[str, Any],
366
+ div_id: dict[str, Any],
367
+ div_style: dict[str, Any],
368
+ ) -> html.Div:
369
+ if field_type == "NumberInput":
370
+ field_content = dmc.NumberInput(id=field_id, **field_properties)
371
+ elif field_type == "TextInput":
372
+ field_content = dmc.TextInput(id=field_id, **field_properties)
373
+ elif field_type == "Checkbox":
374
+ field_content = dmc.Checkbox(id=field_id, **field_properties)
375
+ elif field_type == "Select":
376
+ field_content = dmc.Select(id=field_id, **field_properties)
377
+ elif field_type == "MultiSelect":
378
+ field_content = dmc.MultiSelect(id=field_id, **field_properties)
379
+ elif field_type == "PasswordInput":
380
+ field_content = dmc.PasswordInput(id=field_id, **field_properties)
381
+ elif field_type == "Slider":
382
+ field_content = dmc.Slider(id=field_id, **field_properties)
383
+ elif field_type == "Switch":
384
+ field_content = dmc.Switch(id=field_id, **field_properties)
385
+ else:
386
+ raise ValueError(f"Unsupported field type: {field_type}")
387
+
388
+ return html.Div(
389
+ id=div_id,
390
+ children=field_content,
391
+ style=div_style,
392
+ )
393
+
394
+
395
+ class InputForm(html.Div):
396
+ """
397
+ All-in-one component based on Dash Mantine Components to create input forms.
398
+
399
+ Each row of the form is defined by a dictionary with the following keys:
400
+
401
+ - ``fields`` *(required)*: list of field dicts. Each field dict must contain
402
+ ``"type"`` and ``"id"`` keys plus any additional properties accepted by the
403
+ underlying Dash Mantine component. Supported types: ``NumberInput``,
404
+ ``TextInput``, ``PasswordInput``, ``Checkbox``, ``Select``, ``MultiSelect``,
405
+ ``Slider``, ``Switch``.
406
+ - ``label`` *(optional)*: str — label text shown in the label column.
407
+ - ``unit`` *(optional)*: str — LaTeX-rendered unit shown in the unit column.
408
+ - ``description`` *(optional)*: str — description text shown in the description column.
409
+ - ``help`` *(optional)*: Dash component(s), str, or number — content shown in a
410
+ modal when the help icon is clicked.
411
+ - ``id`` *(optional)*: str — explicit row identifier used as the ``row_index`` in
412
+ component IDs. When omitted, the zero-based row position is used instead.
413
+
414
+ UI-side value persistence is disabled by default. It can be enabled globally for
415
+ all fields via the ``persistence`` and ``persistence_type`` constructor parameters.
416
+ Individual fields can override the global setting by including a ``"persistence"``
417
+ key in their field dict.
418
+
419
+ Parameters
420
+ ----------
421
+ items : list of dict
422
+ A list of row definitions. Each dict must contain a ``"fields"`` key whose
423
+ value is a list of field dicts (each requiring ``"type"`` and ``"id"``).
424
+ Optional row keys: ``"label"``, ``"unit"``, ``"description"``, ``"help"``,
425
+ ``"id"``. Fields also accept a ``"hidden": True`` key to hide the field on
426
+ initialization. To override the global persistence setting for an individual
427
+ field, pass ``"persistence": True`` or ``"persistence": False`` in the field
428
+ dict.
429
+ title : str, optional
430
+ Title displayed above the form.
431
+ with_card : bool, optional
432
+ Whether to wrap the form in a :class:`dmc.Card`. Default is ``True``.
433
+ card_props : dict, optional
434
+ Additional properties forwarded to :class:`dmc.Card`.
435
+ title_props : dict, optional
436
+ Additional properties forwarded to the title :class:`html.P` element.
437
+ column_names : list of str, optional
438
+ Header labels for each column, in the same order as ``columns``. Pass an
439
+ empty list (default) to omit column headers.
440
+ column_widths : dict, optional
441
+ Mapping of column names to integer grid-span widths. Columns omitted here
442
+ use built-in defaults.
443
+ aio_id : str, optional
444
+ Unique identifier for this component instance. A UUID is generated when
445
+ not provided.
446
+ columns : list of str, optional
447
+ Ordered list of column names to display. Supported values: ``"label"``,
448
+ ``"fields"``, ``"unit"``, ``"description"``, ``"help"``. Defaults to all
449
+ five columns in the order listed.
450
+ persistence : bool, optional
451
+ Whether to enable UI-side persistence for all form fields. When ``True``,
452
+ user-entered values survive page navigation within the same browser tab.
453
+ Individual fields can override this setting via ``"persistence"`` in the
454
+ field dict. Default is ``False``.
455
+ persistence_type : str, optional
456
+ The Dash persistence storage type to use when ``persistence`` is ``True``.
457
+ Accepted values: ``"memory"`` (cleared on page reload), ``"session"``
458
+ (cleared when the browser tab is closed), ``"local"`` (persists across
459
+ reloads and browser restarts). Default is ``"memory"``.
460
+ """
461
+
462
+ class InputFormIds(AIOIds):
463
+ """Provides IDs for the subcomponents of :class:`InputForm`."""
464
+
465
+ @classmethod
466
+ def _make_id_dict_with_row_index(
467
+ cls, subcomponent: str, aio_id: str | _Wildcard, row_index: str | int | _Wildcard
468
+ ) -> dict[str, Any]:
469
+ id_dict = cls.make_id_dict("input-form", subcomponent, aio_id)
470
+ id_dict["row_index"] = row_index
471
+ return id_dict
472
+
473
+ @classmethod
474
+ def _make_id_dict_with_field_type_and_identifier_and_row_index(
475
+ cls,
476
+ subcomponent: str,
477
+ aio_id: str | _Wildcard,
478
+ field_type: str | _Wildcard,
479
+ identifier: str | _Wildcard,
480
+ row_index: str | int | _Wildcard,
481
+ ) -> dict[str, Any]:
482
+ id_dict = cls.make_id_dict("input-form", subcomponent, aio_id)
483
+ id_dict["row_index"] = row_index
484
+ id_dict["field_type"] = field_type
485
+ id_dict["identifier"] = identifier
486
+ return id_dict
487
+
488
+ @classmethod
489
+ def label(cls, aio_id: str | _Wildcard, row_index: str | int | _Wildcard) -> dict[str, Any]:
490
+ """
491
+ Return the id for an input form label.
492
+
493
+ Parameters
494
+ ----------
495
+ aio_id : str or _Wildcard
496
+ The component's unique identifier.
497
+ row_index : str, int, or _Wildcard
498
+ The unique row index of the label (row["id"] or row number if
499
+ row["id"] is not present).
500
+
501
+ Returns
502
+ -------
503
+ dict[str, Any]
504
+ The unique id dictionary for a label subcomponent of the input form
505
+ """
506
+ return cls._make_id_dict_with_row_index("label", aio_id, row_index)
507
+
508
+ @classmethod
509
+ def help(cls, aio_id: str | _Wildcard, row_index: str | int | _Wildcard) -> dict[str, Any]:
510
+ """
511
+ Return the id for an input form help text.
512
+
513
+ Parameters
514
+ ----------
515
+ aio_id : str or _Wildcard
516
+ The component's unique identifier.
517
+ row_index : str, int, or _Wildcard
518
+ The unique row index of the help text (row["id"] or row number if
519
+ row["id"] is not present).
520
+
521
+ Returns
522
+ -------
523
+ dict[str, Any]
524
+ The unique id dictionary for a help subcomponent of the input form
525
+ """
526
+ return cls._make_id_dict_with_row_index("help", aio_id, row_index)
527
+
528
+ @classmethod
529
+ def unit(cls, aio_id: str | _Wildcard, row_index: str | int | _Wildcard) -> dict[str, Any]:
530
+ """
531
+ Return the id for an input form unit.
532
+
533
+ Parameters
534
+ ----------
535
+ aio_id : str or _Wildcard
536
+ The component's unique identifier.
537
+ row_index : str, int, or _Wildcard
538
+ The unique row index of the unit (row["id"] or row number if
539
+ row["id"] is not present).
540
+
541
+ Returns
542
+ -------
543
+ dict[str, Any]
544
+ The unique id dictionary for a unit subcomponent of the input form
545
+ """
546
+ return cls._make_id_dict_with_row_index("unit", aio_id, row_index)
547
+
548
+ @classmethod
549
+ def description(
550
+ cls, aio_id: str | _Wildcard, row_index: str | int | _Wildcard
551
+ ) -> dict[str, Any]:
552
+ """
553
+ Return the id for an input form description.
554
+
555
+ Parameters
556
+ ----------
557
+ aio_id : str or _Wildcard
558
+ The component's unique identifier.
559
+ row_index : str, int, or _Wildcard
560
+ The unique row index of the description (row["id"] or row number if
561
+ row["id"] is not present).
562
+
563
+ Returns
564
+ -------
565
+ dict[str, Any]
566
+ The unique id dictionary for a description subcomponent of the input form
567
+ """
568
+ return cls._make_id_dict_with_row_index("description", aio_id, row_index)
569
+
570
+ @classmethod
571
+ def modal(cls, aio_id: str | _Wildcard, row_index: str | int | _Wildcard) -> dict[str, Any]:
572
+ """
573
+ Return the id for an input form modal.
574
+
575
+ Parameters
576
+ ----------
577
+ aio_id : str or _Wildcard
578
+ The component's unique identifier.
579
+ row_index : str, int, or _Wildcard
580
+ The unique row index of the modal (row["id"] or row number if
581
+ row["id"] is not present).
582
+
583
+ Returns
584
+ -------
585
+ dict[str, Any]
586
+ The unique id dictionary for a modal subcomponent of the input form
587
+ """
588
+ return cls._make_id_dict_with_row_index("modal", aio_id, row_index)
589
+
590
+ @classmethod
591
+ def field(
592
+ cls,
593
+ aio_id: str | _Wildcard,
594
+ field_type: str | _Wildcard,
595
+ field_id: str | _Wildcard,
596
+ row_index: str | int | _Wildcard,
597
+ ) -> dict[str, Any]:
598
+ """
599
+ Return the id for an input form field.
600
+
601
+ Parameters
602
+ ----------
603
+ aio_id : str or _Wildcard
604
+ The component's unique identifier.
605
+ field_type : str, or _Wildcard
606
+ The unique type of the field within the form (field["type"]).
607
+ field_id : str, or _Wildcard
608
+ The unique identifier of the field within the form (field["id"]).
609
+ row_index : str, int, or _Wildcard
610
+ The unique row index of the field within the form (row["id"] or row number if
611
+ row["id"] is not present).
612
+
613
+ Returns
614
+ -------
615
+ dict[str, Any]
616
+ The unique id dictionary for a field subcomponent of the input form
617
+ """
618
+ return cls._make_id_dict_with_field_type_and_identifier_and_row_index(
619
+ "field", aio_id, field_type, field_id, row_index
620
+ )
621
+
622
+ @classmethod
623
+ def field_div(
624
+ cls,
625
+ aio_id: str | _Wildcard,
626
+ field_type: str | _Wildcard,
627
+ field_id: str | _Wildcard,
628
+ row_index: str | int | _Wildcard,
629
+ ) -> dict[str, Any]:
630
+ """
631
+ Return the id for an input form field div.
632
+
633
+ Parameters
634
+ ----------
635
+ aio_id : str or _Wildcard
636
+ The component's unique identifier.
637
+ field_type : str, or _Wildcard
638
+ The unique type of the field within the form (field["type"]).
639
+ field_id : str, or _Wildcard
640
+ The unique identifier of the field within the form (field["id"]).
641
+ row_index : str, int, or _Wildcard
642
+ The unique row index of the field within the form (row["id"] or row number if
643
+ row["id"] is not present).
644
+
645
+ Returns
646
+ -------
647
+ dict[str, Any]
648
+ The unique id dictionary for a field div subcomponent of the input form
649
+ """
650
+ return cls._make_id_dict_with_field_type_and_identifier_and_row_index(
651
+ "field-div", aio_id, field_type, field_id, row_index
652
+ )
653
+
654
+ ids = InputFormIds
655
+
656
+ def __init__(
657
+ self,
658
+ items: list[dict[str, Any]],
659
+ title: str | None = None,
660
+ with_card: bool = True,
661
+ card_props: dict[str, Any] | None = None,
662
+ title_props: dict[str, Any] | None = None,
663
+ column_names: list[str] | None = None,
664
+ column_widths: dict[str, int] | None = None,
665
+ aio_id: str | None = None,
666
+ columns: list[str] | None = None,
667
+ persistence: bool = False,
668
+ persistence_type: str | None = None,
669
+ ):
670
+ # Set default properties
671
+ if card_props is None:
672
+ card_props = {}
673
+ if title_props is None:
674
+ title_props = {}
675
+ if column_names is None:
676
+ column_names = []
677
+ if column_widths is None:
678
+ column_widths = {}
679
+ if aio_id is None:
680
+ aio_id = str(uuid.uuid4())
681
+ if columns is None:
682
+ columns = list(DEFAULT_COLUMNS_AND_WIDTHS.keys())
683
+ if persistence_type is None:
684
+ persistence_type = "memory"
685
+
686
+ self.aio_id = aio_id
687
+ self.items = items # Store reference as-is (items are not mutated in this class)
688
+ self.title = title
689
+ self.column_widths = column_widths # Store reference as-is (not mutated)
690
+ self.columns = columns # Store reference as-is (not mutated)
691
+ self.column_names = column_names # Store reference as-is (not mutated)
692
+ self.persistence = persistence
693
+ self.persistence_type = persistence_type
694
+
695
+ default_card_props = {
696
+ "withBorder": True,
697
+ "shadow": "sm",
698
+ "radius": "md",
699
+ }
700
+ card_props = self._populate_with_defaults(card_props, default_card_props)
701
+
702
+ default_title_props = {
703
+ "style": {"fontSize": "18px", "fontWeight": "bold"},
704
+ }
705
+ title_props = self._populate_with_defaults(title_props, default_title_props)
706
+
707
+ _validate_columns(columns)
708
+ _validate_items_definition(self.items)
709
+
710
+ layout = self._make_layout(title, with_card, card_props, title_props)
711
+
712
+ super().__init__(
713
+ layout,
714
+ )
715
+
716
+ def _populate_with_defaults(
717
+ self, properties_input: dict[str, Any], default_properties: dict[str, Any]
718
+ ) -> dict[str, Any]:
719
+ # make a copy to avoid modifying the original dict
720
+ properties_final = properties_input.copy()
721
+ for key, val in default_properties.items():
722
+ if key not in properties_final:
723
+ properties_final[key] = val
724
+ return properties_final
725
+
726
+ def _make_layout(
727
+ self,
728
+ title: str | None,
729
+ with_card: bool,
730
+ card_props: dict[str, Any],
731
+ title_props: dict[str, Any],
732
+ ) -> dmc.Card | html.Div:
733
+ form = make_form(
734
+ self.aio_id,
735
+ self.items,
736
+ self.columns,
737
+ self.column_names,
738
+ self.column_widths,
739
+ self.persistence,
740
+ self.persistence_type,
741
+ )
742
+
743
+ content = (
744
+ html.Div(
745
+ [
746
+ html.P(title, **title_props),
747
+ html.Hr(className="my-2"),
748
+ html.Br(),
749
+ form,
750
+ ],
751
+ )
752
+ if title
753
+ else form
754
+ )
755
+
756
+ layout = dmc.Card(content, **card_props) if with_card else content
757
+
758
+ return layout
759
+
760
+ @staticmethod
761
+ @callback(
762
+ Output(ids.modal(aio_id=MATCH, row_index=MATCH), "opened"),
763
+ Input(ids.help(aio_id=MATCH, row_index=MATCH), "n_clicks"),
764
+ State(ids.modal(aio_id=MATCH, row_index=MATCH), "opened"),
765
+ prevent_initial_call=True,
766
+ )
767
+ def display_help(
768
+ n_clicks: int,
769
+ opened: bool,
770
+ ) -> bool:
771
+ """Display help modal."""
772
+ if not n_clicks:
773
+ raise PreventUpdate
774
+
775
+ return not opened
776
+
777
+ @staticmethod
778
+ @callback(
779
+ Output(
780
+ ids.field(aio_id=MATCH, field_type="NumberInput", field_id=MATCH, row_index=MATCH),
781
+ "error",
782
+ ),
783
+ Input(
784
+ ids.field(aio_id=MATCH, field_type="NumberInput", field_id=MATCH, row_index=MATCH),
785
+ "value",
786
+ ),
787
+ State(
788
+ ids.field(aio_id=MATCH, field_type="NumberInput", field_id=MATCH, row_index=MATCH),
789
+ "max",
790
+ ),
791
+ State(
792
+ ids.field(aio_id=MATCH, field_type="NumberInput", field_id=MATCH, row_index=MATCH),
793
+ "min",
794
+ ),
795
+ prevent_initial_call=True,
796
+ )
797
+ def validate_input(
798
+ value: float,
799
+ vmax: float | None,
800
+ vmin: float | None,
801
+ ) -> str | NoUpdate | None:
802
+ """Validate number input entry."""
803
+ try:
804
+ float(value)
805
+ except Exception:
806
+ return no_update
807
+
808
+ if vmax is not None and float(value) > vmax:
809
+ print(f"Expect value to be less than {vmax}.")
810
+ return f"Expect value to be less than {vmax}."
811
+ if vmin is not None and float(value) < vmin:
812
+ print(f"Expect value to be greater than {vmin}.")
813
+ return f"Expect value to be greater than {vmin}."
814
+
815
+ return None
816
+
817
+
818
+ def compute_final_column_widths(
819
+ columns: list[str],
820
+ column_widths: dict[str, int],
821
+ ) -> dict[str, int]:
822
+ """
823
+ Calculate column widths from given and default column widths.
824
+
825
+ Parameters
826
+ ----------
827
+ columns : list[str]
828
+ List of column names to be used in the form.
829
+ column_widths : dict[str, int]
830
+ Dictionary mapping column names to their desired widths.
831
+ Missing columns will use default widths.
832
+
833
+ Returns
834
+ -------
835
+ dict
836
+ Dictionary containing only the used columns with their calculated widths
837
+ """
838
+ _validate_column_widths(column_widths)
839
+
840
+ # Get the width for each used column (custom or default)
841
+ result: dict[str, int] = {}
842
+ for column in columns:
843
+ if column in column_widths:
844
+ result[column] = column_widths[column]
845
+ else:
846
+ result[column] = DEFAULT_COLUMNS_AND_WIDTHS[column]
847
+
848
+ return result
849
+
850
+
851
+ def _validate_items_definition(items: list[dict[str, Any]]) -> None:
852
+ """Validate the items definition."""
853
+ for item in items:
854
+ fields = item.get("fields")
855
+ if not fields:
856
+ raise ValueError("Each item must have a fields key.")
857
+
858
+ for field in fields:
859
+ field_type = field.get("type", None)
860
+
861
+ if field_type is None:
862
+ raise ValueError(
863
+ "Each field must have a type key. Supported types are: "
864
+ f"{', '.join(SUPPORTED_FIELD_TYPES)}.",
865
+ )
866
+
867
+ if field_type not in SUPPORTED_FIELD_TYPES:
868
+ raise ValueError(
869
+ f"Unsupported field type: {field_type}. Supported types are: "
870
+ f"{', '.join(SUPPORTED_FIELD_TYPES)}.",
871
+ )
872
+
873
+ field_id = field.get("id", None)
874
+ if field_id is None:
875
+ raise ValueError("Each field must have an id key.")
876
+
877
+ field_id_is_string = isinstance(field_id, str)
878
+ if not field_id_is_string or not field_id:
879
+ raise ValueError("Field id must be a non-empty string.")
880
+
881
+
882
+ def _validate_columns(columns: list[str]) -> None:
883
+ """Validate the columns definition."""
884
+ if not columns:
885
+ raise ValueError("Columns list cannot be empty.")
886
+
887
+ if len(set(columns)) != len(columns):
888
+ raise ValueError("Duplicate column names are not allowed in columns list.")
889
+
890
+ for column in columns:
891
+ if column not in DEFAULT_COLUMNS_AND_WIDTHS:
892
+ raise ValueError(f"Unsupported column name: {column}")
893
+
894
+
895
+ def _validate_column_names(column_names: list[str], columns: list[str]) -> None:
896
+ """Validate the column names definition."""
897
+ if column_names and len(column_names) != len(columns):
898
+ raise ValueError("Length of column_names must match length of columns.")
899
+
900
+
901
+ def _validate_column_widths(column_widths: dict[str, int]) -> None:
902
+ """Validate the column widths definition."""
903
+ for column in column_widths:
904
+ if column not in DEFAULT_COLUMNS_AND_WIDTHS:
905
+ raise ValueError(f"Unsupported column name: {column}")
906
+ for width in column_widths.values():
907
+ if not isinstance(width, int) or width <= 0: # type: ignore - explicit type check here
908
+ raise ValueError("Column widths must be positive integers.")