Coverage for src / lexigram / admin / ui / organisms / dynamic_form.py: 19%

36 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-08-13 22:14 +0800

1"""Dynamic Form Component. 

2 

3Renders a FormSchema into HTML using htpy. 

4""" 

5 

6from __future__ import annotations 

7 

8from typing import TYPE_CHECKING, Any 

9 

10import htpy as h 

11 

12from lexigram.ui import Button, Component, Form 

13 

14if TYPE_CHECKING: 

15 from lexigram.admin.forms import FormSchema 

16 

17 

18class DynamicForm(Component): 

19 """Renders a form based on a schema.""" 

20 

21 def __init__( 

22 self, 

23 schema: FormSchema, 

24 action: str, 

25 method: str = "POST", 

26 submit_text: str = "Submit", 

27 hx_post: str | None = None, 

28 hx_target: str | None = None, 

29 hx_swap: str = "outerHTML", 

30 ): 

31 self.schema = schema 

32 self.action = action 

33 self.method = method 

34 self.submit_text = submit_text 

35 self.hx_post = hx_post or action if hx_post else None 

36 self.hx_target = hx_target 

37 self.hx_swap = hx_swap 

38 

39 def render(self) -> Any: 

40 from lexigram.admin.forms import FieldType as FormFieldType 

41 

42 # We wrap the content in a list of htpy nodes 

43 form_content = [] 

44 

45 # Render each field 

46 from lexigram.admin.ui.organisms.form_registry import _form_field_registry 

47 

48 for field in self.schema.fields: 

49 # RBAC: Skip invisible fields 

50 if not getattr(field, "visible", True): 

51 continue 

52 

53 # RBAC: Handle masking 

54 current_value = field.default 

55 if getattr(field, "masked", False) and current_value: 

56 current_value = "********" 

57 

58 renderer = _form_field_registry.get_renderer(field.type) 

59 form_content.append(renderer.render(field, current_value)) # type: ignore[arg-type] 

60 

61 if field.help_text and field.type != FormFieldType.CHECKBOX: 

62 form_content.append( 

63 h.p( 

64 class_="mt-1 text-xs text-muted-foreground mb-4 -mt-4", 

65 )[field.help_text], 

66 ) 

67 

68 # Submit Button 

69 form_content.append( 

70 h.div(class_="flex justify-end pt-4")[ 

71 Button(self.submit_text, type="submit", color="primary") 

72 ], 

73 ) 

74 

75 # Determine attributes for the generic Form wrapper 

76 # The Wrapper handles CSRF injection automatically via the logic we added earlier 

77 form_attrs = { 

78 "action": self.action, 

79 "method": self.method, 

80 "class_": "space-y-4 bg-card p-6 rounded-lg shadow", 

81 } 

82 if self.hx_post: 

83 form_attrs["hx_post"] = self.hx_post 

84 if self.hx_target: 

85 form_attrs["hx_target"] = self.hx_target 

86 if self.hx_swap: 

87 form_attrs["hx_swap"] = self.hx_swap 

88 

89 return Form( 

90 children=[ 

91 h.h2(class_="text-lg font-medium text-foreground mb-4")[ 

92 self.schema.title 

93 ], 

94 form_content, 

95 ], 

96 **form_attrs, # type: ignore[arg-type] 

97 )