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),
}
)