Skip to content

Commit

Permalink
row_actions() plugin hook, closes #2299
Browse files Browse the repository at this point in the history
  • Loading branch information
simonw committed Mar 12, 2024
1 parent 7339cc5 commit b871198
Show file tree
Hide file tree
Showing 7 changed files with 134 additions and 7 deletions.
5 changes: 5 additions & 0 deletions datasette/hookspecs.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,11 @@ def menu_links(datasette, actor, request):
"""Links for the navigation menu"""


@hookspec
def row_actions(datasette, actor, request, database, table, row):
"""Links for the row actions menu"""


@hookspec
def table_actions(datasette, actor, database, table, request):
"""Links for the table actions menu"""
Expand Down
31 changes: 31 additions & 0 deletions datasette/templates/row.html
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,37 @@
{% block content %}
<h1 style="padding-left: 10px; border-left: 10px solid #{{ database_color }}">{{ table }}: {{ ', '.join(primary_key_values) }}{% if private %} 🔒{% endif %}</h1>

{% set links = row_actions %}{% if links %}
<div class="page-action-menu">
<details class="actions-menu-links details-menu">
<summary>
<div class="icon-text">
<svg class="icon" aria-labelledby="actions-menu-links-title" role="img" style="color: #fff" xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 28 28" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<title id="actions-menu-links-title">Row 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>
<span>Row actions</span>
</div>
</summary>
<div class="dropdown-menu">
<div class="hook"></div>
{% if links %}
<ul>
{% for link in links %}
<li><a href="{{ link.href }}">{{ link.label }}
{% if link.description %}
<p class="dropdown-description">{{ link.description }}</p>
{% endif %}</a>
</li>
{% endfor %}
</ul>
{% endif %}
</div>
</details>
</div>
{% endif %}

{{ top_row() }}

{% block description_source_license %}{% include "_description_source_license.html" %}{% endblock %}
Expand Down
17 changes: 17 additions & 0 deletions datasette/views/row.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@
from datasette.events import UpdateRowEvent, DeleteRowEvent
from .base import DataView, BaseView, _error
from datasette.utils import (
await_me_maybe,
make_slot_function,
to_css_class,
escape_sqlite,
)
from datasette.plugins import pm
import json
import sqlite_utils
from .table import display_columns_and_rows
Expand Down Expand Up @@ -55,6 +57,20 @@ async def template_data():
)
for column in display_columns:
column["sortable"] = False

row_actions = []
for hook in pm.hook.row_actions(
datasette=self.ds,
actor=request.actor,
request=request,
database=database,
table=table,
row=rows[0],
):
extra_links = await await_me_maybe(hook)
if extra_links:
row_actions.extend(extra_links)

return {
"private": private,
"foreign_key_tables": await self.foreign_key_tables(
Expand All @@ -68,6 +84,7 @@ async def template_data():
f"_table-row-{to_css_class(database)}-{to_css_class(table)}.html",
"_table.html",
],
"row_actions": row_actions,
"metadata": (self.ds.metadata("databases") or {})
.get(database, {})
.get("tables", {})
Expand Down
57 changes: 50 additions & 7 deletions docs/plugin_hooks.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1557,6 +1557,10 @@ Action hooks

Action hooks can be used to add items to the action menus that appear at the top of different pages within Datasette. Unlike :ref:`menu_links() <plugin_hook_menu_links>`, actions which are displayed on every page, actions should only be relevant to the page the user is currently viewing.

Each of these hooks should return return a list of ``{"href": "...", "label": "..."}`` menu items, with optional ``"description": "..."`` keys describing each action in more detail.

They can alternatively return an ``async def`` awaitable function which, when called, returns a list of those menu items.

.. _plugin_hook_table_actions:

table_actions(datasette, actor, database, table, request)
Expand All @@ -1577,10 +1581,6 @@ 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, 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.

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

.. code-block:: python
Expand Down Expand Up @@ -1653,7 +1653,7 @@ query_actions(datasette, actor, database, query_name, request, sql, params)
``params`` - dictionary
The parameters passed to the SQL query, if any.

This hook is similar to :ref:`plugin_hook_table_actions` but populates an actions menu on the canned query and arbitrary SQL query pages.
Populates a "Query actions" menu on the canned query and arbitrary SQL query pages.

This example adds a new query action linking to a page for explaining a query:

Expand Down Expand Up @@ -1684,6 +1684,49 @@ This example adds a new query action linking to a page for explaining a query:
Example: `datasette-create-view <https://datasette.io/plugins/datasette-create-view>`_

.. _plugin_hook_row_actions:

row_actions(datasette, actor, request, database, table, row)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

``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>`.

``request`` - :ref:`internals_request` or None
The current HTTP request.

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

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

``row`` - ``sqlite.Row``
The SQLite row object being dispayed on the page.

Return links for the "Row actions" menu shown at the top of the row page.

This example displays the row in JSON plus some additional debug information if the user is signed in:

.. code-block:: python
from datasette import hookimpl
@hookimpl
def row_actions(datasette, database, table, actor, row):
if actor:
return [
{
"href": datasette.urls.instance(),
"label": f"Row details for {actor['id']}",
"description": json.dumps(dict(row), default=repr),
},
]
.. _plugin_hook_database_actions:

database_actions(datasette, actor, database, request)
Expand All @@ -1701,7 +1744,7 @@ database_actions(datasette, actor, database, request)
``request`` - :ref:`internals_request`
The current HTTP request.

This hook is similar to :ref:`plugin_hook_table_actions` but populates an actions menu on the database page.
Populates an actions menu on the database page.

This example adds a new database action for creating a table, if the user has the ``edit-schema`` permission:

Expand Down Expand Up @@ -1749,7 +1792,7 @@ homepage_actions(datasette, actor, request)
``request`` - :ref:`internals_request`
The current HTTP request.

This hook is similar to :ref:`plugin_hook_table_actions` but populates an actions menu on the index page of the Datasette instance.
Populates an actions menu on the top-level index homepage of the Datasette instance.

This example adds a link an imagined tool for editing the homepage, only for signed in users:

Expand Down
1 change: 1 addition & 0 deletions tests/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
"register_permissions",
"register_routes",
"render_cell",
"row_actions",
"skip_csrf",
"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 @@ -423,6 +423,18 @@ def query_actions(datasette, database, query_name, sql):
]


@hookimpl
def row_actions(datasette, database, table, actor, row):
if actor:
return [
{
"href": datasette.urls.instance(),
"label": f"Row details for {actor['id']}",
"description": json.dumps(dict(row), default=repr),
},
]


@hookimpl
def database_actions(datasette, database, actor, request):
if actor:
Expand Down
18 changes: 18 additions & 0 deletions tests/test_plugins.py
Original file line number Diff line number Diff line change
Expand Up @@ -1000,6 +1000,24 @@ async def test_hook_query_actions(ds_client, path, expected_url):
]


@pytest.mark.asyncio
async def test_hook_row_actions(ds_client):
response = await ds_client.get("/fixtures/facet_cities/1")
assert get_actions_links(response.text) == []

response_2 = await ds_client.get(
"/fixtures/facet_cities/1",
cookies={"ds_actor": ds_client.actor_cookie({"id": "sam"})},
)
assert get_actions_links(response_2.text) == [
{
"label": "Row details for sam",
"href": "/",
"description": '{"id": 1, "name": "San Francisco"}',
}
]


@pytest.mark.asyncio
async def test_hook_database_actions(ds_client):
response = await ds_client.get("/fixtures")
Expand Down

0 comments on commit b871198

Please sign in to comment.