Coverage for src/lexigram/admin/schema/text.py: 85%
93 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-21 14:56 +0800
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-21 14:56 +0800
1from __future__ import annotations
3from dataclasses import dataclass
4from typing import Any
6from lexigram.admin.schema.base import SchemaField
7from lexigram.admin.schema.exceptions import FieldError
8from lexigram.result import Ok, Result
9from lexigram.ui import Element, TextInput
12@dataclass(frozen=True, kw_only=True)
13class TextField(SchemaField[str]):
14 """A single-line text input field."""
16 def render_form(
17 self, value: str | None, *, errors: list[str] | None = None
18 ) -> Element:
19 kwargs: dict[str, Any] = {
20 "name": self.name,
21 "value": value if value is not None else "",
22 }
23 if self.placeholder is not None:
24 kwargs["placeholder"] = self.placeholder
25 if self.label is not None:
26 kwargs["label"] = self.label
27 if errors:
28 kwargs["error"] = errors[0]
29 if self.readonly:
30 kwargs["disabled"] = True
31 return TextInput(**kwargs).render()
33 def render_column(self, record: Any, value: str | None) -> Element:
34 if value is None:
35 return Element("span", "\u2014", class_="text-muted")
36 return Element("span", value)
38 def from_form(self, raw: str | None) -> Result[str | None, FieldError]:
39 if raw is None:
40 return Ok(None)
41 stripped = raw.strip()
42 if not stripped:
43 if self.nullable:
44 return Ok(None)
45 return Ok("")
46 return Ok(stripped)
49@dataclass(frozen=True, kw_only=True)
50class EmailField(TextField):
51 """An email input field.
53 Renders an ``<input type="email">`` form control and displays
54 the value as a ``mailto:`` link in columns.
55 """
57 def render_infolist_entry(self, value: Any) -> Any:
58 from lexigram.ui import InfolistEntry, InfolistEntryType
60 return InfolistEntry(
61 name=self.name,
62 label=self.label or self.name.replace("_", " ").title(),
63 value=value,
64 type=InfolistEntryType.EMAIL,
65 )
67 def render_form(
68 self, value: str | None, *, errors: list[str] | None = None
69 ) -> Element:
70 kwargs: dict[str, Any] = {
71 "name": self.name,
72 "value": value if value is not None else "",
73 "input_type": "email",
74 }
75 if self.placeholder is not None:
76 kwargs["placeholder"] = self.placeholder
77 if self.label is not None:
78 kwargs["label"] = self.label
79 if errors:
80 kwargs["error"] = errors[0]
81 if self.readonly:
82 kwargs["disabled"] = True
83 return TextInput(**kwargs).render()
85 def render_column(self, record: Any, value: str | None) -> Element:
86 if value is None:
87 return Element("span", "\u2014", class_="text-muted")
88 return Element("a", value, href=f"mailto:{value}")
91@dataclass(frozen=True, kw_only=True)
92class PasswordField(TextField):
93 """A password input field.
95 Renders an ``<input type="password">`` form control and masks
96 the value in columns.
97 """
99 def render_infolist_entry(self, value: Any) -> Any:
100 from lexigram.ui import InfolistEntry, InfolistEntryType
102 return InfolistEntry(
103 name=self.name,
104 label=self.label or self.name.replace("_", " ").title(),
105 value="\u2022\u2022\u2022\u2022\u2022\u2022" if value is not None else None,
106 type=InfolistEntryType.TEXT,
107 )
109 def render_form(
110 self, value: str | None, *, errors: list[str] | None = None
111 ) -> Element:
112 kwargs: dict[str, Any] = {
113 "name": self.name,
114 "value": value if value is not None else "",
115 "input_type": "password",
116 }
117 if self.placeholder is not None:
118 kwargs["placeholder"] = self.placeholder
119 if self.label is not None:
120 kwargs["label"] = self.label
121 if errors:
122 kwargs["error"] = errors[0]
123 if self.readonly:
124 kwargs["disabled"] = True
125 return TextInput(**kwargs).render()
127 def render_column(self, record: Any, value: str | None) -> Element:
128 if value is None:
129 return Element("span", "\u2014", class_="text-muted")
130 return Element("span", "\u2022\u2022\u2022\u2022\u2022\u2022")
133@dataclass(frozen=True, kw_only=True)
134class URLField(TextField):
135 """A URL input field.
137 Renders an ``<input type="url">`` form control and displays
138 the value as a clickable link in columns.
139 """
141 def render_infolist_entry(self, value: Any) -> Any:
142 from lexigram.ui import InfolistEntry, InfolistEntryType
144 return InfolistEntry(
145 name=self.name,
146 label=self.label or self.name.replace("_", " ").title(),
147 value=value,
148 type=InfolistEntryType.URL,
149 )
151 def render_form(
152 self, value: str | None, *, errors: list[str] | None = None
153 ) -> Element:
154 kwargs: dict[str, Any] = {
155 "name": self.name,
156 "value": value if value is not None else "",
157 "input_type": "url",
158 }
159 if self.placeholder is not None:
160 kwargs["placeholder"] = self.placeholder
161 if self.label is not None:
162 kwargs["label"] = self.label
163 if errors:
164 kwargs["error"] = errors[0]
165 if self.readonly:
166 kwargs["disabled"] = True
167 return TextInput(**kwargs).render()
169 def render_column(self, record: Any, value: str | None) -> Element:
170 if value is None:
171 return Element("span", "\u2014", class_="text-muted")
172 return Element("a", value, href=value, target="_blank")