Drop table button in alter dialog

This commit is contained in:
Simon Willison 2026-06-19 22:40:14 -07:00
commit 354780a136
4 changed files with 111 additions and 1 deletions

View file

@ -2542,6 +2542,22 @@ dialog.table-alter-dialog::backdrop {
color: var(--ink);
}
.table-alter-dialog .btn-danger {
background: #b91c1c;
color: #fff;
margin-right: auto;
}
.table-alter-dialog .btn-danger:hover {
background: #991b1b;
}
.table-alter-dialog .btn-danger:disabled,
.table-alter-dialog .btn-danger:disabled:hover {
background: #d98c8c;
color: #fff;
}
.table-alter-dialog .btn-primary {
background: var(--accent);
color: #fff;

View file

@ -1571,6 +1571,7 @@ function setTableAlterDialogSaving(state, isSaving) {
state.cancelButton.disabled = isSaving;
state.addColumnButton.disabled = isSaving;
state.backButton.disabled = isSaving;
state.dropButton.disabled = isSaving;
state.saveButton.textContent = isSaving
? state.mode === "review"
? "Applying..."
@ -2465,6 +2466,8 @@ function showTableAlterEditor(state) {
state.review.hidden = true;
state.review.textContent = "";
state.backButton.hidden = true;
var data = tableAlterData();
state.dropButton.hidden = !(data && data.dropPath);
state.saveButton.textContent = tableAlterSaveButtonText(state);
updateTableAlterMoveButtons(state);
updateTableAlterSaveButtonState(state);
@ -2479,6 +2482,7 @@ function showTableAlterReview(state, result) {
state.review.hidden = false;
state.review.textContent = "";
state.backButton.hidden = false;
state.dropButton.hidden = true;
state.saveButton.textContent = tableAlterSaveButtonText(state);
updateTableAlterSaveButtonState(state);
@ -2565,6 +2569,64 @@ async function applyTableAlterChanges(state, result) {
}
}
function tableAlterDatabaseUrl() {
var data = tableAlterData();
if (!data || !data.path) {
return null;
}
var url = new URL(data.path, location.href);
url.pathname = url.pathname.replace(/\/[^/]+\/-\/alter\/?$/, "");
url.search = "";
url.hash = "";
return url.toString();
}
async function dropTableFromAlterDialog(state) {
if (state.isSaving) {
return;
}
var data = tableAlterData();
if (!data || !data.dropPath) {
return;
}
if (
!window.confirm(
'Permanently delete the table "' +
data.tableName +
'"? This will delete all of its data and cannot be undone.',
)
) {
return;
}
clearTableAlterDialogError(state);
setTableAlterDialogSaving(state, true);
try {
var response = await fetch(data.dropPath, {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
body: JSON.stringify({ confirm: true }),
});
var responseData = null;
try {
responseData = await response.json();
} catch (_error) {
responseData = null;
}
if (!response.ok || (responseData && responseData.ok === false)) {
throw rowMutationRequestError(response, responseData);
}
state.shouldRestoreFocus = false;
state.dialog.close();
window.location.href = tableAlterDatabaseUrl() || "/";
} catch (error) {
setTableAlterDialogSaving(state, false);
showTableAlterDialogError(state, error.message || "Could not drop table");
}
}
async function saveTableAlterDialog(state) {
if (state.isSaving) {
return;
@ -2646,6 +2708,7 @@ function ensureTableAlterDialog(manager) {
</div>
<div class="table-alter-review" hidden></div>
<div class="modal-footer">
<button type="button" class="btn btn-danger table-alter-drop" hidden>Drop table</button>
<button type="button" class="btn btn-ghost table-alter-back" hidden>Back</button>
<button type="button" class="btn btn-ghost table-alter-cancel">Cancel</button>
<button type="submit" class="btn btn-primary table-alter-save">Review changes</button>
@ -2664,6 +2727,7 @@ function ensureTableAlterDialog(manager) {
columnList: dialog.querySelector(".table-alter-column-list"),
addColumnButton: dialog.querySelector(".table-alter-add-column"),
backButton: dialog.querySelector(".table-alter-back"),
dropButton: dialog.querySelector(".table-alter-drop"),
cancelButton: dialog.querySelector(".table-alter-cancel"),
saveButton: dialog.querySelector(".table-alter-save"),
currentButton: null,
@ -2703,6 +2767,10 @@ function ensureTableAlterDialog(manager) {
closeTableAlterDialog(tableAlterDialogState);
});
tableAlterDialogState.dropButton.addEventListener("click", function () {
dropTableFromAlterDialog(tableAlterDialogState);
});
tableAlterDialogState.backButton.addEventListener("click", function () {
if (tableAlterDialogState.isSaving) {
return;

View file

@ -422,6 +422,15 @@ async def _table_alter_ui(
data["customColumnTypes"] = _custom_column_type_options_for_create_table(
datasette
)
can_drop_table = await datasette.allowed(
action="drop-table",
resource=TableResource(database=database_name, table=table_name),
actor=request.actor,
)
if can_drop_table:
data["dropPath"] = "{}/-/drop".format(
datasette.urls.table(database_name, table_name)
)
return data
@ -1193,6 +1202,11 @@ class TableDropView(BaseView):
actor=request.actor, database=database_name, table=table_name
)
)
self.ds.add_message(
request,
"Table {} dropped".format(table_name),
self.ds.WARNING,
)
return Response.json({"ok": True}, status=200)

View file

@ -1089,8 +1089,9 @@ async def test_table_alter_action_button_and_data():
"tables": {
"items": {
"permissions": {
"alter-table": {"id": "root"},
"alter-table": {"id": ["root", "alter-only"]},
"set-column-type": {"id": "root"},
"drop-table": {"id": "root"},
},
"column_types": {"name": "textarea"},
},
@ -1147,6 +1148,7 @@ async def test_table_alter_action_button_and_data():
"textarea",
"url",
]
assert alter_data["dropPath"] == "/data/items/-/drop"
assert alter_data["columns"] == [
{
"name": "id",
@ -1192,6 +1194,16 @@ async def test_table_alter_action_button_and_data():
is None
)
assert "alterTable" not in table_data_from_soup(soup_without_permission)
# An actor that can alter but not drop should not get a dropPath
response_alter_only = await ds.client.get(
"/data/items", actor={"id": "alter-only"}
)
assert response_alter_only.status_code == 200
alter_only_data = table_data_from_soup(
Soup(response_alter_only.text, "html.parser")
)["alterTable"]
assert "dropPath" not in alter_only_data
finally:
ds.close()