row_actions() plugin hook, closes #2299

pull/2082/merge
Simon Willison 2024-03-12 16:13:31 -07:00
rodzic 7339cc51de
commit b8711988b9
7 zmienionych plików z 134 dodań i 7 usunięć

Wyświetl plik

@ -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"""

Wyświetl plik

@ -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 %}

Wyświetl plik

@ -3,10 +3,12 @@ from datasette.database import QueryInterrupted
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
@ -55,6 +57,20 @@ class RowView(DataView):
)
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(
@ -68,6 +84,7 @@ class RowView(DataView):
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", {})

Wyświetl plik

@ -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)
@ -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
@ -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:
@ -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)
@ -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:
@ -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:

Wyświetl plik

@ -53,6 +53,7 @@ EXPECTED_PLUGINS = [
"register_permissions",
"register_routes",
"render_cell",
"row_actions",
"skip_csrf",
"startup",
"table_actions",

Wyświetl plik

@ -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:

Wyświetl plik

@ -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")