From 090dff542bee7f716088835b3cc633e9e04b985d Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Wed, 6 Mar 2024 22:54:06 -0500 Subject: [PATCH] Action menu descriptions * Refactor tests to extract get_actions_links() helper * Table, database and query action menu items now support optional descriptions Closes #2294 --- datasette/static/app.css | 7 ++++ datasette/templates/database.html | 6 ++- datasette/templates/query.html | 6 ++- datasette/templates/table.html | 6 ++- docs/plugin_hooks.rst | 4 +- tests/plugins/my_plugin.py | 1 + tests/test_plugins.py | 64 ++++++++++++++++--------------- 7 files changed, 59 insertions(+), 35 deletions(-) diff --git a/datasette/static/app.css b/datasette/static/app.css index b3223abf..e4a0ee10 100644 --- a/datasette/static/app.css +++ b/datasette/static/app.css @@ -841,6 +841,13 @@ svg.dropdown-menu-icon { .dropdown-menu a:hover { background-color: #eee; } +.dropdown-menu .dropdown-description { + margin: 0; + color: #666; + font-size: 0.8em; + max-width: 80vw; + white-space: normal; +} .dropdown-menu .hook { display: block; position: absolute; diff --git a/datasette/templates/database.html b/datasette/templates/database.html index 6c0cebcd..02e6fb3d 100644 --- a/datasette/templates/database.html +++ b/datasette/templates/database.html @@ -31,7 +31,11 @@ {% if links %} {% endif %} diff --git a/datasette/templates/query.html b/datasette/templates/query.html index b5991772..09a29118 100644 --- a/datasette/templates/query.html +++ b/datasette/templates/query.html @@ -47,7 +47,11 @@ {% if links %} {% endif %} diff --git a/datasette/templates/table.html b/datasette/templates/table.html index 0c2be672..1d328366 100644 --- a/datasette/templates/table.html +++ b/datasette/templates/table.html @@ -42,7 +42,11 @@ {% if links %} {% endif %} diff --git a/docs/plugin_hooks.rst b/docs/plugin_hooks.rst index ecc8f058..4f77d75c 100644 --- a/docs/plugin_hooks.rst +++ b/docs/plugin_hooks.rst @@ -1493,7 +1493,7 @@ table_actions(datasette, actor, database, table, request) ``request`` - :ref:`internals_request` or None The current HTTP request. This can be ``None`` if the request object is not available. -This hook allows table actions to be displayed in a menu accessed via an action icon at the top of the table page. It should return a list of ``{"href": "...", "label": "..."}`` menu items. +This hook allows table actions to be displayed in a menu accessed via an action icon at the top of the table page. It should return a list of ``{"href": "...", "label": "..."}`` menu items, with optional ``"description": "..."`` keys describing each action in more detail. It can alternatively return an ``async def`` awaitable function which returns a list of menu items. @@ -1515,6 +1515,7 @@ This example adds a new table action if the signed in user is ``"root"``: ) ), "label": "Edit schema for this table", + "description": "Add, remove, rename or alter columns for this table.", } ] @@ -1571,6 +1572,7 @@ This example adds a new query action linking to a page for explaining a query: } ), "label": "Explain this query", + "description": "Get a summary of how SQLite executes the query", }, ] diff --git a/tests/plugins/my_plugin.py b/tests/plugins/my_plugin.py index 01324213..5f000537 100644 --- a/tests/plugins/my_plugin.py +++ b/tests/plugins/my_plugin.py @@ -406,6 +406,7 @@ def query_actions(datasette, database, query_name, sql): } ), "label": "Explain this query", + "description": "Runs a SQLite explain", }, ] diff --git a/tests/test_plugins.py b/tests/test_plugins.py index d1da16fa..9f69e4fa 100644 --- a/tests/test_plugins.py +++ b/tests/test_plugins.py @@ -925,26 +925,36 @@ async def test_hook_menu_links(ds_client): @pytest.mark.asyncio @pytest.mark.parametrize("table_or_view", ["facetable", "simple_view"]) async def test_hook_table_actions(ds_client, table_or_view): - def get_table_actions_links(html): - soup = Soup(html, "html.parser") - details = soup.find("details", {"class": "actions-menu-links"}) - if details is None: - return [] - return [{"label": a.text, "href": a["href"]} for a in details.select("a")] - response = await ds_client.get(f"/fixtures/{table_or_view}") - assert get_table_actions_links(response.text) == [] + assert get_actions_links(response.text) == [] response_2 = await ds_client.get(f"/fixtures/{table_or_view}?_bot=1&_hello=BOB") assert sorted( - get_table_actions_links(response_2.text), key=lambda link: link["label"] + get_actions_links(response_2.text), key=lambda link: link["label"] ) == [ - {"label": "Database: fixtures", "href": "/"}, - {"label": "From async BOB", "href": "/"}, - {"label": f"Table: {table_or_view}", "href": "/"}, + {"label": "Database: fixtures", "href": "/", "description": None}, + {"label": "From async BOB", "href": "/", "description": None}, + {"label": f"Table: {table_or_view}", "href": "/", "description": None}, ] +def get_actions_links(html): + soup = Soup(html, "html.parser") + details = soup.find("details", {"class": "actions-menu-links"}) + if details is None: + return [] + links = [] + for a_el in details.select("a"): + description = None + if a_el.find("p") is not None: + description = a_el.find("p").text.strip() + a_el.find("p").extract() + label = a_el.text.strip() + href = a_el["href"] + links.append({"label": label, "href": href, "description": description}) + return links + + @pytest.mark.asyncio @pytest.mark.parametrize( "path,expected_url", @@ -959,37 +969,29 @@ async def test_hook_table_actions(ds_client, table_or_view): ), ) async def test_hook_query_actions(ds_client, path, expected_url): - def get_table_actions_links(html): - soup = Soup(html, "html.parser") - details = soup.find("details", {"class": "actions-menu-links"}) - if details is None: - return [] - return [{"label": a.text, "href": a["href"]} for a in details.select("a")] - response = await ds_client.get(path) assert response.status_code == 200 - links = get_table_actions_links(response.text) + links = get_actions_links(response.text) if expected_url is None: assert links == [] else: - assert links == [{"label": "Explain this query", "href": expected_url}] + assert links == [ + { + "label": "Explain this query", + "href": expected_url, + "description": "Runs a SQLite explain", + } + ] @pytest.mark.asyncio async def test_hook_database_actions(ds_client): - def get_table_actions_links(html): - soup = Soup(html, "html.parser") - details = soup.find("details", {"class": "actions-menu-links"}) - if details is None: - return [] - return [{"label": a.text, "href": a["href"]} for a in details.select("a")] - response = await ds_client.get("/fixtures") - assert get_table_actions_links(response.text) == [] + assert get_actions_links(response.text) == [] response_2 = await ds_client.get("/fixtures?_bot=1&_hello=BOB") - assert get_table_actions_links(response_2.text) == [ - {"label": "Database: fixtures - BOB", "href": "/"}, + assert get_actions_links(response_2.text) == [ + {"label": "Database: fixtures - BOB", "href": "/", "description": None}, ]