kopia lustrzana https://github.com/simonw/datasette
Action menu descriptions
* Refactor tests to extract get_actions_links() helper * Table, database and query action menu items now support optional descriptions Closes #2294pull/2082/merge
rodzic
c6e8a4a76c
commit
090dff542b
|
@ -841,6 +841,13 @@ svg.dropdown-menu-icon {
|
||||||
.dropdown-menu a:hover {
|
.dropdown-menu a:hover {
|
||||||
background-color: #eee;
|
background-color: #eee;
|
||||||
}
|
}
|
||||||
|
.dropdown-menu .dropdown-description {
|
||||||
|
margin: 0;
|
||||||
|
color: #666;
|
||||||
|
font-size: 0.8em;
|
||||||
|
max-width: 80vw;
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
.dropdown-menu .hook {
|
.dropdown-menu .hook {
|
||||||
display: block;
|
display: block;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|
|
@ -31,7 +31,11 @@
|
||||||
{% if links %}
|
{% if links %}
|
||||||
<ul>
|
<ul>
|
||||||
{% for link in links %}
|
{% for link in links %}
|
||||||
<li><a href="{{ link.href }}">{{ link.label }}</a></li>
|
<li><a href="{{ link.href }}">{{ link.label }}
|
||||||
|
{% if link.description %}
|
||||||
|
<p class="dropdown-description">{{ link.description }}</p>
|
||||||
|
{% endif %}</a>
|
||||||
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
@ -47,7 +47,11 @@
|
||||||
{% if links %}
|
{% if links %}
|
||||||
<ul>
|
<ul>
|
||||||
{% for link in links %}
|
{% for link in links %}
|
||||||
<li><a href="{{ link.href }}">{{ link.label }}</a></li>
|
<li><a href="{{ link.href }}">{{ link.label }}
|
||||||
|
{% if link.description %}
|
||||||
|
<p class="dropdown-description">{{ link.description }}</p>
|
||||||
|
{% endif %}</a>
|
||||||
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
@ -42,7 +42,11 @@
|
||||||
{% if links %}
|
{% if links %}
|
||||||
<ul>
|
<ul>
|
||||||
{% for link in links %}
|
{% for link in links %}
|
||||||
<li><a href="{{ link.href }}">{{ link.label }}</a></li>
|
<li><a href="{{ link.href }}">{{ link.label }}
|
||||||
|
{% if link.description %}
|
||||||
|
<p class="dropdown-description">{{ link.description }}</p>
|
||||||
|
{% endif %}</a>
|
||||||
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
@ -1493,7 +1493,7 @@ 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.
|
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.
|
It can alternatively return an ``async def`` awaitable function which returns a list of menu items.
|
||||||
|
|
||||||
|
@ -1515,6 +1515,7 @@ This example adds a new table action if the signed in user is ``"root"``:
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
"label": "Edit schema for this table",
|
"label": "Edit schema for this table",
|
||||||
|
"description": "Add, remove, rename or alter columns for this table.",
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -1571,6 +1572,7 @@ This example adds a new query action linking to a page for explaining a query:
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
"label": "Explain this query",
|
"label": "Explain this query",
|
||||||
|
"description": "Get a summary of how SQLite executes the query",
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -406,6 +406,7 @@ def query_actions(datasette, database, query_name, sql):
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
"label": "Explain this query",
|
"label": "Explain this query",
|
||||||
|
"description": "Runs a SQLite explain",
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -925,26 +925,36 @@ async def test_hook_menu_links(ds_client):
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@pytest.mark.parametrize("table_or_view", ["facetable", "simple_view"])
|
@pytest.mark.parametrize("table_or_view", ["facetable", "simple_view"])
|
||||||
async def test_hook_table_actions(ds_client, table_or_view):
|
async def test_hook_table_actions(ds_client, table_or_view):
|
||||||
def get_table_actions_links(html):
|
|
||||||
soup = Soup(html, "html.parser")
|
|
||||||
details = soup.find("details", {"class": "actions-menu-links"})
|
|
||||||
if details is None:
|
|
||||||
return []
|
|
||||||
return [{"label": a.text, "href": a["href"]} for a in details.select("a")]
|
|
||||||
|
|
||||||
response = await ds_client.get(f"/fixtures/{table_or_view}")
|
response = await ds_client.get(f"/fixtures/{table_or_view}")
|
||||||
assert get_table_actions_links(response.text) == []
|
assert get_actions_links(response.text) == []
|
||||||
|
|
||||||
response_2 = await ds_client.get(f"/fixtures/{table_or_view}?_bot=1&_hello=BOB")
|
response_2 = await ds_client.get(f"/fixtures/{table_or_view}?_bot=1&_hello=BOB")
|
||||||
assert sorted(
|
assert sorted(
|
||||||
get_table_actions_links(response_2.text), key=lambda link: link["label"]
|
get_actions_links(response_2.text), key=lambda link: link["label"]
|
||||||
) == [
|
) == [
|
||||||
{"label": "Database: fixtures", "href": "/"},
|
{"label": "Database: fixtures", "href": "/", "description": None},
|
||||||
{"label": "From async BOB", "href": "/"},
|
{"label": "From async BOB", "href": "/", "description": None},
|
||||||
{"label": f"Table: {table_or_view}", "href": "/"},
|
{"label": f"Table: {table_or_view}", "href": "/", "description": None},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def get_actions_links(html):
|
||||||
|
soup = Soup(html, "html.parser")
|
||||||
|
details = soup.find("details", {"class": "actions-menu-links"})
|
||||||
|
if details is None:
|
||||||
|
return []
|
||||||
|
links = []
|
||||||
|
for a_el in details.select("a"):
|
||||||
|
description = None
|
||||||
|
if a_el.find("p") is not None:
|
||||||
|
description = a_el.find("p").text.strip()
|
||||||
|
a_el.find("p").extract()
|
||||||
|
label = a_el.text.strip()
|
||||||
|
href = a_el["href"]
|
||||||
|
links.append({"label": label, "href": href, "description": description})
|
||||||
|
return links
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"path,expected_url",
|
"path,expected_url",
|
||||||
|
@ -959,37 +969,29 @@ async def test_hook_table_actions(ds_client, table_or_view):
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
async def test_hook_query_actions(ds_client, path, expected_url):
|
async def test_hook_query_actions(ds_client, path, expected_url):
|
||||||
def get_table_actions_links(html):
|
|
||||||
soup = Soup(html, "html.parser")
|
|
||||||
details = soup.find("details", {"class": "actions-menu-links"})
|
|
||||||
if details is None:
|
|
||||||
return []
|
|
||||||
return [{"label": a.text, "href": a["href"]} for a in details.select("a")]
|
|
||||||
|
|
||||||
response = await ds_client.get(path)
|
response = await ds_client.get(path)
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
links = get_table_actions_links(response.text)
|
links = get_actions_links(response.text)
|
||||||
if expected_url is None:
|
if expected_url is None:
|
||||||
assert links == []
|
assert links == []
|
||||||
else:
|
else:
|
||||||
assert links == [{"label": "Explain this query", "href": expected_url}]
|
assert links == [
|
||||||
|
{
|
||||||
|
"label": "Explain this query",
|
||||||
|
"href": expected_url,
|
||||||
|
"description": "Runs a SQLite explain",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_hook_database_actions(ds_client):
|
async def test_hook_database_actions(ds_client):
|
||||||
def get_table_actions_links(html):
|
|
||||||
soup = Soup(html, "html.parser")
|
|
||||||
details = soup.find("details", {"class": "actions-menu-links"})
|
|
||||||
if details is None:
|
|
||||||
return []
|
|
||||||
return [{"label": a.text, "href": a["href"]} for a in details.select("a")]
|
|
||||||
|
|
||||||
response = await ds_client.get("/fixtures")
|
response = await ds_client.get("/fixtures")
|
||||||
assert get_table_actions_links(response.text) == []
|
assert get_actions_links(response.text) == []
|
||||||
|
|
||||||
response_2 = await ds_client.get("/fixtures?_bot=1&_hello=BOB")
|
response_2 = await ds_client.get("/fixtures?_bot=1&_hello=BOB")
|
||||||
assert get_table_actions_links(response_2.text) == [
|
assert get_actions_links(response_2.text) == [
|
||||||
{"label": "Database: fixtures - BOB", "href": "/"},
|
{"label": "Database: fixtures - BOB", "href": "/", "description": None},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
Ładowanie…
Reference in New Issue