fh-pydantic-form 0.1.2__py3-none-any.whl → 0.2.0__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 fh-pydantic-form might be problematic. Click here for more details.

@@ -1,4 +1,5 @@
1
1
  import logging
2
+ from enum import Enum
2
3
  from typing import (
3
4
  Any,
4
5
  Dict,
@@ -10,6 +11,7 @@ from typing import (
10
11
 
11
12
  from fh_pydantic_form.type_helpers import (
12
13
  _get_underlying_type_if_optional,
14
+ _is_enum_type,
13
15
  _is_literal_type,
14
16
  _is_optional_type,
15
17
  )
@@ -49,6 +51,7 @@ def _parse_non_list_fields(
49
51
  model_class,
50
52
  list_field_defs: Dict[str, Dict[str, Any]],
51
53
  base_prefix: str = "",
54
+ exclude_fields: Optional[List[str]] = None,
52
55
  ) -> Dict[str, Any]:
53
56
  """
54
57
  Parses non-list fields from form data based on the model definition.
@@ -58,16 +61,22 @@ def _parse_non_list_fields(
58
61
  model_class: The Pydantic model class defining the structure
59
62
  list_field_defs: Dictionary of list field definitions (to skip)
60
63
  base_prefix: Prefix to use when looking up field names in form_data
64
+ exclude_fields: Optional list of field names to exclude from parsing
61
65
 
62
66
  Returns:
63
67
  Dictionary with parsed non-list fields
64
68
  """
65
69
  result: Dict[str, Any] = {}
70
+ exclude_fields = exclude_fields or []
66
71
 
67
72
  for field_name, field_info in model_class.model_fields.items():
68
73
  if field_name in list_field_defs:
69
74
  continue # Skip list fields, handled separately
70
75
 
76
+ # Skip excluded fields - they will be handled by default injection later
77
+ if field_name in exclude_fields:
78
+ continue
79
+
71
80
  # Create full key with prefix
72
81
  full_key = f"{base_prefix}{field_name}"
73
82
 
@@ -84,6 +93,10 @@ def _parse_non_list_fields(
84
93
  elif _is_literal_type(annotation):
85
94
  result[field_name] = _parse_literal_field(full_key, form_data, field_info)
86
95
 
96
+ # Handle Enum fields (including Optional[Enum])
97
+ elif _is_enum_type(annotation):
98
+ result[field_name] = _parse_enum_field(full_key, form_data, field_info)
99
+
87
100
  # Handle nested model fields (including Optional[NestedModel])
88
101
  elif (
89
102
  isinstance(annotation, type)
@@ -157,6 +170,49 @@ def _parse_literal_field(field_name: str, form_data: Dict[str, Any], field_info)
157
170
  return value
158
171
 
159
172
 
173
+ def _parse_enum_field(field_name: str, form_data: Dict[str, Any], field_info) -> Any:
174
+ """
175
+ Parse an Enum field, converting empty string OR '-- None --' to None for optional fields.
176
+
177
+ Args:
178
+ field_name: Name of the field to parse
179
+ form_data: Dictionary containing form field data
180
+ field_info: FieldInfo object to check for optionality
181
+
182
+ Returns:
183
+ The parsed value or None for empty/None values with optional fields
184
+ """
185
+ value = form_data.get(field_name)
186
+
187
+ # Check if the field is Optional[Enum]
188
+ if _is_optional_type(field_info.annotation):
189
+ # If the submitted value is the empty string OR the display text for None, treat it as None
190
+ if value == "" or value == "-- None --":
191
+ return None
192
+
193
+ enum_cls = _get_underlying_type_if_optional(field_info.annotation)
194
+ if isinstance(enum_cls, type) and issubclass(enum_cls, Enum) and value is not None:
195
+ try:
196
+ first = next(iter(enum_cls))
197
+ # Handle integer enums - convert string to int
198
+ if isinstance(first.value, int):
199
+ try:
200
+ value = int(value)
201
+ except (TypeError, ValueError):
202
+ # leave it as-is; pydantic will raise if really invalid
203
+ pass
204
+ # Handle string enums - keep the value as-is, let Pydantic handle validation
205
+ elif isinstance(first.value, str):
206
+ # Keep the submitted value unchanged for string enums
207
+ pass
208
+ except StopIteration:
209
+ # Empty enum, leave value as-is
210
+ pass
211
+
212
+ # Return the actual submitted value for Pydantic validation
213
+ return value
214
+
215
+
160
216
  def _parse_simple_field(
161
217
  field_name: str, form_data: Dict[str, Any], field_info=None
162
218
  ) -> Any:
@@ -416,6 +472,25 @@ def _parse_list_fields(
416
472
  )
417
473
  item_data[model_field_name] = False
418
474
 
475
+ # Convert string to int for integer-valued enums in simple lists
476
+ if (
477
+ not field_def["is_model_type"]
478
+ and isinstance(item_type, type)
479
+ and issubclass(item_type, Enum)
480
+ and isinstance(item_data, str)
481
+ ):
482
+ try:
483
+ first = next(iter(item_type))
484
+ if isinstance(first.value, int):
485
+ try:
486
+ item_data = int(item_data)
487
+ except (TypeError, ValueError):
488
+ # leave it as-is; pydantic will raise if really invalid
489
+ pass
490
+ except StopIteration:
491
+ # Empty enum, leave item_data as-is
492
+ pass
493
+
419
494
  # For model types, keep as dict for now
420
495
  items.append(item_data)
421
496
 
@@ -9,6 +9,7 @@ from typing import (
9
9
  Tuple,
10
10
  Type,
11
11
  TypeVar,
12
+ Union,
12
13
  )
13
14
 
14
15
  import fasthtml.common as fh
@@ -16,6 +17,7 @@ import monsterui.all as mui
16
17
  from fastcore.xml import FT
17
18
  from pydantic import BaseModel
18
19
 
20
+ from fh_pydantic_form.defaults import default_dict_for_model, default_for_annotation
19
21
  from fh_pydantic_form.field_renderers import (
20
22
  BaseFieldRenderer,
21
23
  ListFieldRenderer,
@@ -27,6 +29,13 @@ from fh_pydantic_form.form_parser import (
27
29
  _parse_non_list_fields,
28
30
  )
29
31
  from fh_pydantic_form.registry import FieldRendererRegistry
32
+ from fh_pydantic_form.type_helpers import _UNSET, get_default
33
+ from fh_pydantic_form.ui_style import (
34
+ SpacingTheme,
35
+ SpacingValue,
36
+ _normalize_spacing,
37
+ spacing,
38
+ )
30
39
 
31
40
  logger = logging.getLogger(__name__)
32
41
 
@@ -181,7 +190,11 @@ document.addEventListener('DOMContentLoaded', () => {
181
190
 
182
191
  class PydanticForm(Generic[ModelType]):
183
192
  """
184
- Renders a form from a Pydantic model class
193
+ Renders a form from a Pydantic model class with robust schema drift handling
194
+
195
+ Accepts initial values as either BaseModel instances or dictionaries.
196
+ Gracefully handles missing fields and schema mismatches by rendering
197
+ available fields and skipping problematic ones.
185
198
 
186
199
  This class handles:
187
200
  - Finding appropriate renderers for each field
@@ -194,15 +207,29 @@ class PydanticForm(Generic[ModelType]):
194
207
  - validating request data against the model
195
208
  """
196
209
 
210
+ def _compact_wrapper(self, inner: FT) -> FT:
211
+ """
212
+ Wrap inner markup with a '.compact-form' div and inject one <style>
213
+ block when compact theme is used.
214
+ """
215
+ if self.spacing == SpacingTheme.COMPACT:
216
+ from fh_pydantic_form.ui_style import COMPACT_EXTRA_CSS
217
+
218
+ return fh.Div(COMPACT_EXTRA_CSS, inner, cls="compact-form")
219
+ else:
220
+ return inner
221
+
197
222
  def __init__(
198
223
  self,
199
224
  form_name: str,
200
225
  model_class: Type[ModelType],
201
- initial_values: Optional[ModelType] = None,
226
+ initial_values: Optional[Union[ModelType, Dict[str, Any]]] = None,
202
227
  custom_renderers: Optional[List[Tuple[Type, Type[BaseFieldRenderer]]]] = None,
203
228
  disabled: bool = False,
204
229
  disabled_fields: Optional[List[str]] = None,
205
230
  label_colors: Optional[Dict[str, str]] = None,
231
+ exclude_fields: Optional[List[str]] = None,
232
+ spacing: SpacingValue = SpacingTheme.NORMAL,
206
233
  ):
207
234
  """
208
235
  Initialize the form renderer
@@ -210,22 +237,56 @@ class PydanticForm(Generic[ModelType]):
210
237
  Args:
211
238
  form_name: Unique name for this form
212
239
  model_class: The Pydantic model class to render
213
- initial_values: Optional initial Pydantic model instance
240
+ initial_values: Initial values as BaseModel instance or dict.
241
+ Missing fields will not be auto-filled with defaults.
242
+ Supports robust handling of schema drift.
214
243
  custom_renderers: Optional list of tuples (field_type, renderer_cls) to register
215
244
  disabled: Whether all form inputs should be disabled
216
245
  disabled_fields: Optional list of top-level field names to disable specifically
217
246
  label_colors: Optional dictionary mapping field names to label colors (CSS color values)
247
+ exclude_fields: Optional list of top-level field names to exclude from the form
248
+ spacing: Spacing theme to use for form layout ("normal", "compact", or SpacingTheme enum)
218
249
  """
219
250
  self.name = form_name
220
251
  self.model_class = model_class
221
- self.initial_data_model = initial_values # Store original model for fallback
222
- self.values_dict = initial_values.model_dump() if initial_values else {}
252
+
253
+ self.initial_values_dict: Dict[str, Any] = {}
254
+
255
+ # Store initial values as dict for robustness to schema drift
256
+ if initial_values is None:
257
+ self.initial_values_dict = {}
258
+ elif isinstance(initial_values, dict):
259
+ self.initial_values_dict = initial_values.copy()
260
+ elif hasattr(initial_values, "model_dump"):
261
+ self.initial_values_dict = initial_values.model_dump()
262
+ else:
263
+ # Fallback - attempt dict conversion
264
+ try:
265
+ temp_dict = dict(initial_values)
266
+ model_field_names = set(self.model_class.model_fields.keys())
267
+ # Only accept if all keys are in the model's field names
268
+ if not isinstance(temp_dict, dict) or not set(
269
+ temp_dict.keys()
270
+ ).issubset(model_field_names):
271
+ raise ValueError("Converted to dict with keys not in model fields")
272
+ self.initial_values_dict = temp_dict
273
+ except (TypeError, ValueError):
274
+ logger.warning(
275
+ "Could not convert initial_values to dict, using empty dict"
276
+ )
277
+ self.initial_values_dict = {}
278
+
279
+ # Use copy for rendering to avoid mutations
280
+ self.values_dict: Dict[str, Any] = self.initial_values_dict.copy()
281
+
223
282
  self.base_prefix = f"{form_name}_"
224
283
  self.disabled = disabled
225
284
  self.disabled_fields = (
226
285
  disabled_fields or []
227
286
  ) # Store as list for easier checking
228
287
  self.label_colors = label_colors or {} # Store label colors mapping
288
+ self.exclude_fields = exclude_fields or [] # Store excluded fields list
289
+ self.spacing = _normalize_spacing(spacing) # Store normalized spacing
229
290
 
230
291
  # Register custom renderers with the global registry if provided
231
292
  if custom_renderers:
@@ -247,11 +308,22 @@ class PydanticForm(Generic[ModelType]):
247
308
  )
248
309
 
249
310
  for field_name, field_info in self.model_class.model_fields.items():
250
- # Determine initial value
311
+ # Skip excluded fields
312
+ if field_name in self.exclude_fields:
313
+ logger.debug(f"Skipping excluded field: {field_name}")
314
+ continue
315
+
316
+ # Only use what was explicitly provided in initial values
251
317
  initial_value = (
252
318
  self.values_dict.get(field_name) if self.values_dict else None
253
319
  )
254
320
 
321
+ # Only use model defaults if field was not provided at all
322
+ # (not if it was provided as None/empty)
323
+ field_was_provided = (
324
+ field_name in self.values_dict if self.values_dict else False
325
+ )
326
+
255
327
  # Log the initial value type and a summary for debugging
256
328
  if initial_value is not None:
257
329
  value_type = type(initial_value).__name__
@@ -259,26 +331,39 @@ class PydanticForm(Generic[ModelType]):
259
331
  value_size = f"size={len(initial_value)}"
260
332
  else:
261
333
  value_size = ""
262
- logger.debug(f"Field '{field_name}': {value_type} {value_size}")
334
+ logger.debug(
335
+ f"Field '{field_name}': {value_type} {value_size} (provided: {field_was_provided})"
336
+ )
263
337
  else:
264
338
  logger.debug(
265
- f"Field '{field_name}': None (will use default if available)"
339
+ f"Field '{field_name}': None (provided: {field_was_provided})"
266
340
  )
267
341
 
268
- # Use default if no value is provided
269
- if initial_value is None:
342
+ # Only use defaults if field was not provided at all
343
+ if not field_was_provided:
344
+ # Field not provided - use model defaults
270
345
  if field_info.default is not None:
271
346
  initial_value = field_info.default
272
347
  logger.debug(f" - Using default value for '{field_name}'")
273
348
  elif getattr(field_info, "default_factory", None) is not None:
274
349
  try:
275
- initial_value = field_info.default_factory()
276
- logger.debug(f" - Using default_factory for '{field_name}'")
350
+ default_factory = field_info.default_factory
351
+ if callable(default_factory):
352
+ initial_value = default_factory()
353
+ logger.debug(
354
+ f" - Using default_factory for '{field_name}'"
355
+ )
356
+ else:
357
+ initial_value = None
358
+ logger.warning(
359
+ f" - default_factory for '{field_name}' is not callable"
360
+ )
277
361
  except Exception as e:
278
362
  initial_value = None
279
363
  logger.warning(
280
364
  f" - Error in default_factory for '{field_name}': {e}"
281
365
  )
366
+ # If field was provided (even as None), respect that value
282
367
 
283
368
  # Get renderer from global registry
284
369
  renderer_cls = registry.get_renderer(field_name, field_info)
@@ -307,21 +392,28 @@ class PydanticForm(Generic[ModelType]):
307
392
  prefix=self.base_prefix,
308
393
  disabled=is_field_disabled, # Pass the calculated disabled state
309
394
  label_color=label_color, # Pass the label color if specified
395
+ spacing=self.spacing, # Pass the spacing
310
396
  )
311
397
 
312
398
  rendered_field = renderer.render()
313
399
  form_inputs.append(rendered_field)
314
400
 
315
401
  # Create container for inputs, ensuring items stretch to full width
316
- inputs_container = mui.DivVStacked(*form_inputs, cls="space-y-3 items-stretch")
402
+ inputs_container = mui.DivVStacked(
403
+ *form_inputs,
404
+ cls=f"{spacing('stack_gap', self.spacing)} items-stretch",
405
+ )
317
406
 
318
407
  # Define the ID for the wrapper div - this is what the HTMX request targets
319
408
  form_content_wrapper_id = f"{self.name}-inputs-wrapper"
320
409
  logger.debug(f"Creating form inputs wrapper with ID: {form_content_wrapper_id}")
321
410
 
322
- # Return only the inner container without the wrapper div
323
- # The wrapper will be added by the main route handler instead
324
- return fh.Div(inputs_container, id=form_content_wrapper_id)
411
+ # Create the wrapper div and apply compact styling if needed
412
+ wrapped = self._compact_wrapper(
413
+ fh.Div(inputs_container, id=form_content_wrapper_id)
414
+ )
415
+
416
+ return wrapped
325
417
 
326
418
  # ---- Form Renderer Methods (continued) ----
327
419
 
@@ -351,9 +443,9 @@ class PydanticForm(Generic[ModelType]):
351
443
  f"Error parsing form data for refresh on form '{self.name}': {e}",
352
444
  exc_info=True,
353
445
  )
354
- # Fallback: Use original initial data model dump if available, otherwise empty dict
446
+ # Fallback: Use original initial values dict if available, otherwise empty dict
355
447
  parsed_data = (
356
- self.initial_data_model.model_dump() if self.initial_data_model else {}
448
+ self.initial_values_dict.copy() if self.initial_values_dict else {}
357
449
  )
358
450
  alert_ft = mui.Alert(
359
451
  f"Warning: Could not fully process current form values for refresh. Display might not be fully updated. Error: {str(e)}",
@@ -365,6 +457,7 @@ class PydanticForm(Generic[ModelType]):
365
457
  form_name=self.name,
366
458
  model_class=self.model_class,
367
459
  # No initial_data needed here, we set values_dict below
460
+ spacing=self.spacing,
368
461
  )
369
462
  # Set the values based on the parsed (or fallback) data
370
463
  temp_renderer.values_dict = parsed_data
@@ -395,27 +488,28 @@ class PydanticForm(Generic[ModelType]):
395
488
  Returns:
396
489
  HTML response with reset form inputs
397
490
  """
398
- logger.info(
399
- f"Resetting form '{self.name}' to initial values. Initial model: {self.initial_data_model}"
400
- )
491
+ logger.info(f"Resetting form '{self.name}' to initial values")
401
492
 
402
- # Create a temporary renderer with the original initial data
493
+ # Create temporary renderer with original initial dict
403
494
  temp_renderer = PydanticForm(
404
495
  form_name=self.name,
405
496
  model_class=self.model_class,
406
- initial_values=self.initial_data_model, # Use the originally stored model
497
+ initial_values=self.initial_values_dict, # Use dict instead of BaseModel
498
+ custom_renderers=getattr(self, "custom_renderers", None),
499
+ disabled=self.disabled,
500
+ disabled_fields=self.disabled_fields,
501
+ label_colors=self.label_colors,
502
+ exclude_fields=self.exclude_fields,
503
+ spacing=self.spacing,
407
504
  )
408
505
 
409
- # Render inputs with the initial data
410
506
  reset_inputs_component = temp_renderer.render_inputs()
411
507
 
412
508
  if reset_inputs_component is None:
413
509
  logger.error(f"Reset for form '{self.name}' failed to render inputs.")
414
510
  return mui.Alert("Error resetting form.", cls=mui.AlertT.error)
415
511
 
416
- logger.info(
417
- f"Reset form '{self.name}' successful. Component: {reset_inputs_component}"
418
- )
512
+ logger.info(f"Reset form '{self.name}' successful")
419
513
  return reset_inputs_component
420
514
 
421
515
  def parse(self, form_dict: Dict[str, Any]) -> Dict[str, Any]:
@@ -434,20 +528,93 @@ class PydanticForm(Generic[ModelType]):
434
528
 
435
529
  list_field_defs = _identify_list_fields(self.model_class)
436
530
 
437
- # Parse non-list fields first - pass the base_prefix
531
+ # Filter out excluded fields from list field definitions
532
+ filtered_list_field_defs = {
533
+ field_name: field_def
534
+ for field_name, field_def in list_field_defs.items()
535
+ if field_name not in self.exclude_fields
536
+ }
438
537
 
538
+ # Parse non-list fields first - pass the base_prefix and exclude_fields
439
539
  result = _parse_non_list_fields(
440
- form_dict, self.model_class, list_field_defs, self.base_prefix
540
+ form_dict,
541
+ self.model_class,
542
+ list_field_defs,
543
+ self.base_prefix,
544
+ self.exclude_fields,
441
545
  )
442
546
 
443
547
  # Parse list fields based on keys present in form_dict - pass the base_prefix
444
- list_results = _parse_list_fields(form_dict, list_field_defs, self.base_prefix)
548
+ # Use filtered list field definitions to skip excluded list fields
549
+ list_results = _parse_list_fields(
550
+ form_dict, filtered_list_field_defs, self.base_prefix
551
+ )
445
552
 
446
553
  # Merge list results into the main result
447
554
  result.update(list_results)
448
555
 
556
+ # Inject defaults for excluded fields before returning
557
+ self._inject_default_values_for_excluded(result)
558
+
449
559
  return result
450
560
 
561
+ def _inject_default_values_for_excluded(
562
+ self, data: Dict[str, Any]
563
+ ) -> Dict[str, Any]:
564
+ """
565
+ Ensures that every field listed in self.exclude_fields is present in data
566
+ if the model defines a default or default_factory, or if initial_values were provided.
567
+
568
+ Priority order:
569
+ 1. initial_values (if provided during form creation)
570
+ 2. model defaults/default_factory
571
+
572
+ Operates top-level only (exclude_fields spec is top-level names).
573
+
574
+ Args:
575
+ data: Dictionary to modify in-place
576
+
577
+ Returns:
578
+ The same dictionary instance for method chaining
579
+ """
580
+ for field_name in self.exclude_fields:
581
+ # Skip if already present (e.g., user provided initial_values)
582
+ if field_name in data:
583
+ continue
584
+
585
+ # First priority: check if initial_values_dict has this field
586
+ if field_name in self.initial_values_dict:
587
+ initial_val = self.initial_values_dict[field_name]
588
+ # If the initial value is a BaseModel, convert to dict for consistency
589
+ if hasattr(initial_val, "model_dump"):
590
+ initial_val = initial_val.model_dump()
591
+ data[field_name] = initial_val
592
+ logger.debug(
593
+ f"Injected initial value for excluded field '{field_name}'"
594
+ )
595
+ continue
596
+
597
+ # Second priority: use model defaults
598
+ field_info = self.model_class.model_fields.get(field_name)
599
+ if field_info is None:
600
+ logger.warning(f"exclude_fields contains unknown field '{field_name}'")
601
+ continue
602
+
603
+ default_val = get_default(field_info)
604
+ if default_val is not _UNSET:
605
+ # If the default is a BaseModel, convert to dict for consistency
606
+ if hasattr(default_val, "model_dump"):
607
+ default_val = default_val.model_dump()
608
+ data[field_name] = default_val
609
+ logger.debug(
610
+ f"Injected model default value for excluded field '{field_name}'"
611
+ )
612
+ else:
613
+ # No default → leave missing; validation will surface error
614
+ logger.debug(f"No default found for excluded field '{field_name}'")
615
+
616
+ return data
617
+
451
618
  def register_routes(self, app):
452
619
  """
453
620
  Register HTMX routes for list manipulation and form refresh
@@ -521,40 +688,29 @@ class PydanticForm(Generic[ModelType]):
521
688
  cls=mui.AlertT.error,
522
689
  )
523
690
 
524
- # Create a default item
691
+ # Create a default item using smart defaults
692
+ default_item_dict: Dict[str, Any] | str | Any | None = None
525
693
  try:
526
- # Ensure item_type is not None before checking attributes or type
527
- if item_type:
528
- # For Pydantic models, try to use model_construct for default values
529
- if hasattr(item_type, "model_construct"):
530
- try:
531
- default_item = item_type.model_construct()
532
- except Exception as e:
533
- return fh.Li(
534
- mui.Alert(
535
- f"Error creating model instance: {str(e)}",
536
- cls=mui.AlertT.error,
537
- ),
538
- cls="mb-2",
539
- )
540
- # Handle simple types with appropriate defaults
541
- elif item_type is str:
542
- default_item = ""
543
- elif item_type is int:
544
- default_item = 0
545
- elif item_type is float:
546
- default_item = 0.0
547
- elif item_type is bool:
548
- default_item = False
549
- else:
550
- default_item = None
551
- else:
552
- # Case where item_type itself was None (should ideally be caught earlier)
553
- default_item = None
694
+ if not item_type:
554
695
  logger.warning(
555
696
  f"item_type was None when trying to create default for {field_name}"
556
697
  )
698
+ default_item_dict = ""
699
+ elif hasattr(item_type, "model_fields"):
700
+ # For Pydantic models, use smart default generation
701
+ default_item_dict = default_dict_for_model(item_type)
702
+ else:
703
+ # For simple types, use annotation-based defaults
704
+ default_item_dict = default_for_annotation(item_type)
705
+
706
+ # Final fallback for exotic cases
707
+ if default_item_dict is None:
708
+ default_item_dict = ""
709
+
557
710
  except Exception as e:
711
+ logger.error(
712
+ f"Error creating default item for {field_name}: {e}", exc_info=True
713
+ )
558
714
  return fh.Li(
559
715
  mui.Alert(
560
716
  f"Error creating default item: {str(e)}", cls=mui.AlertT.error
@@ -565,17 +721,33 @@ class PydanticForm(Generic[ModelType]):
565
721
  # Generate a unique placeholder index
566
722
  placeholder_idx = f"new_{int(pytime.time() * 1000)}"
567
723
 
568
- # Create a list renderer and render the new item
569
- list_renderer = ListFieldRenderer(
570
- field_name=field_name,
571
- field_info=field_info,
572
- value=[], # Empty list, we only need to render one item
573
- prefix=self.base_prefix, # Use the form's base prefix
724
+ # Create a list renderer
725
+ if field_info is not None:
726
+ list_renderer = ListFieldRenderer(
727
+ field_name=field_name,
728
+ field_info=field_info,
729
+ value=[], # Empty list, we only need to render one item
730
+ prefix=self.base_prefix, # Use the form's base prefix
731
+ )
732
+ else:
733
+ logger.error(f"field_info is None for field {field_name}")
734
+ return mui.Alert(
735
+ f"Field info not found for {field_name}",
736
+ cls=mui.AlertT.error,
737
+ )
738
+
739
+ # The default_item_dict is already in the correct format (dict for models, primitive for simple types)
740
+ item_data_for_renderer = default_item_dict
741
+ logger.debug(
742
+ f"Add item: Using smart default for renderer: {item_data_for_renderer}"
574
743
  )
575
744
 
576
745
  # Render the new item card, set is_open=True to make it expanded by default
577
746
  new_item_card = list_renderer._render_item_card(
578
- default_item, placeholder_idx, item_type, is_open=True
747
+ item_data_for_renderer, # Pass the dictionary or simple value
748
+ placeholder_idx,
749
+ item_type,
750
+ is_open=True,
579
751
  )
580
752
 
581
753
  return new_item_card