diff --git a/datasette/static/app.css b/datasette/static/app.css
index 91379dee..b430e208 100644
--- a/datasette/static/app.css
+++ b/datasette/static/app.css
@@ -1819,15 +1819,12 @@ textarea.row-edit-input {
display: none;
}
-.row-edit-bulk-label {
- color: var(--ink);
- font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
- font-size: 0.82rem;
-}
-
.row-edit-bulk-actions {
display: flex;
- justify-content: flex-end;
+ align-items: center;
+ flex-wrap: wrap;
+ gap: 8px;
+ justify-content: flex-start;
}
.row-edit-bulk-actions .btn {
@@ -1835,6 +1832,19 @@ textarea.row-edit-input {
padding-right: 12px;
}
+.row-edit-copy-template-label-narrow {
+ display: none;
+}
+
+.row-edit-bulk-template-note {
+ color: var(--muted);
+ font-size: 0.82rem;
+}
+
+.row-edit-bulk-template-note-narrow {
+ display: none;
+}
+
.row-edit-bulk-textarea {
min-height: 16rem;
resize: vertical;
@@ -1855,6 +1865,28 @@ textarea.row-edit-input {
margin: 0;
}
+.row-edit-bulk-note .button-as-link {
+ font: inherit;
+}
+
+@media (max-width: 640px) {
+ .row-edit-copy-template-label-wide {
+ display: none;
+ }
+
+ .row-edit-copy-template-label-narrow {
+ display: inline;
+ }
+
+ .row-edit-bulk-template-note-wide {
+ display: none;
+ }
+
+ .row-edit-bulk-template-note-narrow {
+ display: inline;
+ }
+}
+
.row-edit-bulk-preview {
display: grid;
gap: 8px;
diff --git a/datasette/static/edit-tools.js b/datasette/static/edit-tools.js
index 12ddb842..0415d772 100644
--- a/datasette/static/edit-tools.js
+++ b/datasette/static/edit-tools.js
@@ -5474,6 +5474,22 @@ function readTextFile(file) {
});
}
+async function loadBulkInsertTextFile(state, file) {
+ if (!file) {
+ return;
+ }
+ try {
+ state.bulkInsertTextarea.value = await readTextFile(file);
+ state.bulkInsertTextarea.dispatchEvent(
+ new Event("input", { bubbles: true }),
+ );
+ clearRowEditDialogError(state);
+ state.bulkInsertTextarea.focus();
+ } catch (_error) {
+ showRowEditDialogError(state, "Could not read that text file.");
+ }
+}
+
function bulkInsertTemplateText(state) {
return (state.bulkInsertColumns || []).join("\t");
}
@@ -5498,11 +5514,23 @@ async function copyTextToClipboard(text) {
}
}
+function setBulkInsertCopyButtonReady(state) {
+ state.copyTemplateButton.textContent = "";
+ var wideLabel = document.createElement("span");
+ wideLabel.className = "row-edit-copy-template-label-wide";
+ wideLabel.textContent = "Copy spreadsheet template";
+ state.copyTemplateButton.appendChild(wideLabel);
+ var narrowLabel = document.createElement("span");
+ narrowLabel.className = "row-edit-copy-template-label-narrow";
+ narrowLabel.textContent = "Copy template";
+ state.copyTemplateButton.appendChild(narrowLabel);
+}
+
function setBulkInsertCopyButtonCopied(state) {
state.copyTemplateButton.textContent = "Copied";
clearTimeout(state.copyTemplateResetTimer);
state.copyTemplateResetTimer = setTimeout(function () {
- state.copyTemplateButton.textContent = "Copy spreadsheet template";
+ setBulkInsertCopyButtonReady(state);
}, 1500);
}
@@ -6255,7 +6283,7 @@ function renderRowInsertFields(state, data) {
return column.name;
});
state.copyTemplateButton.disabled = !state.bulkInsertColumns.length;
- state.copyTemplateButton.textContent = "Copy spreadsheet template";
+ setBulkInsertCopyButtonReady(state);
clearTimeout(state.copyTemplateResetTimer);
state.copyTemplateResetTimer = null;
resetBulkInsertPreview(state);
@@ -6350,12 +6378,13 @@ function ensureRowEditDialog(manager) {
-
+
Paste TSV, CSV, or JSON. You can also or drop it onto this textarea
+
+
-
+
+ You can paste the template into Google Sheets or Excel.Paste into Google Sheets or Excel
-
-
Paste TSV, CSV, or JSON. You can also drop a text file onto this textarea.
@@ -6391,6 +6420,8 @@ function ensureRowEditDialog(manager) {
".row-edit-bulk-progress-status",
),
copyTemplateButton: dialog.querySelector(".row-edit-copy-template"),
+ bulkInsertOpenFileButton: dialog.querySelector(".row-edit-bulk-open-file"),
+ bulkInsertFileInput: dialog.querySelector(".row-edit-bulk-file-input"),
bulkInsertLink: dialog.querySelector(".row-edit-bulk-insert"),
singleInsertLink: dialog.querySelector(".row-edit-single-insert"),
cancelButton: dialog.querySelector(".row-edit-cancel"),
@@ -6472,6 +6503,25 @@ function ensureRowEditDialog(manager) {
},
);
+ rowEditDialogState.bulkInsertOpenFileButton.addEventListener(
+ "click",
+ function () {
+ rowEditDialogState.bulkInsertFileInput.click();
+ },
+ );
+
+ rowEditDialogState.bulkInsertFileInput.addEventListener(
+ "change",
+ async function (ev) {
+ var files = ev.target.files;
+ await loadBulkInsertTextFile(
+ rowEditDialogState,
+ files && files.length ? files[0] : null,
+ );
+ ev.target.value = "";
+ },
+ );
+
rowEditDialogState.bulkInsertTextarea.addEventListener(
"dragenter",
function (ev) {
@@ -6512,20 +6562,7 @@ function ensureRowEditDialog(manager) {
if (!files || !files.length) {
return;
}
- try {
- rowEditDialogState.bulkInsertTextarea.value = await readTextFile(
- files[0],
- );
- rowEditDialogState.bulkInsertTextarea.dispatchEvent(
- new Event("input", { bubbles: true }),
- );
- clearRowEditDialogError(rowEditDialogState);
- } catch (_error) {
- showRowEditDialogError(
- rowEditDialogState,
- "Could not read that text file.",
- );
- }
+ await loadBulkInsertTextFile(rowEditDialogState, files[0]);
},
);
@@ -6573,7 +6610,7 @@ function ensureRowEditDialog(manager) {
state.redirectOnCloseUrl = null;
clearTimeout(state.copyTemplateResetTimer);
state.copyTemplateResetTimer = null;
- state.copyTemplateButton.textContent = "Copy spreadsheet template";
+ setBulkInsertCopyButtonReady(state);
resetBulkInsertPreview(state);
clearRowEditDialogError(state);
state.hasLoaded = false;
diff --git a/tests/test_playwright.py b/tests/test_playwright.py
index 7e475738..bc30b599 100644
--- a/tests/test_playwright.py
+++ b/tests/test_playwright.py
@@ -894,10 +894,32 @@ def test_insert_row_flow_uses_custom_column_field(page, datasette_server):
assert dialog.locator(".row-edit-save").inner_text() == "Preview rows"
assert (
dialog.locator(".row-edit-bulk-note").inner_text()
- == "Paste TSV, CSV, or JSON. You can also drop a text file onto this textarea."
+ == "Paste TSV, CSV, or JSON. You can also open a file or drop it onto this textarea"
+ )
+ assert dialog.locator(".row-edit-bulk-open-file").inner_text() == "open a file"
+ assert (
+ dialog.locator(".row-edit-bulk-editor").evaluate(
+ """node => Array.from(node.children)
+ .filter((child) => !child.hidden)
+ .map((child) => child.className)
+ .join(" ")"""
+ )
+ == (
+ "row-edit-bulk-note row-edit-input row-edit-bulk-textarea "
+ "row-edit-bulk-actions"
+ )
)
copy_template = dialog.locator(".row-edit-copy-template")
assert copy_template.inner_text() == "Copy spreadsheet template"
+ assert dialog.locator(".row-edit-copy-template-label-narrow").text_content() == (
+ "Copy template"
+ )
+ assert dialog.locator(".row-edit-bulk-template-note").inner_text() == (
+ "You can paste the template into Google Sheets or Excel."
+ )
+ assert dialog.locator(".row-edit-bulk-template-note-narrow").text_content() == (
+ "Paste into Google Sheets or Excel"
+ )
copy_template.click()
page.wait_for_function(
"""() => document.querySelector(".row-edit-copy-template").textContent === "Copied" """