Coverage for src/lexigram/admin/ui/molecules/toast_notification.py: 7%
54 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-24 23:31 +0800
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-24 23:31 +0800
1"""Fluent toast notification builder (Filament ``Notification::make()`` parity).
3Builds a ``ToastData`` for ``ServerToastChannel`` through a chainable API, and
4can either render the toast HTML directly or flash it to the admin session for
5the next request. This is intentionally distinct from the lifecycle-email
6``Notification`` model in ``lexigram.admin.services.notifications`` — it only
7concerns the ephemeral admin toast surface.
8"""
10from __future__ import annotations
12from typing import Self
14from lexigram.admin.state.context import flash
15from lexigram.ui import ServerToastChannel, ToastData, ToastType
17_TOAST_CATEGORY = {
18 ToastType.SUCCESS: "success",
19 ToastType.ERROR: "error",
20 ToastType.WARNING: "warning",
21 ToastType.INFO: "info",
22}
25class ToastNotification:
26 """Chainable toast builder targeting ``ServerToastChannel``.
28 Args:
29 message: Default toast message.
31 Example:
32 ```python
33 ToastNotification.make("Resource updated")
34 .success()
35 .title("Saved")
36 .duration(3000)
37 .send()
39 html = ToastNotification.make("Export queued")
40 .info()
41 .persistent()
42 .actions([{"label": "View", "onclick": "openReport()"}])
43 .render()
44 ```
45 """
47 def __init__(self, message: str = "") -> None:
48 """Initialize the builder with an empty toast."""
49 self._data = ToastData(message=message)
51 @classmethod
52 def make(cls, message: str = "") -> Self:
53 """Start a new toast notification.
55 Args:
56 message: Initial toast message.
58 Returns:
59 A new builder instance.
60 """
61 return cls(message)
63 def title(self, title: str) -> Self:
64 """Set the toast heading.
66 Args:
67 title: Heading text.
69 Returns:
70 The builder for chaining.
71 """
72 self._data.title = title
73 return self
75 def message(self, message: str) -> Self:
76 """Set the toast body text.
78 Args:
79 message: Body text.
81 Returns:
82 The builder for chaining.
83 """
84 self._data.message = message
85 return self
87 def icon(self, icon: str | None) -> Self:
88 """Override the Lucide icon name for this toast.
90 Args:
91 icon: Lucide icon name, or ``None`` to use the type default.
93 Returns:
94 The builder for chaining.
95 """
96 self._data.icon = icon
97 return self
99 def success(self) -> Self:
100 """Mark the toast as a success notification.
102 Returns:
103 The builder for chaining.
104 """
105 self._data.type = ToastType.SUCCESS
106 return self
108 def error(self) -> Self:
109 """Mark the toast as an error notification.
111 Returns:
112 The builder for chaining.
113 """
114 self._data.type = ToastType.ERROR
115 return self
117 def warning(self) -> Self:
118 """Mark the toast as a warning notification.
120 Returns:
121 The builder for chaining.
122 """
123 self._data.type = ToastType.WARNING
124 return self
126 def info(self) -> Self:
127 """Mark the toast as an informational notification.
129 Returns:
130 The builder for chaining.
131 """
132 self._data.type = ToastType.INFO
133 return self
135 def duration(self, duration_ms: int) -> Self:
136 """Set the auto-dismiss delay.
138 Args:
139 duration_ms: Delay in milliseconds. A non-positive value disables
140 auto-dismiss.
142 Returns:
143 The builder for chaining.
144 """
145 self._data.duration_ms = max(duration_ms, 0)
146 self._data.auto_dismiss = self._data.duration_ms > 0
147 return self
149 def persistent(self) -> Self:
150 """Keep the toast until manually dismissed.
152 Returns:
153 The builder for chaining.
154 """
155 self._data.auto_dismiss = False
156 self._data.duration_ms = 0
157 return self
159 def dismissible(self, value: bool = True) -> Self:
160 """Set whether the toast shows a dismiss button.
162 Args:
163 value: Whether the toast can be dismissed by the user.
165 Returns:
166 The builder for chaining.
167 """
168 self._data.dismissible = value
169 return self
171 def actions(self, actions: list[dict[str, str]]) -> Self:
172 """Attach action buttons to the toast.
174 Args:
175 actions: List of ``{"label": ..., "onclick": ...}`` entries.
177 Returns:
178 The builder for chaining.
179 """
180 self._data.actions = list(actions)
181 return self
183 def to_toast(self) -> ToastData:
184 """Return the configured ``ToastData`` payload.
186 Returns:
187 The toast data this builder has configured so far.
188 """
189 return self._data
191 def render(self) -> str:
192 """Render the toast as standalone HTML.
194 Returns:
195 HTML for a single toast element (works inside the toast container).
196 """
197 return ServerToastChannel().render_toast(self._data)
199 def send(self) -> None:
200 """Flash the toast to the admin session for the next request.
202 Category is derived from the toast type; title/icon/duration are not
203 carried by the flash channel, so use :meth:`render` for full fidelity.
204 Outside a request context this is a no-op.
206 Returns:
207 None.
208 """
209 category = _TOAST_CATEGORY.get(ToastType(self._data.type), "info")
210 flash(self._data.title or self._data.message, category)
213__all__ = ["ToastNotification"]