diff --git a/datasette/static/edit-tools.js b/datasette/static/edit-tools.js index b1928f1d..8142d02b 100644 --- a/datasette/static/edit-tools.js +++ b/datasette/static/edit-tools.js @@ -1396,6 +1396,38 @@ function closeRowEditDialogIfConfirmed(state) { return true; } +function scheduleCloseRowEditDialogIfConfirmed(state) { + // Fix for an issue in Safari where hitting Esc would show + // the confirm() prompt asking if state should be discarded + // but the Esc key press would then cancel that dialog too. + // Wait for keyup, then move the confirm() to a fresh timer tick. + if (!state || state.isSaving || state.isClosePending) { + return false; + } + if (!rowEditDialogHasChanges(state)) { + state.shouldRestoreFocus = true; + state.dialog.close(); + return true; + } + state.isClosePending = true; + var closeAfterKeyup = function () { + if (!state.isClosePending) { + return; + } + state.isClosePending = false; + closeRowEditDialogIfConfirmed(state); + }; + var onKeyup = function (ev) { + if (ev.key !== "Escape") { + return; + } + document.removeEventListener("keyup", onKeyup, true); + setTimeout(closeAfterKeyup, 0); + }; + document.addEventListener("keyup", onKeyup, true); + return true; +} + function findDataRowElement(root, rowId) { var elements = root.querySelectorAll("[data-row]"); for (var i = 0; i < elements.length; i += 1) { @@ -1770,6 +1802,7 @@ function ensureRowEditDialog(manager) { manager: manager, isLoading: false, isSaving: false, + isClosePending: false, hasLoaded: false, shouldRestoreFocus: true, }; @@ -1797,23 +1830,18 @@ function ensureRowEditDialog(manager) { return; } ev.preventDefault(); - closeRowEditDialogIfConfirmed(rowEditDialogState); + scheduleCloseRowEditDialogIfConfirmed(rowEditDialogState); }); dialog.addEventListener("cancel", function (ev) { - if ( - rowEditDialogState.isSaving || - !confirmDiscardRowEditChanges(rowEditDialogState) - ) { - ev.preventDefault(); - } else { - rowEditDialogState.shouldRestoreFocus = true; - } + ev.preventDefault(); + scheduleCloseRowEditDialogIfConfirmed(rowEditDialogState); }); dialog.addEventListener("close", function () { var state = rowEditDialogState; state.loadId += 1; + state.isClosePending = false; clearRowEditDialogError(state); state.hasLoaded = false; destroyRowEditFields(state); diff --git a/datasette/utils/asgi.py b/datasette/utils/asgi.py index 55eba1bb..93084365 100644 --- a/datasette/utils/asgi.py +++ b/datasette/utils/asgi.py @@ -155,6 +155,10 @@ class Request: body = await self.post_body() return dict(parse_qsl(body.decode("utf-8"), keep_blank_values=True)) + async def json(self): + body = await self.post_body() + return json.loads(body) + async def form( self, files: bool = False, diff --git a/datasette/views/database.py b/datasette/views/database.py index 6afd9734..cd9565c6 100644 --- a/datasette/views/database.py +++ b/datasette/views/database.py @@ -1093,9 +1093,8 @@ class TableCreateView(BaseView): ): return _error(["Permission denied"], 403) - body = await request.post_body() try: - data = json.loads(body) + data = await request.json() except json.JSONDecodeError as e: return _error(["Invalid JSON: {}".format(e)]) diff --git a/datasette/views/row.py b/datasette/views/row.py index e6eaa92b..4d61eb91 100644 --- a/datasette/views/row.py +++ b/datasette/views/row.py @@ -418,9 +418,8 @@ class RowUpdateView(BaseView): if not ok: return resolved - body = await request.post_body() try: - data = json.loads(body) + data = await request.json() except json.JSONDecodeError as e: return _error(["Invalid JSON: {}".format(e)]) diff --git a/datasette/views/table.py b/datasette/views/table.py index de7b0216..c5448c85 100644 --- a/datasette/views/table.py +++ b/datasette/views/table.py @@ -673,9 +673,8 @@ class TableInsertView(BaseView): if not request.headers.get("content-type").startswith("application/json"): # TODO: handle form-encoded data return _errors(["Invalid content-type, must be application/json"]) - body = await request.post_body() try: - data = json.loads(body) + data = await request.json() except json.JSONDecodeError as e: return _errors(["Invalid JSON: {}".format(e)]) if not isinstance(data, dict): @@ -974,7 +973,7 @@ class TableSetColumnTypeView(BaseView): return _error(["Invalid content-type, must be application/json"], 400) try: - data = json.loads(await request.post_body()) + data = await request.json() except json.JSONDecodeError as e: return _error(["Invalid JSON: {}".format(e)], 400) @@ -1091,7 +1090,7 @@ class TableDropView(BaseView): return _error(["Database is immutable"], 403) confirm = False try: - data = json.loads(await request.post_body()) + data = await request.json() confirm = data.get("confirm") except json.JSONDecodeError: pass diff --git a/docs/changelog.rst b/docs/changelog.rst index c0bd7e6b..e4aae2b8 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -3,6 +3,26 @@ ========= Changelog ========= +.. _unreleased: + +Unreleased +---------- + +The big feature in this alpha is tools to **insert, edit and delete** rows within the Datasette interface. These features are available on table pages, and edit and delete are also available as action items on the row page. + +The edit interface takes :ref:`custom column types ` into account. Plugins that define their own column types can use JavaScript to customize how those column types are presented in the edit interface. + +- ``datasette.allowed_many()`` method for :ref:`resolving multiple permission checks at once `. (:pr:`2775`) +- Permission checks are now cached on a per-request basis, speeding up table pages with multiple plugins that check permissions in order to populate the :ref:`table actions menu `. +- Fixed a warning about ``gen.throw(*sys.exc_info())``. (:issue:`2776`) +- New default custom column type ``textarea`` for multi-line text content. This is rendered as a ``