diff --git a/datasette/static/app.css b/datasette/static/app.css index 6d675d9f..ec3a85fb 100644 --- a/datasette/static/app.css +++ b/datasette/static/app.css @@ -1192,6 +1192,50 @@ dialog.set-column-type-dialog::backdrop { cursor: wait; } +.row-link-with-actions { + display: inline-flex; + align-items: center; + gap: 0.4rem; + flex-wrap: wrap; +} + +.row-inline-actions { + display: inline-flex; + gap: 0.2rem; + align-items: center; +} + +.row-inline-action { + appearance: none; + border: 1px solid rgba(74, 85, 104, 0.24); + background: transparent; + color: #4a5568; + border-radius: 4px; + cursor: pointer; + display: inline-grid; + place-items: center; + min-height: 24px; + min-width: 24px; + padding: 2px; + position: relative; +} + +.row-inline-action:hover, +.row-inline-action:focus { + background: rgba(74, 85, 104, 0.07); +} + +.row-inline-action:focus { + outline: 3px solid #b3d4ff; + outline-offset: 1px; +} + +.row-inline-action-icon { + display: block; + height: 13px; + width: 13px; +} + @media (max-width: 640px) { dialog.mobile-column-actions-dialog { width: 95vw; @@ -1239,6 +1283,17 @@ dialog.set-column-type-dialog::backdrop { padding-left: 18px; padding-right: 18px; } + + .row-inline-action { + min-height: 30px; + min-width: 30px; + padding: 4px; + } + + .row-inline-action-icon { + height: 14px; + width: 14px; + } } @media only screen and (max-width: 576px) { diff --git a/datasette/views/table.py b/datasette/views/table.py index 65388c9c..49238ff4 100644 --- a/datasette/views/table.py +++ b/datasette/views/table.py @@ -194,6 +194,13 @@ async def display_columns_and_rows( pks_for_display = pks if not pks_for_display: pks_for_display = ["rowid"] + row_action_permissions = {} + if link_column and request is not None and db.is_mutable: + row_action_permissions = await datasette.allowed_many( + actions=["update-row", "delete-row"], + resource=TableResource(database=database_name, table=table_name), + actor=request.actor, + ) columns = [] for r in description: @@ -233,19 +240,65 @@ async def display_columns_and_rows( if link_column: is_special_link_column = len(pks) != 1 pk_path = path_from_row_pks(row, pks, not pks, False) + row_path = path_from_row_pks(row, pks, not pks) + row_link = '{flat_pks}'.format( + table_path=datasette.urls.table(database_name, table_name), + flat_pks=str(markupsafe.escape(pk_path)), + flat_pks_quoted=row_path, + ) + edit_icon = ( + '" + ) + delete_icon = ( + '" + ) + row_actions = [] + if row_action_permissions.get("update-row"): + row_actions.append( + '".format( + edit_icon=edit_icon, + row_label=markupsafe.escape(pk_path), + ) + ) + if row_action_permissions.get("delete-row"): + row_actions.append( + '".format( + delete_icon=delete_icon, + row_label=markupsafe.escape(pk_path), + ) + ) + if row_actions: + row_link = ( + '{row_link}' + '' + "{row_actions}" + ).format(row_link=row_link, row_actions="".join(row_actions)) cells.append( { "column": pks[0] if len(pks) == 1 else "Link", "value_type": "pk", "is_special_link_column": is_special_link_column, "raw": pk_path, - "value": markupsafe.Markup( - '{flat_pks}'.format( - table_path=datasette.urls.table(database_name, table_name), - flat_pks=str(markupsafe.escape(pk_path)), - flat_pks_quoted=path_from_row_pks(row, pks, not pks), - ) - ), + "value": markupsafe.Markup(row_link), } )