kopia lustrzana https://github.com/simonw/datasette
row_actions() plugin hook, closes #2299
rodzic
7339cc51de
commit
b8711988b9
|
@ -140,6 +140,11 @@ def menu_links(datasette, actor, request):
|
||||||
"""Links for the navigation menu"""
|
"""Links for the navigation menu"""
|
||||||
|
|
||||||
|
|
||||||
|
@hookspec
|
||||||
|
def row_actions(datasette, actor, request, database, table, row):
|
||||||
|
"""Links for the row actions menu"""
|
||||||
|
|
||||||
|
|
||||||
@hookspec
|
@hookspec
|
||||||
def table_actions(datasette, actor, database, table, request):
|
def table_actions(datasette, actor, database, table, request):
|
||||||
"""Links for the table actions menu"""
|
"""Links for the table actions menu"""
|
||||||
|
|
|
@ -22,6 +22,37 @@
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h1 style="padding-left: 10px; border-left: 10px solid #{{ database_color }}">{{ table }}: {{ ', '.join(primary_key_values) }}{% if private %} 🔒{% endif %}</h1>
|
<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() }}
|
{{ top_row() }}
|
||||||
|
|
||||||
{% block description_source_license %}{% include "_description_source_license.html" %}{% endblock %}
|
{% block description_source_license %}{% include "_description_source_license.html" %}{% endblock %}
|
||||||
|
|
|
@ -3,10 +3,12 @@ from datasette.database import QueryInterrupted
|
||||||
from datasette.events import UpdateRowEvent, DeleteRowEvent
|
from datasette.events import UpdateRowEvent, DeleteRowEvent
|
||||||
from .base import DataView, BaseView, _error
|
from .base import DataView, BaseView, _error
|
||||||
from datasette.utils import (
|
from datasette.utils import (
|
||||||
|
await_me_maybe,
|
||||||
make_slot_function,
|
make_slot_function,
|
||||||
to_css_class,
|
to_css_class,
|
||||||
escape_sqlite,
|
escape_sqlite,
|
||||||
)
|
)
|
||||||
|
from datasette.plugins import pm
|
||||||
import json
|
import json
|
||||||
import sqlite_utils
|
import sqlite_utils
|
||||||
from .table import display_columns_and_rows
|
from .table import display_columns_and_rows
|
||||||
|
@ -55,6 +57,20 @@ class RowView(DataView):
|
||||||
)
|
)
|
||||||
for column in display_columns:
|
for column in display_columns:
|
||||||
column["sortable"] = False
|
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 {
|
return {
|
||||||
"private": private,
|
"private": private,
|
||||||
"foreign_key_tables": await self.foreign_key_tables(
|
"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",
|
f"_table-row-{to_css_class(database)}-{to_css_class(table)}.html",
|
||||||
"_table.html",
|
"_table.html",
|
||||||
],
|
],
|
||||||
|
"row_actions": row_actions,
|
||||||
"metadata": (self.ds.metadata("databases") or {})
|
"metadata": (self.ds.metadata("databases") or {})
|
||||||
.get(database, {})
|
.get(database, {})
|
||||||
.get("tables", {})
|
.get("tables", {})
|
||||||
|
|
|
@ -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.
|
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:
|
.. _plugin_hook_table_actions:
|
||||||
|
|
||||||
table_actions(datasette, actor, database, table, request)
|
table_actions(datasette, actor, database, table, request)
|
||||||
|
@ -1577,10 +1581,6 @@ table_actions(datasette, actor, database, table, request)
|
||||||
``request`` - :ref:`internals_request` or None
|
``request`` - :ref:`internals_request` or None
|
||||||
The current HTTP request. This can be ``None`` if the request object is not available.
|
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"``:
|
This example adds a new table action if the signed in user is ``"root"``:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
@ -1653,7 +1653,7 @@ query_actions(datasette, actor, database, query_name, request, sql, params)
|
||||||
``params`` - dictionary
|
``params`` - dictionary
|
||||||
The parameters passed to the SQL query, if any.
|
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:
|
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>`_
|
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:
|
.. _plugin_hook_database_actions:
|
||||||
|
|
||||||
database_actions(datasette, actor, database, request)
|
database_actions(datasette, actor, database, request)
|
||||||
|
@ -1701,7 +1744,7 @@ database_actions(datasette, actor, database, request)
|
||||||
``request`` - :ref:`internals_request`
|
``request`` - :ref:`internals_request`
|
||||||
The current HTTP 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:
|
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`
|
``request`` - :ref:`internals_request`
|
||||||
The current HTTP 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:
|
This example adds a link an imagined tool for editing the homepage, only for signed in users:
|
||||||
|
|
||||||
|
|
|
@ -53,6 +53,7 @@ EXPECTED_PLUGINS = [
|
||||||
"register_permissions",
|
"register_permissions",
|
||||||
"register_routes",
|
"register_routes",
|
||||||
"render_cell",
|
"render_cell",
|
||||||
|
"row_actions",
|
||||||
"skip_csrf",
|
"skip_csrf",
|
||||||
"startup",
|
"startup",
|
||||||
"table_actions",
|
"table_actions",
|
||||||
|
|
|
@ -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
|
@hookimpl
|
||||||
def database_actions(datasette, database, actor, request):
|
def database_actions(datasette, database, actor, request):
|
||||||
if actor:
|
if actor:
|
||||||
|
|
|
@ -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
|
@pytest.mark.asyncio
|
||||||
async def test_hook_database_actions(ds_client):
|
async def test_hook_database_actions(ds_client):
|
||||||
response = await ds_client.get("/fixtures")
|
response = await ds_client.get("/fixtures")
|
||||||
|
|
Ładowanie…
Reference in New Issue