Coverage for src/lexigram/admin/resources/config.py: 69%

165 statements  

« prev     ^ index     » next       coverage.py v7.15.4, created at 2026-08-21 14:56 +0800

1"""Admin resource and table configuration types. 

2 

3Provides :class:`TableConfiguration` (static DataTable config) and 

4:class:`ResourceConfig` (fluent builder for resource metadata). 

5""" 

6 

7from __future__ import annotations 

8 

9from dataclasses import dataclass 

10from typing import Any, Literal 

11 

12from lexigram.domain import DomainModel 

13from lexigram.validation import ConfigDict, Field 

14 

15__all__ = ["ResourceConfig", "TableConfiguration"] 

16 

17 

18@dataclass(init=False) 

19class TableConfiguration(DomainModel): 

20 """Static configuration for a DataTable.""" 

21 

22 model_config = ConfigDict(arbitrary_types_allowed=True) 

23 

24 columns: list[Any] = Field(default_factory=list) 

25 actions: list[Any] = Field(default_factory=list) 

26 header_actions: list[Any] = Field(default_factory=list) 

27 bulk_actions: list[Any] = Field(default_factory=list) 

28 filter_options: Any | None = None 

29 default_view: str = "tabular" 

30 default_layout: str = "stack" 

31 action_layout: Literal["horizontal", "stack"] = "horizontal" 

32 default_sort_by: str | None = None 

33 default_sort_order: str = "asc" 

34 per_page: int = 20 

35 enable_search: bool = True 

36 search_fields: list[str] | None = None 

37 reorderable_columns: bool = False 

38 group_by: str | None = None 

39 resource_name: str | None = None 

40 resource_prefix: str | None = None 

41 expandable_relationship: str | None = None 

42 density: str = "normal" 

43 empty_state_title: str | None = None 

44 empty_state_message: str | None = None 

45 empty_state_icon: str | None = None 

46 

47 @property 

48 def filters(self) -> Any | None: 

49 """Alias for ``filter_options`` used by the toolbar renderer.""" 

50 return self.filter_options 

51 

52 @property 

53 def density_css_class(self) -> str: 

54 """CSS class for current density setting.""" 

55 return f"table-density-{self.density}" 

56 

57 @property 

58 def density_row_height(self) -> str: 

59 """Row height in px for current density.""" 

60 heights = {"compact": "32px", "normal": "48px", "comfortable": "64px"} 

61 return heights.get(self.density, "48px") 

62 

63 

64class ResourceConfig: 

65 """Fluent configuration builder for Resource metadata.""" 

66 

67 def __init__(self) -> None: 

68 self._layout: str = "sidebar" 

69 self._view: str = "tabular" 

70 self._per_page: int = 20 

71 self._default_sort_field: str | None = None 

72 self._default_sort_order: str = "asc" 

73 self._display_name: str | None = None 

74 self._description: str | None = None 

75 self._icon: str = "box" 

76 self._expandable: bool = False 

77 self._record_title_func: Any = None 

78 self._columns: list[Any] = [] 

79 self._actions: list[Any] = [] 

80 self._filters_list: list[Any] = [] 

81 self._views_list: list[Any] = [] 

82 self._action_layout: str = "horizontal" 

83 self._form_display_mode: str = "modal" 

84 self._name: str | None = None 

85 self._group: str | None = None 

86 self._group_label: str | None = None 

87 self._group_icon: str | None = None 

88 self._group_order: int | None = None 

89 self._empty_state_title: str | None = None 

90 self._empty_state_message: str | None = None 

91 self._empty_state_icon: str | None = None 

92 self._group_by: str | None = None 

93 

94 @staticmethod 

95 def builder() -> ResourceConfig: 

96 """Return a new :class:`ResourceConfig` builder instance.""" 

97 return ResourceConfig() 

98 

99 def layout(self, layout: Literal["sidebar", "stack"]) -> ResourceConfig: 

100 """Set the resource layout.""" 

101 self._layout = layout 

102 return self 

103 

104 def name(self, name: str) -> ResourceConfig: 

105 """Set the resource registration name.""" 

106 self._name = name 

107 return self 

108 

109 def group( 

110 self, 

111 group: str, 

112 *, 

113 label: str | None = None, 

114 icon: str | None = None, 

115 order: int | None = None, 

116 ) -> ResourceConfig: 

117 """Set the navigation group this resource belongs to. 

118 

119 Args: 

120 group: Navigation group key. 

121 label: Group display label (used when auto-creating the group). 

122 icon: Group icon (used when auto-creating the group). 

123 order: Group sort order (used when auto-creating the group). 

124 """ 

125 self._group = group 

126 if label is not None: 

127 self._group_label = label 

128 if icon is not None: 

129 self._group_icon = icon 

130 if order is not None: 

131 self._group_order = order 

132 return self 

133 

134 def view(self, view: Literal["tabular", "grid", "kanban"] | Any) -> ResourceConfig: 

135 """Set the default view.""" 

136 self._view = str(view) if hasattr(view, "__str__") else "tabular" 

137 return self 

138 

139 def views(self, views: list[Any]) -> ResourceConfig: 

140 """Add multiple view definitions.""" 

141 self._views_list.extend(views) 

142 return self 

143 

144 def per_page(self, count: int) -> ResourceConfig: 

145 """Set items per page.""" 

146 self._per_page = max(1, count) 

147 return self 

148 

149 def group_by(self, column: str) -> ResourceConfig: 

150 """Set the default grouping column for the resource list view. 

151 

152 Users can override the group via the table toolbar dropdown. 

153 

154 Args: 

155 column: Column name to group rows by. 

156 """ 

157 self._group_by = column 

158 return self 

159 

160 def sort(self, field: str, order: Literal["asc", "desc"] = "asc") -> ResourceConfig: 

161 """Set the default sort field and order.""" 

162 self._default_sort_field = field 

163 self._default_sort_order = order if order in ("asc", "desc") else "asc" 

164 return self 

165 

166 def label(self, label: str) -> ResourceConfig: 

167 """Set the display label.""" 

168 self._display_name = label 

169 return self 

170 

171 def resource_name(self, name: str) -> ResourceConfig: 

172 """Set the resource name (alias for label).""" 

173 self._display_name = name 

174 return self 

175 

176 def icon(self, icon: str) -> ResourceConfig: 

177 """Set the icon name.""" 

178 self._icon = icon 

179 return self 

180 

181 def description(self, description: str) -> ResourceConfig: 

182 """Set the resource description.""" 

183 self._description = description 

184 return self 

185 

186 def empty_state( 

187 self, 

188 *, 

189 title: str | None = None, 

190 message: str | None = None, 

191 icon: str | None = None, 

192 ) -> ResourceConfig: 

193 """Override empty-state copy for the resource list view. 

194 

195 Args: 

196 title: Empty-state heading. 

197 message: Empty-state helper text. 

198 icon: Empty-state icon (emoji or icon name). 

199 """ 

200 if title is not None: 

201 self._empty_state_title = title 

202 if message is not None: 

203 self._empty_state_message = message 

204 if icon is not None: 

205 self._empty_state_icon = icon 

206 return self 

207 

208 def record_title(self, func: Any) -> ResourceConfig: 

209 """Set the record title function.""" 

210 self._record_title_func = func 

211 return self 

212 

213 def column(self, col: Any) -> ResourceConfig: 

214 """Add a single column definition.""" 

215 self._columns.append(col) 

216 return self 

217 

218 def columns(self, cols: list[Any]) -> ResourceConfig: 

219 """Add multiple column definitions.""" 

220 self._columns.extend(cols) 

221 return self 

222 

223 def action(self, action: Any) -> ResourceConfig: 

224 """Add a single action.""" 

225 self._actions.append(action) 

226 return self 

227 

228 def actions(self, actions: list[Any]) -> ResourceConfig: 

229 """Add multiple actions.""" 

230 self._actions.extend(actions) 

231 return self 

232 

233 def filter(self, filter_obj: Any) -> ResourceConfig: 

234 """Add a single filter.""" 

235 self._filters_list.append(filter_obj) 

236 return self 

237 

238 def filters(self, filters: list[Any]) -> ResourceConfig: 

239 """Add multiple filters.""" 

240 self._filters_list.extend(filters) 

241 return self 

242 

243 def action_layout(self, layout: Literal["horizontal", "stack"]) -> ResourceConfig: 

244 """Set the action layout direction.""" 

245 self._action_layout = layout 

246 return self 

247 

248 def form_display_mode( 

249 self, mode: Literal["page", "modal", "slider"] 

250 ) -> ResourceConfig: 

251 """Set how forms are displayed.""" 

252 if mode in ("page", "modal", "slider"): 

253 self._form_display_mode = mode 

254 return self 

255 

256 @property 

257 def display_name(self) -> str | None: 

258 """Return the configured display name.""" 

259 return self._display_name 

260 

261 @property 

262 def default_sort_field(self) -> str | None: 

263 """Return the configured sort field.""" 

264 return self._default_sort_field 

265 

266 @property 

267 def default_sort_order(self) -> str: 

268 """Return the configured sort order.""" 

269 return self._default_sort_order 

270 

271 @property 

272 def filters_list(self) -> list[Any]: 

273 """Return the configured filters.""" 

274 return self._filters_list 

275 

276 @property 

277 def views_list(self) -> list[Any]: 

278 """Return the configured views.""" 

279 return self._views_list