mirror of
https://github.com/simonw/datasette.git
synced 2026-07-13 11:04:37 +02:00
Show insert message in correct place, 1s inactive delay
Since I want this to be usable by untrusted datasette-apps it is important that they cannot show the dialog in a way that tricks the user into clicking insert. So a 1s delay on making that button active.
This commit is contained in:
parent
76e6a3bc38
commit
693f4ed601
2 changed files with 117 additions and 13 deletions
|
|
@ -13,22 +13,27 @@ function datasetteManagerPath(manager, path) {
|
|||
|
||||
function ensureRowMutationStatus(manager) {
|
||||
var status = document.querySelector(".row-mutation-status");
|
||||
if (status) {
|
||||
return status;
|
||||
var tableWrapper = document.querySelector(manager.selectors.tableWrapper);
|
||||
var content = document.querySelector("section.content");
|
||||
var fallbackParent = content && content.parentNode;
|
||||
|
||||
if (!status) {
|
||||
status = document.createElement("p");
|
||||
status.className = "row-mutation-status";
|
||||
status.hidden = true;
|
||||
status.setAttribute("role", "status");
|
||||
status.setAttribute("aria-live", "polite");
|
||||
status.setAttribute("tabindex", "-1");
|
||||
}
|
||||
|
||||
status = document.createElement("p");
|
||||
status.className = "row-mutation-status";
|
||||
status.hidden = true;
|
||||
status.setAttribute("role", "status");
|
||||
status.setAttribute("aria-live", "polite");
|
||||
status.setAttribute("tabindex", "-1");
|
||||
|
||||
var tableWrapper = document.querySelector(manager.selectors.tableWrapper);
|
||||
if (tableWrapper && tableWrapper.parentNode) {
|
||||
tableWrapper.parentNode.insertBefore(status, tableWrapper);
|
||||
} else {
|
||||
} else if (content && fallbackParent) {
|
||||
fallbackParent.insertBefore(status, content);
|
||||
} else if (!status.parentNode) {
|
||||
document.body.appendChild(status);
|
||||
} else {
|
||||
document.body.insertBefore(status, document.body.firstChild);
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
|
@ -1189,7 +1194,10 @@ function showRowEditDialogError(state, message) {
|
|||
|
||||
function updateRowEditDialogButtons(state) {
|
||||
state.saveButton.disabled =
|
||||
state.isLoading || state.isSaving || !state.hasLoaded;
|
||||
state.isLoading ||
|
||||
state.isSaving ||
|
||||
!state.hasLoaded ||
|
||||
state.submitDelayActive;
|
||||
state.cancelButton.disabled = state.isSaving;
|
||||
var saveLabel = state.mode === "insert" ? "Insert row" : "Save";
|
||||
state.saveButton.textContent = state.isSaving ? "Saving..." : saveLabel;
|
||||
|
|
@ -1210,6 +1218,30 @@ function setRowEditDialogSaving(state, isSaving) {
|
|||
updateRowEditDialogButtons(state);
|
||||
}
|
||||
|
||||
function clearRowEditDialogSubmitDelay(state) {
|
||||
if (state.submitDelayTimer) {
|
||||
clearTimeout(state.submitDelayTimer);
|
||||
}
|
||||
state.submitDelayTimer = null;
|
||||
state.submitDelayActive = false;
|
||||
}
|
||||
|
||||
function setRowEditDialogSubmitDelay(state, delayMs) {
|
||||
clearRowEditDialogSubmitDelay(state);
|
||||
delayMs = Number(delayMs) || 0;
|
||||
if (delayMs <= 0) {
|
||||
updateRowEditDialogButtons(state);
|
||||
return;
|
||||
}
|
||||
state.submitDelayActive = true;
|
||||
state.submitDelayTimer = setTimeout(function () {
|
||||
state.submitDelayTimer = null;
|
||||
state.submitDelayActive = false;
|
||||
updateRowEditDialogButtons(state);
|
||||
}, delayMs);
|
||||
updateRowEditDialogButtons(state);
|
||||
}
|
||||
|
||||
function valueFromRowEditControl(control) {
|
||||
var value = control.value;
|
||||
return valueFromRowEditText(
|
||||
|
|
@ -1545,7 +1577,12 @@ function addInsertedRowToPage(rowElement) {
|
|||
}
|
||||
|
||||
async function saveRowEditDialog(state) {
|
||||
if (state.isLoading || state.isSaving || !state.hasLoaded) {
|
||||
if (
|
||||
state.isLoading ||
|
||||
state.isSaving ||
|
||||
!state.hasLoaded ||
|
||||
state.submitDelayActive
|
||||
) {
|
||||
return;
|
||||
}
|
||||
clearRowEditDialogError(state);
|
||||
|
|
@ -1907,6 +1944,8 @@ function ensureRowEditDialog(manager) {
|
|||
isSaving: false,
|
||||
isClosePending: false,
|
||||
hasLoaded: false,
|
||||
submitDelayActive: false,
|
||||
submitDelayTimer: null,
|
||||
shouldRestoreFocus: true,
|
||||
};
|
||||
|
||||
|
|
@ -1955,6 +1994,7 @@ function ensureRowEditDialog(manager) {
|
|||
state.isClosePending = false;
|
||||
clearRowEditDialogError(state);
|
||||
state.hasLoaded = false;
|
||||
clearRowEditDialogSubmitDelay(state);
|
||||
destroyRowEditFields(state);
|
||||
setRowEditDialogLoading(state, false);
|
||||
setRowEditDialogSaving(state, false);
|
||||
|
|
@ -2173,6 +2213,7 @@ function openRowInsertDialogWithData(options) {
|
|||
clearRowEditDialogError(state);
|
||||
setRowEditDialogLoading(state, false);
|
||||
destroyRowEditFields(state);
|
||||
setRowEditDialogSubmitDelay(state, options.submitDelayMs);
|
||||
setRowDialogTitle(
|
||||
state.title,
|
||||
insertData.table_name ? "Insert row into" : "Insert row",
|
||||
|
|
@ -2232,6 +2273,7 @@ async function insertDialog(manager, database, table, row, message, options) {
|
|||
resolve: resolve,
|
||||
flashMessage: !!options.flashMessage,
|
||||
refreshAfterInsert: false,
|
||||
submitDelayMs: options.submitDelayMs,
|
||||
});
|
||||
if (!opened) {
|
||||
reject(new Error("Could not open the insert dialog"));
|
||||
|
|
|
|||
|
|
@ -416,6 +416,15 @@ def test_insert_dialog_javascript_api_returns_inserted_row(page, datasette_serve
|
|||
assert result["row"]["notes"] == "Created through insertDialog()"
|
||||
assert result["row"]["license"] == "apache-2.0"
|
||||
assert result["row"]["score"] == 7
|
||||
page.locator(".row-mutation-status", has_text="Inserted row 2").wait_for()
|
||||
assert page.evaluate("""
|
||||
() => {
|
||||
const status = document.querySelector(".row-mutation-status");
|
||||
const footer = document.querySelector("footer.ft");
|
||||
return !!(status.compareDocumentPosition(footer) &
|
||||
Node.DOCUMENT_POSITION_FOLLOWING);
|
||||
}
|
||||
""")
|
||||
|
||||
data = project_row(datasette_server, 2)
|
||||
assert data["title"] == "Suggested from JavaScript"
|
||||
|
|
@ -425,6 +434,59 @@ def test_insert_dialog_javascript_api_returns_inserted_row(page, datasette_serve
|
|||
assert data["score"] == 7
|
||||
|
||||
|
||||
@pytest.mark.playwright
|
||||
def test_insert_dialog_submit_delay_blocks_submit_until_elapsed(page, datasette_server):
|
||||
page.goto(datasette_server)
|
||||
page.wait_for_function("window.datasette && window.datasette.insertDialog")
|
||||
|
||||
page.evaluate("""
|
||||
() => {
|
||||
window.delayedInsertSettled = false;
|
||||
window.delayedInsertResult = window.datasette.insertDialog(
|
||||
"data",
|
||||
"projects",
|
||||
{
|
||||
title: "Delayed JavaScript insert",
|
||||
metadata: '{"delayed": true}',
|
||||
notes: "The submit button starts disabled.",
|
||||
license: "mit"
|
||||
},
|
||||
"Review this suggested project before inserting it.",
|
||||
{submitDelayMs: 1000}
|
||||
);
|
||||
window.delayedInsertResult.then(() => {
|
||||
window.delayedInsertSettled = true;
|
||||
});
|
||||
}
|
||||
""")
|
||||
|
||||
dialog = page.locator("#row-edit-dialog")
|
||||
dialog.wait_for()
|
||||
save = dialog.locator(".row-edit-save")
|
||||
save.wait_for()
|
||||
assert save.is_disabled()
|
||||
|
||||
page.evaluate("""
|
||||
() => document.querySelector("#row-edit-dialog form").requestSubmit()
|
||||
""")
|
||||
page.wait_for_timeout(100)
|
||||
assert page.evaluate("() => window.delayedInsertSettled") is False
|
||||
assert not project_rows(datasette_server, title="Delayed JavaScript insert")
|
||||
|
||||
page.wait_for_function("""
|
||||
() => !document.querySelector("#row-edit-dialog .row-edit-save").disabled
|
||||
""")
|
||||
save.click()
|
||||
result = page.evaluate("() => window.delayedInsertResult")
|
||||
assert result["ok"] is True
|
||||
assert result["status"] == "inserted"
|
||||
assert result["row"]["title"] == "Delayed JavaScript insert"
|
||||
|
||||
data = project_row(datasette_server, 2)
|
||||
assert data["title"] == "Delayed JavaScript insert"
|
||||
assert data["license"] == "mit"
|
||||
|
||||
|
||||
@pytest.mark.playwright
|
||||
def test_edit_row_flow_validates_json_and_saves_changes(page, datasette_server):
|
||||
page.goto(f"{datasette_server}data/projects")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue