Coverage for src / lexigram / admin / actions / standard.py: 42%
121 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"""Standard action implementations for lexigram-admin.
3Provides ready-to-use RowAction, BulkAction, and HeaderAction subclasses
4that wrap common data-source operations such as edit, view, delete, create,
5and bulk delete. Each action comes with sensible defaults for name, label,
6icon, and color.
7"""
9from __future__ import annotations
11from typing import Any
13from lexigram.admin.actions.base import BulkAction, HeaderAction, RowAction
14from lexigram.admin.actions.types import ActionColor, ActionContext, ConfirmationConfig
15from lexigram.result import Ok, Result
16from lexigram.ui import Zones
19class EditAction(RowAction):
20 """Edit a single record."""
22 def __init__(
23 self,
24 name: str = "edit",
25 label: str | None = None,
26 ) -> None:
27 super().__init__(
28 name=name,
29 label=label or "Edit",
30 icon="pencil",
31 color=ActionColor.PRIMARY,
32 )
34 def _get_url(self, record: Any, ctx: ActionContext) -> str | None:
35 record_id = self._get_record_id(record)
36 if not record_id:
37 return None
38 prefix = ctx.resource_prefix or f"/{ctx.resource_name}"
39 return f"{prefix}/{record_id}/edit"
41 def _get_htmx_attrs(
42 self, url: str, record: Any, ctx: ActionContext
43 ) -> dict[str, str]:
44 return {
45 "hx-get": url,
46 "hx-target": Zones.SLIDE_OVER.selector,
47 "hx-swap": Zones.SLIDE_OVER.swap_mode.value,
48 }
50 async def execute(self, record: Any, ctx: ActionContext) -> Result[Any, Any]:
51 return Ok({"message": f"Edited {record}"})
54class ViewAction(RowAction):
55 """View a single record."""
57 def __init__(
58 self,
59 name: str = "view",
60 label: str | None = None,
61 ) -> None:
62 super().__init__(
63 name=name,
64 label=label or "View",
65 icon="eye",
66 color=ActionColor.GRAY,
67 )
69 def _get_url(self, record: Any, ctx: ActionContext) -> str | None:
70 record_id = self._get_record_id(record)
71 if not record_id:
72 return None
73 prefix = ctx.resource_prefix or f"/{ctx.resource_name}"
74 return f"{prefix}/{record_id}"
76 async def execute(self, record: Any, ctx: ActionContext) -> Result[Any, Any]:
77 return Ok({"message": f"Viewed {record}"})
80class DeleteAction(RowAction):
81 """Delete a single record with confirmation."""
83 def __init__(
84 self,
85 name: str = "delete",
86 label: str | None = None,
87 confirm_title: str = "Delete Record",
88 confirm_message: str | None = None,
89 ) -> None:
90 super().__init__(
91 name=name,
92 label=label or "Delete",
93 icon="trash",
94 color=ActionColor.DANGER,
95 )
96 self._confirm_title = confirm_title
97 self._confirm_message = confirm_message or (
98 "Are you sure you want to delete this record? This action cannot be undone."
99 )
101 def _get_url(self, record: Any, ctx: ActionContext) -> str | None:
102 record_id = self._get_record_id(record)
103 if not record_id:
104 return None
105 prefix = ctx.resource_prefix or f"/{ctx.resource_name}"
106 return f"{prefix}/{record_id}/delete-confirm"
108 def _get_htmx_attrs(
109 self, url: str, record: Any, ctx: ActionContext
110 ) -> dict[str, str]:
111 return {
112 "hx-get": url,
113 "hx-target": Zones.SLIDE_OVER.selector,
114 "hx-swap": Zones.SLIDE_OVER.swap_mode.value,
115 "hx-push-url": "false",
116 }
118 def confirm(self) -> ConfirmationConfig | None:
119 return ConfirmationConfig(
120 title=self._confirm_title,
121 message=self._confirm_message,
122 style=ActionColor.DANGER,
123 )
125 async def execute(self, record: Any, ctx: ActionContext) -> Result[Any, Any]:
126 return Ok({"message": f"Deleted {record}", "deleted": True})
129class CreateAction(HeaderAction):
130 """Create a new record."""
132 def __init__(
133 self,
134 name: str = "create",
135 label: str | None = None,
136 ) -> None:
137 super().__init__(
138 name=name,
139 label=label or "Create",
140 icon="plus",
141 color=ActionColor.PRIMARY,
142 )
144 def _get_htmx_attrs(
145 self, url: str, record: None, ctx: ActionContext
146 ) -> dict[str, str]:
147 return {
148 "hx-get": url,
149 "hx-target": Zones.SLIDE_OVER.selector,
150 "hx-swap": Zones.SLIDE_OVER.swap_mode.value,
151 }
153 async def execute(self, record: None, ctx: ActionContext) -> Result[Any, Any]:
154 return Ok({"message": "Created new record"})
157class DeleteBulkAction(BulkAction):
158 """Delete multiple selected records."""
160 def __init__(
161 self,
162 name: str = "delete",
163 label: str | None = None,
164 ) -> None:
165 super().__init__(
166 name=name,
167 label=label or "Delete Selected",
168 icon="trash",
169 color=ActionColor.DANGER,
170 )
172 def _get_htmx_attrs(
173 self, url: str, records: list[Any], ctx: ActionContext
174 ) -> dict[str, str]:
175 # Open bulk delete confirmation slide-over instead of native hx-confirm
176 prefix = ctx.resource_prefix or f"/{ctx.resource_name}"
177 confirm_url = f"{prefix}/bulk-delete-confirm"
178 return {
179 "hx-get": confirm_url,
180 "hx-target": "#slide-over-container",
181 "hx-swap": "innerHTML",
182 "hx-push-url": "false",
183 "hx-include": "#lexigram-table [name='ids']:checked",
184 }
186 def confirm(self) -> ConfirmationConfig | None:
187 return ConfirmationConfig(
188 title="Delete Selected Records",
189 message="Are you sure you want to delete the selected records? "
190 "This action cannot be undone.",
191 style=ActionColor.DANGER,
192 )
194 async def execute(self, records: list[Any], ctx: ActionContext) -> Result[Any, Any]:
195 count = len(records)
196 return Ok({"message": f"Deleted {count} record(s)", "deleted_count": count})
199class CloneAction(RowAction):
200 """Clone a single record."""
202 def __init__(
203 self,
204 name: str = "clone",
205 label: str | None = None,
206 ) -> None:
207 super().__init__(
208 name=name,
209 label=label or "Clone",
210 icon="copy",
211 color=ActionColor.SECONDARY,
212 )
214 def _get_url(self, record: Any, ctx: ActionContext) -> str | None:
215 record_id = self._get_record_id(record)
216 if not record_id:
217 return None
218 prefix = ctx.resource_prefix or f"/{ctx.resource_name}"
219 return f"{prefix}/{record_id}/clone"
221 async def execute(self, record: Any, ctx: ActionContext) -> Result[Any, Any]:
222 return Ok({"message": f"Cloned {record}"})
225class RestoreAction(RowAction):
226 """Restore a single soft-deleted record."""
228 def __init__(
229 self,
230 name: str = "restore",
231 label: str | None = None,
232 ) -> None:
233 super().__init__(
234 name=name,
235 label=label or "Restore",
236 icon="rotate-ccw",
237 color=ActionColor.SUCCESS,
238 )
240 def _get_url(self, record: Any, ctx: ActionContext) -> str | None:
241 record_id = self._get_record_id(record)
242 if not record_id:
243 return None
244 prefix = ctx.resource_prefix or f"/{ctx.resource_name}"
245 return f"{prefix}/{record_id}/restore"
247 async def execute(self, record: Any, ctx: ActionContext) -> Result[Any, Any]:
248 return Ok({"message": f"Restored {record}"})
251class PurgeAction(RowAction):
252 """Permanently delete a soft-deleted record."""
254 def __init__(
255 self,
256 name: str = "purge",
257 label: str | None = None,
258 confirm_title: str = "Permanently Delete Record",
259 confirm_message: str | None = None,
260 ) -> None:
261 super().__init__(
262 name=name,
263 label=label or "Purge",
264 icon="trash-2",
265 color=ActionColor.DANGER,
266 )
267 self._confirm_title = confirm_title
268 self._confirm_message = confirm_message or (
269 "Are you sure you want to permanently delete this record? "
270 "This action cannot be undone."
271 )
273 def _get_url(self, record: Any, ctx: ActionContext) -> str | None:
274 record_id = self._get_record_id(record)
275 if not record_id:
276 return None
277 prefix = ctx.resource_prefix or f"/{ctx.resource_name}"
278 return f"{prefix}/{record_id}/purge"
280 def _get_htmx_attrs(
281 self, url: str, record: Any, ctx: ActionContext
282 ) -> dict[str, str]:
283 attrs: dict[str, str] = {
284 "hx-delete": url,
285 "hx-target": "#table-data",
286 "hx-swap": "innerHTML",
287 }
288 confirmation = self.confirm()
289 if confirmation and confirmation.message:
290 attrs["hx-confirm"] = confirmation.message
291 return attrs
293 def confirm(self) -> ConfirmationConfig | None:
294 return ConfirmationConfig(
295 title=self._confirm_title,
296 message=self._confirm_message,
297 style=ActionColor.DANGER,
298 )
300 async def execute(self, record: Any, ctx: ActionContext) -> Result[Any, Any]:
301 return Ok({"message": f"Purged {record}", "purged": True})
304class ExportAction(RowAction):
305 """Export a single record."""
307 def __init__(
308 self,
309 name: str = "export",
310 label: str | None = None,
311 ) -> None:
312 super().__init__(
313 name=name,
314 label=label or "Export",
315 icon="download",
316 color=ActionColor.GRAY,
317 )
319 async def execute(self, record: Any, ctx: ActionContext) -> Result[Any, Any]:
320 return Ok({"message": f"Exported {record}"})
323class ExportBulkAction(BulkAction):
324 """Export multiple selected records."""
326 def __init__(
327 self,
328 name: str = "export",
329 label: str | None = None,
330 ) -> None:
331 super().__init__(
332 name=name,
333 label=label or "Export Selected",
334 icon="download",
335 color=ActionColor.GRAY,
336 )
338 async def execute(self, records: list[Any], ctx: ActionContext) -> Result[Any, Any]:
339 count = len(records)
340 return Ok({"message": f"Exported {count} record(s)", "exported_count": count})
343__all__ = [
344 "CloneAction",
345 "CreateAction",
346 "DeleteAction",
347 "DeleteBulkAction",
348 "EditAction",
349 "ExportAction",
350 "ExportBulkAction",
351 "PurgeAction",
352 "RestoreAction",
353 "ViewAction",
354]