Skip to content

Commit

Permalink
Implement InfoBox, InAppGuide, and FirstTimeUserMessage widgets
Browse files Browse the repository at this point in the history
  • Loading branch information
edan-bainglass committed Aug 21, 2024
1 parent ceda522 commit 82f066b
Show file tree
Hide file tree
Showing 4 changed files with 309 additions and 0 deletions.
119 changes: 119 additions & 0 deletions aiidalab_widgets_base/infobox.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
from __future__ import annotations

import ipywidgets as ipw


class InfoBox(ipw.VBox):
"""The `InfoBox` component is used to provide additional info regarding a widget or an app."""

def __init__(self, **kwargs):
"""`InfoBox` constructor."""
custom_css = kwargs.pop("custom-css", "")
super().__init__(**kwargs)
self.add_class("info-box")
if custom_css:
self.add_class(custom_css)


class InAppGuide(InfoBox):
"""The `InfoAppGuide` is used to set up in-app guides that may be toggle in unison."""

def __init__(self, guide_class="", **kwargs):
"""`InAppGuide` constructor.
parameters
----------
`guide_class` : `str`
A CSS class marking the widget as part of a guide collection.
May also be used for custom styling.
"""
super().__init__(**kwargs)
self.add_class("in-app-guide")
self.add_class(guide_class)


class FirstTimeUserMessage(ipw.VBox):
"""The `FirstTimeUserMessage` is used to display a message to first time users."""

def __init__(self, message="", **kwargs):
"""`FirstTimeUserMessage` constructor."""

self.close_button = ipw.Button(
icon="times",
tooltip="Close",
)

self.message_box = InfoBox(
children=[
ipw.HTML(message),
self.close_button,
],
)

self.undo_button = ipw.Button(
icon="undo",
tooltip="Undo",
description="undo",
)

self.closing_message = ipw.HBox(
children=[
ipw.HTML("This message will not show next time"),
self.undo_button,
],
)
self.closing_message.add_class("closing-message")

super().__init__(
children=[
self.closing_message,
self.message_box,
],
**kwargs,
)

self.add_class("first-time-users-infobox")

self._check_if_first_time_user()

self._set_event_listeners()

def _check_if_first_time_user(self):
"""Add a message for first-time users."""
try:
with open(".app-user-config") as file:
first_time_user = file.read().find("existing-user") == -1
except FileNotFoundError:
first_time_user = True

if first_time_user:
self.layout.display = "flex"
self.message_box.layout.display = "flex"
self.closing_message.layout.display = "none"

def _on_close(self, _=None):
"""Hide the first time info box and write existing user token to file."""
self.message_box.layout.display = "none"
self.closing_message.layout.display = "flex"
self._write_existing_user_token_to_file()

def _write_existing_user_token_to_file(self):
"""Write a token to file marking the user as an existing user."""
with open(".app-user-config", "w") as file:
file.write("existing-user")

def _on_undo(self, _=None):
"""Undo the action of closing the first time user message."""
from contextlib import suppress
from pathlib import Path

self.message_box.layout.display = "flex"
self.closing_message.layout.display = "none"

with suppress(FileNotFoundError):
Path(".app-user-config").unlink()

def _set_event_listeners(self):
"""Set the event listeners."""
self.close_button.on_click(self._on_close)
self.undo_button.on_click(self._on_undo)
62 changes: 62 additions & 0 deletions aiidalab_widgets_base/static/styles/infobox.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
.info-box {
display: none;
margin: 2px;
padding: 1em;
border: 3px solid orangered;
background-color: #ffedcc;
border-radius: 1em;
-webkit-border-radius: 1em;
-moz-border-radius: 1em;
-ms-border-radius: 1em;
-o-border-radius: 1em;
}
.info-box p {
line-height: 24px;
}
.info-box.in-app-guide.show {
display: flex !important;
}

.first-time-users-infobox {
display: none;
max-width: 500px;
margin: 2px auto !important;
}
.first-time-users-infobox button {
width: fit-content;
background: none;
color: black;
cursor: pointer !important;
}
.first-time-users-infobox button:hover, .first-time-users-infobox button:focus {
outline: none !important;
box-shadow: none !important;
color: orangered;
}
.first-time-users-infobox button:active {
background: none;
color: #cc3700;
}
.first-time-users-infobox .info-box {
display: none;
}
.first-time-users-infobox .info-box button {
position: absolute;
top: 0;
right: 6px;
padding: 0;
font-size: large;
}
.first-time-users-infobox .info-box button:hover, .first-time-users-infobox .info-box button:focus {
transform: scale(1.1);
-webkit-transform: scale(1.1);
-moz-transform: scale(1.1);
-ms-transform: scale(1.1);
-o-transform: scale(1.1);
}
.first-time-users-infobox .info-box .widget-html {
padding-right: 20px;
}
.first-time-users-infobox .closing-message {
display: none;
}
76 changes: 76 additions & 0 deletions aiidalab_widgets_base/static/styles/scss/infobox.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
.info-box {
display: none;
margin: 2px;
padding: 1em;
border: 3px solid orangered;
background-color: #ffedcc;
border-radius: 1em;
-webkit-border-radius: 1em;
-moz-border-radius: 1em;
-ms-border-radius: 1em;
-o-border-radius: 1em;

p {
line-height: 24px;
}

&.in-app-guide {
&.show {
display: flex !important;
}
}
}

.first-time-users-infobox {
display: none;
max-width: 500px;
margin: 2px auto !important;

button {
width: fit-content;
background: none;
color: black;
cursor: pointer !important;

&:hover,
&:focus {
outline: none !important;
box-shadow: none !important;
color: orangered;
}

&:active {
background: none;
color: darken($color: orangered, $amount: 10);
}
}

.info-box {
display: none;

button {
position: absolute;
top: 0;
right: 6px;
padding: 0;
font-size: large;

&:hover,
&:focus {
transform: scale(1.1);
-webkit-transform: scale(1.1);
-moz-transform: scale(1.1);
-ms-transform: scale(1.1);
-o-transform: scale(1.1);
}
}

.widget-html {
padding-right: 20px;
}
}

.closing-message {
display: none;
}
}
52 changes: 52 additions & 0 deletions tests/test_infobox.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
from pathlib import Path

from aiidalab_widgets_base.infobox import FirstTimeUserMessage, InAppGuide, InfoBox


def test_infobox_classes():
"""Test `InfoBox` classes."""
infobox = InfoBox()
assert "info-box" in infobox._dom_classes
infobox = InfoBox(**{"custom-css": "custom-info-box"})
assert all(
css_class in infobox._dom_classes
for css_class in (
"info-box",
"custom-info-box",
)
)


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",
guide_class,
)
)


def test_first_time_user_message():
"""Test `FirstTimeUserMessage` class."""
message = "Hello, first-time user!"
widget = FirstTimeUserMessage(message=message)
assert "first-time-users-infobox" in widget._dom_classes
assert widget.message_box.children[0].value == message # type: ignore
assert widget.layout.display == "flex"
assert widget.message_box.layout.display == "flex"
assert widget.closing_message.layout.display == "none"
widget.close_button.click()
assert widget.message_box.layout.display == "none"
assert widget.closing_message.layout.display == "flex"
path = Path(".app-user-config")
assert path.exists()
assert path.read_text().find("existing-user") != -1
widget.undo_button.click()
assert widget.message_box.layout.display == "flex"
assert widget.closing_message.layout.display == "none"
assert not path.exists()

0 comments on commit 82f066b

Please sign in to comment.