Skip to content

Commit 35d49ca

Browse files
committed
ErrorMessageAIO update
1 parent 1acf645 commit 35d49ca

4 files changed

Lines changed: 124 additions & 102 deletions

File tree

crystal_toolkit/components/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
)
77
from crystal_toolkit.components.diffraction import XRayDiffractionComponent
88
from crystal_toolkit.components.diffraction_tem import TEMDiffractionComponent
9-
from crystal_toolkit.components.error_msg import ErrorMessage
9+
from crystal_toolkit.components.error_msg import ErrorMessageAIO
1010
from crystal_toolkit.components.fermi_surface import FermiSurfaceComponent
1111
from crystal_toolkit.components.localenv import LocalEnvironmentPanel
1212
from crystal_toolkit.components.phase_diagram import (

crystal_toolkit/components/error_msg.py

Lines changed: 100 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,32 @@
11
"""
22
Author: Sheng Pang
3+
Modifier: Min-Hsueh Chiu
34
45
Message Snake - A reusable Dash notification snackbar component.
56
67
Provides fixed-position toast notifications callable from any page.
78
Supports fade-in/fade-out animations, auto-dismiss, and manual close.
89
910
Usage:
10-
from mp_web.layouts.message_snake.message_snake import message_snake, register_message_snake_callbacks
11+
from crystal_toolkit.components.error_msg import ErrorMessageAIO
1112
1213
# 1. Include in layout
13-
notification = message_snake(
14-
message="Operation completed successfully!",
15-
msg_type="success",
16-
position="bottom-right",
17-
snake_id="my-toast",
18-
)
14+
ErrorMessageAIO(
15+
"Invalid composition input!",
16+
aio_id=self.id("invalid-comp-alarm"),
17+
msg_type="error",
18+
).layout(),
1919
20-
# 2. Register callbacks (in generate_callbacks or app setup)
21-
register_message_snake_callbacks(app, "my-toast")
20+
# 2. Add to callback:
21+
Output(ErrorMessage.ids.visible(self.id("invalid-comp-alarm")), "data"),
22+
# Return True to display the message, and False to hide it.
23+
24+
Note: Do not need to register callbacks as using All-in-one pattern
2225
"""
2326

2427
from __future__ import annotations
2528

26-
from dash import Input, Output, dcc, html
29+
from dash import MATCH, Input, Output, callback, ctx, dcc, html
2730

2831
from crystal_toolkit.core.mpcomponent import MPComponent
2932

@@ -91,11 +94,47 @@
9194
}
9295

9396

94-
class ErrorMessage(MPComponent):
97+
class ErrorMessageAIO(MPComponent):
98+
class ids:
99+
wrapper = lambda aio_id: {
100+
"component": "ErrorMessageAIO",
101+
"subcomponents": "wrapper",
102+
"aio_id": aio_id,
103+
}
104+
close_button = lambda aio_id: {
105+
"component": "ErrorMessageAIO",
106+
"subcomponents": "close_button",
107+
"aio_id": aio_id,
108+
}
109+
message = lambda aio_id: {
110+
"component": "ErrorMessageAIO",
111+
"subcomponents": "message",
112+
"aio_id": aio_id,
113+
}
114+
div = lambda aio_id: {
115+
"component": "ErrorMessageAIO",
116+
"subcomponents": "div",
117+
"aio_id": aio_id,
118+
}
119+
timer = lambda aio_id: {
120+
"component": "ErrorMessageAIO",
121+
"subcomponents": "timer",
122+
"aio_id": aio_id,
123+
}
124+
visible = lambda aio_id: {
125+
"component": "ErrorMessageAIO",
126+
"subcomponents": "visible",
127+
"aio_id": aio_id,
128+
}
129+
130+
ids = ids
131+
132+
# _callbacks_registered = False # no instance registry
133+
95134
def __init__(
96135
self,
97136
message,
98-
id,
137+
aio_id,
99138
msg_type,
100139
position="bottom-right",
101140
style=None,
@@ -125,13 +164,9 @@ def __init__(
125164
z_index (int): CSS z-index for layering.
126165
auto_dismiss_ms (int): Auto-dismiss delay in milliseconds. Defaults to 50000 (50s).
127166
128-
Returns:
129-
html.Div: A wrapper containing the notification, CSS animation, and auto-dismiss timer.
130167
"""
131-
# super().__init__(id=id)
132-
self.snake_id = id
133-
print("aaaaa")
134-
print(self.snake_id)
168+
169+
self.snake_id = aio_id
135170
self.show_icon = show_icon
136171
self.msg_type = msg_type
137172
self.message = message
@@ -187,7 +222,9 @@ def _sub_layouts(self):
187222

188223
# Message text
189224
children.append(
190-
html.Span(self.message, id=f"{self.snake_id}-message", style={"flex": "1"})
225+
html.Span(
226+
self.message, id=self.ids.message(self.snake_id), style={"flex": "1"}
227+
)
191228
)
192229

193230
# Close button
@@ -206,21 +243,21 @@ def _sub_layouts(self):
206243
"opacity": "0.8",
207244
"flexShrink": "0",
208245
},
209-
id=f"{self.snake_id}-close",
246+
id=self.ids.close_button(self.snake_id),
210247
n_clicks=0,
211248
)
212249
)
213250

214251
# Notification div
215252
notification_div = html.Div(
216253
children=children,
217-
id=f"{self.snake_id}-div",
254+
id=self.ids.div(self.snake_id),
218255
style=self.notification_style,
219256
)
220257

221258
# Auto-dismiss interval timer (fires once after auto_dismiss_ms)
222259
interval = dcc.Interval(
223-
id=f"{self.snake_id}-timer",
260+
id=self.ids.timer(self.snake_id),
224261
interval=self.auto_dismiss_ms,
225262
n_intervals=0,
226263
max_intervals=1,
@@ -232,55 +269,53 @@ def layout(self) -> html.Div:
232269
sub_layouts = self._sub_layouts
233270
return html.Div(
234271
[
272+
dcc.Store(id=self.ids.visible(self.snake_id), data=False),
235273
sub_layouts["notification_div"],
236274
sub_layouts["interval"],
237275
],
238-
id=self.snake_id,
276+
id=self.ids.wrapper(self.snake_id),
239277
style={"display": "none"},
240278
)
241279

242-
def generate_callbacks(self, app, cache) -> None:
243-
"""Register auto-dismiss and close button callbacks for a message snake.
244-
245-
Must be called once per snake_id during app setup (e.g., in generate_callbacks).
246-
247-
Args:
248-
app: The Dash app instance.
249-
snake_id (str): The snake_id used when creating the message_snake.
250-
"""
280+
@callback(
281+
Output(ids.wrapper(MATCH), "style"),
282+
Input(ids.visible(MATCH), "data"),
283+
Input(ids.close_button(MATCH), "n_clicks"),
284+
prevent_initial_call=True,
285+
)
286+
def sync_message(command_visible, close_clicks):
287+
triggered = ctx.triggered_id
251288

252-
@app.callback(
253-
Output(self.snake_id, "style"),
254-
Input(f"{self.snake_id}-close", "n_clicks"),
255-
prevent_initial_call=True,
256-
)
257-
def close_message(n_clicks):
258-
print("what?")
289+
if (
290+
isinstance(triggered, dict)
291+
and triggered.get("subcomponents") == "close_button"
292+
):
259293
return {"display": "none"}
260294

261-
"""
262-
@app.callback(
263-
Output(self.id(self.snake_id), "style"),
264-
Input(self.id(f"{self.snake_id}-timer"), "n_intervals"),
265-
Input(self.id(f"{self.snake_id}-close"), "n_clicks"),
266-
State(self.id(self.snake_id), "style"),
267-
prevent_initial_call=True,
268-
)
269-
def _dismiss_message_snake(n_intervals, n_clicks, current_style):
270-
# Fade out and hide the message snake on timer or close click.
271-
if not current_style:
272-
raise PreventUpdate
273-
274-
ctx = callback_context
275-
if not ctx.triggered:
276-
raise PreventUpdate
277-
278-
# Apply fade-out: transition opacity to 0, then hide
279-
new_style = {**current_style}
280-
new_style["transition"] = "opacity 0.4s ease"
281-
new_style["opacity"] = "0"
282-
new_style["pointerEvents"] = "none"
283-
# Override the fade-in animation so it does not reset
284-
new_style["animation"] = "none"
285-
return new_style
286-
"""
295+
return {"display": "block"} if command_visible else {"display": "none"}
296+
297+
"""
298+
@callback(
299+
Output(ids.wrapper(MATCH), "style"),
300+
Input(ids.timer(MATCH), "n_intervals"),
301+
Input(ids.close_button(MATCH), "n_clicks"),
302+
State(ids.wrapper(MATCH), "style"),
303+
prevent_initial_call=True,
304+
)
305+
def _dismiss_message_snake(n_intervals, n_clicks, current_style):
306+
# Fade out and hide the message snake on timer or close click.
307+
if not current_style:
308+
raise PreventUpdate
309+
310+
if not ctx.triggered:
311+
raise PreventUpdate
312+
313+
# Apply fade-out: transition opacity to 0, then hide
314+
new_style = {**current_style}
315+
new_style["transition"] = "opacity 0.4s ease"
316+
new_style["opacity"] = "0"
317+
new_style["pointerEvents"] = "none"
318+
# Override the fade-in animation so it does not reset
319+
new_style["animation"] = "none"
320+
return new_style
321+
"""

crystal_toolkit/components/pourbaix.py

Lines changed: 23 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
from shapely.geometry import Polygon
1818

1919
import crystal_toolkit.helpers.layouts as ctl
20-
from crystal_toolkit.components.error_msg import ErrorMessage
20+
from crystal_toolkit.components.error_msg import ErrorMessageAIO
2121
from crystal_toolkit.core.mpcomponent import MPComponent
2222

2323
try:
@@ -444,17 +444,6 @@ def get_figure_div(self, figure=None):
444444

445445
@property
446446
def _sub_layouts(self) -> dict[str, Component]:
447-
invalid_comp_error = ErrorMessage(
448-
"Invalid composition input!",
449-
id=self.id("invalid-comp-alarm"),
450-
msg_type="error",
451-
)
452-
invalid_conc_error = ErrorMessage(
453-
"Invalid concentration input!",
454-
id=self.id("invalid-conc-alarm"),
455-
msg_type="error",
456-
)
457-
458447
options = html.Div(
459448
[
460449
self.get_bool_input(
@@ -470,13 +459,18 @@ def _sub_layouts(self) -> dict[str, Component]:
470459
),
471460
html.Div(
472461
[
462+
ErrorMessageAIO(
463+
"Invalid composition input!",
464+
aio_id=self.id("invalid-comp-alarm"),
465+
msg_type="error",
466+
).layout(),
467+
ErrorMessageAIO(
468+
"Invalid concentration input!",
469+
aio_id=self.id("invalid-conc-alarm"),
470+
msg_type="error",
471+
).layout(),
473472
html.Div(
474473
[
475-
invalid_comp_error.layout(),
476-
# self.get_alarm_window(
477-
# self.id("invalid-comp-alarm"),
478-
# message="Illegal composition entry!",
479-
# ),
480474
html.Div(
481475
[
482476
html.H5(
@@ -533,13 +527,7 @@ def _sub_layouts(self) -> dict[str, Component]:
533527
style={"display": "none"},
534528
),
535529
html.Div(
536-
[
537-
invalid_conc_error.layout(),
538-
# self.get_alarm_window(
539-
# id=self.id("invalid-conc-alarm"),
540-
# message=f"Illegal concentration entry! Must be between {MIN_CONCENTRATION} and {MAX_CONCENTRATION} M",
541-
# ),
542-
],
530+
[],
543531
id=self.id("conc-panel"),
544532
style={"display": "none"},
545533
),
@@ -808,8 +796,8 @@ def get_pourbaix_diagram(pourbaix_entries, **kwargs):
808796

809797
@app.callback(
810798
Output(self.id("graph-panel"), "children"),
811-
Output(self.id("invalid-comp-alarm"), "style"),
812-
Output(self.id("invalid-conc-alarm"), "style"),
799+
Output(ErrorMessageAIO.ids.visible(self.id("invalid-comp-alarm")), "data"),
800+
Output(ErrorMessageAIO.ids.visible(self.id("invalid-conc-alarm")), "data"),
813801
Output(self.id("display-composition"), "children"),
814802
Input(self.id(), "data"),
815803
Input(self.id("display-composition"), "children"),
@@ -848,8 +836,8 @@ def make_figure(
848836
logger.error("Invalid composition input!")
849837
return (
850838
self.get_figure_div(),
851-
{"display": "block"},
852-
{"display": "none"},
839+
True,
840+
False,
853841
"",
854842
)
855843
try:
@@ -865,8 +853,8 @@ def make_figure(
865853
logger.error("Invalid composition input!")
866854
return (
867855
self.get_figure_div(),
868-
{"display": "block"},
869-
{"display": "none"},
856+
True,
857+
False,
870858
"",
871859
)
872860

@@ -897,12 +885,12 @@ def make_figure(
897885
for key, val in kwargs.items():
898886
if "conc" in key: # keys are encoded like "conc-Ag"
899887
if val is None:
900-
print("oooooooo")
888+
logger.error("Invalid concentration input!")
901889
# if the input is out of pre-defined range, Input will get None
902890
return (
903891
self.get_figure_div(),
904-
{"display": "none"},
905-
{"display": "block"},
892+
False,
893+
True,
906894
"",
907895
)
908896

@@ -931,7 +919,7 @@ def make_figure(
931919

932920
return (
933921
self.get_figure_div(figure=figure),
934-
{"display": "none"},
935-
{"display": "none"},
922+
False,
923+
False,
936924
html.Small(f"Pourbaix composition set to {unicodeify(formula)}."),
937925
)

crystal_toolkit/core/plugin.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,6 @@ def crystal_toolkit_layout(self, layout) -> html.Div:
100100
layout.children += stores_to_add
101101

102102
for component in mpcomp_module.MPComponent._callbacks_to_generate:
103-
print(component.__module__)
104103
component.generate_callbacks(self.app, self.cache)
105104

106105
return layout

0 commit comments

Comments
 (0)