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
« prev ^ index » next coverage.py v7.13.5, created at 2026-08-13 22:14 +0800
1"""Dynamic Form Component.
3Renders a FormSchema into HTML using htpy.
4"""
6from __future__ import annotations
8from typing import TYPE_CHECKING, Any
10import htpy as h
12from lexigram.ui import Button, Component, Form
14if TYPE_CHECKING:
15 from lexigram.admin.forms import FormSchema
18class DynamicForm(Component):
19 """Renders a form based on a schema."""
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
39 def render(self) -> Any:
40 from lexigram.admin.forms import FieldType as FormFieldType
42 # We wrap the content in a list of htpy nodes
43 form_content = []
45 # Render each field
46 from lexigram.admin.ui.organisms.form_registry import _form_field_registry
48 for field in self.schema.fields:
49 # RBAC: Skip invisible fields
50 if not getattr(field, "visible", True):
51 continue
53 # RBAC: Handle masking
54 current_value = field.default
55 if getattr(field, "masked", False) and current_value:
56 current_value = "********"
58 renderer = _form_field_registry.get_renderer(field.type)
59 form_content.append(renderer.render(field, current_value)) # type: ignore[arg-type]
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 )
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 )
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
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 )