From 7fff3c30aa186b6688c85a845ce4d674991711f4 Mon Sep 17 00:00:00 2001 From: Edan Bainglass Date: Tue, 22 Oct 2024 10:52:37 +0000 Subject: [PATCH] Add in-app guide mechanism --- src/aiidalab_qe/app/static/styles/infobox.css | 3 ++ .../app/static/templates/guide.jinja | 3 ++ src/aiidalab_qe/app/wrapper.py | 37 ++++++++++++++++++- src/aiidalab_qe/common/infobox.py | 18 +++++++++ tests/test_infobox.py | 16 +++++++- tests/test_wrapper.py | 12 +++--- 6 files changed, 80 insertions(+), 9 deletions(-) diff --git a/src/aiidalab_qe/app/static/styles/infobox.css b/src/aiidalab_qe/app/static/styles/infobox.css index a25861e42..8818bc6b3 100644 --- a/src/aiidalab_qe/app/static/styles/infobox.css +++ b/src/aiidalab_qe/app/static/styles/infobox.css @@ -13,3 +13,6 @@ .info-box p { line-height: 24px; } +.info-box.in-app-guide.show { + display: flex !important; +} diff --git a/src/aiidalab_qe/app/static/templates/guide.jinja b/src/aiidalab_qe/app/static/templates/guide.jinja index 14b10f914..b22d67ee1 100644 --- a/src/aiidalab_qe/app/static/templates/guide.jinja +++ b/src/aiidalab_qe/app/static/templates/guide.jinja @@ -38,5 +38,8 @@ For a more in-depth dive into the app's features, please refer to the how-to guides.

+ +

+ Alternatively, you can select one of our in-app guides below to walk through an example use-case.

diff --git a/src/aiidalab_qe/app/wrapper.py b/src/aiidalab_qe/app/wrapper.py index d1b32e598..bc9c1cf55 100644 --- a/src/aiidalab_qe/app/wrapper.py +++ b/src/aiidalab_qe/app/wrapper.py @@ -2,6 +2,7 @@ import ipywidgets as ipw import traitlets +from IPython.display import Javascript, display def without_triggering(toggle: str): @@ -54,7 +55,10 @@ def enable_toggles(self) -> None: def _on_guide_toggle(self, change: dict): """Toggle the guide section.""" if change["new"]: - self._view.info_container.children = [self._view.guide] + self._view.info_container.children = [ + self._view.guide, + self._view.guide_selection, + ] self._view.info_container.layout.display = "flex" self._view.job_history_toggle.value = False else: @@ -82,11 +86,34 @@ def _on_job_history_toggle(self, change: dict): else: self._view.main.children = [self._view.app] + def _on_guide_select(self, change: dict): + """Toggle the guide section.""" + display( + Javascript(f""" + document.querySelectorAll('.{change["old"]}-guide-identifier').forEach( + (guide) => {'{'} + guide.classList.remove('show'); + {'}'} + ); + """) + ) + if (guide_class := change["new"]) != "none": + display( + Javascript(f""" + document.querySelectorAll('.{guide_class}-guide-identifier').forEach( + (guide) => {'{'} + guide.classList.add('show'); + {'}'} + ); + """) + ) + def _set_event_handlers(self) -> None: """Set up event handlers.""" self._view.guide_toggle.observe(self._on_guide_toggle, "value") self._view.about_toggle.observe(self._on_about_toggle, "value") self._view.job_history_toggle.observe(self._on_job_history_toggle, "value") + self._view.guide_selection.observe(self._on_guide_select, "value") class AppWrapperModel(traitlets.HasTraits): @@ -107,7 +134,7 @@ def __init__(self) -> None: from datetime import datetime from importlib_resources import files - from IPython.display import Image, display + from IPython.display import Image from jinja2 import Environment from aiidalab_qe.app.static import templates @@ -175,6 +202,12 @@ def __init__(self) -> None: self.guide = ipw.HTML(env.from_string(guide_template).render()) self.about = ipw.HTML(env.from_string(about_template).render()) + self.guide_selection = ipw.RadioButtons( + options=["none", "basic", "advanced"], + description="Guides:", + value="none", + ) + self.info_container = InfoBox() self.job_history = QueryInterface() diff --git a/src/aiidalab_qe/common/infobox.py b/src/aiidalab_qe/common/infobox.py index 86a7eb26b..1af98e654 100644 --- a/src/aiidalab_qe/common/infobox.py +++ b/src/aiidalab_qe/common/infobox.py @@ -20,3 +20,21 @@ def __init__(self, classes: list[str] | None = None, **kwargs): for custom_class in custom_classes.split(" "): if custom_class: self.add_class(custom_class) + + +class InAppGuide(InfoBox): + """The `InfoAppGuide` is used to set up in-app guides that may be toggle in unison.""" + + def __init__(self, guide_class: str, classes: list[str] | None = None, **kwargs): + """`InAppGuide` constructor. + + Parameters + ---------- + `guide_class` : `str` + The unique identifier for the guide. + `classes` : `list[str]`, optional + One or more CSS classes. + """ + + classes = ["in-app-guide", *(classes or []), f"{guide_class}-guide-identifier"] + super().__init__(classes=classes, **kwargs) diff --git a/tests/test_infobox.py b/tests/test_infobox.py index 892335da9..314d7928d 100644 --- a/tests/test_infobox.py +++ b/tests/test_infobox.py @@ -1,4 +1,4 @@ -from aiidalab_qe.common.infobox import InfoBox +from aiidalab_qe.common.infobox import InAppGuide, InfoBox def test_infobox_classes(): @@ -14,3 +14,17 @@ def test_infobox_classes(): "custom-3", ) ) + + +def test_in_app_guide(): + """Test `InAppGuide` class.""" + guide_class = "some_guide" + in_app_guide = InAppGuide(guide_class=guide_class) + assert all( + css_class in in_app_guide._dom_classes + for css_class in ( + "info-box", + "in-app-guide", + f"{guide_class}-guide-identifier", + ) + ) diff --git a/tests/test_wrapper.py b/tests/test_wrapper.py index 609fa40d8..e4eed6a1c 100644 --- a/tests/test_wrapper.py +++ b/tests/test_wrapper.py @@ -18,7 +18,7 @@ def test_guide_toggle(self): self.controller._on_guide_toggle({"new": True}) self._assert_guide_is_on() self.controller._on_guide_toggle({"new": False}) - self._assert_no_guide_info() + self._assert_no_info() def test_about_toggle(self): """Test about_toggle method.""" @@ -27,13 +27,13 @@ def test_about_toggle(self): self.controller._on_about_toggle({"new": True}) self._assert_about_is_on() self.controller._on_about_toggle({"new": False}) - self._assert_no_guide_info() + self._assert_no_info() def test_toggle_switch(self): """Test toggle_switch method.""" self._instansiate_mvc_components() self.controller.enable_toggles() - self._assert_no_guide_info() + self._assert_no_info() self.controller._on_guide_toggle({"new": True}) self._assert_guide_is_on() self.controller._on_about_toggle({"new": True}) @@ -41,11 +41,11 @@ def test_toggle_switch(self): self.controller._on_guide_toggle({"new": True}) self._assert_guide_is_on() self.controller._on_guide_toggle({"new": False}) - self._assert_no_guide_info() + self._assert_no_info() def _assert_guide_is_on(self): """Assert guide is on.""" - assert len(self.view.info_container.children) == 1 + assert len(self.view.info_container.children) == 2 assert self.view.guide in self.view.info_container.children def _assert_about_is_on(self): @@ -53,7 +53,7 @@ def _assert_about_is_on(self): assert len(self.view.info_container.children) == 1 assert self.view.about in self.view.info_container.children - def _assert_no_guide_info(self): + def _assert_no_info(self): """Assert no info is shown.""" assert len(self.view.info_container.children) == 0