Tweaks to bulp insert screen

Moved text around, added clickable 'open a file' button for mobile.

Screenshot: https://github.com/simonw/datasette/pull/2813#issuecomment-4837736664
This commit is contained in:
Simon Willison 2026-06-29 15:16:33 -07:00
commit e0a138ffbd
3 changed files with 120 additions and 29 deletions

View file

@ -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;

View file

@ -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) {
<div class="row-edit-fields"></div>
<div class="row-edit-bulk" hidden>
<div class="row-edit-bulk-editor">
<label class="row-edit-bulk-label" for="row-edit-bulk-textarea">Rows to insert</label>
<p class="row-edit-bulk-note">Paste TSV, CSV, or JSON. You can also <button type="button" class="button-as-link row-edit-bulk-open-file">open a file</button> or drop it onto this textarea</p>
<input class="row-edit-bulk-file-input" type="file" accept=".csv,.tsv,.json,.txt,text/csv,text/tab-separated-values,application/json,text/plain" hidden>
<textarea class="row-edit-input row-edit-bulk-textarea" id="row-edit-bulk-textarea" name="_bulk_rows" rows="12" spellcheck="false" aria-label="Bulk rows"></textarea>
<div class="row-edit-bulk-actions">
<button type="button" class="btn btn-ghost row-edit-copy-template">Copy spreadsheet template</button>
<button type="button" class="btn btn-ghost row-edit-copy-template"><span class="row-edit-copy-template-label-wide">Copy spreadsheet template</span><span class="row-edit-copy-template-label-narrow">Copy template</span></button>
<span class="row-edit-bulk-template-note"><span class="row-edit-bulk-template-note-wide">You can paste the template into Google Sheets or Excel.</span><span class="row-edit-bulk-template-note-narrow">Paste into Google Sheets or Excel</span></span>
</div>
<textarea class="row-edit-input row-edit-bulk-textarea" id="row-edit-bulk-textarea" name="_bulk_rows" rows="12" spellcheck="false"></textarea>
<p class="row-edit-bulk-note">Paste TSV, CSV, or JSON. You can also drop a text file onto this textarea.</p>
</div>
<div class="row-edit-bulk-preview" hidden></div>
<div class="row-edit-bulk-progress" hidden>
@ -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;

View file

@ -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" """