mirror of
https://github.com/simonw/datasette.git
synced 2026-06-23 17:24:35 +02:00
Add create table UI
Adds a permission-gated database action that opens a create table modal on database pages, backed by the existing create-table JSON API. The modal starts with an id integer primary key column plus a blank text column, supports SQLite type selection, and shows custom column type controls only when the actor can set column types. Selected custom column types are applied after table creation with follow-up set-column-type API calls. Includes styling plus HTML and Playwright coverage for the action payload and create-table flow.
This commit is contained in:
parent
57e7bba38f
commit
2d3c85dfc0
6 changed files with 1303 additions and 20 deletions
|
|
@ -117,6 +117,10 @@ def write_playwright_config(config_path):
|
|||
{
|
||||
"databases": {
|
||||
"data": {
|
||||
"permissions": {
|
||||
"create-table": True,
|
||||
"set-column-type": True,
|
||||
},
|
||||
"tables": {
|
||||
"projects": {
|
||||
"label_column": "title",
|
||||
|
|
@ -275,6 +279,55 @@ def test_datasette_homepage_contains_datasette(page, datasette_server):
|
|||
assert "Datasette" in page.locator("body").inner_text()
|
||||
|
||||
|
||||
@pytest.mark.playwright
|
||||
def test_create_table_flow(page, datasette_server):
|
||||
page.goto(f"{datasette_server}data")
|
||||
page.locator("details.actions-menu-links summary").click()
|
||||
page.locator('button[data-database-action="create-table"]').click()
|
||||
|
||||
dialog = page.locator("#table-create-dialog")
|
||||
dialog.wait_for()
|
||||
assert dialog.locator(".modal-title").inner_text() == "Create a table in data"
|
||||
placeholder_select = dialog.locator(".table-create-custom-column-type").nth(0)
|
||||
assert placeholder_select.input_value() == ""
|
||||
assert (
|
||||
placeholder_select.locator("option:checked").inner_text() == "- custom type -"
|
||||
)
|
||||
assert "table-create-input-placeholder" in placeholder_select.get_attribute("class")
|
||||
dialog.locator('input[name="table"]').fill("playwright_created")
|
||||
dialog.locator(".table-create-column-name").nth(1).fill("title")
|
||||
dialog.locator(".table-create-add-column").click()
|
||||
dialog.locator(".table-create-column-name").nth(2).fill("score")
|
||||
dialog.locator(".table-create-column-type").nth(2).select_option("integer")
|
||||
dialog.locator(".table-create-add-column").click()
|
||||
dialog.locator(".table-create-column-name").nth(3).fill("metadata")
|
||||
dialog.locator(".table-create-column-type").nth(3).select_option("integer")
|
||||
dialog.locator(".table-create-custom-column-type").nth(3).select_option("json")
|
||||
assert dialog.locator(".table-create-column-type").nth(3).input_value() == "text"
|
||||
assert "table-create-input-placeholder" not in dialog.locator(
|
||||
".table-create-custom-column-type"
|
||||
).nth(3).get_attribute("class")
|
||||
|
||||
dialog.locator(".table-create-save").click()
|
||||
page.wait_for_url("**/data/playwright_created")
|
||||
assert "playwright_created" in page.locator("h1").inner_text()
|
||||
|
||||
response = httpx.get(
|
||||
f"{datasette_server}data/playwright_created.json?_extra=columns,column_types"
|
||||
)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
assert data["columns"] == [
|
||||
"id",
|
||||
"title",
|
||||
"score",
|
||||
"metadata",
|
||||
]
|
||||
assert data["column_types"] == {
|
||||
"metadata": {"type": "json", "config": None},
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.playwright
|
||||
def test_navigation_search_tracks_and_renders_recent_items(page, datasette_server):
|
||||
page.goto(datasette_server)
|
||||
|
|
|
|||
|
|
@ -23,6 +23,23 @@ def table_data_from_soup(soup):
|
|||
return json.loads(match.group(1))
|
||||
|
||||
|
||||
def database_data_from_soup(soup):
|
||||
import json
|
||||
import re
|
||||
|
||||
database_script = [
|
||||
s
|
||||
for s in soup.find_all("script")
|
||||
if "_datasetteDatabaseData" in (s.string or "")
|
||||
][0]
|
||||
match = re.search(
|
||||
r"window\._datasetteDatabaseData\s*=\s*({.*?});",
|
||||
database_script.string,
|
||||
re.DOTALL,
|
||||
)
|
||||
return json.loads(match.group(1))
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
"path,expected_definition_sql",
|
||||
|
|
@ -934,6 +951,133 @@ async def test_row_delete_action_data_attributes():
|
|||
ds.close()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_database_create_table_action_button_and_data():
|
||||
ds = Datasette(
|
||||
[],
|
||||
config={
|
||||
"databases": {
|
||||
"data": {
|
||||
"permissions": {
|
||||
"create-table": {"id": "root"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
try:
|
||||
db = ds.add_database(
|
||||
Database(ds, memory_name="test_database_create_table_action"), name="data"
|
||||
)
|
||||
await db.execute_write_script("""
|
||||
create table items (id integer primary key, name text);
|
||||
""")
|
||||
|
||||
response = await ds.client.get("/data", actor={"id": "root"})
|
||||
assert response.status_code == 200
|
||||
soup = Soup(response.text, "html.parser")
|
||||
|
||||
button = soup.select_one(
|
||||
'button.action-menu-button[data-database-action="create-table"]'
|
||||
)
|
||||
assert button is not None
|
||||
assert button["aria-label"] == "Create table in data"
|
||||
assert button["role"] == "menuitem"
|
||||
description = button.find("span", class_="dropdown-description")
|
||||
assert description.text.strip() == "Create a new table in this database."
|
||||
description.extract()
|
||||
assert button.text.strip() == "Create table"
|
||||
assert any(
|
||||
"edit-tools.js" in script.get("src", "")
|
||||
for script in soup.find_all("script")
|
||||
)
|
||||
assert database_data_from_soup(soup) == {
|
||||
"createTable": {
|
||||
"path": "/data/-/create",
|
||||
"databaseName": "data",
|
||||
"columnTypes": ["text", "integer", "float", "blob"],
|
||||
},
|
||||
}
|
||||
assert "customColumnTypes" not in database_data_from_soup(soup)["createTable"]
|
||||
|
||||
response_without_permission = await ds.client.get(
|
||||
"/data", actor={"id": "someone-else"}
|
||||
)
|
||||
assert response_without_permission.status_code == 200
|
||||
soup_without_permission = Soup(response_without_permission.text, "html.parser")
|
||||
assert (
|
||||
soup_without_permission.select_one(
|
||||
'button[data-database-action="create-table"]'
|
||||
)
|
||||
is None
|
||||
)
|
||||
assert not any(
|
||||
"_datasetteDatabaseData" in (script.string or "")
|
||||
for script in soup_without_permission.find_all("script")
|
||||
)
|
||||
finally:
|
||||
ds.close()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_database_create_table_data_includes_custom_column_types():
|
||||
ds = Datasette(
|
||||
[],
|
||||
config={
|
||||
"databases": {
|
||||
"data": {
|
||||
"permissions": {
|
||||
"create-table": {"id": "root"},
|
||||
"set-column-type": {"id": "root"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
try:
|
||||
db = ds.add_database(
|
||||
Database(ds, memory_name="test_database_create_table_custom_types"),
|
||||
name="data",
|
||||
)
|
||||
await db.execute_write_script("""
|
||||
create table items (id integer primary key, name text);
|
||||
""")
|
||||
|
||||
response = await ds.client.get("/data", actor={"id": "root"})
|
||||
assert response.status_code == 200
|
||||
create_table_data = database_data_from_soup(Soup(response.text, "html.parser"))[
|
||||
"createTable"
|
||||
]
|
||||
assert create_table_data["customColumnTypes"] == [
|
||||
{
|
||||
"name": "email",
|
||||
"description": "Email address",
|
||||
"sqliteTypes": ["text"],
|
||||
"fixedSqliteType": "text",
|
||||
},
|
||||
{
|
||||
"name": "json",
|
||||
"description": "JSON data",
|
||||
"sqliteTypes": ["text"],
|
||||
"fixedSqliteType": "text",
|
||||
},
|
||||
{
|
||||
"name": "textarea",
|
||||
"description": "Multiline text",
|
||||
"sqliteTypes": ["text"],
|
||||
"fixedSqliteType": "text",
|
||||
},
|
||||
{
|
||||
"name": "url",
|
||||
"description": "URL",
|
||||
"sqliteTypes": ["text"],
|
||||
"fixedSqliteType": "text",
|
||||
},
|
||||
]
|
||||
finally:
|
||||
ds.close()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_table_insert_action_button_and_data():
|
||||
ds = Datasette(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue