mirror of
https://github.com/simonw/datasette.git
synced 2026-07-08 16:44:34 +02:00
Improvements to create table from data
This commit is contained in:
parent
e0a138ffbd
commit
9ec42b2dad
3 changed files with 124 additions and 22 deletions
|
|
@ -1865,6 +1865,7 @@ textarea.row-edit-input {
|
|||
margin: 0;
|
||||
}
|
||||
|
||||
.row-edit-bulk-note label,
|
||||
.row-edit-bulk-note .button-as-link {
|
||||
font: inherit;
|
||||
}
|
||||
|
|
@ -1924,10 +1925,11 @@ textarea.row-edit-input {
|
|||
border-bottom: 1px solid var(--rule);
|
||||
border-right: 1px solid var(--rule);
|
||||
max-width: 18rem;
|
||||
overflow-wrap: anywhere;
|
||||
padding: 6px 8px;
|
||||
text-align: left;
|
||||
vertical-align: top;
|
||||
white-space: nowrap;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.row-edit-bulk-preview-table th {
|
||||
|
|
@ -2296,6 +2298,11 @@ select.table-create-input {
|
|||
margin: 0;
|
||||
}
|
||||
|
||||
.table-create-data-note label,
|
||||
.table-create-data-note .button-as-link {
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
.table-create-data-preview {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
|
|
@ -2335,10 +2342,11 @@ select.table-create-input {
|
|||
border-bottom: 1px solid var(--rule);
|
||||
border-right: 1px solid var(--rule);
|
||||
max-width: 18rem;
|
||||
overflow-wrap: anywhere;
|
||||
padding: 6px 8px;
|
||||
text-align: left;
|
||||
vertical-align: top;
|
||||
white-space: nowrap;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.table-create-data-preview-table th {
|
||||
|
|
|
|||
|
|
@ -1850,6 +1850,36 @@ function resetTableCreateDataPreview(state) {
|
|||
syncTableCreateModeUi(state);
|
||||
}
|
||||
|
||||
function tableCreateTableNameFromFileName(fileName) {
|
||||
var baseName = (fileName || "").replace(/^.*[\\/]/, "");
|
||||
var nameWithoutExtension = baseName.replace(/\.[^.]*$/, "");
|
||||
return nameWithoutExtension
|
||||
.trim()
|
||||
.replace(/\s+/g, "_")
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9_]/g, "");
|
||||
}
|
||||
|
||||
async function loadTableCreateDataTextFile(state, file) {
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
var text = await readTextFile(file);
|
||||
var tableName = tableCreateTableNameFromFileName(file.name);
|
||||
if (tableName) {
|
||||
state.tableName.value = tableName;
|
||||
state.tableName.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
}
|
||||
state.dataTextarea.value = text;
|
||||
state.dataTextarea.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
clearTableCreateDialogError(state);
|
||||
state.dataTextarea.focus();
|
||||
} catch (_error) {
|
||||
showTableCreateDialogError(state, "Could not read that text file.");
|
||||
}
|
||||
}
|
||||
|
||||
function previewTableCreateDataRows(state) {
|
||||
clearTableCreateDialogError(state);
|
||||
resetTableCreateDataPreview(state);
|
||||
|
|
@ -2152,9 +2182,9 @@ function ensureTableCreateDialog(manager) {
|
|||
</div>
|
||||
<div class="table-create-data" hidden>
|
||||
<div class="table-create-data-editor">
|
||||
<label class="table-create-data-label" for="table-create-data-textarea">Rows for the new table</label>
|
||||
<p class="table-create-data-note"><label for="table-create-data-textarea">Paste TSV, CSV, or JSON</label>. You can also <button type="button" class="button-as-link table-create-data-open-file">open a file</button> or drop it onto this textarea</p>
|
||||
<input class="table-create-data-file-input" type="file" accept=".csv,.tsv,.json,.txt,text/csv,text/tab-separated-values,application/json,text/plain" hidden>
|
||||
<textarea class="table-create-input table-create-data-textarea" id="table-create-data-textarea" name="_create_rows" rows="12" spellcheck="false"></textarea>
|
||||
<p class="table-create-data-note">Paste TSV, CSV, or JSON. You can also drop a text file onto this textarea.</p>
|
||||
</div>
|
||||
<div class="table-create-data-preview" hidden></div>
|
||||
</div>
|
||||
|
|
@ -2182,6 +2212,8 @@ function ensureTableCreateDialog(manager) {
|
|||
dataPanel: dialog.querySelector(".table-create-data"),
|
||||
dataEditor: dialog.querySelector(".table-create-data-editor"),
|
||||
dataTextarea: dialog.querySelector(".table-create-data-textarea"),
|
||||
dataOpenFileButton: dialog.querySelector(".table-create-data-open-file"),
|
||||
dataFileInput: dialog.querySelector(".table-create-data-file-input"),
|
||||
dataPreview: dialog.querySelector(".table-create-data-preview"),
|
||||
createFromDataLink: dialog.querySelector(".table-create-from-data"),
|
||||
manualCreateLink: dialog.querySelector(".table-create-manual"),
|
||||
|
|
@ -2251,6 +2283,25 @@ function ensureTableCreateDialog(manager) {
|
|||
clearTableCreateDialogError(tableCreateDialogState);
|
||||
});
|
||||
|
||||
tableCreateDialogState.dataOpenFileButton.addEventListener(
|
||||
"click",
|
||||
function () {
|
||||
tableCreateDialogState.dataFileInput.click();
|
||||
},
|
||||
);
|
||||
|
||||
tableCreateDialogState.dataFileInput.addEventListener(
|
||||
"change",
|
||||
async function (ev) {
|
||||
var files = ev.target.files;
|
||||
await loadTableCreateDataTextFile(
|
||||
tableCreateDialogState,
|
||||
files && files.length ? files[0] : null,
|
||||
);
|
||||
ev.target.value = "";
|
||||
},
|
||||
);
|
||||
|
||||
tableCreateDialogState.dataTextarea.addEventListener(
|
||||
"dragenter",
|
||||
function (ev) {
|
||||
|
|
@ -2291,20 +2342,7 @@ function ensureTableCreateDialog(manager) {
|
|||
if (!files || !files.length) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
tableCreateDialogState.dataTextarea.value = await readTextFile(
|
||||
files[0],
|
||||
);
|
||||
tableCreateDialogState.dataTextarea.dispatchEvent(
|
||||
new Event("input", { bubbles: true }),
|
||||
);
|
||||
clearTableCreateDialogError(tableCreateDialogState);
|
||||
} catch (_error) {
|
||||
showTableCreateDialogError(
|
||||
tableCreateDialogState,
|
||||
"Could not read that text file.",
|
||||
);
|
||||
}
|
||||
await loadTableCreateDataTextFile(tableCreateDialogState, files[0]);
|
||||
},
|
||||
);
|
||||
|
||||
|
|
@ -6378,9 +6416,9 @@ function ensureRowEditDialog(manager) {
|
|||
<div class="row-edit-fields"></div>
|
||||
<div class="row-edit-bulk" hidden>
|
||||
<div class="row-edit-bulk-editor">
|
||||
<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>
|
||||
<p class="row-edit-bulk-note"><label for="row-edit-bulk-textarea">Paste TSV, CSV, or JSON</label>. 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>
|
||||
<textarea class="row-edit-input row-edit-bulk-textarea" id="row-edit-bulk-textarea" name="_bulk_rows" rows="12" spellcheck="false"></textarea>
|
||||
<div class="row-edit-bulk-actions">
|
||||
<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>
|
||||
|
|
|
|||
|
|
@ -399,14 +399,42 @@ def test_create_table_from_data_flow(page, datasette_server):
|
|||
assert dialog.locator(".table-create-save").inner_text() == "Preview rows"
|
||||
assert (
|
||||
dialog.locator(".table-create-data-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(".table-create-data-open-file").inner_text() == "open a file"
|
||||
assert (
|
||||
dialog.locator(".table-create-data-editor").evaluate(
|
||||
"""node => Array.from(node.children)
|
||||
.filter((child) => !child.hidden)
|
||||
.map((child) => child.className)
|
||||
.join(" ")"""
|
||||
)
|
||||
== ("table-create-data-note table-create-input table-create-data-textarea")
|
||||
)
|
||||
assert dialog.locator(".table-create-manual").inner_text() == (
|
||||
"Create table manually"
|
||||
)
|
||||
assert (
|
||||
dialog.get_by_label("Paste TSV, CSV, or JSON").get_attribute("id")
|
||||
== "table-create-data-textarea"
|
||||
)
|
||||
|
||||
textarea = dialog.locator(".table-create-data-textarea")
|
||||
dropped_value = textarea.evaluate("""node => new Promise((resolve) => {
|
||||
node.addEventListener("input", () => resolve(node.value), { once: true });
|
||||
const file = new File(["short_id,name\\nx,Ada"], "Repo Export 2026!!.CSV", { type: "text/csv" });
|
||||
const dataTransfer = new DataTransfer();
|
||||
dataTransfer.items.add(file);
|
||||
node.dispatchEvent(new DragEvent("dragenter", { bubbles: true, cancelable: true, dataTransfer }));
|
||||
node.dispatchEvent(new DragEvent("dragover", { bubbles: true, cancelable: true, dataTransfer }));
|
||||
node.dispatchEvent(new DragEvent("drop", { bubbles: true, cancelable: true, dataTransfer }));
|
||||
})""")
|
||||
assert dropped_value == "short_id,name\nx,Ada"
|
||||
assert dialog.locator(".table-create-table-name").input_value() == (
|
||||
"repo_export_2026"
|
||||
)
|
||||
|
||||
dialog.locator(".table-create-table-name").fill("playwright_from_data")
|
||||
textarea = dialog.locator(".table-create-data-textarea")
|
||||
textarea.fill(
|
||||
json.dumps(
|
||||
{
|
||||
|
|
@ -431,6 +459,18 @@ def test_create_table_from_data_flow(page, datasette_server):
|
|||
assert "short_id" in preview_text
|
||||
assert "Ada" in preview_text
|
||||
assert "2.5" in preview_text
|
||||
preview_cell_style = dialog.locator(
|
||||
".table-create-data-preview-table td"
|
||||
).first.evaluate(
|
||||
"""node => ({
|
||||
overflowWrap: getComputedStyle(node).overflowWrap,
|
||||
whiteSpace: getComputedStyle(node).whiteSpace
|
||||
})"""
|
||||
)
|
||||
assert preview_cell_style == {
|
||||
"overflowWrap": "anywhere",
|
||||
"whiteSpace": "normal",
|
||||
}
|
||||
|
||||
dialog.locator(".table-create-cancel").click()
|
||||
assert dialog.evaluate("node => node.open")
|
||||
|
|
@ -911,6 +951,10 @@ def test_insert_row_flow_uses_custom_column_field(page, datasette_server):
|
|||
)
|
||||
copy_template = dialog.locator(".row-edit-copy-template")
|
||||
assert copy_template.inner_text() == "Copy spreadsheet template"
|
||||
assert (
|
||||
dialog.get_by_label("Paste TSV, CSV, or JSON").get_attribute("id")
|
||||
== "row-edit-bulk-textarea"
|
||||
)
|
||||
assert dialog.locator(".row-edit-copy-template-label-narrow").text_content() == (
|
||||
"Copy template"
|
||||
)
|
||||
|
|
@ -953,6 +997,18 @@ def test_insert_row_flow_uses_custom_column_field(page, datasette_server):
|
|||
assert "id" not in preview_text
|
||||
assert "From CSV" in preview_text
|
||||
assert "null" in preview_text
|
||||
preview_cell_style = dialog.locator(
|
||||
".row-edit-bulk-preview-table td"
|
||||
).first.evaluate(
|
||||
"""node => ({
|
||||
overflowWrap: getComputedStyle(node).overflowWrap,
|
||||
whiteSpace: getComputedStyle(node).whiteSpace
|
||||
})"""
|
||||
)
|
||||
assert preview_cell_style == {
|
||||
"overflowWrap": "anywhere",
|
||||
"whiteSpace": "normal",
|
||||
}
|
||||
assert dialog.locator(".row-edit-cancel").inner_text() == "Back"
|
||||
dialog.locator(".row-edit-cancel").click()
|
||||
assert dialog.evaluate("node => node.open")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue