Skip to content

Commit

Permalink
table_actions() plugin hook plus menu, closes #1066
Browse files Browse the repository at this point in the history
Refs #690
  • Loading branch information
simonw committed Oct 30, 2020
1 parent 8a4639b commit 2f7731e
Show file tree
Hide file tree
Showing 10 changed files with 166 additions and 14 deletions.
5 changes: 5 additions & 0 deletions datasette/hookspecs.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,3 +102,8 @@ def forbidden(datasette, request, message):
@hookspec
def menu_links(datasette, actor):
"Links for the navigation menu"


@hookspec
def table_actions(datasette, actor, database, table):
"Links for the table actions menu"
34 changes: 33 additions & 1 deletion datasette/static/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ h6,
.header3,
.header4,
.header5,
.header6 {
.header6 {
font-weight: 700;
font-size: 1rem;
margin: 0;
Expand Down Expand Up @@ -162,6 +162,29 @@ h6,
text-decoration: underline;
}

.page-header {
padding-left: 10px;
border-left: 10px solid #666;
margin-bottom: 0.75rem;
margin-top: 1rem;
}
.page-header h1 {
display: inline;
margin: 0;
font-size: 2rem;
padding-right: 0.2em;
}
.page-header details {
display: inline;
}
.page-header details > summary {
list-style: none;
display: inline;
}
.page-header details > summary::-webkit-details-marker {
display: none;
}

div,
section,
article,
Expand Down Expand Up @@ -335,6 +358,15 @@ details .nav-menu-inner {
display: block;
}

/* Table actions menu */
.table-menu-links {
position: relative;
}
.table-menu-links .dropdown-menu {
position: absolute;
top: 2rem;
right: 0;
}

/* Components ============================================================== */

Expand Down
16 changes: 8 additions & 8 deletions datasette/templates/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -60,19 +60,19 @@
<footer class="ft">{% block footer %}{% include "_footer.html" %}{% endblock %}</footer>

<script>
var menuDetails = document.querySelector('.nav-menu');
document.body.addEventListener('click', (ev) => {
/* was this click outside the menu? */
if (menuDetails.getAttribute('open') !== "") {
return;
}
/* Close any open details elements that this click is outside of */
var target = ev.target;
while (target && target != menuDetails) {
var detailsClickedWithin = null;
while (target && target.tagName != 'DETAILS') {
target = target.parentNode;
}
if (!target) {
menuDetails.removeAttribute('open');
if (target && target.tagName == 'DETAILS') {
detailsClickedWithin = target;
}
Array.from(document.getElementsByTagName('details')).filter(
(details) => details.open && details != detailsClickedWithin
).forEach(details => details.open = false);
});
</script>
{% for body_script in body_scripts %}
Expand Down
25 changes: 23 additions & 2 deletions datasette/templates/table.html
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,29 @@
{% endblock %}

{% block content %}

<h1 style="padding-left: 10px; border-left: 10px solid #{{ database_color(database) }}">{{ metadata.title or table }}{% if is_view %} (view){% endif %}{% if private %} 🔒{% endif %}</h1>
<div class="page-header" style="border-color: #{{ database_color(database) }}">
<h1>{{ metadata.title or table }}{% if is_view %} (view){% endif %}{% if private %} 🔒{% endif %}</h1>
{% set links = table_actions() %}{% if links %}
<details class="table-menu-links">
<summary><svg aria-labelledby="table-menu-links-title" role="img"
style="color: #666" xmlns="http://www.w3.org/2000/svg"
width="28" height="28" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<title id="table-menu-links-title">Table actions</title>
<circle cx="12" cy="12" r="3"></circle>
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path>
</svg></summary>
<div class="dropdown-menu">
{% if links %}
<ul>
{% for link in links %}
<li><a href="{{ link.href }}">{{ link.label }}</a></li>
{% endfor %}
</ul>
{% endif %}
</div>
</details>{% endif %}
</div>

{% block description_source_license %}{% include "_description_source_license.html" %}{% endblock %}

Expand Down
16 changes: 16 additions & 0 deletions datasette/views/table.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from datasette.plugins import pm
from datasette.database import QueryInterrupted
from datasette.utils import (
await_me_maybe,
CustomRow,
MultiParams,
append_querystring,
Expand Down Expand Up @@ -840,7 +841,21 @@ async def extra_template():
elif use_rowid:
sort = "rowid"

async def table_actions():
links = []
for hook in pm.hook.table_actions(
datasette=self.ds,
table=table,
database=database,
actor=request.actor,
):
extra_links = await await_me_maybe(hook)
if extra_links:
links.extend(extra_links)
return links

return {
"table_actions": table_actions,
"supports_search": bool(fts_table),
"search": search or "",
"use_rowid": use_rowid,
Expand Down Expand Up @@ -959,6 +974,7 @@ async def template_data():
)
for column in display_columns:
column["sortable"] = False

return {
"foreign_key_tables": await self.foreign_key_tables(
database, table, pk_values
Expand Down
42 changes: 39 additions & 3 deletions docs/plugin_hooks.rst
Original file line number Diff line number Diff line change
Expand Up @@ -998,10 +998,10 @@ menu_links(datasette, actor)
``datasette`` - :ref:`internals_datasette`
You can use this to access plugin configuration options via ``datasette.plugin_config(your_plugin_name)``, or to execute SQL queries.

``request`` - object
The current HTTP :ref:`internals_request`.
``actor`` - dictionary or None
The currently authenticated :ref:`actor <authentication_actor>`.

This hook provides items to be included in the menu displayed by Datasette's top right menu icon.
This hook allows additional items to be included in the menu displayed by Datasette's top right menu icon.

The hook should return a list of ``{"href": "...", "label": "..."}`` menu items. These will be added to the menu.

Expand All @@ -1021,3 +1021,39 @@ This example adds a new menu item but only if the signed in user is ``"root"``:
]
Using :ref:`internals_datasette_urls` here ensures that links in the menu will take the :ref:`config_base_url` setting into account.


.. _plugin_hook_table_actions:

table_actions(datasette, actor, database, table)
------------------------------------------------

``datasette`` - :ref:`internals_datasette`
You can use this to access plugin configuration options via ``datasette.plugin_config(your_plugin_name)``, or to execute SQL queries.

``actor`` - dictionary or None
The currently authenticated :ref:`actor <authentication_actor>`.

``database`` - string
The name of the database.

``table`` - string
The name of the table.

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.

It can alternatively return an ``async def`` awaitable function which returns a list of menu items.

This example adds a new table action if the signed in user is ``"root"``:

.. code-block:: python
from datasette import hookimpl
@hookimpl
def table_actions(datasette, actor):
if actor and actor.get("id") == "root":
return [{
"href": datasette.urls.path("/-/edit-schema/{}/{}".format(database, table)),
"label": "Edit schema for this table",
}]
2 changes: 2 additions & 0 deletions tests/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
"register_routes",
"render_cell",
"startup",
"table_actions",
],
},
{
Expand All @@ -69,6 +70,7 @@
"permission_allowed",
"render_cell",
"startup",
"table_actions",
],
},
{
Expand Down
12 changes: 12 additions & 0 deletions tests/plugins/my_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -296,3 +296,15 @@ def forbidden(datasette, request, message):
def menu_links(datasette, actor):
if actor:
return [{"href": datasette.urls.instance(), "label": "Hello"}]


@hookimpl
def table_actions(datasette, database, table, actor):
if actor:
return [
{
"href": datasette.urls.instance(),
"label": "Database: {}".format(database),
},
{"href": datasette.urls.instance(), "label": "Table: {}".format(table)},
]
9 changes: 9 additions & 0 deletions tests/plugins/my_plugin_2.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,3 +155,12 @@ async def inner():
return [{"href": datasette.urls.instance(), "label": "Hello 2"}]

return inner


@hookimpl
def table_actions(datasette, database, table, actor):
async def inner():
if actor:
return [{"href": datasette.urls.instance(), "label": "From async"}]

return inner
19 changes: 19 additions & 0 deletions tests/test_plugins.py
Original file line number Diff line number Diff line change
Expand Up @@ -782,3 +782,22 @@ def get_menu_links(html):
{"label": "Hello", "href": "/"},
{"label": "Hello 2", "href": "/"},
]


def test_hook_table_actions(app_client):
def get_table_actions_links(html):
soup = Soup(html, "html.parser")
details = soup.find("details", {"class": "table-menu-links"})
if details is None:
return []
return [{"label": a.text, "href": a["href"]} for a in details.select("a")]

response = app_client.get("/fixtures/facetable")
assert get_table_actions_links(response.text) == []

response_2 = app_client.get("/fixtures/facetable?_bot=1")
assert get_table_actions_links(response_2.text) == [
{"label": "From async", "href": "/"},
{"label": "Database: fixtures", "href": "/"},
{"label": "Table: facetable", "href": "/"},
]

0 comments on commit 2f7731e

Please sign in to comment.