Coverage for src/lexigram/admin/actions/header_manager/manager.py: 31%

137 statements  

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

1""" 

2Header action manager implementation. 

3 

4Provides the main HeaderActionManager class for managing header actions. 

5""" 

6 

7from __future__ import annotations 

8 

9from collections.abc import Callable 

10from typing import Any 

11 

12from lexigram.admin.actions.header_manager.actions import ( 

13 BASIC_ACTIONS, 

14 BULK_ACTIONS, 

15 IMPORT_EXPORT_ACTIONS, 

16 UTILITY_ACTIONS, 

17) 

18from lexigram.admin.actions.header_manager.density import DensityManager 

19from lexigram.admin.actions.header_manager.shortcuts import KeyboardShortcutManager 

20from lexigram.admin.actions.header_manager.types import ( 

21 ColumnVisibilityConfig, 

22 DensityConfig, 

23 HeaderAction, 

24 HeaderActionStyle, 

25 IHeaderDataSource, 

26 TableDensity, 

27) 

28from lexigram.admin.actions.header_manager.visibility import ColumnVisibilityManager 

29 

30 

31class HeaderActionManager: 

32 """Manages header actions for data tables.""" 

33 

34 def __init__( 

35 self, 

36 data_source: IHeaderDataSource[Any] | None = None, 

37 storage: Callable[[str, str], None] | None = None, 

38 retriever: Callable[[str], str | None] | None = None, 

39 ) -> None: 

40 """Initialize the header action manager. 

41 

42 Args: 

43 data_source: Data source for table operations 

44 storage: Function to store user preferences 

45 retriever: Function to retrieve user preferences 

46 """ 

47 self.data_source = data_source 

48 self._storage = storage 

49 self._retriever = retriever 

50 

51 # Initialize sub-managers 

52 self.visibility_manager = ColumnVisibilityManager( 

53 ColumnVisibilityConfig(), 

54 storage, 

55 retriever, 

56 ) 

57 self.density_manager = DensityManager( 

58 DensityConfig(), 

59 storage, 

60 retriever, 

61 ) 

62 self.shortcut_manager = KeyboardShortcutManager() 

63 

64 # Action collections 

65 self._actions: dict[str, HeaderAction] = {} 

66 self._action_groups: dict[str, list[str]] = {} 

67 

68 # Initialize with default actions 

69 self._initialize_default_actions() 

70 

71 def _initialize_default_actions(self) -> None: 

72 """Initialize default header actions.""" 

73 # Add basic actions 

74 for action in BASIC_ACTIONS: 

75 self.add_action(action) 

76 

77 # Add import/export actions 

78 for action in IMPORT_EXPORT_ACTIONS: 

79 self.add_action(action) 

80 

81 # Add utility actions 

82 for action in UTILITY_ACTIONS: 

83 self.add_action(action) 

84 

85 # Add bulk actions (initially hidden) 

86 for action in BULK_ACTIONS: 

87 self.add_action(action) 

88 

89 def add_action(self, action: HeaderAction) -> None: 

90 """Add a header action.""" 

91 self._actions[action.name] = action 

92 self.shortcut_manager.register_action(action) 

93 

94 def remove_action(self, action_name: str) -> None: 

95 """Remove a header action.""" 

96 if action_name in self._actions: 

97 action = self._actions[action_name] 

98 if action.keyboard_shortcut: 

99 self.shortcut_manager.unregister_action(action.keyboard_shortcut) 

100 del self._actions[action_name] 

101 

102 def get_action(self, action_name: str) -> HeaderAction | None: 

103 """Get a header action by name.""" 

104 return self._actions.get(action_name) 

105 

106 def get_all_actions(self) -> list[HeaderAction]: 

107 """Get all header actions.""" 

108 return list(self._actions.values()) 

109 

110 def get_visible_actions(self, position: str | None = None) -> list[HeaderAction]: 

111 """Get actions that are currently visible.""" 

112 actions = [action for action in self._actions.values() if action.visible()] 

113 if position: 

114 actions = [action for action in actions if action.position == position] 

115 return actions 

116 

117 async def execute_action(self, action_name: str) -> Any: 

118 """Execute a header action.""" 

119 action = self.get_action(action_name) 

120 if action and action.handler: 

121 result = action.handler() 

122 # If the result is a coroutine, await it 

123 if hasattr(result, "__await__"): 

124 return await result 

125 return result 

126 return None 

127 

128 def handle_keyboard_shortcut(self, shortcut: str) -> HeaderAction | None: 

129 """Handle a keyboard shortcut.""" 

130 return self.shortcut_manager.get_action_for_shortcut(shortcut) 

131 

132 # Column visibility methods 

133 def show_column(self, column: str) -> None: 

134 """Show a column.""" 

135 self.visibility_manager.show_column(column) 

136 

137 def hide_column(self, column: str) -> None: 

138 """Hide a column.""" 

139 self.visibility_manager.hide_column(column) 

140 

141 def toggle_column_visibility(self, column: str) -> None: 

142 """Toggle column visibility.""" 

143 self.visibility_manager.toggle_column(column) 

144 

145 def get_visible_columns(self) -> set[str]: 

146 """Get visible columns.""" 

147 return self.visibility_manager.visible_columns 

148 

149 def is_column_visible(self, column: str) -> bool: 

150 """Check if column is visible.""" 

151 return self.visibility_manager.is_column_visible(column) 

152 

153 # Density methods 

154 def set_table_density(self, density: TableDensity) -> None: 

155 """Set table density.""" 

156 self.density_manager.set_density(density) 

157 

158 def cycle_table_density(self) -> TableDensity: 

159 """Cycle to next density option.""" 

160 return self.density_manager.cycle_density() 

161 

162 def get_current_density(self) -> TableDensity: 

163 """Get current table density.""" 

164 return self.density_manager.current_density 

165 

166 def get_density_css_class(self) -> str: 

167 """Get CSS class for current density.""" 

168 return self.density_manager.get_css_class() 

169 

170 # Bulk action management 

171 def update_bulk_actions_visibility(self, has_selection: bool) -> None: 

172 """Update visibility of bulk actions based on selection.""" 

173 for action_name in ["bulk_delete", "bulk_edit"]: 

174 action = self.get_action(action_name) 

175 if action: 

176 # Create new action with updated visibility 

177 updated_action = HeaderAction( 

178 name=action.name, 

179 label=action.label, 

180 handler=action.handler, 

181 icon=action.icon, 

182 style=action.style, 

183 url=action.url, 

184 method=action.method, 

185 open_in_modal=action.open_in_modal, 

186 keyboard_shortcut=action.keyboard_shortcut, 

187 visible=lambda: has_selection, 

188 disabled=action.disabled, 

189 tooltip=action.tooltip, 

190 badge=action.badge, 

191 position=action.position, 

192 metadata=action.metadata, 

193 ) 

194 self.add_action(updated_action) 

195 

196 # Data source operations 

197 async def refresh_data(self) -> list[Any]: 

198 """Refresh table data.""" 

199 if self.data_source: 

200 return await self.data_source.refresh() 

201 return [] 

202 

203 async def create_record(self, data: dict[str, Any]) -> Any: 

204 """Create a new record.""" 

205 if self.data_source: 

206 return await self.data_source.create(data) 

207 return None 

208 

209 async def import_data(self, file_path: str, file_format: str = "csv") -> int: 

210 """Import data from file.""" 

211 if self.data_source: 

212 return await self.data_source.import_data(file_path, file_format) 

213 return 0 

214 

215 async def export_data(self, file_format: str = "csv") -> str: 

216 """Export data to file.""" 

217 if self.data_source: 

218 return await self.data_source.export_all(file_format) 

219 return "" 

220 

221 # Configuration methods 

222 def configure_visibility( 

223 self, 

224 enabled: bool = True, 

225 default_visible: list[str] | None = None, 

226 always_visible: list[str] | None = None, 

227 save_preference: bool = True, 

228 ) -> None: 

229 """Configure column visibility settings.""" 

230 config = ColumnVisibilityConfig( 

231 enabled=enabled, 

232 default_visible=default_visible or [], 

233 always_visible=always_visible or [], 

234 save_preference=save_preference, 

235 ) 

236 self.visibility_manager = ColumnVisibilityManager( 

237 config, 

238 self._storage, 

239 self._retriever, 

240 ) 

241 

242 def configure_density( 

243 self, 

244 enabled: bool = True, 

245 default: TableDensity = TableDensity.NORMAL, 

246 options: list[TableDensity] | None = None, 

247 save_preference: bool = True, 

248 ) -> None: 

249 """Configure table density settings.""" 

250 config = DensityConfig( 

251 enabled=enabled, 

252 default=default, 

253 options=options 

254 or [ 

255 TableDensity.COMPACT, 

256 TableDensity.NORMAL, 

257 TableDensity.COMFORTABLE, 

258 ], 

259 save_preference=save_preference, 

260 ) 

261 self.density_manager = DensityManager( 

262 config, 

263 self._storage, 

264 self._retriever, 

265 ) 

266 

267 # Standard actions 

268 def add_create_action(self, **kwargs) -> None: 

269 """Add a create action.""" 

270 from lexigram.admin.actions.header_manager.actions import create_create_action 

271 

272 action = create_create_action(**kwargs) 

273 self.add_action(action) 

274 

275 def add_import_action(self, **kwargs) -> None: 

276 """Add an import action.""" 

277 from lexigram.admin.actions.header_manager.actions import create_import_action 

278 

279 action = create_import_action(**kwargs) 

280 self.add_action(action) 

281 

282 def add_export_action(self, **kwargs) -> None: 

283 """Add an export action.""" 

284 from lexigram.admin.actions.header_manager.actions import create_export_action 

285 

286 action = create_export_action(**kwargs) 

287 if self.data_source: 

288 action = HeaderAction( 

289 name=action.name, 

290 label=action.label, 

291 handler=self.export_data, 

292 icon=action.icon, 

293 style=action.style, 

294 url=action.url, 

295 method=action.method, 

296 open_in_modal=action.open_in_modal, 

297 keyboard_shortcut=action.keyboard_shortcut, 

298 visible=action.visible, 

299 disabled=action.disabled, 

300 tooltip=action.tooltip, 

301 badge=action.badge, 

302 position=action.position, 

303 metadata=action.metadata, 

304 ) 

305 self.add_action(action) 

306 

307 def add_refresh_action(self, **kwargs) -> None: 

308 """Add a refresh action.""" 

309 from lexigram.admin.actions.header_manager.actions import create_refresh_action 

310 

311 action = create_refresh_action(**kwargs) 

312 if self.data_source: 

313 action = HeaderAction( 

314 name=action.name, 

315 label=action.label, 

316 handler=self.refresh_data, 

317 icon=action.icon, 

318 style=action.style, 

319 url=action.url, 

320 method=action.method, 

321 open_in_modal=action.open_in_modal, 

322 keyboard_shortcut=action.keyboard_shortcut, 

323 visible=action.visible, 

324 disabled=action.disabled, 

325 tooltip=action.tooltip, 

326 badge=action.badge, 

327 position=action.position, 

328 metadata=action.metadata, 

329 ) 

330 self.add_action(action) 

331 

332 def add_custom_action( 

333 self, 

334 name: str, 

335 label: str, 

336 handler: Callable[[], Any] | None = None, 

337 **kwargs, 

338 ) -> None: 

339 """Add a custom header action.""" 

340 action = HeaderAction( 

341 name=name, 

342 label=label, 

343 handler=handler, 

344 icon=kwargs.get("icon"), 

345 style=kwargs.get("style", HeaderActionStyle.SECONDARY), 

346 url=kwargs.get("url"), 

347 method=kwargs.get("method"), # type: ignore[arg-type] 

348 open_in_modal=kwargs.get("open_in_modal", False), 

349 keyboard_shortcut=kwargs.get("keyboard_shortcut"), 

350 visible=kwargs.get("visible", lambda: True), 

351 disabled=kwargs.get("disabled", lambda: False), 

352 tooltip=kwargs.get("tooltip"), 

353 badge=kwargs.get("badge"), 

354 position=kwargs.get("position", "end"), 

355 metadata=kwargs.get("metadata", {}), 

356 ) 

357 self.add_action(action) 

358 

359 # Utility methods 

360 def get_action_groups(self) -> dict[str, list[HeaderAction]]: 

361 """Get actions grouped by position.""" 

362 groups: dict[str, list[HeaderAction]] = {"start": [], "end": []} 

363 for action in self.get_visible_actions(): 

364 position = action.position 

365 if position in groups: 

366 groups[position].append(action) 

367 return groups 

368 

369 def get_registered_shortcuts(self) -> dict[str, str]: 

370 """Get all registered keyboard shortcuts.""" 

371 return self.shortcut_manager.get_registered_shortcuts() 

372 

373 def clear_all_actions(self) -> None: 

374 """Clear all actions and shortcuts.""" 

375 self._actions.clear() 

376 self.shortcut_manager.clear_all_shortcuts()