Skip to content

Commit f50a127

Browse files
authored
Add PersistentDialog component (#199)
* Add PersistentDialog component * Remove dev tag
1 parent 153efde commit f50a127

10 files changed

Lines changed: 548 additions & 406 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
### nova-trame, 1.6.0
2+
3+
* Adds a PersistentDialog component that allows creating persistent dialogs with more customization than default Vuetify VDialogs (thanks to John Duggan).
4+
15
### nova-trame, 1.5.0
26

37
* Add ability to specify actions on DataSelector files (thanks to John Duggan).

docs/api.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,10 @@ General Purpose Components
5050
:members:
5151
:special-members: __new__
5252

53+
.. autoclass:: nova.trame.view.components.PersistentDialog
54+
:members:
55+
:special-members: __new__
56+
5357
.. _api_remotefileinput:
5458

5559
.. autoclass:: nova.trame.view.components.RemoteFileInput

pixi.lock

Lines changed: 428 additions & 404 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "nova-trame"
3-
version = "1.5.0"
3+
version = "1.6.0"
44
description = "A Python Package for injecting curated themes and custom components into Trame applications"
55
authors = [
66
{ name = "John Duggan", email = "dugganjw@ornl.gov" },

src/nova/trame/view/components/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from .execution_buttons import ExecutionButtons
33
from .file_upload import FileUpload
44
from .input_field import InputField
5+
from .persistent_dialog import PersistentDialog
56
from .progress_bar import ProgressBar
67
from .remote_file_input import RemoteFileInput
78
from .tool_outputs import ToolOutputWindows
@@ -12,6 +13,7 @@
1213
"FileUpload",
1314
"InputField",
1415
"ProgressBar",
16+
"PersistentDialog",
1517
"RemoteFileInput",
1618
"ToolOutputWindows",
1719
]
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
"""View implementation for PersistentDialog."""
2+
3+
from typing import Any, Tuple, Union
4+
from warnings import warn
5+
6+
from trame.app import get_server
7+
from trame.widgets import vuetify3 as vuetify
8+
from trame_server.core import State
9+
10+
from nova.trame._internal.utils import get_state_name, get_state_param
11+
12+
13+
class PersistentDialog(vuetify.VDialog):
14+
"""Component for creating a Vuetify dialog that closes on the escape key but not outside clicks."""
15+
16+
def __init__(
17+
self,
18+
v_model: Union[str, Tuple],
19+
close_on_escape: bool = True,
20+
**kwargs: Any,
21+
) -> None:
22+
"""Constructor for PersistentDialog.
23+
24+
For all parameters, tuples have a special syntax. See :ref:`TrameTuple <api_trame_tuple>` for a description of
25+
it.
26+
27+
Parameters
28+
----------
29+
v_model : Union[str, Tuple]
30+
The state variable determining if the dialog is opened or closed.
31+
close_on_escape : bool
32+
If true, then the dialog will still close when the user presses the escape key. Defaults to true. This
33+
parameter does not support binding as dynamic behavior here would present a confusing user experience.
34+
**kwargs
35+
All other arguments will be passed to the underlying
36+
`Dialog component <https://trame.readthedocs.io/en/latest/trame.widgets.vuetify3.html#trame.widgets.vuetify3.VDialog>`_.
37+
38+
Returns
39+
-------
40+
None
41+
"""
42+
self._server = get_server(None, client_type="vue3")
43+
self._v_model = v_model
44+
self._field_name = get_state_param(self.state, v_model)
45+
self._model_name = get_state_name(self._field_name)
46+
47+
if "persistent" in kwargs:
48+
warn(
49+
"PersistentDialog will ignore the provided 'persistent' parameter as it forces persistent=True.",
50+
stacklevel=1,
51+
)
52+
kwargs.pop("persistent", None)
53+
54+
if close_on_escape:
55+
super().__init__(
56+
v_model=self._v_model,
57+
v_on_keydown_esc=(
58+
f"window.trame.state.state.{self._field_name} = false; flushState('{self._model_name}');"
59+
),
60+
persistent=True,
61+
**kwargs,
62+
)
63+
else:
64+
super().__init__(v_model=self._v_model, persistent=True, **kwargs)
65+
66+
@property
67+
def state(self) -> State:
68+
return self._server.state

tests/gallery/models/config.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ class Config(BaseModel):
1616
selected_folder: str = Field(default="", title="Selected Folder")
1717
snackbar: bool = Field(default=True)
1818
active_tab: int = Field(default=0)
19+
dialog_open: bool = Field(default=False)
1920
debounce_rate: int = Field(default=1000, title="Debounce Rate")
2021
debounce: str = Field(
2122
default="",
@@ -59,6 +60,9 @@ def on_throttle(cls, text: str) -> str:
5960

6061
return text
6162

63+
def open_dialog(self) -> None:
64+
self.dialog_open = True
65+
6266

6367
class LocalStorageState(BaseModel):
6468
"""Separate Pydantic model to hold local storage state which can leak in unit tests."""

tests/gallery/view_models/config.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ def append_to_autoscroll(self, value: str) -> None:
2424
def get_local_storage(self) -> str:
2525
return self.local_storage_state.value
2626

27+
def open_persistent_dialog(self) -> None:
28+
self.config.open_dialog()
29+
self.config_bind.update_in_view(self.config)
30+
2731
def set_local_storage(self, value: Optional[str]) -> None:
2832
if value is not None:
2933
self.local_storage_state.value = value

tests/gallery/views/app.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
from nova.common.signals import Signal, ToolCommand, get_signal_id
2424
from nova.mvvm.trame_binding import TrameBinding
2525
from nova.trame import ThemedApp
26-
from nova.trame.view.components import DataSelector, FileUpload, InputField, RemoteFileInput
26+
from nova.trame.view.components import DataSelector, FileUpload, InputField, PersistentDialog, RemoteFileInput
2727
from nova.trame.view.components.execution_buttons import ExecutionButtons
2828
from nova.trame.view.components.ornl import NeutronDataSelector
2929
from nova.trame.view.components.progress_bar import ProgressBar
@@ -217,6 +217,9 @@ def create_ui(self, **kwargs: Any) -> None:
217217
vuetify.VBtn("Open Dialog", v_bind="props")
218218
with vuetify.Template(v_slot_default=True):
219219
vuetify.VCard(title="Dialog")
220+
with PersistentDialog(v_model="config.dialog_open", max_width=500):
221+
vuetify.VCard(title="Dialog")
222+
vuetify.VBtn("Persistent Dialog", click=self.view_model.open_persistent_dialog)
220223
with html.Div():
221224
html.Span("Divider")
222225
vuetify.VDivider()

tests/test_persistent_dialog.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
"""Unit tests for PersistentDialog."""
2+
3+
from trame.app import get_server
4+
from trame_server.core import Server
5+
6+
from nova.trame.view.components import PersistentDialog
7+
from nova.trame.view.theme import ThemedApp
8+
9+
10+
def test_persistent_dialog() -> None:
11+
class MyTrameApp(ThemedApp):
12+
def __init__(self, server: Server = None) -> None:
13+
server = get_server(None, client_type="vue3")
14+
super().__init__(server=server)
15+
self.create_ui()
16+
17+
def create_ui(self) -> None:
18+
with super().create_ui():
19+
dialog = PersistentDialog("config.test", max_width=500)
20+
assert dialog.v_model == "config.test"
21+
assert dialog.v_on_keydown_esc == "window.trame.state.state.config.test = false; flushState('config');"
22+
assert dialog.max_width == 500
23+
24+
dialog = PersistentDialog("config.test2", close_on_escape=False, max_width=505)
25+
assert dialog.v_model == "config.test2"
26+
assert dialog.v_on_keydown_esc is None
27+
assert dialog.max_width == 505
28+
29+
MyTrameApp()

0 commit comments

Comments
 (0)