{% block description_source_license %}{% include "_description_source_license.html" %}{% endblock %}
diff --git a/datasette/views/table.py b/datasette/views/table.py
index 079e0b0ae8..65fe7f8b80 100644
--- a/datasette/views/table.py
+++ b/datasette/views/table.py
@@ -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,
@@ -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,
@@ -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
diff --git a/docs/plugin_hooks.rst b/docs/plugin_hooks.rst
index 82bc56a908..1c28c72e4a 100644
--- a/docs/plugin_hooks.rst
+++ b/docs/plugin_hooks.rst
@@ -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 `.
-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.
@@ -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 `.
+
+``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",
+ }]
diff --git a/tests/fixtures.py b/tests/fixtures.py
index 69853b7d1e..2f8383ef61 100644
--- a/tests/fixtures.py
+++ b/tests/fixtures.py
@@ -52,6 +52,7 @@
"register_routes",
"render_cell",
"startup",
+ "table_actions",
],
},
{
@@ -69,6 +70,7 @@
"permission_allowed",
"render_cell",
"startup",
+ "table_actions",
],
},
{
diff --git a/tests/plugins/my_plugin.py b/tests/plugins/my_plugin.py
index 7f8a48719e..8fc6a1b40c 100644
--- a/tests/plugins/my_plugin.py
+++ b/tests/plugins/my_plugin.py
@@ -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)},
+ ]
diff --git a/tests/plugins/my_plugin_2.py b/tests/plugins/my_plugin_2.py
index 981b24cca0..7d8095eda1 100644
--- a/tests/plugins/my_plugin_2.py
+++ b/tests/plugins/my_plugin_2.py
@@ -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
diff --git a/tests/test_plugins.py b/tests/test_plugins.py
index 191d943de0..be36a517bf 100644
--- a/tests/test_plugins.py
+++ b/tests/test_plugins.py
@@ -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": "/"},
+ ]