mirror of
https://github.com/simonw/datasette.git
synced 2026-06-23 09:14:34 +02:00
4744 lines
140 KiB
JavaScript
4744 lines
140 KiB
JavaScript
var ROW_DELETE_DIALOG_ID = "row-delete-dialog";
|
|
var rowDeleteDialogState = null;
|
|
var ROW_EDIT_DIALOG_ID = "row-edit-dialog";
|
|
var rowEditDialogState = null;
|
|
var TABLE_CREATE_DIALOG_ID = "table-create-dialog";
|
|
var tableCreateDialogState = null;
|
|
var TABLE_ALTER_DIALOG_ID = "table-alter-dialog";
|
|
var tableAlterDialogState = null;
|
|
|
|
function ensureRowMutationStatus(manager) {
|
|
var status = document.querySelector(".row-mutation-status");
|
|
if (status) {
|
|
return status;
|
|
}
|
|
|
|
status = document.createElement("p");
|
|
status.className = "row-mutation-status";
|
|
status.hidden = true;
|
|
status.setAttribute("role", "status");
|
|
status.setAttribute("aria-live", "polite");
|
|
status.setAttribute("tabindex", "-1");
|
|
|
|
var tableWrapper = document.querySelector(manager.selectors.tableWrapper);
|
|
if (tableWrapper && tableWrapper.parentNode) {
|
|
tableWrapper.parentNode.insertBefore(status, tableWrapper);
|
|
} else {
|
|
document.body.appendChild(status);
|
|
}
|
|
return status;
|
|
}
|
|
|
|
function showRowMutationStatus(manager, message, isError) {
|
|
var status = ensureRowMutationStatus(manager);
|
|
status.hidden = false;
|
|
status.classList.toggle("row-mutation-status-error", !!isError);
|
|
status.textContent = message;
|
|
return status;
|
|
}
|
|
|
|
function hideRowMutationStatus() {
|
|
var status = document.querySelector(".row-mutation-status");
|
|
if (!status) {
|
|
return;
|
|
}
|
|
status.hidden = true;
|
|
status.classList.remove("row-mutation-status-error");
|
|
status.textContent = "";
|
|
}
|
|
|
|
function databaseCreateTableData() {
|
|
return (
|
|
window._datasetteDatabaseData && window._datasetteDatabaseData.createTable
|
|
);
|
|
}
|
|
|
|
function tableCreateColumnTypes() {
|
|
var data = databaseCreateTableData() || {};
|
|
return data.columnTypes && data.columnTypes.length
|
|
? data.columnTypes
|
|
: ["text", "integer", "float", "blob"];
|
|
}
|
|
|
|
var SQLITE_COLUMN_TYPE_LABELS = {
|
|
float: "floating point number",
|
|
real: "floating point number",
|
|
blob: "blob - binary data",
|
|
};
|
|
|
|
function sqliteColumnTypeLabel(type) {
|
|
return SQLITE_COLUMN_TYPE_LABELS[type] || type;
|
|
}
|
|
|
|
function populateSqliteColumnTypeSelect(select, type, options) {
|
|
options.forEach(function (option) {
|
|
var optionElement = document.createElement("option");
|
|
optionElement.value = option;
|
|
optionElement.textContent = sqliteColumnTypeLabel(option);
|
|
select.appendChild(optionElement);
|
|
});
|
|
select.value = options.indexOf(type) === -1 ? options[0] : type;
|
|
}
|
|
|
|
function updateSelectPlaceholder(select, placeholderClass) {
|
|
select.classList.toggle(placeholderClass, !select.value);
|
|
}
|
|
|
|
function createCustomColumnTypeSelect(options, className, placeholderClass) {
|
|
var select = document.createElement("select");
|
|
select.className = className;
|
|
select.setAttribute("aria-label", "Custom column type");
|
|
var blankOption = document.createElement("option");
|
|
blankOption.value = "";
|
|
blankOption.textContent = "- custom type -";
|
|
select.appendChild(blankOption);
|
|
options.forEach(function (option) {
|
|
var optionElement = document.createElement("option");
|
|
optionElement.value = option.name;
|
|
optionElement.textContent = option.description
|
|
? option.description + " (" + option.name + ")"
|
|
: option.name;
|
|
select.appendChild(optionElement);
|
|
});
|
|
updateSelectPlaceholder(select, placeholderClass);
|
|
return select;
|
|
}
|
|
|
|
var COLUMN_MOVE_ICONS = {
|
|
top: '<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m18 17-6-6-6 6"></path><path d="m18 11-6-6-6 6"></path></svg>',
|
|
up: '<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m18 15-6-6-6 6"></path></svg>',
|
|
down: '<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6"></path></svg>',
|
|
bottom:
|
|
'<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 7 6 6 6-6"></path><path d="m6 13 6 6 6-6"></path></svg>',
|
|
remove:
|
|
'<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"></path><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"></path></svg>',
|
|
};
|
|
|
|
function createSchemaDialogIconButton(prefix, modifier, ariaLabel, title, svg) {
|
|
var button = document.createElement("button");
|
|
button.type = "button";
|
|
button.className = prefix + "-icon-button " + prefix + "-" + modifier;
|
|
button.setAttribute("aria-label", ariaLabel);
|
|
button.title = title;
|
|
button.dataset.defaultTitle = title;
|
|
button.innerHTML = svg;
|
|
return button;
|
|
}
|
|
|
|
function createSchemaDialogMoveControls(prefix) {
|
|
var moveControls = document.createElement("div");
|
|
moveControls.className = prefix + "-move-controls";
|
|
|
|
var moveTopButton = createSchemaDialogIconButton(
|
|
prefix,
|
|
"move-top",
|
|
"Move column to top",
|
|
"Move column to top",
|
|
COLUMN_MOVE_ICONS.top,
|
|
);
|
|
var moveUpButton = createSchemaDialogIconButton(
|
|
prefix,
|
|
"move-up",
|
|
"Move column up",
|
|
"Move column up",
|
|
COLUMN_MOVE_ICONS.up,
|
|
);
|
|
var moveDownButton = createSchemaDialogIconButton(
|
|
prefix,
|
|
"move-down",
|
|
"Move column down",
|
|
"Move column down",
|
|
COLUMN_MOVE_ICONS.down,
|
|
);
|
|
var moveBottomButton = createSchemaDialogIconButton(
|
|
prefix,
|
|
"move-bottom",
|
|
"Move column to bottom",
|
|
"Move column to bottom",
|
|
COLUMN_MOVE_ICONS.bottom,
|
|
);
|
|
|
|
moveControls.appendChild(moveTopButton);
|
|
moveControls.appendChild(moveUpButton);
|
|
moveControls.appendChild(moveDownButton);
|
|
moveControls.appendChild(moveBottomButton);
|
|
|
|
return {
|
|
controls: moveControls,
|
|
topButton: moveTopButton,
|
|
upButton: moveUpButton,
|
|
downButton: moveDownButton,
|
|
bottomButton: moveBottomButton,
|
|
};
|
|
}
|
|
|
|
function createSchemaDialogMoreOptionsButton(prefix, details) {
|
|
var expandButton = document.createElement("button");
|
|
expandButton.type = "button";
|
|
expandButton.className = prefix + "-more-options";
|
|
expandButton.setAttribute("aria-label", "Toggle column settings");
|
|
expandButton.setAttribute("aria-controls", details.id);
|
|
expandButton.setAttribute("aria-expanded", details.hidden ? "false" : "true");
|
|
updateSchemaDialogMoreOptionsButton(expandButton);
|
|
return expandButton;
|
|
}
|
|
|
|
function updateSchemaDialogMoreOptionsButton(button) {
|
|
var isExpanded = button.getAttribute("aria-expanded") === "true";
|
|
button.textContent = isExpanded ? "v Hide options" : "> Advanced options";
|
|
button.title = isExpanded ? "Hide column settings" : "Show column settings";
|
|
}
|
|
|
|
function toggleSchemaDialogMoreOptions(button, details) {
|
|
var isExpanded = button.getAttribute("aria-expanded") === "true";
|
|
details.hidden = isExpanded;
|
|
button.setAttribute("aria-expanded", isExpanded ? "false" : "true");
|
|
updateSchemaDialogMoreOptionsButton(button);
|
|
}
|
|
|
|
function schemaDialogRows(state, prefix) {
|
|
return Array.prototype.slice.call(
|
|
state.columnList.querySelectorAll("." + prefix + "-column-row"),
|
|
);
|
|
}
|
|
|
|
function schemaDialogRowIsPrimaryKey(row, prefix) {
|
|
var input = row && row.querySelector("." + prefix + "-primary-key-input");
|
|
return !!(input && input.checked);
|
|
}
|
|
|
|
function schemaDialogFirstNonPrimaryRow(state, prefix) {
|
|
var rows = schemaDialogRows(state, prefix);
|
|
for (var i = 0; i < rows.length; i += 1) {
|
|
if (!schemaDialogRowIsPrimaryKey(rows[i], prefix)) {
|
|
return rows[i];
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function updateSchemaDialogMoveButtons(state, prefix) {
|
|
if (!state || !state.columnList) {
|
|
return;
|
|
}
|
|
var firstNonPrimary = schemaDialogFirstNonPrimaryRow(state, prefix);
|
|
var rows = schemaDialogRows(state, prefix);
|
|
var hasPrimaryKeys = rows.some(function (row) {
|
|
return schemaDialogRowIsPrimaryKey(row, prefix);
|
|
});
|
|
var primaryKeyMoveTitle = "Primary key columns are always listed first";
|
|
rows.forEach(function (row) {
|
|
var isPrimaryKey = schemaDialogRowIsPrimaryKey(row, prefix);
|
|
var previous = row.previousElementSibling;
|
|
var next = row.nextElementSibling;
|
|
row
|
|
.querySelectorAll("." + prefix + "-move-controls button")
|
|
.forEach(function (button) {
|
|
button.title = button.dataset.defaultTitle || button.title;
|
|
button.disabled = state.isSaving || isPrimaryKey;
|
|
if (isPrimaryKey) {
|
|
button.title = primaryKeyMoveTitle;
|
|
}
|
|
});
|
|
if (!isPrimaryKey) {
|
|
var topButton = row.querySelector("." + prefix + "-move-top");
|
|
var upButton = row.querySelector("." + prefix + "-move-up");
|
|
var downButton = row.querySelector("." + prefix + "-move-down");
|
|
var bottomButton = row.querySelector("." + prefix + "-move-bottom");
|
|
topButton.disabled =
|
|
state.isSaving || !firstNonPrimary || row === firstNonPrimary;
|
|
upButton.disabled =
|
|
state.isSaving || !previous || schemaDialogRowIsPrimaryKey(previous, prefix);
|
|
downButton.disabled = state.isSaving || !next;
|
|
bottomButton.disabled = state.isSaving || !next;
|
|
if (hasPrimaryKeys && row === firstNonPrimary) {
|
|
topButton.title = primaryKeyMoveTitle;
|
|
upButton.title = primaryKeyMoveTitle;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
function normalizeSchemaDialogPrimaryKeyRows(state, prefix) {
|
|
var rows = schemaDialogRows(state, prefix);
|
|
rows
|
|
.filter(function (row) {
|
|
return schemaDialogRowIsPrimaryKey(row, prefix);
|
|
})
|
|
.concat(
|
|
rows.filter(function (row) {
|
|
return !schemaDialogRowIsPrimaryKey(row, prefix);
|
|
}),
|
|
)
|
|
.forEach(function (row) {
|
|
state.columnList.appendChild(row);
|
|
});
|
|
}
|
|
|
|
function tableCreateCustomColumnTypes() {
|
|
var data = databaseCreateTableData() || {};
|
|
return data.customColumnTypes || [];
|
|
}
|
|
|
|
function tableCreateCustomColumnType(name) {
|
|
var options = tableCreateCustomColumnTypes();
|
|
for (var i = 0; i < options.length; i += 1) {
|
|
if (options[i].name === name) {
|
|
return options[i];
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function tableCreateCustomTypeAppliesToSqliteType(option, sqliteType) {
|
|
return (
|
|
option &&
|
|
option.sqliteTypes &&
|
|
option.sqliteTypes.indexOf(sqliteType) !== -1
|
|
);
|
|
}
|
|
|
|
function tableCreateDialogRows(state) {
|
|
return schemaDialogRows(state, "table-create");
|
|
}
|
|
|
|
function tableCreateRowIsPrimaryKey(row) {
|
|
return schemaDialogRowIsPrimaryKey(row, "table-create");
|
|
}
|
|
|
|
function tableCreateFirstNonPrimaryRow(state) {
|
|
return schemaDialogFirstNonPrimaryRow(state, "table-create");
|
|
}
|
|
|
|
function updateTableCreateMoveButtons(state) {
|
|
updateSchemaDialogMoveButtons(state, "table-create");
|
|
}
|
|
|
|
function tableCreateTypeAffinity(type) {
|
|
if (type === "float") {
|
|
return "real";
|
|
}
|
|
return type;
|
|
}
|
|
|
|
function foreignKeyTypesCompatible(sourceAffinity, targetAffinity) {
|
|
if (sourceAffinity === targetAffinity) {
|
|
return true;
|
|
}
|
|
var numericAffinities = ["integer", "real", "numeric"];
|
|
if (sourceAffinity === "numeric") {
|
|
return numericAffinities.indexOf(targetAffinity) !== -1;
|
|
}
|
|
if (targetAffinity === "numeric") {
|
|
return numericAffinities.indexOf(sourceAffinity) !== -1;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function tableCreateForeignKeyTargetKey(target) {
|
|
return target.fk_table + "\u001f" + target.fk_column;
|
|
}
|
|
|
|
function tableCreateForeignKeyTargetLabel(target) {
|
|
return (
|
|
target.fk_table +
|
|
"." +
|
|
target.fk_column +
|
|
" (" +
|
|
sqliteColumnTypeLabel(target.type) +
|
|
")"
|
|
);
|
|
}
|
|
|
|
function tableCreateForeignKeyTargetsUrl() {
|
|
var data = databaseCreateTableData() || {};
|
|
if (data.foreignKeyTargetsPath) {
|
|
return data.foreignKeyTargetsPath;
|
|
}
|
|
if (!data.path) {
|
|
return null;
|
|
}
|
|
return data.path.replace(/\/-\/create$/, "/-/foreign-key-targets");
|
|
}
|
|
|
|
function populateTableCreateForeignKeySelect(select, state, sourceType) {
|
|
var previousKey = select.value || select.dataset.selectedKey || "";
|
|
select.textContent = "";
|
|
|
|
var blankOption = document.createElement("option");
|
|
blankOption.value = "";
|
|
blankOption.textContent = "- no foreign key -";
|
|
select.appendChild(blankOption);
|
|
|
|
if (state.foreignKeyTargetsLoading) {
|
|
var loadingOption = document.createElement("option");
|
|
loadingOption.value = "";
|
|
loadingOption.disabled = true;
|
|
loadingOption.textContent = "Loading foreign keys...";
|
|
select.appendChild(loadingOption);
|
|
} else if (state.foreignKeyTargetsError) {
|
|
var errorOption = document.createElement("option");
|
|
errorOption.value = "";
|
|
errorOption.disabled = true;
|
|
errorOption.textContent = "Could not load foreign keys";
|
|
select.appendChild(errorOption);
|
|
} else {
|
|
var sourceAffinity = tableCreateTypeAffinity(sourceType);
|
|
(state.foreignKeyTargets || []).forEach(function (target) {
|
|
if (!foreignKeyTypesCompatible(sourceAffinity, target.type)) {
|
|
return;
|
|
}
|
|
var optionElement = document.createElement("option");
|
|
optionElement.value = tableCreateForeignKeyTargetKey(target);
|
|
optionElement.dataset.fkTable = target.fk_table;
|
|
optionElement.dataset.fkColumn = target.fk_column;
|
|
optionElement.textContent = tableCreateForeignKeyTargetLabel(target);
|
|
select.appendChild(optionElement);
|
|
});
|
|
}
|
|
|
|
select.value = previousKey;
|
|
if (select.value !== previousKey) {
|
|
select.value = "";
|
|
}
|
|
select.dataset.selectedKey = select.value;
|
|
select.disabled = state.isSaving || select.options.length <= 1;
|
|
updateSelectPlaceholder(select, "table-create-input-placeholder");
|
|
}
|
|
|
|
function syncTableCreateForeignKeyOptions(row, state) {
|
|
var typeSelect = row.querySelector(".table-create-column-type");
|
|
var foreignKeySelect = row.querySelector(".table-create-foreign-key-target");
|
|
if (!typeSelect || !foreignKeySelect) {
|
|
return;
|
|
}
|
|
populateTableCreateForeignKeySelect(
|
|
foreignKeySelect,
|
|
state,
|
|
typeSelect.value,
|
|
);
|
|
}
|
|
|
|
function syncTableCreateCustomTypeAndForeignKey(row, state, isFirstColumn) {
|
|
var customTypeSelect = row.querySelector(".table-create-custom-column-type");
|
|
var foreignKeySelect = row.querySelector(".table-create-foreign-key-target");
|
|
if (!foreignKeySelect) {
|
|
return;
|
|
}
|
|
|
|
var hasCustomType = customTypeSelect && !!customTypeSelect.value;
|
|
var hasForeignKey = !!foreignKeySelect.value;
|
|
|
|
if (customTypeSelect && hasForeignKey) {
|
|
customTypeSelect.value = "";
|
|
updateTableCreateCustomColumnTypePlaceholder(customTypeSelect);
|
|
hasCustomType = false;
|
|
}
|
|
|
|
if (isFirstColumn || hasCustomType) {
|
|
foreignKeySelect.value = "";
|
|
foreignKeySelect.dataset.selectedKey = "";
|
|
updateTableCreateCustomColumnTypePlaceholder(foreignKeySelect);
|
|
hasForeignKey = false;
|
|
}
|
|
|
|
if (customTypeSelect) {
|
|
customTypeSelect.disabled = state.isSaving;
|
|
}
|
|
foreignKeySelect.disabled =
|
|
state.isSaving || isFirstColumn || foreignKeySelect.options.length <= 1;
|
|
}
|
|
|
|
function refreshTableCreateForeignKeyControls(state) {
|
|
tableCreateDialogRows(state).forEach(function (row, index) {
|
|
if (index > 0) {
|
|
syncTableCreateForeignKeyOptions(row, state);
|
|
}
|
|
syncTableCreateCustomTypeAndForeignKey(row, state, index === 0);
|
|
});
|
|
}
|
|
|
|
function updateTableCreateColumnRules(state) {
|
|
tableCreateDialogRows(state).forEach(function (row, index) {
|
|
var isFirstColumn = index === 0;
|
|
var pkLabel = row.querySelector(".table-create-primary-key");
|
|
var pkInput = row.querySelector(".table-create-primary-key-input");
|
|
var foreignKeyField = row.querySelector(".table-create-foreign-key-field");
|
|
var foreignKeySelect = row.querySelector(".table-create-foreign-key-target");
|
|
|
|
if (pkLabel && pkInput) {
|
|
pkLabel.hidden = !isFirstColumn;
|
|
if (!isFirstColumn) {
|
|
pkInput.checked = false;
|
|
}
|
|
}
|
|
|
|
if (foreignKeyField && foreignKeySelect) {
|
|
foreignKeyField.hidden = isFirstColumn;
|
|
if (isFirstColumn) {
|
|
foreignKeySelect.value = "";
|
|
foreignKeySelect.dataset.selectedKey = "";
|
|
foreignKeySelect.disabled = true;
|
|
updateTableCreateCustomColumnTypePlaceholder(foreignKeySelect);
|
|
} else {
|
|
syncTableCreateForeignKeyOptions(row, state);
|
|
}
|
|
syncTableCreateCustomTypeAndForeignKey(row, state, isFirstColumn);
|
|
}
|
|
});
|
|
updateTableCreateMoveButtons(state);
|
|
}
|
|
|
|
async function loadTableCreateForeignKeyTargets(state) {
|
|
var url = tableCreateForeignKeyTargetsUrl();
|
|
if (!url || !window.fetch) {
|
|
state.foreignKeyTargets = [];
|
|
state.foreignKeyTargetsLoading = false;
|
|
refreshTableCreateForeignKeyControls(state);
|
|
return;
|
|
}
|
|
state.foreignKeyTargets = [];
|
|
state.foreignKeyTargetsError = null;
|
|
state.foreignKeyTargetsLoading = true;
|
|
refreshTableCreateForeignKeyControls(state);
|
|
try {
|
|
var response = await fetch(url, {
|
|
headers: {
|
|
Accept: "application/json",
|
|
},
|
|
});
|
|
var data = await response.json();
|
|
if (!response.ok || data.ok === false) {
|
|
throw rowMutationRequestError(response, data);
|
|
}
|
|
state.foreignKeyTargets = data.targets || [];
|
|
} catch (error) {
|
|
state.foreignKeyTargets = [];
|
|
state.foreignKeyTargetsError = error;
|
|
} finally {
|
|
state.foreignKeyTargetsLoading = false;
|
|
refreshTableCreateForeignKeyControls(state);
|
|
}
|
|
}
|
|
|
|
function tableCreateDialogSignature(state) {
|
|
if (!state || !state.form) {
|
|
return "";
|
|
}
|
|
return JSON.stringify({
|
|
table: state.tableName.value,
|
|
columns: tableCreateDialogRows(state).map(function (row) {
|
|
return {
|
|
name: row.querySelector(".table-create-column-name").value,
|
|
type: row.querySelector(".table-create-column-type").value,
|
|
customType:
|
|
(
|
|
row.querySelector(".table-create-custom-column-type") || {
|
|
value: "",
|
|
}
|
|
).value || "",
|
|
pk: row.querySelector(".table-create-primary-key-input").checked,
|
|
foreignKey:
|
|
(
|
|
row.querySelector(".table-create-foreign-key-target") || {
|
|
value: "",
|
|
}
|
|
).value || "",
|
|
};
|
|
}),
|
|
});
|
|
}
|
|
|
|
function tableCreateDialogHasChanges(state) {
|
|
return (
|
|
!!state &&
|
|
!state.isSaving &&
|
|
tableCreateDialogSignature(state) !== state.initialSignature
|
|
);
|
|
}
|
|
|
|
function clearTableCreateDialogError(state) {
|
|
state.error.hidden = true;
|
|
state.error.textContent = "";
|
|
state.dialog.removeAttribute("aria-describedby");
|
|
}
|
|
|
|
function showTableCreateDialogError(state, message) {
|
|
state.error.hidden = false;
|
|
state.error.textContent = message;
|
|
state.dialog.setAttribute("aria-describedby", "table-create-error");
|
|
state.error.focus();
|
|
}
|
|
|
|
function setTableCreateDialogSaving(state, isSaving) {
|
|
state.isSaving = isSaving;
|
|
state.cancelButton.disabled = isSaving;
|
|
state.saveButton.disabled = isSaving;
|
|
state.addColumnButton.disabled = isSaving;
|
|
state.saveButton.textContent = isSaving ? "Creating..." : "Create table";
|
|
state.columnList
|
|
.querySelectorAll("input, select, button")
|
|
.forEach(function (control) {
|
|
control.disabled = isSaving;
|
|
});
|
|
if (!isSaving) {
|
|
updateTableCreateColumnRules(state);
|
|
}
|
|
updateTableCreateMoveButtons(state);
|
|
}
|
|
|
|
function tableCreateSelectTypeValue(select, type) {
|
|
var options = tableCreateColumnTypes();
|
|
populateSqliteColumnTypeSelect(select, type, options);
|
|
}
|
|
|
|
function updateTableCreateCustomColumnTypePlaceholder(select) {
|
|
updateSelectPlaceholder(select, "table-create-input-placeholder");
|
|
}
|
|
|
|
function createTableCustomColumnTypeSelect() {
|
|
var options = tableCreateCustomColumnTypes();
|
|
return createCustomColumnTypeSelect(
|
|
options,
|
|
"table-create-input table-create-custom-column-type",
|
|
"table-create-input-placeholder",
|
|
);
|
|
}
|
|
|
|
function syncTableCreateCustomTypeForSqliteType(row) {
|
|
var typeSelect = row.querySelector(".table-create-column-type");
|
|
var customTypeSelect = row.querySelector(".table-create-custom-column-type");
|
|
if (!typeSelect || !customTypeSelect || !customTypeSelect.value) {
|
|
return;
|
|
}
|
|
var option = tableCreateCustomColumnType(customTypeSelect.value);
|
|
if (!tableCreateCustomTypeAppliesToSqliteType(option, typeSelect.value)) {
|
|
customTypeSelect.value = "";
|
|
updateTableCreateCustomColumnTypePlaceholder(customTypeSelect);
|
|
}
|
|
}
|
|
|
|
function createTableColumnRow(state, column) {
|
|
var index = state.nextColumnIndex;
|
|
state.nextColumnIndex += 1;
|
|
|
|
var row = document.createElement("div");
|
|
row.className = "table-create-column-row";
|
|
|
|
var main = document.createElement("div");
|
|
main.className = "table-create-column-main";
|
|
|
|
var details = document.createElement("div");
|
|
details.className = "table-create-column-details";
|
|
details.id = "table-create-column-details-" + index;
|
|
details.hidden = !(column && column.expanded);
|
|
|
|
var expandButton = createSchemaDialogMoreOptionsButton(
|
|
"table-create",
|
|
details,
|
|
);
|
|
|
|
var nameId = "table-create-column-name-" + index;
|
|
var nameLabel = document.createElement("label");
|
|
nameLabel.className = "table-create-column-label";
|
|
nameLabel.setAttribute("for", nameId);
|
|
nameLabel.textContent = "Column";
|
|
|
|
var nameInput = document.createElement("input");
|
|
nameInput.id = nameId;
|
|
nameInput.className = "table-create-input table-create-column-name";
|
|
nameInput.type = "text";
|
|
nameInput.required = true;
|
|
nameInput.autocomplete = "off";
|
|
nameInput.value = column && column.name ? column.name : "";
|
|
|
|
var typeSelect = document.createElement("select");
|
|
typeSelect.className = "table-create-input table-create-column-type";
|
|
typeSelect.setAttribute("aria-label", "Column type");
|
|
tableCreateSelectTypeValue(typeSelect, column && column.type);
|
|
|
|
var customTypeSelect = null;
|
|
var customTypeField = null;
|
|
if (tableCreateCustomColumnTypes().length) {
|
|
var customTypeId = "table-create-column-custom-type-" + index;
|
|
customTypeField = document.createElement("div");
|
|
customTypeField.className =
|
|
"table-create-detail-field table-create-custom-type-field";
|
|
var customTypeLabel = document.createElement("label");
|
|
customTypeLabel.className = "table-create-detail-label";
|
|
customTypeLabel.setAttribute("for", customTypeId);
|
|
customTypeLabel.textContent = "Custom type";
|
|
var customTypeHelpId = "table-create-column-custom-type-help-" + index;
|
|
var customTypeHelp = document.createElement("p");
|
|
customTypeHelp.id = customTypeHelpId;
|
|
customTypeHelp.className = "table-create-detail-help";
|
|
customTypeHelp.textContent =
|
|
"Controls how Datasette displays and edits this column";
|
|
customTypeSelect = createTableCustomColumnTypeSelect();
|
|
customTypeSelect.id = customTypeId;
|
|
customTypeSelect.setAttribute("aria-describedby", customTypeHelpId);
|
|
customTypeSelect.value = column && column.customType ? column.customType : "";
|
|
updateTableCreateCustomColumnTypePlaceholder(customTypeSelect);
|
|
customTypeField.appendChild(customTypeLabel);
|
|
customTypeField.appendChild(customTypeHelp);
|
|
customTypeField.appendChild(customTypeSelect);
|
|
}
|
|
|
|
var pkLabel = document.createElement("label");
|
|
pkLabel.className = "table-create-detail-check table-create-primary-key";
|
|
var pkInput = document.createElement("input");
|
|
pkInput.type = "checkbox";
|
|
pkInput.className = "table-create-primary-key-input";
|
|
pkInput.checked = !!(column && column.primaryKey);
|
|
var pkText = document.createElement("span");
|
|
var pkStrong = document.createElement("strong");
|
|
pkStrong.textContent = "Primary key";
|
|
pkText.appendChild(pkStrong);
|
|
pkText.appendChild(
|
|
document.createTextNode(" This ID uniquely identifies the record"),
|
|
);
|
|
pkLabel.appendChild(pkInput);
|
|
pkLabel.appendChild(pkText);
|
|
|
|
var foreignKeyId = "table-create-column-foreign-key-" + index;
|
|
var foreignKeyHelpId = "table-create-column-foreign-key-help-" + index;
|
|
var foreignKeyField = document.createElement("div");
|
|
foreignKeyField.className =
|
|
"table-create-detail-field table-create-foreign-key-field";
|
|
var foreignKeyLabel = document.createElement("label");
|
|
foreignKeyLabel.className = "table-create-detail-label";
|
|
foreignKeyLabel.setAttribute("for", foreignKeyId);
|
|
foreignKeyLabel.textContent = "Foreign key";
|
|
var foreignKeyHelp = document.createElement("p");
|
|
foreignKeyHelp.id = foreignKeyHelpId;
|
|
foreignKeyHelp.className = "table-create-detail-help";
|
|
foreignKeyHelp.textContent = "Link this column to another table.";
|
|
var foreignKeySelect = document.createElement("select");
|
|
foreignKeySelect.id = foreignKeyId;
|
|
foreignKeySelect.className =
|
|
"table-create-input table-create-foreign-key-target";
|
|
foreignKeySelect.setAttribute("aria-label", "Foreign key target");
|
|
foreignKeySelect.setAttribute("aria-describedby", foreignKeyHelpId);
|
|
foreignKeyField.appendChild(foreignKeyLabel);
|
|
foreignKeyField.appendChild(foreignKeyHelp);
|
|
foreignKeyField.appendChild(foreignKeySelect);
|
|
|
|
var moveControls = createSchemaDialogMoveControls("table-create");
|
|
|
|
var removeButton = createSchemaDialogIconButton(
|
|
"table-create",
|
|
"remove-column",
|
|
"Remove column",
|
|
"Remove column",
|
|
COLUMN_MOVE_ICONS.remove,
|
|
);
|
|
|
|
main.appendChild(nameLabel);
|
|
main.appendChild(nameInput);
|
|
main.appendChild(typeSelect);
|
|
main.appendChild(moveControls.controls);
|
|
main.appendChild(removeButton);
|
|
main.appendChild(expandButton);
|
|
|
|
if (customTypeField) {
|
|
details.appendChild(customTypeField);
|
|
}
|
|
details.appendChild(foreignKeyField);
|
|
details.appendChild(pkLabel);
|
|
row.appendChild(main);
|
|
row.appendChild(details);
|
|
|
|
removeButton.addEventListener("click", function () {
|
|
if (state.isSaving) {
|
|
return;
|
|
}
|
|
row.remove();
|
|
clearTableCreateDialogError(state);
|
|
updateTableCreateColumnRules(state);
|
|
var nextInput = state.columnList.querySelector(".table-create-column-name");
|
|
if (nextInput) {
|
|
nextInput.focus();
|
|
} else {
|
|
state.addColumnButton.focus();
|
|
}
|
|
});
|
|
|
|
nameInput.addEventListener("input", function () {
|
|
clearTableCreateDialogError(state);
|
|
});
|
|
typeSelect.addEventListener("change", function () {
|
|
clearTableCreateDialogError(state);
|
|
syncTableCreateCustomTypeForSqliteType(row);
|
|
syncTableCreateForeignKeyOptions(row, state);
|
|
syncTableCreateCustomTypeAndForeignKey(
|
|
row,
|
|
state,
|
|
row === state.columnList.firstElementChild,
|
|
);
|
|
});
|
|
if (customTypeSelect) {
|
|
customTypeSelect.addEventListener("change", function () {
|
|
clearTableCreateDialogError(state);
|
|
updateTableCreateCustomColumnTypePlaceholder(customTypeSelect);
|
|
if (customTypeSelect.value) {
|
|
foreignKeySelect.value = "";
|
|
foreignKeySelect.dataset.selectedKey = "";
|
|
}
|
|
var option = tableCreateCustomColumnType(customTypeSelect.value);
|
|
if (
|
|
option &&
|
|
option.fixedSqliteType &&
|
|
tableCreateColumnTypes().indexOf(option.fixedSqliteType) !== -1
|
|
) {
|
|
typeSelect.value = option.fixedSqliteType;
|
|
syncTableCreateForeignKeyOptions(row, state);
|
|
}
|
|
syncTableCreateCustomTypeAndForeignKey(
|
|
row,
|
|
state,
|
|
row === state.columnList.firstElementChild,
|
|
);
|
|
});
|
|
}
|
|
pkInput.addEventListener("change", function () {
|
|
clearTableCreateDialogError(state);
|
|
updateTableCreateColumnRules(state);
|
|
});
|
|
foreignKeySelect.addEventListener("change", function () {
|
|
foreignKeySelect.dataset.selectedKey = foreignKeySelect.value;
|
|
clearTableCreateDialogError(state);
|
|
updateTableCreateCustomColumnTypePlaceholder(foreignKeySelect);
|
|
if (customTypeSelect && foreignKeySelect.value) {
|
|
customTypeSelect.value = "";
|
|
updateTableCreateCustomColumnTypePlaceholder(customTypeSelect);
|
|
}
|
|
syncTableCreateCustomTypeAndForeignKey(
|
|
row,
|
|
state,
|
|
row === state.columnList.firstElementChild,
|
|
);
|
|
});
|
|
|
|
expandButton.addEventListener("click", function () {
|
|
toggleSchemaDialogMoreOptions(expandButton, details);
|
|
});
|
|
|
|
moveControls.topButton.addEventListener("click", function () {
|
|
var first = tableCreateFirstNonPrimaryRow(state);
|
|
if (
|
|
state.isSaving ||
|
|
tableCreateRowIsPrimaryKey(row) ||
|
|
!first ||
|
|
first === row
|
|
) {
|
|
return;
|
|
}
|
|
state.columnList.insertBefore(row, first);
|
|
clearTableCreateDialogError(state);
|
|
updateTableCreateColumnRules(state);
|
|
row.querySelector(".table-create-column-name").focus();
|
|
});
|
|
|
|
moveControls.upButton.addEventListener("click", function () {
|
|
var previous = row.previousElementSibling;
|
|
if (
|
|
state.isSaving ||
|
|
tableCreateRowIsPrimaryKey(row) ||
|
|
!previous ||
|
|
tableCreateRowIsPrimaryKey(previous)
|
|
) {
|
|
return;
|
|
}
|
|
state.columnList.insertBefore(row, previous);
|
|
clearTableCreateDialogError(state);
|
|
updateTableCreateColumnRules(state);
|
|
row.querySelector(".table-create-column-name").focus();
|
|
});
|
|
|
|
moveControls.downButton.addEventListener("click", function () {
|
|
var next = row.nextElementSibling;
|
|
if (state.isSaving || tableCreateRowIsPrimaryKey(row) || !next) {
|
|
return;
|
|
}
|
|
state.columnList.insertBefore(next, row);
|
|
clearTableCreateDialogError(state);
|
|
updateTableCreateColumnRules(state);
|
|
row.querySelector(".table-create-column-name").focus();
|
|
});
|
|
|
|
moveControls.bottomButton.addEventListener("click", function () {
|
|
var last = state.columnList.lastElementChild;
|
|
if (
|
|
state.isSaving ||
|
|
tableCreateRowIsPrimaryKey(row) ||
|
|
!last ||
|
|
last === row
|
|
) {
|
|
return;
|
|
}
|
|
state.columnList.appendChild(row);
|
|
clearTableCreateDialogError(state);
|
|
updateTableCreateColumnRules(state);
|
|
row.querySelector(".table-create-column-name").focus();
|
|
});
|
|
|
|
return row;
|
|
}
|
|
|
|
function addTableCreateColumn(state, column) {
|
|
var row = createTableColumnRow(state, column || { type: "text" });
|
|
state.columnList.appendChild(row);
|
|
updateTableCreateColumnRules(state);
|
|
return row;
|
|
}
|
|
|
|
function resetTableCreateDialog(state) {
|
|
state.nextColumnIndex = 0;
|
|
state.tableName.value = "";
|
|
state.columnList.textContent = "";
|
|
addTableCreateColumn(state, {
|
|
name: "id",
|
|
type: "integer",
|
|
primaryKey: true,
|
|
});
|
|
addTableCreateColumn(state, {
|
|
name: "",
|
|
type: "text",
|
|
primaryKey: false,
|
|
});
|
|
updateTableCreateColumnRules(state);
|
|
state.initialSignature = tableCreateDialogSignature(state);
|
|
}
|
|
|
|
function collectTableCreatePayload(state) {
|
|
var payload = {
|
|
table: state.tableName.value.trim(),
|
|
columns: [],
|
|
};
|
|
var primaryKeys = [];
|
|
tableCreateDialogRows(state).forEach(function (row, index) {
|
|
var name = row.querySelector(".table-create-column-name").value.trim();
|
|
var type = row.querySelector(".table-create-column-type").value;
|
|
var column = { name: name, type: type };
|
|
var foreignKeySelect = row.querySelector(".table-create-foreign-key-target");
|
|
var foreignKeyOption =
|
|
foreignKeySelect && foreignKeySelect.selectedOptions
|
|
? foreignKeySelect.selectedOptions[0]
|
|
: null;
|
|
if (
|
|
index > 0 &&
|
|
foreignKeyOption &&
|
|
foreignKeyOption.dataset.fkTable &&
|
|
foreignKeyOption.dataset.fkColumn
|
|
) {
|
|
column.fk_table = foreignKeyOption.dataset.fkTable;
|
|
column.fk_column = foreignKeyOption.dataset.fkColumn;
|
|
}
|
|
payload.columns.push(column);
|
|
if (
|
|
index === 0 &&
|
|
row.querySelector(".table-create-primary-key-input").checked
|
|
) {
|
|
primaryKeys.push(name);
|
|
}
|
|
});
|
|
if (primaryKeys.length === 1) {
|
|
payload.pk = primaryKeys[0];
|
|
} else if (primaryKeys.length > 1) {
|
|
payload.pks = primaryKeys;
|
|
}
|
|
return payload;
|
|
}
|
|
|
|
function collectTableCreateColumnTypeAssignments(state) {
|
|
var assignments = [];
|
|
tableCreateDialogRows(state).forEach(function (row) {
|
|
var customTypeSelect = row.querySelector(
|
|
".table-create-custom-column-type",
|
|
);
|
|
if (!customTypeSelect || !customTypeSelect.value) {
|
|
return;
|
|
}
|
|
assignments.push({
|
|
column: row.querySelector(".table-create-column-name").value.trim(),
|
|
columnType: customTypeSelect.value,
|
|
sqliteType: row.querySelector(".table-create-column-type").value,
|
|
});
|
|
});
|
|
return assignments;
|
|
}
|
|
|
|
function validateTableCreatePayload(payload) {
|
|
if (!payload.table) {
|
|
return "Table name is required.";
|
|
}
|
|
if (payload.table.indexOf("\n") !== -1) {
|
|
return "Table name cannot contain newlines.";
|
|
}
|
|
if (/^sqlite_/i.test(payload.table)) {
|
|
return "Table name cannot start with sqlite_.";
|
|
}
|
|
if (!payload.columns.length) {
|
|
return "At least one column is required.";
|
|
}
|
|
var seen = {};
|
|
var supportedTypes = tableCreateColumnTypes();
|
|
for (var i = 0; i < payload.columns.length; i += 1) {
|
|
var column = payload.columns[i];
|
|
if (!column.name) {
|
|
return "Column name is required.";
|
|
}
|
|
if (column.name.indexOf("\n") !== -1) {
|
|
return "Column names cannot contain newlines.";
|
|
}
|
|
var columnKey = column.name.toLowerCase();
|
|
if (seen[columnKey]) {
|
|
return "Duplicate column name: " + column.name;
|
|
}
|
|
seen[columnKey] = true;
|
|
if (supportedTypes.indexOf(column.type) === -1) {
|
|
return "Unsupported column type: " + column.type;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function validateTableCreateColumnTypeAssignments(assignments) {
|
|
for (var i = 0; i < assignments.length; i += 1) {
|
|
var assignment = assignments[i];
|
|
var option = tableCreateCustomColumnType(assignment.columnType);
|
|
if (!option) {
|
|
return "Unknown custom column type: " + assignment.columnType;
|
|
}
|
|
if (
|
|
!tableCreateCustomTypeAppliesToSqliteType(option, assignment.sqliteType)
|
|
) {
|
|
return (
|
|
"Custom type " +
|
|
assignment.columnType +
|
|
" cannot be used with SQLite type " +
|
|
assignment.sqliteType +
|
|
"."
|
|
);
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function fallbackTableUrl(tableName) {
|
|
var data = databaseCreateTableData() || {};
|
|
if (!data.path) {
|
|
return null;
|
|
}
|
|
return data.path.replace(/\/-\/create$/, "/" + encodeURIComponent(tableName));
|
|
}
|
|
|
|
function tableCreateSetColumnTypeUrl(responseData, payload) {
|
|
var tableUrl =
|
|
responseData.table_url ||
|
|
fallbackTableUrl(responseData.table || payload.table);
|
|
if (!tableUrl) {
|
|
return null;
|
|
}
|
|
var url = new URL(tableUrl, location.href);
|
|
url.hash = "";
|
|
url.search = "";
|
|
url.pathname = url.pathname.replace(/\/$/, "") + "/-/set-column-type";
|
|
return url.toString();
|
|
}
|
|
|
|
async function assignTableCreateColumnTypes(
|
|
responseData,
|
|
payload,
|
|
assignments,
|
|
) {
|
|
if (!assignments.length) {
|
|
return;
|
|
}
|
|
var url = tableCreateSetColumnTypeUrl(responseData, payload);
|
|
if (!url) {
|
|
throw new Error("Could not find the set column type URL.");
|
|
}
|
|
for (var i = 0; i < assignments.length; i += 1) {
|
|
var assignment = assignments[i];
|
|
var response = await fetch(url, {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
Accept: "application/json",
|
|
},
|
|
body: JSON.stringify({
|
|
column: assignment.column,
|
|
column_type: {
|
|
type: assignment.columnType,
|
|
},
|
|
}),
|
|
});
|
|
var data = null;
|
|
try {
|
|
data = await response.json();
|
|
} catch (_error) {
|
|
data = null;
|
|
}
|
|
if (!response.ok || (data && data.ok === false)) {
|
|
var error = rowMutationRequestError(response, data);
|
|
throw new Error(
|
|
"Created table, but could not set custom type for " +
|
|
assignment.column +
|
|
": " +
|
|
error.message,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
async function saveTableCreateDialog(state) {
|
|
if (state.isSaving) {
|
|
return;
|
|
}
|
|
var data = databaseCreateTableData();
|
|
if (!data || !data.path) {
|
|
showTableCreateDialogError(state, "Could not find the create table URL.");
|
|
return;
|
|
}
|
|
clearTableCreateDialogError(state);
|
|
var payload = collectTableCreatePayload(state);
|
|
var columnTypeAssignments = collectTableCreateColumnTypeAssignments(state);
|
|
var validationError = validateTableCreatePayload(payload);
|
|
if (validationError) {
|
|
showTableCreateDialogError(state, validationError);
|
|
return;
|
|
}
|
|
var columnTypeValidationError = validateTableCreateColumnTypeAssignments(
|
|
columnTypeAssignments,
|
|
);
|
|
if (columnTypeValidationError) {
|
|
showTableCreateDialogError(state, columnTypeValidationError);
|
|
return;
|
|
}
|
|
setTableCreateDialogSaving(state, true);
|
|
try {
|
|
var response = await fetch(data.path, {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
Accept: "application/json",
|
|
},
|
|
body: JSON.stringify(payload),
|
|
});
|
|
var responseData = null;
|
|
try {
|
|
responseData = await response.json();
|
|
} catch (_error) {
|
|
responseData = null;
|
|
}
|
|
if (!response.ok || (responseData && responseData.ok === false)) {
|
|
throw rowMutationRequestError(response, responseData);
|
|
}
|
|
await assignTableCreateColumnTypes(
|
|
responseData,
|
|
payload,
|
|
columnTypeAssignments,
|
|
);
|
|
var tableUrl =
|
|
responseData.table_url ||
|
|
fallbackTableUrl(responseData.table || payload.table);
|
|
state.shouldRestoreFocus = false;
|
|
state.dialog.close();
|
|
if (tableUrl) {
|
|
location.href = tableUrl;
|
|
} else {
|
|
location.reload();
|
|
}
|
|
} catch (error) {
|
|
setTableCreateDialogSaving(state, false);
|
|
showTableCreateDialogError(
|
|
state,
|
|
error.message || "Could not create table",
|
|
);
|
|
}
|
|
}
|
|
|
|
function confirmDiscardTableCreateChanges(state) {
|
|
if (!tableCreateDialogHasChanges(state)) {
|
|
return true;
|
|
}
|
|
return window.confirm("Discard this new table?");
|
|
}
|
|
|
|
function closeTableCreateDialogIfConfirmed(state) {
|
|
if (!state || state.isSaving) {
|
|
return false;
|
|
}
|
|
if (!confirmDiscardTableCreateChanges(state)) {
|
|
return false;
|
|
}
|
|
state.shouldRestoreFocus = true;
|
|
state.dialog.close();
|
|
return true;
|
|
}
|
|
|
|
function ensureTableCreateDialog(manager) {
|
|
if (tableCreateDialogState) {
|
|
return tableCreateDialogState;
|
|
}
|
|
if (!window.HTMLDialogElement) {
|
|
return null;
|
|
}
|
|
|
|
var dialog = document.createElement("dialog");
|
|
dialog.id = TABLE_CREATE_DIALOG_ID;
|
|
dialog.className = "table-create-dialog";
|
|
dialog.setAttribute("aria-labelledby", "table-create-title");
|
|
dialog.innerHTML = `
|
|
<div class="modal-header">
|
|
<span class="modal-title" id="table-create-title">Create table</span>
|
|
</div>
|
|
<form class="table-create-form" method="post" novalidate>
|
|
<p class="table-create-error" id="table-create-error" role="alert" tabindex="-1" hidden></p>
|
|
<div class="table-create-fields">
|
|
<div class="table-create-field">
|
|
<label class="table-create-label" for="table-create-name">Table name</label>
|
|
<input class="table-create-input table-create-table-name" id="table-create-name" type="text" name="table" required autocomplete="off">
|
|
</div>
|
|
<div class="table-create-columns">
|
|
<div class="table-create-column-headings" aria-hidden="true">
|
|
<span>Column</span>
|
|
<span>Type</span>
|
|
<span>Move</span>
|
|
<span></span>
|
|
</div>
|
|
<div class="table-create-column-list"></div>
|
|
<button type="button" class="table-create-add-column"><svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14"></path><path d="M12 5v14"></path></svg><span>Add column</span></button>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-ghost table-create-cancel">Cancel</button>
|
|
<button type="submit" class="btn btn-primary table-create-save">Create table</button>
|
|
</div>
|
|
</form>
|
|
`;
|
|
document.body.appendChild(dialog);
|
|
|
|
tableCreateDialogState = {
|
|
dialog: dialog,
|
|
form: dialog.querySelector(".table-create-form"),
|
|
title: dialog.querySelector(".modal-title"),
|
|
error: dialog.querySelector(".table-create-error"),
|
|
fields: dialog.querySelector(".table-create-fields"),
|
|
tableName: dialog.querySelector(".table-create-table-name"),
|
|
columnList: dialog.querySelector(".table-create-column-list"),
|
|
addColumnButton: dialog.querySelector(".table-create-add-column"),
|
|
cancelButton: dialog.querySelector(".table-create-cancel"),
|
|
saveButton: dialog.querySelector(".table-create-save"),
|
|
currentButton: null,
|
|
shouldRestoreFocus: true,
|
|
isSaving: false,
|
|
initialSignature: "",
|
|
nextColumnIndex: 0,
|
|
foreignKeyTargets: [],
|
|
foreignKeyTargetsError: null,
|
|
foreignKeyTargetsLoading: false,
|
|
manager: manager,
|
|
};
|
|
|
|
tableCreateDialogState.form.addEventListener("submit", function (ev) {
|
|
ev.preventDefault();
|
|
saveTableCreateDialog(tableCreateDialogState);
|
|
});
|
|
|
|
tableCreateDialogState.addColumnButton.addEventListener("click", function () {
|
|
if (tableCreateDialogState.isSaving) {
|
|
return;
|
|
}
|
|
var row = addTableCreateColumn(tableCreateDialogState, { type: "text" });
|
|
clearTableCreateDialogError(tableCreateDialogState);
|
|
row.querySelector(".table-create-column-name").focus();
|
|
});
|
|
|
|
tableCreateDialogState.cancelButton.addEventListener("click", function () {
|
|
closeTableCreateDialogIfConfirmed(tableCreateDialogState);
|
|
});
|
|
|
|
tableCreateDialogState.tableName.addEventListener("input", function () {
|
|
clearTableCreateDialogError(tableCreateDialogState);
|
|
});
|
|
|
|
dialog.addEventListener("click", function (ev) {
|
|
if (ev.target === dialog) {
|
|
closeTableCreateDialogIfConfirmed(tableCreateDialogState);
|
|
}
|
|
});
|
|
|
|
dialog.addEventListener("keydown", function (ev) {
|
|
if (ev.key !== "Escape") {
|
|
return;
|
|
}
|
|
ev.preventDefault();
|
|
closeTableCreateDialogIfConfirmed(tableCreateDialogState);
|
|
});
|
|
|
|
dialog.addEventListener("cancel", function (ev) {
|
|
ev.preventDefault();
|
|
closeTableCreateDialogIfConfirmed(tableCreateDialogState);
|
|
});
|
|
|
|
dialog.addEventListener("close", function () {
|
|
var state = tableCreateDialogState;
|
|
clearTableCreateDialogError(state);
|
|
setTableCreateDialogSaving(state, false);
|
|
if (
|
|
state.shouldRestoreFocus &&
|
|
state.currentButton &&
|
|
document.contains(state.currentButton)
|
|
) {
|
|
state.currentButton.focus();
|
|
}
|
|
});
|
|
|
|
return tableCreateDialogState;
|
|
}
|
|
|
|
function openTableCreateDialog(button, manager) {
|
|
var data = databaseCreateTableData();
|
|
if (!data) {
|
|
return;
|
|
}
|
|
var state = ensureTableCreateDialog(manager);
|
|
if (!state) {
|
|
return;
|
|
}
|
|
|
|
var menu = button.closest("details");
|
|
if (menu) {
|
|
menu.open = false;
|
|
}
|
|
state.manager = manager;
|
|
state.currentButton = button;
|
|
state.shouldRestoreFocus = true;
|
|
state.title.textContent = "Create a table in " + data.databaseName;
|
|
clearTableCreateDialogError(state);
|
|
resetTableCreateDialog(state);
|
|
loadTableCreateForeignKeyTargets(state);
|
|
if (!state.dialog.open) {
|
|
state.dialog.showModal();
|
|
}
|
|
state.tableName.focus();
|
|
}
|
|
|
|
function initTableCreateActions(manager) {
|
|
if (
|
|
!window.fetch ||
|
|
!window.HTMLDialogElement ||
|
|
!databaseCreateTableData()
|
|
) {
|
|
return;
|
|
}
|
|
document.addEventListener("click", function (ev) {
|
|
var button = ev.target.closest(
|
|
'button[data-database-action="create-table"]',
|
|
);
|
|
if (!button) {
|
|
return;
|
|
}
|
|
ev.preventDefault();
|
|
openTableCreateDialog(button, manager);
|
|
});
|
|
}
|
|
|
|
function setRowDeleteDialogBusy(state, isBusy) {
|
|
state.isBusy = isBusy;
|
|
state.confirmButton.disabled = isBusy;
|
|
state.cancelButton.disabled = isBusy;
|
|
state.confirmButton.textContent = isBusy ? "Deleting..." : "Delete row";
|
|
}
|
|
|
|
function clearRowDeleteDialogError(state) {
|
|
state.error.hidden = true;
|
|
state.error.textContent = "";
|
|
}
|
|
|
|
function showRowDeleteDialogError(state, message) {
|
|
state.error.hidden = false;
|
|
state.error.textContent = message;
|
|
}
|
|
|
|
function rowMutationRequestError(response, data) {
|
|
if (data && data.errors) {
|
|
return new Error(data.errors.join(" "));
|
|
}
|
|
if (data && data.error) {
|
|
return new Error(data.error);
|
|
}
|
|
if (data && data.title) {
|
|
return new Error(data.title);
|
|
}
|
|
return new Error("Request failed with HTTP " + response.status);
|
|
}
|
|
|
|
function tildeDecode(value) {
|
|
if (!value) {
|
|
return "";
|
|
}
|
|
var placeholder = "__datasette_percent_placeholder__";
|
|
try {
|
|
return decodeURIComponent(
|
|
value.replace(/%/g, placeholder).replace(/~/g, "%").replace(/\+/g, " "),
|
|
).replace(new RegExp(placeholder, "g"), "%");
|
|
} catch (_error) {
|
|
return value;
|
|
}
|
|
}
|
|
|
|
function tildeEncode(value) {
|
|
var bytes = new TextEncoder().encode(String(value));
|
|
var encoded = "";
|
|
bytes.forEach(function (byte) {
|
|
var isSafe =
|
|
(byte >= 65 && byte <= 90) ||
|
|
(byte >= 97 && byte <= 122) ||
|
|
(byte >= 48 && byte <= 57) ||
|
|
byte === 95 ||
|
|
byte === 45;
|
|
if (isSafe) {
|
|
encoded += String.fromCharCode(byte);
|
|
} else if (byte === 32) {
|
|
encoded += "+";
|
|
} else {
|
|
encoded += "~" + byte.toString(16).toUpperCase().padStart(2, "0");
|
|
}
|
|
});
|
|
return encoded;
|
|
}
|
|
|
|
function rowDisplayLabel(row) {
|
|
return tildeDecode(row.getAttribute("data-row") || "");
|
|
}
|
|
|
|
function rowTitleLabel(row) {
|
|
return row.getAttribute("data-row-label") || "";
|
|
}
|
|
|
|
function insertedRowStatusMessage(rowId, rowLabel) {
|
|
var message = "Inserted row " + rowId;
|
|
if (rowLabel && rowLabel !== rowId) {
|
|
message += " (" + rowLabel + ")";
|
|
}
|
|
return message + ".";
|
|
}
|
|
|
|
function tableBaseUrl() {
|
|
var tableUrl =
|
|
window._datasetteTableData && window._datasetteTableData.tableUrl;
|
|
var url = new URL(tableUrl || location.href, location.href);
|
|
url.hash = "";
|
|
url.search = "";
|
|
return url;
|
|
}
|
|
|
|
function tablePageData() {
|
|
return window._datasetteTableData || {};
|
|
}
|
|
|
|
function tableInsertData() {
|
|
return tablePageData().insertRow;
|
|
}
|
|
|
|
function tableAlterData() {
|
|
return tablePageData().alterTable;
|
|
}
|
|
|
|
function tableAlterColumnTypes() {
|
|
var data = tableAlterData() || {};
|
|
return data.columnTypes && data.columnTypes.length
|
|
? data.columnTypes
|
|
: ["text", "integer", "float", "blob"];
|
|
}
|
|
|
|
function tableAlterDefaultExpressions() {
|
|
var data = tableAlterData() || {};
|
|
return data.defaultExpressions || [];
|
|
}
|
|
|
|
function tableAlterCustomColumnTypes() {
|
|
var data = tableAlterData() || {};
|
|
return data.customColumnTypes || [];
|
|
}
|
|
|
|
function tableAlterCustomColumnType(name) {
|
|
var options = tableAlterCustomColumnTypes();
|
|
for (var i = 0; i < options.length; i += 1) {
|
|
if (options[i].name === name) {
|
|
return options[i];
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function tableAlterCustomTypeAppliesToSqliteType(option, sqliteType) {
|
|
return (
|
|
option &&
|
|
option.sqliteTypes &&
|
|
option.sqliteTypes.indexOf(sqliteType) !== -1
|
|
);
|
|
}
|
|
|
|
function tableAlterDialogRows(state) {
|
|
return schemaDialogRows(state, "table-alter");
|
|
}
|
|
|
|
function tableAlterRowSignature(row) {
|
|
return {
|
|
existing: row.dataset.existing === "1",
|
|
originalName: row.dataset.originalName || "",
|
|
name: row.querySelector(".table-alter-column-name").value,
|
|
type: row.querySelector(".table-alter-column-type").value,
|
|
customType:
|
|
(
|
|
row.querySelector(".table-alter-custom-column-type") || {
|
|
value: "",
|
|
}
|
|
).value || "",
|
|
notNull: row.querySelector(".table-alter-not-null-input").checked,
|
|
defaultValue: row.querySelector(".table-alter-default").value,
|
|
defaultExpr: row.querySelector(".table-alter-default-expr").value,
|
|
pk: row.querySelector(".table-alter-primary-key-input").checked,
|
|
};
|
|
}
|
|
|
|
function tableAlterDialogSignature(state) {
|
|
if (!state || !state.form) {
|
|
return "";
|
|
}
|
|
return JSON.stringify({
|
|
columns: tableAlterDialogRows(state).map(tableAlterRowSignature),
|
|
deletedColumns: state.deletedColumns.slice(),
|
|
});
|
|
}
|
|
|
|
function tableAlterDialogHasChanges(state) {
|
|
return (
|
|
!!state &&
|
|
!state.isSaving &&
|
|
tableAlterDialogSignature(state) !== state.initialSignature
|
|
);
|
|
}
|
|
|
|
function updateTableAlterSaveButtonState(state) {
|
|
if (!state || !state.saveButton) {
|
|
return;
|
|
}
|
|
state.saveButton.disabled =
|
|
state.isSaving ||
|
|
(state.mode !== "review" &&
|
|
tableAlterDialogSignature(state) === state.initialSignature);
|
|
}
|
|
|
|
function tableAlterRowIsPrimaryKey(row) {
|
|
return schemaDialogRowIsPrimaryKey(row, "table-alter");
|
|
}
|
|
|
|
function tableAlterFirstNonPrimaryRow(state) {
|
|
return schemaDialogFirstNonPrimaryRow(state, "table-alter");
|
|
}
|
|
|
|
function updateTableAlterMoveButtons(state) {
|
|
updateSchemaDialogMoveButtons(state, "table-alter");
|
|
}
|
|
|
|
function normalizeTableAlterPrimaryKeyRows(state) {
|
|
normalizeSchemaDialogPrimaryKeyRows(state, "table-alter");
|
|
}
|
|
|
|
function clearTableAlterDialogError(state) {
|
|
state.error.hidden = true;
|
|
state.error.textContent = "";
|
|
state.dialog.removeAttribute("aria-describedby");
|
|
}
|
|
|
|
function showTableAlterDialogError(state, message) {
|
|
state.error.hidden = false;
|
|
state.error.textContent = message;
|
|
state.dialog.setAttribute("aria-describedby", "table-alter-error");
|
|
state.error.focus();
|
|
}
|
|
|
|
function setTableAlterDialogSaving(state, isSaving) {
|
|
state.isSaving = isSaving;
|
|
state.cancelButton.disabled = isSaving;
|
|
state.addColumnButton.disabled = isSaving;
|
|
state.backButton.disabled = isSaving;
|
|
state.dropButton.disabled = isSaving;
|
|
state.saveButton.textContent = isSaving
|
|
? state.mode === "review"
|
|
? "Applying..."
|
|
: "Preparing..."
|
|
: tableAlterSaveButtonText(state);
|
|
state.columnList
|
|
.querySelectorAll("input, select, button")
|
|
.forEach(function (control) {
|
|
control.disabled = isSaving;
|
|
});
|
|
if (!isSaving) {
|
|
state.columnList
|
|
.querySelectorAll(".table-alter-default-expr")
|
|
.forEach(function (select) {
|
|
syncTableAlterDefaultControls(
|
|
select.closest(".table-alter-column-row"),
|
|
);
|
|
});
|
|
}
|
|
updateTableAlterMoveButtons(state);
|
|
updateTableAlterSaveButtonState(state);
|
|
}
|
|
|
|
function tableAlterSaveButtonText(state) {
|
|
return state && state.mode === "review" ? "Apply changes" : "Review changes";
|
|
}
|
|
|
|
function tableAlterSelectTypeValue(select, type) {
|
|
var options = tableAlterColumnTypes();
|
|
populateSqliteColumnTypeSelect(select, type, options);
|
|
}
|
|
|
|
function updateTableAlterCustomColumnTypePlaceholder(select) {
|
|
updateSelectPlaceholder(select, "table-alter-input-placeholder");
|
|
}
|
|
|
|
function createTableAlterCustomColumnTypeSelect() {
|
|
var options = tableAlterCustomColumnTypes();
|
|
return createCustomColumnTypeSelect(
|
|
options,
|
|
"table-alter-input table-alter-custom-column-type",
|
|
"table-alter-input-placeholder",
|
|
);
|
|
}
|
|
|
|
function syncTableAlterCustomTypeForSqliteType(row) {
|
|
var typeSelect = row.querySelector(".table-alter-column-type");
|
|
var customTypeSelect = row.querySelector(".table-alter-custom-column-type");
|
|
if (!typeSelect || !customTypeSelect || !customTypeSelect.value) {
|
|
return;
|
|
}
|
|
var option = tableAlterCustomColumnType(customTypeSelect.value);
|
|
if (!tableAlterCustomTypeAppliesToSqliteType(option, typeSelect.value)) {
|
|
customTypeSelect.value = "";
|
|
updateTableAlterCustomColumnTypePlaceholder(customTypeSelect);
|
|
}
|
|
}
|
|
|
|
function tableAlterSelectDefaultExprValue(select, value) {
|
|
var blankOption = document.createElement("option");
|
|
blankOption.value = "";
|
|
blankOption.textContent = "- default expr -";
|
|
select.appendChild(blankOption);
|
|
tableAlterDefaultExpressions().forEach(function (option) {
|
|
var optionElement = document.createElement("option");
|
|
optionElement.value = option;
|
|
optionElement.textContent = option.replace(/_/g, " ");
|
|
select.appendChild(optionElement);
|
|
});
|
|
select.value = value || "";
|
|
updateTableAlterDefaultExprPlaceholder(select);
|
|
}
|
|
|
|
function updateTableAlterDefaultExprPlaceholder(select) {
|
|
updateSelectPlaceholder(select, "table-alter-input-placeholder");
|
|
}
|
|
|
|
function syncTableAlterDefaultControls(row) {
|
|
if (!row) {
|
|
return;
|
|
}
|
|
var defaultInput = row.querySelector(".table-alter-default");
|
|
var defaultExprSelect = row.querySelector(".table-alter-default-expr");
|
|
if (!defaultInput || !defaultExprSelect) {
|
|
return;
|
|
}
|
|
defaultInput.disabled = !!defaultExprSelect.value;
|
|
updateTableAlterDefaultExprPlaceholder(defaultExprSelect);
|
|
}
|
|
|
|
function createTableAlterColumnRow(state, column) {
|
|
var index = state.nextColumnIndex;
|
|
state.nextColumnIndex += 1;
|
|
var existing = !!(column && column.existing);
|
|
var originalName = existing ? column.name || "" : "";
|
|
var originalCustomType =
|
|
existing && column.column_type ? column.column_type.type || "" : "";
|
|
var originalDefault =
|
|
existing && column.has_default && column.default !== null
|
|
? String(column.default)
|
|
: "";
|
|
|
|
var row = document.createElement("div");
|
|
row.className = "table-alter-column-row";
|
|
row.dataset.existing = existing ? "1" : "0";
|
|
row.dataset.originalName = originalName;
|
|
row.dataset.originalType = existing ? column.type || "text" : "";
|
|
row.dataset.originalNotNull = existing && column.notnull ? "1" : "0";
|
|
row.dataset.originalHasDefault = existing && column.has_default ? "1" : "0";
|
|
row.dataset.originalDefault = originalDefault;
|
|
row.dataset.originalPk = existing && column.is_pk ? "1" : "0";
|
|
row.dataset.originalCustomType = originalCustomType;
|
|
|
|
var main = document.createElement("div");
|
|
main.className = "table-alter-column-main";
|
|
|
|
var details = document.createElement("div");
|
|
details.className = "table-alter-column-details";
|
|
details.id = "table-alter-column-details-" + index;
|
|
details.hidden = !(column && column.expanded);
|
|
|
|
var expandButton = createSchemaDialogMoreOptionsButton(
|
|
"table-alter",
|
|
details,
|
|
);
|
|
|
|
var nameId = "table-alter-column-name-" + index;
|
|
var nameLabel = document.createElement("label");
|
|
nameLabel.className = "table-alter-column-label";
|
|
nameLabel.setAttribute("for", nameId);
|
|
nameLabel.textContent = "Column";
|
|
|
|
var nameInput = document.createElement("input");
|
|
nameInput.id = nameId;
|
|
nameInput.className = "table-alter-input table-alter-column-name";
|
|
nameInput.type = "text";
|
|
nameInput.required = true;
|
|
nameInput.autocomplete = "off";
|
|
nameInput.value = column && column.name ? column.name : "";
|
|
|
|
var typeSelect = document.createElement("select");
|
|
typeSelect.className = "table-alter-input table-alter-column-type";
|
|
typeSelect.setAttribute("aria-label", "Column type");
|
|
tableAlterSelectTypeValue(typeSelect, column && column.type);
|
|
|
|
var customTypeSelect = null;
|
|
var customTypeField = null;
|
|
if (tableAlterCustomColumnTypes().length) {
|
|
var customTypeId = "table-alter-column-custom-type-" + index;
|
|
customTypeField = document.createElement("div");
|
|
customTypeField.className =
|
|
"table-alter-detail-field table-alter-custom-type-field";
|
|
var customTypeLabel = document.createElement("label");
|
|
customTypeLabel.className = "table-alter-detail-label";
|
|
customTypeLabel.setAttribute("for", customTypeId);
|
|
customTypeLabel.textContent = "Custom type";
|
|
var customTypeHelpId = "table-alter-column-custom-type-help-" + index;
|
|
var customTypeHelp = document.createElement("p");
|
|
customTypeHelp.id = customTypeHelpId;
|
|
customTypeHelp.className = "table-alter-detail-help";
|
|
customTypeHelp.textContent =
|
|
"Controls how Datasette displays and edits this column";
|
|
customTypeSelect = createTableAlterCustomColumnTypeSelect();
|
|
customTypeSelect.id = customTypeId;
|
|
customTypeSelect.setAttribute("aria-describedby", customTypeHelpId);
|
|
customTypeSelect.value = originalCustomType;
|
|
updateTableAlterCustomColumnTypePlaceholder(customTypeSelect);
|
|
customTypeField.appendChild(customTypeLabel);
|
|
customTypeField.appendChild(customTypeHelp);
|
|
customTypeField.appendChild(customTypeSelect);
|
|
}
|
|
|
|
var notNullLabel = document.createElement("label");
|
|
notNullLabel.className = "table-alter-detail-check table-alter-not-null";
|
|
var notNullInput = document.createElement("input");
|
|
notNullInput.type = "checkbox";
|
|
notNullInput.className = "table-alter-not-null-input";
|
|
notNullInput.checked = !!(column && column.notnull);
|
|
var notNullText = document.createElement("span");
|
|
var notNullStrong = document.createElement("strong");
|
|
notNullStrong.textContent = "Not null";
|
|
notNullText.appendChild(notNullStrong);
|
|
notNullText.appendChild(
|
|
document.createTextNode(" This value cannot be left unset"),
|
|
);
|
|
notNullLabel.appendChild(notNullInput);
|
|
notNullLabel.appendChild(notNullText);
|
|
|
|
var defaultId = "table-alter-column-default-" + index;
|
|
var defaultField = document.createElement("div");
|
|
defaultField.className = "table-alter-detail-field";
|
|
var defaultLabel = document.createElement("label");
|
|
defaultLabel.className = "table-alter-detail-label";
|
|
defaultLabel.setAttribute("for", defaultId);
|
|
defaultLabel.textContent = "Default value";
|
|
var defaultInput = document.createElement("input");
|
|
defaultInput.id = defaultId;
|
|
defaultInput.className = "table-alter-input table-alter-default";
|
|
defaultInput.type = "text";
|
|
defaultInput.autocomplete = "off";
|
|
defaultInput.placeholder = "default";
|
|
defaultInput.setAttribute("aria-label", "Default value");
|
|
defaultInput.value = originalDefault;
|
|
defaultField.appendChild(defaultLabel);
|
|
defaultField.appendChild(defaultInput);
|
|
|
|
var defaultExprId = "table-alter-column-default-expr-" + index;
|
|
var defaultExprField = document.createElement("div");
|
|
defaultExprField.className = "table-alter-detail-field";
|
|
var defaultExprLabel = document.createElement("label");
|
|
defaultExprLabel.className = "table-alter-detail-label";
|
|
defaultExprLabel.setAttribute("for", defaultExprId);
|
|
defaultExprLabel.textContent = "or default to a specific time";
|
|
var defaultExprSelect = document.createElement("select");
|
|
defaultExprSelect.id = defaultExprId;
|
|
defaultExprSelect.className = "table-alter-input table-alter-default-expr";
|
|
defaultExprSelect.setAttribute("aria-label", "or default to a specific time");
|
|
tableAlterSelectDefaultExprValue(defaultExprSelect, "");
|
|
defaultExprField.appendChild(defaultExprLabel);
|
|
defaultExprField.appendChild(defaultExprSelect);
|
|
|
|
var pkLabel = document.createElement("label");
|
|
pkLabel.className = "table-alter-detail-check table-alter-primary-key";
|
|
var pkInput = document.createElement("input");
|
|
pkInput.type = "checkbox";
|
|
pkInput.className = "table-alter-primary-key-input";
|
|
pkInput.checked = !!(column && column.is_pk);
|
|
var pkText = document.createElement("span");
|
|
var pkStrong = document.createElement("strong");
|
|
pkStrong.textContent = "Primary key";
|
|
pkText.appendChild(pkStrong);
|
|
pkText.appendChild(
|
|
document.createTextNode(" This ID uniquely identifies the record"),
|
|
);
|
|
pkLabel.appendChild(pkInput);
|
|
pkLabel.appendChild(pkText);
|
|
|
|
var moveControls = createSchemaDialogMoveControls("table-alter");
|
|
|
|
var removeButton = createSchemaDialogIconButton(
|
|
"table-alter",
|
|
"remove-column",
|
|
existing ? "Drop column" : "Remove column",
|
|
existing ? "Drop column" : "Remove column",
|
|
COLUMN_MOVE_ICONS.remove,
|
|
);
|
|
main.appendChild(nameLabel);
|
|
main.appendChild(nameInput);
|
|
main.appendChild(typeSelect);
|
|
main.appendChild(moveControls.controls);
|
|
main.appendChild(removeButton);
|
|
main.appendChild(expandButton);
|
|
|
|
if (customTypeField) {
|
|
details.appendChild(customTypeField);
|
|
}
|
|
details.appendChild(notNullLabel);
|
|
details.appendChild(defaultField);
|
|
details.appendChild(defaultExprField);
|
|
details.appendChild(pkLabel);
|
|
row.appendChild(main);
|
|
row.appendChild(details);
|
|
|
|
var controls = [
|
|
nameInput,
|
|
typeSelect,
|
|
notNullInput,
|
|
defaultInput,
|
|
defaultExprSelect,
|
|
pkInput,
|
|
];
|
|
if (customTypeSelect) {
|
|
controls.push(customTypeSelect);
|
|
}
|
|
controls.forEach(function (control) {
|
|
control.addEventListener("input", function () {
|
|
clearTableAlterDialogError(state);
|
|
updateTableAlterSaveButtonState(state);
|
|
});
|
|
control.addEventListener("change", function () {
|
|
clearTableAlterDialogError(state);
|
|
updateTableAlterSaveButtonState(state);
|
|
});
|
|
});
|
|
|
|
defaultInput.addEventListener("input", function () {
|
|
if (defaultInput.value) {
|
|
defaultExprSelect.value = "";
|
|
syncTableAlterDefaultControls(row);
|
|
}
|
|
updateTableAlterSaveButtonState(state);
|
|
});
|
|
defaultExprSelect.addEventListener("change", function () {
|
|
if (defaultExprSelect.value) {
|
|
defaultInput.value = "";
|
|
}
|
|
syncTableAlterDefaultControls(row);
|
|
updateTableAlterSaveButtonState(state);
|
|
});
|
|
pkInput.addEventListener("change", function () {
|
|
normalizeTableAlterPrimaryKeyRows(state);
|
|
updateTableAlterMoveButtons(state);
|
|
updateTableAlterSaveButtonState(state);
|
|
});
|
|
|
|
expandButton.addEventListener("click", function () {
|
|
toggleSchemaDialogMoreOptions(expandButton, details);
|
|
});
|
|
|
|
typeSelect.addEventListener("change", function () {
|
|
syncTableAlterCustomTypeForSqliteType(row);
|
|
updateTableAlterSaveButtonState(state);
|
|
});
|
|
if (customTypeSelect) {
|
|
customTypeSelect.addEventListener("change", function () {
|
|
updateTableAlterCustomColumnTypePlaceholder(customTypeSelect);
|
|
var option = tableAlterCustomColumnType(customTypeSelect.value);
|
|
if (
|
|
option &&
|
|
option.fixedSqliteType &&
|
|
tableAlterColumnTypes().indexOf(option.fixedSqliteType) !== -1
|
|
) {
|
|
typeSelect.value = option.fixedSqliteType;
|
|
}
|
|
updateTableAlterSaveButtonState(state);
|
|
});
|
|
}
|
|
|
|
moveControls.topButton.addEventListener("click", function () {
|
|
var first = tableAlterFirstNonPrimaryRow(state);
|
|
if (
|
|
state.isSaving ||
|
|
tableAlterRowIsPrimaryKey(row) ||
|
|
!first ||
|
|
first === row
|
|
) {
|
|
return;
|
|
}
|
|
state.columnList.insertBefore(row, first);
|
|
clearTableAlterDialogError(state);
|
|
updateTableAlterMoveButtons(state);
|
|
updateTableAlterSaveButtonState(state);
|
|
row.querySelector(".table-alter-column-name").focus();
|
|
});
|
|
|
|
moveControls.upButton.addEventListener("click", function () {
|
|
var previous = row.previousElementSibling;
|
|
if (
|
|
state.isSaving ||
|
|
tableAlterRowIsPrimaryKey(row) ||
|
|
!previous ||
|
|
tableAlterRowIsPrimaryKey(previous)
|
|
) {
|
|
return;
|
|
}
|
|
state.columnList.insertBefore(row, previous);
|
|
clearTableAlterDialogError(state);
|
|
updateTableAlterMoveButtons(state);
|
|
updateTableAlterSaveButtonState(state);
|
|
row.querySelector(".table-alter-column-name").focus();
|
|
});
|
|
|
|
moveControls.downButton.addEventListener("click", function () {
|
|
var next = row.nextElementSibling;
|
|
if (state.isSaving || tableAlterRowIsPrimaryKey(row) || !next) {
|
|
return;
|
|
}
|
|
state.columnList.insertBefore(next, row);
|
|
clearTableAlterDialogError(state);
|
|
updateTableAlterMoveButtons(state);
|
|
updateTableAlterSaveButtonState(state);
|
|
row.querySelector(".table-alter-column-name").focus();
|
|
});
|
|
|
|
moveControls.bottomButton.addEventListener("click", function () {
|
|
var last = state.columnList.lastElementChild;
|
|
if (
|
|
state.isSaving ||
|
|
tableAlterRowIsPrimaryKey(row) ||
|
|
!last ||
|
|
last === row
|
|
) {
|
|
return;
|
|
}
|
|
state.columnList.appendChild(row);
|
|
clearTableAlterDialogError(state);
|
|
updateTableAlterMoveButtons(state);
|
|
updateTableAlterSaveButtonState(state);
|
|
row.querySelector(".table-alter-column-name").focus();
|
|
});
|
|
|
|
removeButton.addEventListener("click", function () {
|
|
if (state.isSaving) {
|
|
return;
|
|
}
|
|
if (row.dataset.existing === "1") {
|
|
state.deletedColumns.push(row.dataset.originalName);
|
|
}
|
|
row.remove();
|
|
clearTableAlterDialogError(state);
|
|
updateTableAlterMoveButtons(state);
|
|
updateTableAlterSaveButtonState(state);
|
|
var nextInput = state.columnList.querySelector(".table-alter-column-name");
|
|
if (nextInput) {
|
|
nextInput.focus();
|
|
} else {
|
|
state.addColumnButton.focus();
|
|
}
|
|
});
|
|
|
|
syncTableAlterDefaultControls(row);
|
|
return row;
|
|
}
|
|
|
|
function addTableAlterColumn(state, column) {
|
|
var row = createTableAlterColumnRow(state, column || { type: "text" });
|
|
state.columnList.appendChild(row);
|
|
return row;
|
|
}
|
|
|
|
function resetTableAlterDialog(state, data) {
|
|
state.nextColumnIndex = 0;
|
|
state.deletedColumns = [];
|
|
state.originalPrimaryKeys = (data.primaryKeys || []).slice();
|
|
state.originalColumnNames = (data.columns || []).map(function (column) {
|
|
return column.name;
|
|
});
|
|
state.columnList.textContent = "";
|
|
(data.columns || []).forEach(function (column) {
|
|
addTableAlterColumn(
|
|
state,
|
|
Object.assign({}, column, {
|
|
existing: true,
|
|
}),
|
|
);
|
|
});
|
|
normalizeTableAlterPrimaryKeyRows(state);
|
|
state.initialSignature = tableAlterDialogSignature(state);
|
|
showTableAlterEditor(state);
|
|
}
|
|
|
|
function collectTableAlterRows(state) {
|
|
return tableAlterDialogRows(state).map(function (row) {
|
|
var signature = tableAlterRowSignature(row);
|
|
signature.originalType = row.dataset.originalType || "";
|
|
signature.originalNotNull = row.dataset.originalNotNull === "1";
|
|
signature.originalHasDefault = row.dataset.originalHasDefault === "1";
|
|
signature.originalDefault = row.dataset.originalDefault || "";
|
|
signature.originalPk = row.dataset.originalPk === "1";
|
|
signature.originalCustomType = row.dataset.originalCustomType || "";
|
|
return signature;
|
|
});
|
|
}
|
|
|
|
function validateTableAlterRows(state, rows) {
|
|
if (!rows.length) {
|
|
return "At least one column is required.";
|
|
}
|
|
var seen = {};
|
|
var supportedTypes = tableAlterColumnTypes();
|
|
for (var i = 0; i < rows.length; i += 1) {
|
|
var row = rows[i];
|
|
var name = row.name.trim();
|
|
if (!name) {
|
|
return "Column name is required.";
|
|
}
|
|
if (name.indexOf("\n") !== -1) {
|
|
return "Column names cannot contain newlines.";
|
|
}
|
|
var columnKey = name.toLowerCase();
|
|
if (seen[columnKey]) {
|
|
return "Duplicate column name: " + name;
|
|
}
|
|
seen[columnKey] = true;
|
|
if (supportedTypes.indexOf(row.type) === -1) {
|
|
return "Unsupported column type: " + row.type;
|
|
}
|
|
if (row.customType) {
|
|
var option = tableAlterCustomColumnType(row.customType);
|
|
if (!option) {
|
|
return "Unknown custom column type: " + row.customType;
|
|
}
|
|
if (!tableAlterCustomTypeAppliesToSqliteType(option, row.type)) {
|
|
return (
|
|
"Custom type " +
|
|
row.customType +
|
|
" cannot be used with SQLite type " +
|
|
row.type +
|
|
"."
|
|
);
|
|
}
|
|
}
|
|
if (row.defaultValue && row.defaultExpr) {
|
|
return "Use either a default value or a default expression.";
|
|
}
|
|
if (!row.existing && row.notNull && !row.defaultValue && !row.defaultExpr) {
|
|
return "New NOT NULL columns need a default or default expression.";
|
|
}
|
|
}
|
|
var pkColumns = rows.filter(function (row) {
|
|
return row.pk;
|
|
});
|
|
if (state.originalPrimaryKeys.length && !pkColumns.length) {
|
|
return "At least one primary key column is required.";
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function collectTableAlterColumnTypeAssignments(rows) {
|
|
var assignments = [];
|
|
if (!tableAlterCustomColumnTypes().length) {
|
|
return assignments;
|
|
}
|
|
rows.forEach(function (row) {
|
|
var renamed = row.existing && row.name.trim() !== row.originalName;
|
|
if (row.customType === row.originalCustomType && !renamed) {
|
|
return;
|
|
}
|
|
if (!row.customType && !row.originalCustomType) {
|
|
return;
|
|
}
|
|
assignments.push({
|
|
column: row.name.trim(),
|
|
columnType: row.customType || null,
|
|
sqliteType: row.type,
|
|
});
|
|
});
|
|
return assignments;
|
|
}
|
|
|
|
function tableAlterPkIdentityColumns(rows) {
|
|
return rows
|
|
.filter(function (row) {
|
|
return row.pk;
|
|
})
|
|
.map(function (row) {
|
|
return row.existing ? row.originalName : row.name.trim();
|
|
});
|
|
}
|
|
|
|
function tableAlterPkChanged(state, rows) {
|
|
return (
|
|
JSON.stringify(tableAlterPkIdentityColumns(rows)) !==
|
|
JSON.stringify(state.originalPrimaryKeys)
|
|
);
|
|
}
|
|
|
|
function tableAlterNaturalColumnOrder(state, rows) {
|
|
var existingRowsByOriginalName = {};
|
|
var newRows = [];
|
|
rows.forEach(function (row) {
|
|
if (row.existing) {
|
|
existingRowsByOriginalName[row.originalName] = row;
|
|
} else {
|
|
newRows.push(row);
|
|
}
|
|
});
|
|
var naturalOrder = [];
|
|
state.originalColumnNames.forEach(function (originalName) {
|
|
var row = existingRowsByOriginalName[originalName];
|
|
if (row) {
|
|
naturalOrder.push(row.name.trim());
|
|
}
|
|
});
|
|
newRows.forEach(function (row) {
|
|
naturalOrder.push(row.name.trim());
|
|
});
|
|
return naturalOrder;
|
|
}
|
|
|
|
function tableAlterColumnsReordered(state, rows) {
|
|
var finalOrder = rows.map(function (row) {
|
|
return row.name.trim();
|
|
});
|
|
return (
|
|
JSON.stringify(finalOrder) !==
|
|
JSON.stringify(tableAlterNaturalColumnOrder(state, rows))
|
|
);
|
|
}
|
|
|
|
function collectTableAlterPayload(state) {
|
|
var rows = collectTableAlterRows(state);
|
|
var validationError = validateTableAlterRows(state, rows);
|
|
if (validationError) {
|
|
return { error: validationError };
|
|
}
|
|
|
|
var operations = [];
|
|
var columnTypeAssignments = collectTableAlterColumnTypeAssignments(rows);
|
|
rows.forEach(function (row) {
|
|
var name = row.name.trim();
|
|
if (!row.existing) {
|
|
var addArgs = {
|
|
name: name,
|
|
type: row.type,
|
|
not_null: row.notNull,
|
|
};
|
|
if (row.defaultExpr) {
|
|
addArgs.default_expr = row.defaultExpr;
|
|
} else if (row.defaultValue || row.notNull) {
|
|
addArgs.default = row.defaultValue;
|
|
}
|
|
operations.push({ op: "add_column", args: addArgs });
|
|
return;
|
|
}
|
|
|
|
var originalName = row.originalName;
|
|
if (name !== originalName) {
|
|
operations.push({
|
|
op: "rename_column",
|
|
args: { name: originalName, to: name },
|
|
});
|
|
}
|
|
|
|
var alterArgs = { name: originalName };
|
|
if (row.type !== row.originalType) {
|
|
alterArgs.type = row.type;
|
|
}
|
|
if (row.notNull !== row.originalNotNull) {
|
|
alterArgs.not_null = row.notNull;
|
|
}
|
|
if (row.defaultExpr) {
|
|
alterArgs.default_expr = row.defaultExpr;
|
|
} else if (row.originalHasDefault) {
|
|
if (row.defaultValue !== row.originalDefault) {
|
|
alterArgs.default = row.defaultValue === "" ? null : row.defaultValue;
|
|
}
|
|
} else if (row.defaultValue) {
|
|
alterArgs.default = row.defaultValue;
|
|
}
|
|
if (Object.keys(alterArgs).length > 1) {
|
|
operations.push({ op: "alter_column", args: alterArgs });
|
|
}
|
|
});
|
|
|
|
state.deletedColumns.forEach(function (name) {
|
|
operations.push({ op: "drop_column", args: { name: name } });
|
|
});
|
|
|
|
var pkColumns = rows
|
|
.filter(function (row) {
|
|
return row.pk;
|
|
})
|
|
.map(function (row) {
|
|
return row.name.trim();
|
|
});
|
|
if (tableAlterPkChanged(state, rows)) {
|
|
operations.push({ op: "set_primary_key", args: { columns: pkColumns } });
|
|
}
|
|
|
|
if (tableAlterColumnsReordered(state, rows)) {
|
|
operations.push({
|
|
op: "reorder_columns",
|
|
args: {
|
|
columns: rows.map(function (row) {
|
|
return row.name.trim();
|
|
}),
|
|
},
|
|
});
|
|
}
|
|
|
|
if (!operations.length && !columnTypeAssignments.length) {
|
|
return { error: "No changes to apply." };
|
|
}
|
|
return {
|
|
payload: operations.length ? { operations: operations } : null,
|
|
columnTypeAssignments: columnTypeAssignments,
|
|
};
|
|
}
|
|
|
|
function tableAlterQuotedName(name) {
|
|
return '"' + name + '"';
|
|
}
|
|
|
|
function tableAlterReadableDefaultExpression(value) {
|
|
return value ? value.replace(/_/g, " ") : "";
|
|
}
|
|
|
|
function tableAlterReadableValue(value) {
|
|
if (value === null) {
|
|
return "NULL";
|
|
}
|
|
return '"' + String(value) + '"';
|
|
}
|
|
|
|
function tableAlterOperationSummary(operation) {
|
|
var args = operation.args || {};
|
|
if (operation.op === "add_column") {
|
|
var addDetails = ["as " + args.type];
|
|
if (args.not_null) {
|
|
addDetails.push("with values required");
|
|
}
|
|
if (args.default_expr) {
|
|
addDetails.push(
|
|
"defaulting to " +
|
|
tableAlterReadableDefaultExpression(args.default_expr),
|
|
);
|
|
} else if (Object.prototype.hasOwnProperty.call(args, "default")) {
|
|
addDetails.push(
|
|
"with default value " + tableAlterReadableValue(args.default),
|
|
);
|
|
}
|
|
return {
|
|
text:
|
|
"Add column " +
|
|
tableAlterQuotedName(args.name) +
|
|
" " +
|
|
addDetails.join(", ") +
|
|
".",
|
|
damaging: false,
|
|
};
|
|
}
|
|
if (operation.op === "rename_column") {
|
|
return {
|
|
text:
|
|
"Rename column " +
|
|
tableAlterQuotedName(args.name) +
|
|
" to " +
|
|
tableAlterQuotedName(args.to) +
|
|
".",
|
|
damaging: false,
|
|
};
|
|
}
|
|
if (operation.op === "alter_column") {
|
|
var changes = [];
|
|
if (args.type) {
|
|
changes.push("set type to " + args.type);
|
|
}
|
|
if (Object.prototype.hasOwnProperty.call(args, "not_null")) {
|
|
changes.push(
|
|
args.not_null ? "not null (require values)" : "allow unset values",
|
|
);
|
|
}
|
|
if (args.default_expr) {
|
|
changes.push(
|
|
"default to " + tableAlterReadableDefaultExpression(args.default_expr),
|
|
);
|
|
} else if (Object.prototype.hasOwnProperty.call(args, "default")) {
|
|
changes.push(
|
|
args.default === null
|
|
? "remove the default value"
|
|
: "set default value to " + tableAlterReadableValue(args.default),
|
|
);
|
|
}
|
|
return {
|
|
text:
|
|
"Change column " +
|
|
tableAlterQuotedName(args.name) +
|
|
": " +
|
|
changes.join(", ") +
|
|
".",
|
|
damaging: false,
|
|
};
|
|
}
|
|
if (operation.op === "drop_column") {
|
|
return {
|
|
text: "Drop column " + tableAlterQuotedName(args.name) + ".",
|
|
damaging: true,
|
|
};
|
|
}
|
|
if (operation.op === "set_primary_key") {
|
|
return {
|
|
text:
|
|
"Set primary key to " +
|
|
(args.columns || []).map(tableAlterQuotedName).join(", ") +
|
|
".",
|
|
damaging: false,
|
|
};
|
|
}
|
|
if (operation.op === "reorder_columns") {
|
|
return {
|
|
text:
|
|
"Set column order to " +
|
|
(args.columns || []).map(tableAlterQuotedName).join(", ") +
|
|
".",
|
|
damaging: false,
|
|
};
|
|
}
|
|
return {
|
|
text: "Run " + operation.op + ".",
|
|
damaging: false,
|
|
};
|
|
}
|
|
|
|
function tableAlterColumnTypeAssignmentSummary(assignment) {
|
|
return {
|
|
text: assignment.columnType
|
|
? "Set custom type for column " +
|
|
tableAlterQuotedName(assignment.column) +
|
|
" to " +
|
|
assignment.columnType +
|
|
"."
|
|
: "Remove custom type from column " +
|
|
tableAlterQuotedName(assignment.column) +
|
|
".",
|
|
damaging: false,
|
|
};
|
|
}
|
|
|
|
function tableAlterReviewItems(result) {
|
|
var items = [];
|
|
var operations = result.payload ? result.payload.operations || [] : [];
|
|
operations.forEach(function (operation) {
|
|
items.push(tableAlterOperationSummary(operation));
|
|
});
|
|
(result.columnTypeAssignments || []).forEach(function (assignment) {
|
|
items.push(tableAlterColumnTypeAssignmentSummary(assignment));
|
|
});
|
|
return items;
|
|
}
|
|
|
|
function tableAlterReviewHasDamagingItems(items) {
|
|
return items.some(function (item) {
|
|
return item.damaging;
|
|
});
|
|
}
|
|
|
|
function appendTableAlterReviewText(element, text) {
|
|
text.split(/("[^"]+")/g).forEach(function (part) {
|
|
if (!part) {
|
|
return;
|
|
}
|
|
if (part.charAt(0) === '"' && part.charAt(part.length - 1) === '"') {
|
|
var name = document.createElement("code");
|
|
name.className = "table-alter-review-name";
|
|
name.textContent = part.slice(1, -1);
|
|
element.appendChild(name);
|
|
} else {
|
|
element.appendChild(document.createTextNode(part));
|
|
}
|
|
});
|
|
}
|
|
|
|
function tableAlterSetColumnTypeUrl() {
|
|
var data = tableAlterData();
|
|
if (!data || !data.path) {
|
|
return null;
|
|
}
|
|
var url = new URL(data.path, location.href);
|
|
url.pathname = url.pathname.replace(/\/-\/alter\/?$/, "/-/set-column-type");
|
|
return url.toString();
|
|
}
|
|
|
|
async function assignTableAlterColumnTypes(assignments) {
|
|
if (!assignments.length) {
|
|
return;
|
|
}
|
|
var url = tableAlterSetColumnTypeUrl();
|
|
if (!url) {
|
|
throw new Error("Could not find the set column type URL.");
|
|
}
|
|
for (var i = 0; i < assignments.length; i += 1) {
|
|
var assignment = assignments[i];
|
|
var response = await fetch(url, {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
Accept: "application/json",
|
|
},
|
|
body: JSON.stringify({
|
|
column: assignment.column,
|
|
column_type: assignment.columnType
|
|
? {
|
|
type: assignment.columnType,
|
|
}
|
|
: null,
|
|
}),
|
|
});
|
|
var data = null;
|
|
try {
|
|
data = await response.json();
|
|
} catch (_error) {
|
|
data = null;
|
|
}
|
|
if (!response.ok || (data && data.ok === false)) {
|
|
var error = rowMutationRequestError(response, data);
|
|
throw new Error(
|
|
"Saved schema changes, but could not set custom type for " +
|
|
assignment.column +
|
|
": " +
|
|
error.message,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
function showTableAlterEditor(state) {
|
|
state.mode = "edit";
|
|
state.reviewResult = null;
|
|
state.dialog.classList.remove("table-alter-reviewing");
|
|
state.fields.hidden = false;
|
|
state.review.hidden = true;
|
|
state.review.textContent = "";
|
|
state.backButton.hidden = true;
|
|
var data = tableAlterData();
|
|
state.dropButton.hidden = !(data && data.dropPath);
|
|
state.saveButton.textContent = tableAlterSaveButtonText(state);
|
|
updateTableAlterMoveButtons(state);
|
|
updateTableAlterSaveButtonState(state);
|
|
}
|
|
|
|
function showTableAlterReview(state, result) {
|
|
var items = tableAlterReviewItems(result);
|
|
state.mode = "review";
|
|
state.reviewResult = result;
|
|
state.dialog.classList.add("table-alter-reviewing");
|
|
state.fields.hidden = true;
|
|
state.review.hidden = false;
|
|
state.review.textContent = "";
|
|
state.backButton.hidden = false;
|
|
state.dropButton.hidden = true;
|
|
state.saveButton.textContent = tableAlterSaveButtonText(state);
|
|
updateTableAlterSaveButtonState(state);
|
|
|
|
var heading = document.createElement("h3");
|
|
heading.className = "table-alter-review-title";
|
|
heading.tabIndex = -1;
|
|
heading.textContent = "Review changes";
|
|
state.review.appendChild(heading);
|
|
|
|
var intro = document.createElement("p");
|
|
intro.className = "table-alter-review-intro";
|
|
intro.textContent = "These changes will be applied to the table.";
|
|
state.review.appendChild(intro);
|
|
|
|
if (tableAlterReviewHasDamagingItems(items)) {
|
|
var warning = document.createElement("p");
|
|
warning.className = "table-alter-review-warning";
|
|
warning.setAttribute("role", "alert");
|
|
warning.textContent =
|
|
"Warning: data in dropped columns will be permanently lost.";
|
|
state.review.appendChild(warning);
|
|
}
|
|
|
|
var list = document.createElement("ol");
|
|
list.className = "table-alter-review-list";
|
|
items.forEach(function (item) {
|
|
var listItem = document.createElement("li");
|
|
appendTableAlterReviewText(listItem, item.text);
|
|
if (item.damaging) {
|
|
listItem.className = "table-alter-review-damaging";
|
|
}
|
|
list.appendChild(listItem);
|
|
});
|
|
state.review.appendChild(list);
|
|
heading.focus();
|
|
}
|
|
|
|
async function applyTableAlterChanges(state, result) {
|
|
if (state.isSaving) {
|
|
return;
|
|
}
|
|
if (!result) {
|
|
showTableAlterDialogError(state, "Could not find the reviewed changes.");
|
|
return;
|
|
}
|
|
var data = tableAlterData();
|
|
if (!data || !data.path) {
|
|
showTableAlterDialogError(state, "Could not find the alter table URL.");
|
|
return;
|
|
}
|
|
clearTableAlterDialogError(state);
|
|
if (result.error) {
|
|
showTableAlterDialogError(state, result.error);
|
|
return;
|
|
}
|
|
setTableAlterDialogSaving(state, true);
|
|
try {
|
|
if (result.payload) {
|
|
var response = await fetch(data.path, {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
Accept: "application/json",
|
|
},
|
|
body: JSON.stringify(result.payload),
|
|
});
|
|
var responseData = null;
|
|
try {
|
|
responseData = await response.json();
|
|
} catch (_error) {
|
|
responseData = null;
|
|
}
|
|
if (!response.ok || (responseData && responseData.ok === false)) {
|
|
throw rowMutationRequestError(response, responseData);
|
|
}
|
|
}
|
|
await assignTableAlterColumnTypes(result.columnTypeAssignments || []);
|
|
state.shouldRestoreFocus = false;
|
|
state.dialog.close();
|
|
location.reload();
|
|
} catch (error) {
|
|
setTableAlterDialogSaving(state, false);
|
|
showTableAlterDialogError(state, error.message || "Could not alter table");
|
|
}
|
|
}
|
|
|
|
function tableAlterDatabaseUrl() {
|
|
var data = tableAlterData();
|
|
if (!data || !data.path) {
|
|
return null;
|
|
}
|
|
var url = new URL(data.path, location.href);
|
|
url.pathname = url.pathname.replace(/\/[^/]+\/-\/alter\/?$/, "");
|
|
url.search = "";
|
|
url.hash = "";
|
|
return url.toString();
|
|
}
|
|
|
|
async function dropTableFromAlterDialog(state) {
|
|
if (state.isSaving) {
|
|
return;
|
|
}
|
|
var data = tableAlterData();
|
|
if (!data || !data.dropPath) {
|
|
return;
|
|
}
|
|
if (
|
|
!window.confirm(
|
|
'Permanently delete the table "' +
|
|
data.tableName +
|
|
'"? This will delete all of its data and cannot be undone.',
|
|
)
|
|
) {
|
|
return;
|
|
}
|
|
clearTableAlterDialogError(state);
|
|
setTableAlterDialogSaving(state, true);
|
|
try {
|
|
var response = await fetch(data.dropPath, {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
Accept: "application/json",
|
|
},
|
|
body: JSON.stringify({ confirm: true }),
|
|
});
|
|
var responseData = null;
|
|
try {
|
|
responseData = await response.json();
|
|
} catch (_error) {
|
|
responseData = null;
|
|
}
|
|
if (!response.ok || (responseData && responseData.ok === false)) {
|
|
throw rowMutationRequestError(response, responseData);
|
|
}
|
|
state.shouldRestoreFocus = false;
|
|
state.dialog.close();
|
|
window.location.href = tableAlterDatabaseUrl() || "/";
|
|
} catch (error) {
|
|
setTableAlterDialogSaving(state, false);
|
|
showTableAlterDialogError(state, error.message || "Could not drop table");
|
|
}
|
|
}
|
|
|
|
async function saveTableAlterDialog(state) {
|
|
if (state.isSaving) {
|
|
return;
|
|
}
|
|
if (state.mode === "review") {
|
|
if (!state.reviewResult) {
|
|
showTableAlterDialogError(state, "Could not find the reviewed changes.");
|
|
return;
|
|
}
|
|
await applyTableAlterChanges(state, state.reviewResult);
|
|
return;
|
|
}
|
|
clearTableAlterDialogError(state);
|
|
var result = collectTableAlterPayload(state);
|
|
if (result.error) {
|
|
showTableAlterDialogError(state, result.error);
|
|
return;
|
|
}
|
|
showTableAlterReview(state, result);
|
|
}
|
|
|
|
function confirmDiscardTableAlterChanges(state) {
|
|
if (!tableAlterDialogHasChanges(state)) {
|
|
return true;
|
|
}
|
|
return window.confirm("Discard table changes?");
|
|
}
|
|
|
|
function closeTableAlterDialogIfConfirmed(state) {
|
|
if (!state || state.isSaving) {
|
|
return false;
|
|
}
|
|
if (!confirmDiscardTableAlterChanges(state)) {
|
|
return false;
|
|
}
|
|
state.shouldRestoreFocus = true;
|
|
state.dialog.close();
|
|
return true;
|
|
}
|
|
|
|
function closeTableAlterDialog(state) {
|
|
if (!state || state.isSaving) {
|
|
return false;
|
|
}
|
|
state.shouldRestoreFocus = true;
|
|
state.dialog.close();
|
|
return true;
|
|
}
|
|
|
|
function ensureTableAlterDialog(manager) {
|
|
if (tableAlterDialogState) {
|
|
return tableAlterDialogState;
|
|
}
|
|
if (!window.HTMLDialogElement) {
|
|
return null;
|
|
}
|
|
|
|
var dialog = document.createElement("dialog");
|
|
dialog.id = TABLE_ALTER_DIALOG_ID;
|
|
dialog.className = "table-alter-dialog";
|
|
dialog.setAttribute("aria-labelledby", "table-alter-title");
|
|
dialog.innerHTML = `
|
|
<div class="modal-header">
|
|
<span class="modal-title" id="table-alter-title">Alter table</span>
|
|
</div>
|
|
<form class="table-alter-form" method="post" novalidate>
|
|
<p class="table-alter-error" id="table-alter-error" role="alert" tabindex="-1" hidden></p>
|
|
<div class="table-alter-fields">
|
|
<div class="table-alter-columns">
|
|
<div class="table-alter-column-headings" aria-hidden="true">
|
|
<span>Column</span>
|
|
<span>Type</span>
|
|
<span>Move</span>
|
|
<span></span>
|
|
</div>
|
|
<div class="table-alter-column-list"></div>
|
|
<button type="button" class="table-alter-add-column"><svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14"></path><path d="M12 5v14"></path></svg><span>Add column</span></button>
|
|
</div>
|
|
</div>
|
|
<div class="table-alter-review" hidden></div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-danger table-alter-drop" hidden>Drop table</button>
|
|
<button type="button" class="btn btn-ghost table-alter-back" hidden>Back</button>
|
|
<button type="button" class="btn btn-ghost table-alter-cancel">Cancel</button>
|
|
<button type="submit" class="btn btn-primary table-alter-save">Review changes</button>
|
|
</div>
|
|
</form>
|
|
`;
|
|
document.body.appendChild(dialog);
|
|
|
|
tableAlterDialogState = {
|
|
dialog: dialog,
|
|
form: dialog.querySelector(".table-alter-form"),
|
|
title: dialog.querySelector(".modal-title"),
|
|
error: dialog.querySelector(".table-alter-error"),
|
|
fields: dialog.querySelector(".table-alter-fields"),
|
|
review: dialog.querySelector(".table-alter-review"),
|
|
columnList: dialog.querySelector(".table-alter-column-list"),
|
|
addColumnButton: dialog.querySelector(".table-alter-add-column"),
|
|
backButton: dialog.querySelector(".table-alter-back"),
|
|
dropButton: dialog.querySelector(".table-alter-drop"),
|
|
cancelButton: dialog.querySelector(".table-alter-cancel"),
|
|
saveButton: dialog.querySelector(".table-alter-save"),
|
|
currentButton: null,
|
|
shouldRestoreFocus: true,
|
|
isSaving: false,
|
|
initialSignature: "",
|
|
nextColumnIndex: 0,
|
|
deletedColumns: [],
|
|
originalColumnNames: [],
|
|
originalPrimaryKeys: [],
|
|
mode: "edit",
|
|
reviewResult: null,
|
|
manager: manager,
|
|
};
|
|
|
|
tableAlterDialogState.form.addEventListener("submit", function (ev) {
|
|
ev.preventDefault();
|
|
saveTableAlterDialog(tableAlterDialogState);
|
|
});
|
|
|
|
tableAlterDialogState.addColumnButton.addEventListener("click", function () {
|
|
if (tableAlterDialogState.isSaving) {
|
|
return;
|
|
}
|
|
var row = addTableAlterColumn(tableAlterDialogState, {
|
|
type: "text",
|
|
existing: false,
|
|
expanded: true,
|
|
});
|
|
clearTableAlterDialogError(tableAlterDialogState);
|
|
updateTableAlterMoveButtons(tableAlterDialogState);
|
|
updateTableAlterSaveButtonState(tableAlterDialogState);
|
|
row.querySelector(".table-alter-column-name").focus();
|
|
});
|
|
|
|
tableAlterDialogState.cancelButton.addEventListener("click", function () {
|
|
closeTableAlterDialog(tableAlterDialogState);
|
|
});
|
|
|
|
tableAlterDialogState.dropButton.addEventListener("click", function () {
|
|
dropTableFromAlterDialog(tableAlterDialogState);
|
|
});
|
|
|
|
tableAlterDialogState.backButton.addEventListener("click", function () {
|
|
if (tableAlterDialogState.isSaving) {
|
|
return;
|
|
}
|
|
clearTableAlterDialogError(tableAlterDialogState);
|
|
showTableAlterEditor(tableAlterDialogState);
|
|
var firstName = tableAlterDialogState.columnList.querySelector(
|
|
".table-alter-column-name",
|
|
);
|
|
if (firstName) {
|
|
firstName.focus();
|
|
}
|
|
});
|
|
|
|
dialog.addEventListener("click", function (ev) {
|
|
if (ev.target === dialog) {
|
|
closeTableAlterDialogIfConfirmed(tableAlterDialogState);
|
|
}
|
|
});
|
|
|
|
dialog.addEventListener("keydown", function (ev) {
|
|
if (ev.key !== "Escape") {
|
|
return;
|
|
}
|
|
ev.preventDefault();
|
|
closeTableAlterDialogIfConfirmed(tableAlterDialogState);
|
|
});
|
|
|
|
dialog.addEventListener("cancel", function (ev) {
|
|
ev.preventDefault();
|
|
closeTableAlterDialogIfConfirmed(tableAlterDialogState);
|
|
});
|
|
|
|
dialog.addEventListener("close", function () {
|
|
var state = tableAlterDialogState;
|
|
clearTableAlterDialogError(state);
|
|
setTableAlterDialogSaving(state, false);
|
|
if (
|
|
state.shouldRestoreFocus &&
|
|
state.currentButton &&
|
|
document.contains(state.currentButton)
|
|
) {
|
|
state.currentButton.focus();
|
|
}
|
|
});
|
|
|
|
return tableAlterDialogState;
|
|
}
|
|
|
|
function openTableAlterDialog(button, manager) {
|
|
var data = tableAlterData();
|
|
if (!data) {
|
|
return;
|
|
}
|
|
var state = ensureTableAlterDialog(manager);
|
|
if (!state) {
|
|
return;
|
|
}
|
|
|
|
var menu = button.closest("details");
|
|
if (menu) {
|
|
menu.open = false;
|
|
}
|
|
state.manager = manager;
|
|
state.currentButton = button;
|
|
state.shouldRestoreFocus = true;
|
|
state.title.textContent = "Alter table " + data.tableName;
|
|
clearTableAlterDialogError(state);
|
|
resetTableAlterDialog(state, data);
|
|
if (!state.dialog.open) {
|
|
state.dialog.showModal();
|
|
}
|
|
var firstName = state.columnList.querySelector(".table-alter-column-name");
|
|
if (firstName) {
|
|
firstName.focus();
|
|
}
|
|
}
|
|
|
|
function initTableAlterActions(manager) {
|
|
if (!window.fetch || !window.HTMLDialogElement || !tableAlterData()) {
|
|
return;
|
|
}
|
|
document.addEventListener("click", function (ev) {
|
|
var button = ev.target.closest('button[data-table-action="alter-table"]');
|
|
if (!button) {
|
|
return;
|
|
}
|
|
ev.preventDefault();
|
|
openTableAlterDialog(button, manager);
|
|
});
|
|
}
|
|
|
|
function tableForeignKeys() {
|
|
return tablePageData().foreignKeys || {};
|
|
}
|
|
|
|
function isRowPage() {
|
|
return document.body && document.body.classList.contains("row");
|
|
}
|
|
|
|
function rowElementForActionButton(button) {
|
|
return (
|
|
button.closest("[data-row]") ||
|
|
(button.getAttribute("data-row") ? button : null)
|
|
);
|
|
}
|
|
|
|
function foreignKeyAutocompleteUrl(column) {
|
|
return tableForeignKeys()[column] || null;
|
|
}
|
|
|
|
function autocompleteRowPk(row) {
|
|
var pks = (row && row.pks) || {};
|
|
var keys = Object.keys(pks);
|
|
if (keys.length !== 1) {
|
|
return null;
|
|
}
|
|
return pks[keys[0]];
|
|
}
|
|
|
|
function foreignKeyRowUrl(autocompleteUrl, pk) {
|
|
var url = new URL(autocompleteUrl, location.href);
|
|
if (!/\/-\/autocomplete\/?$/.test(url.pathname)) {
|
|
return null;
|
|
}
|
|
url.pathname =
|
|
url.pathname.replace(/\/-\/autocomplete\/?$/, "") + "/" + tildeEncode(pk);
|
|
url.search = "";
|
|
url.hash = "";
|
|
return url.toString();
|
|
}
|
|
|
|
function foreignKeyLabelText(row) {
|
|
var pk = autocompleteRowPk(row);
|
|
var label = row && row.label;
|
|
if (
|
|
label !== null &&
|
|
typeof label !== "undefined" &&
|
|
String(label) !== String(pk)
|
|
) {
|
|
return String(label);
|
|
}
|
|
return "View row";
|
|
}
|
|
|
|
function rowEditMetaTextWithoutCurrentValue(meta) {
|
|
return (meta.dataset.baseMeta || "")
|
|
.split(" · ")
|
|
.filter(function (part) {
|
|
return part !== "Current value: NULL";
|
|
})
|
|
.join(" · ");
|
|
}
|
|
|
|
function updateRowEditForeignKeySeparator(meta) {
|
|
var separator = meta.querySelector(".row-edit-fk-separator");
|
|
if (!separator) {
|
|
return;
|
|
}
|
|
var baseMeta = meta.querySelector(".row-edit-base-meta");
|
|
var hasBaseMeta = !!(baseMeta && baseMeta.textContent);
|
|
separator.textContent = hasBaseMeta ? " · " : "";
|
|
separator.hidden = !hasBaseMeta;
|
|
}
|
|
|
|
function updateRowEditFieldMetaHidden(meta) {
|
|
var baseMeta = meta.querySelector(".row-edit-base-meta");
|
|
var hasBaseMeta = !!(baseMeta && baseMeta.textContent);
|
|
var foreignKeyLinkWrap = meta.querySelector(".row-edit-fk-link-wrap");
|
|
var hasForeignKeyLink = foreignKeyLinkWrap && !foreignKeyLinkWrap.hidden;
|
|
meta.hidden =
|
|
meta.dataset.reserveSpace !== "1" && !hasBaseMeta && !hasForeignKeyLink;
|
|
}
|
|
|
|
function setRowEditBaseMetaText(meta, text) {
|
|
var baseMeta = meta.querySelector(".row-edit-base-meta");
|
|
if (!baseMeta) {
|
|
return;
|
|
}
|
|
baseMeta.textContent = text || "";
|
|
updateRowEditForeignKeySeparator(meta);
|
|
updateRowEditFieldMetaHidden(meta);
|
|
}
|
|
|
|
function setForeignKeyMetaLink(meta, autocompleteUrl, row) {
|
|
var wrap = meta.querySelector(".row-edit-fk-link-wrap");
|
|
if (!wrap) {
|
|
return;
|
|
}
|
|
var pkSpan = wrap.querySelector(".row-edit-fk-pk");
|
|
var link = wrap.querySelector("a");
|
|
var pk = autocompleteRowPk(row);
|
|
var url =
|
|
pk === null || typeof pk === "undefined"
|
|
? null
|
|
: foreignKeyRowUrl(autocompleteUrl, pk);
|
|
if (!url) {
|
|
wrap.hidden = true;
|
|
pkSpan.textContent = "";
|
|
link.removeAttribute("href");
|
|
link.textContent = "";
|
|
link.removeAttribute("aria-label");
|
|
setRowEditBaseMetaText(meta, meta.dataset.baseMeta || "");
|
|
updateRowEditFieldMetaHidden(meta);
|
|
return;
|
|
}
|
|
setRowEditBaseMetaText(meta, rowEditMetaTextWithoutCurrentValue(meta));
|
|
var pkText = String(pk);
|
|
var linkText = foreignKeyLabelText(row);
|
|
pkSpan.textContent = pkText;
|
|
link.href = url;
|
|
link.textContent = linkText;
|
|
link.setAttribute(
|
|
"aria-label",
|
|
"Open referenced row " + pkText + " " + linkText + " in a new tab",
|
|
);
|
|
wrap.hidden = false;
|
|
updateRowEditFieldMetaHidden(meta);
|
|
}
|
|
|
|
async function resolveForeignKeyMetaLink(control, autocompleteUrl, meta) {
|
|
var value = control.value.trim();
|
|
if (!value) {
|
|
setForeignKeyMetaLink(meta, autocompleteUrl, null);
|
|
return;
|
|
}
|
|
|
|
var url = new URL(autocompleteUrl, location.href);
|
|
url.searchParams.set("q", value);
|
|
try {
|
|
var response = await fetch(url.toString(), {
|
|
headers: {
|
|
Accept: "application/json",
|
|
},
|
|
});
|
|
if (!response.ok) {
|
|
throw new Error("HTTP " + response.status);
|
|
}
|
|
var data = await response.json();
|
|
if (control.value.trim() !== value) {
|
|
return;
|
|
}
|
|
var rows = (data && data.rows) || [];
|
|
var row = rows.find(function (candidate) {
|
|
var pk = autocompleteRowPk(candidate);
|
|
return pk !== null && typeof pk !== "undefined" && String(pk) === value;
|
|
});
|
|
setForeignKeyMetaLink(meta, autocompleteUrl, row || null);
|
|
} catch (_error) {
|
|
if (control.value.trim() === value) {
|
|
setForeignKeyMetaLink(meta, autocompleteUrl, null);
|
|
}
|
|
}
|
|
}
|
|
|
|
function tableInsertUrl() {
|
|
var data = tableInsertData();
|
|
if (data && data.path) {
|
|
return new URL(data.path, location.href).toString();
|
|
}
|
|
var url = tableBaseUrl();
|
|
url.pathname = url.pathname.replace(/\/$/, "") + "/-/insert";
|
|
return url.toString();
|
|
}
|
|
|
|
function rowResourceUrl(row) {
|
|
var rowId = row.getAttribute("data-row");
|
|
if (!rowId) {
|
|
return null;
|
|
}
|
|
var url = tableBaseUrl();
|
|
url.pathname = url.pathname.replace(/\/$/, "") + "/" + rowId;
|
|
return url;
|
|
}
|
|
|
|
function rowJsonUrl(row) {
|
|
var url = rowResourceUrl(row);
|
|
if (!url) {
|
|
return "";
|
|
}
|
|
url.pathname = url.pathname + ".json";
|
|
url.searchParams.set("_extra", "columns,column_types");
|
|
return url.toString();
|
|
}
|
|
|
|
function rowDeleteUrl(row) {
|
|
var url = rowResourceUrl(row);
|
|
if (!url) {
|
|
return "";
|
|
}
|
|
url.pathname = url.pathname.replace(/\/$/, "") + "/-/delete";
|
|
if (isRowPage()) {
|
|
url.searchParams.set("_redirect_to_table", "1");
|
|
}
|
|
return url.toString();
|
|
}
|
|
|
|
function rowUpdateUrl(row) {
|
|
var url = rowResourceUrl(row);
|
|
if (!url) {
|
|
return "";
|
|
}
|
|
url.pathname = url.pathname.replace(/\/$/, "") + "/-/update";
|
|
if (isRowPage()) {
|
|
url.searchParams.set("_message", "1");
|
|
}
|
|
return url.toString();
|
|
}
|
|
|
|
function rowFragmentUrl(row) {
|
|
var rowId = row.getAttribute("data-row");
|
|
return rowFragmentUrlById(rowId);
|
|
}
|
|
|
|
function rowFragmentUrlById(rowId) {
|
|
if (!rowId) {
|
|
return "";
|
|
}
|
|
var url = tableBaseUrl();
|
|
url.search = new URL(location.href).search;
|
|
url.pathname = url.pathname.replace(/\/$/, "") + "/-/fragment";
|
|
url.searchParams.delete("_next");
|
|
url.searchParams.set("_row", rowId);
|
|
url.searchParams.set("_nocount", "1");
|
|
url.searchParams.set("_nofacet", "1");
|
|
url.searchParams.set("_nosuggest", "1");
|
|
return url.toString();
|
|
}
|
|
|
|
function nextRowActionFocusTarget(row, action) {
|
|
var selector = 'button[data-row-action="' + action + '"]:not([disabled])';
|
|
var sibling = row.nextElementSibling;
|
|
while (sibling) {
|
|
var nextButton = sibling.querySelector(selector);
|
|
if (nextButton) {
|
|
return nextButton;
|
|
}
|
|
sibling = sibling.nextElementSibling;
|
|
}
|
|
|
|
sibling = row.previousElementSibling;
|
|
while (sibling) {
|
|
var previousButton = sibling.querySelector(selector);
|
|
if (previousButton) {
|
|
return previousButton;
|
|
}
|
|
sibling = sibling.previousElementSibling;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function nextRowDeleteFocusTarget(row, manager) {
|
|
return (
|
|
nextRowActionFocusTarget(row, "delete") || ensureRowMutationStatus(manager)
|
|
);
|
|
}
|
|
|
|
function ensureRowDeleteDialog(manager) {
|
|
if (rowDeleteDialogState) {
|
|
return rowDeleteDialogState;
|
|
}
|
|
if (!window.HTMLDialogElement) {
|
|
return null;
|
|
}
|
|
|
|
var dialog = document.createElement("dialog");
|
|
dialog.id = ROW_DELETE_DIALOG_ID;
|
|
dialog.className = "row-delete-dialog";
|
|
dialog.setAttribute("aria-labelledby", "row-delete-title");
|
|
dialog.setAttribute("aria-describedby", "row-delete-message");
|
|
dialog.innerHTML = `
|
|
<div class="modal-header">
|
|
<span class="modal-title" id="row-delete-title">Delete row</span>
|
|
</div>
|
|
<p class="row-delete-message" id="row-delete-message">Delete row <span class="row-delete-id"></span>?</p>
|
|
<p class="row-delete-error" role="alert" hidden></p>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-ghost row-delete-cancel">Cancel</button>
|
|
<button type="button" class="btn btn-primary row-delete-confirm">Delete row</button>
|
|
</div>
|
|
`;
|
|
document.body.appendChild(dialog);
|
|
|
|
rowDeleteDialogState = {
|
|
dialog: dialog,
|
|
title: dialog.querySelector(".modal-title"),
|
|
message: dialog.querySelector(".row-delete-message"),
|
|
rowId: dialog.querySelector(".row-delete-id"),
|
|
error: dialog.querySelector(".row-delete-error"),
|
|
cancelButton: dialog.querySelector(".row-delete-cancel"),
|
|
confirmButton: dialog.querySelector(".row-delete-confirm"),
|
|
currentRow: null,
|
|
currentDeleteUrl: null,
|
|
currentPkPath: null,
|
|
manager: manager,
|
|
isBusy: false,
|
|
shouldRestoreFocus: true,
|
|
};
|
|
|
|
rowDeleteDialogState.cancelButton.addEventListener("click", function () {
|
|
if (!rowDeleteDialogState.isBusy) {
|
|
rowDeleteDialogState.shouldRestoreFocus = true;
|
|
dialog.close();
|
|
}
|
|
});
|
|
|
|
dialog.addEventListener("click", function (ev) {
|
|
if (ev.target === dialog && !rowDeleteDialogState.isBusy) {
|
|
rowDeleteDialogState.shouldRestoreFocus = true;
|
|
dialog.close();
|
|
}
|
|
});
|
|
|
|
dialog.addEventListener("keydown", function (ev) {
|
|
if (
|
|
ev.key === "Enter" &&
|
|
document.activeElement === rowDeleteDialogState.confirmButton
|
|
) {
|
|
ev.preventDefault();
|
|
if (!rowDeleteDialogState.isBusy) {
|
|
rowDeleteDialogState.confirmButton.click();
|
|
}
|
|
return;
|
|
}
|
|
if (ev.key !== "Escape") {
|
|
return;
|
|
}
|
|
if (rowDeleteDialogState.isBusy) {
|
|
ev.preventDefault();
|
|
return;
|
|
}
|
|
ev.preventDefault();
|
|
rowDeleteDialogState.shouldRestoreFocus = true;
|
|
dialog.close();
|
|
});
|
|
|
|
dialog.addEventListener("cancel", function (ev) {
|
|
if (rowDeleteDialogState.isBusy) {
|
|
ev.preventDefault();
|
|
} else {
|
|
rowDeleteDialogState.shouldRestoreFocus = true;
|
|
}
|
|
});
|
|
|
|
dialog.addEventListener("close", function () {
|
|
var state = rowDeleteDialogState;
|
|
clearRowDeleteDialogError(state);
|
|
setRowDeleteDialogBusy(state, false);
|
|
if (
|
|
state.shouldRestoreFocus &&
|
|
state.currentButton &&
|
|
document.contains(state.currentButton)
|
|
) {
|
|
state.currentButton.focus();
|
|
}
|
|
});
|
|
|
|
rowDeleteDialogState.confirmButton.addEventListener(
|
|
"click",
|
|
async function () {
|
|
var state = rowDeleteDialogState;
|
|
clearRowDeleteDialogError(state);
|
|
setRowDeleteDialogBusy(state, true);
|
|
|
|
try {
|
|
var response = await fetch(state.currentDeleteUrl, {
|
|
method: "POST",
|
|
headers: {
|
|
Accept: "application/json",
|
|
},
|
|
});
|
|
var data = null;
|
|
try {
|
|
data = await response.json();
|
|
} catch (_error) {
|
|
data = null;
|
|
}
|
|
if (!response.ok || (data && data.ok === false)) {
|
|
throw rowMutationRequestError(response, data);
|
|
}
|
|
if (data && data.redirect) {
|
|
state.shouldRestoreFocus = false;
|
|
state.dialog.close();
|
|
location.href = data.redirect;
|
|
return;
|
|
}
|
|
|
|
var focusTarget = nextRowDeleteFocusTarget(
|
|
state.currentRow,
|
|
state.manager,
|
|
);
|
|
var statusMessage = state.currentPkPath
|
|
? "Deleted row " + state.currentPkPath + "."
|
|
: "Deleted row.";
|
|
state.shouldRestoreFocus = false;
|
|
state.dialog.close();
|
|
state.currentRow.remove();
|
|
showRowMutationStatus(state.manager, statusMessage, false);
|
|
if (focusTarget && document.contains(focusTarget)) {
|
|
focusTarget.focus();
|
|
} else {
|
|
ensureRowMutationStatus(state.manager).focus();
|
|
}
|
|
} catch (error) {
|
|
setRowDeleteDialogBusy(state, false);
|
|
showRowDeleteDialogError(state, error.message || "Delete failed");
|
|
}
|
|
},
|
|
);
|
|
|
|
return rowDeleteDialogState;
|
|
}
|
|
|
|
function openRowDeleteDialog(button, manager) {
|
|
var row = rowElementForActionButton(button);
|
|
if (!row || !row.getAttribute("data-row")) {
|
|
return;
|
|
}
|
|
var state = ensureRowDeleteDialog(manager);
|
|
if (!state) {
|
|
return;
|
|
}
|
|
|
|
state.manager = manager;
|
|
state.currentButton = button;
|
|
state.currentRow = row;
|
|
state.currentDeleteUrl = rowDeleteUrl(row);
|
|
state.currentPkPath = rowDisplayLabel(row);
|
|
state.shouldRestoreFocus = true;
|
|
|
|
clearRowDeleteDialogError(state);
|
|
setRowDeleteDialogBusy(state, false);
|
|
setRowDialogTitle(
|
|
state.title,
|
|
"Delete row",
|
|
state.currentPkPath || "this row",
|
|
rowTitleLabel(row),
|
|
);
|
|
state.rowId.textContent = state.currentPkPath || "this row";
|
|
|
|
if (!state.dialog.open) {
|
|
state.dialog.showModal();
|
|
}
|
|
state.confirmButton.focus();
|
|
}
|
|
|
|
function initRowDeleteActions(manager) {
|
|
if (!window.fetch || !window.HTMLDialogElement) {
|
|
return;
|
|
}
|
|
document.addEventListener("click", function (ev) {
|
|
var button = ev.target.closest('button[data-row-action="delete"]');
|
|
if (!button) {
|
|
return;
|
|
}
|
|
ev.preventDefault();
|
|
openRowDeleteDialog(button, manager);
|
|
});
|
|
}
|
|
|
|
function valueToEditText(value) {
|
|
if (value === null || typeof value === "undefined") {
|
|
return "";
|
|
}
|
|
if (typeof value === "object") {
|
|
return JSON.stringify(value, null, 2);
|
|
}
|
|
return String(value);
|
|
}
|
|
|
|
function shouldUseTextarea(value, columnType) {
|
|
if (columnType && columnType.type === "textarea") {
|
|
return true;
|
|
}
|
|
if (value && typeof value === "object") {
|
|
return true;
|
|
}
|
|
var text = valueToEditText(value);
|
|
return text.length > 80 || /[\r\n]/.test(text);
|
|
}
|
|
|
|
function rowEditValueKind(value) {
|
|
if (value === null || typeof value === "undefined") {
|
|
return "null";
|
|
}
|
|
if (typeof value === "number") {
|
|
return "number";
|
|
}
|
|
if (typeof value === "boolean") {
|
|
return "boolean";
|
|
}
|
|
return "string";
|
|
}
|
|
|
|
function rowEditControlElement(control, autocompleteUrl) {
|
|
if (!autocompleteUrl || control.nodeName !== "INPUT") {
|
|
return control;
|
|
}
|
|
var autocomplete = document.createElement("datasette-autocomplete");
|
|
autocomplete.setAttribute("src", autocompleteUrl);
|
|
autocomplete.setAttribute("suggest-on-focus", "");
|
|
autocomplete.appendChild(control);
|
|
return autocomplete;
|
|
}
|
|
|
|
function columnTypeForContext(columnType) {
|
|
if (!columnType) {
|
|
return null;
|
|
}
|
|
return {
|
|
type: columnType.type,
|
|
config: columnType.config || {},
|
|
};
|
|
}
|
|
|
|
function defaultExpressionForContext(expression) {
|
|
if (expression === null || typeof expression === "undefined") {
|
|
return null;
|
|
}
|
|
return expression;
|
|
}
|
|
|
|
function columnFormControlContext(column, isPk, columnType, options) {
|
|
var pageData = tablePageData();
|
|
var defaultExpression = defaultExpressionForContext(
|
|
options.defaultExpression,
|
|
);
|
|
return {
|
|
mode: options.mode || "edit",
|
|
database: pageData.database || null,
|
|
table:
|
|
pageData.table ||
|
|
(tableInsertData() && tableInsertData().tableName) ||
|
|
null,
|
|
tableUrl: pageData.tableUrl || null,
|
|
column: column,
|
|
columnType: columnTypeForContext(columnType),
|
|
sqliteType: options.sqliteType || null,
|
|
notNull: !!options.notnull,
|
|
isPk: !!isPk,
|
|
defaultExpression: defaultExpression,
|
|
form: options.form || null,
|
|
dialog: options.dialog || null,
|
|
};
|
|
}
|
|
|
|
function makeColumnField(manager, context) {
|
|
if (!manager || !manager.makeColumnField) {
|
|
return null;
|
|
}
|
|
return manager.makeColumnField(context);
|
|
}
|
|
|
|
function createColumnFieldApi(options) {
|
|
var control = options.control;
|
|
var context = options.context;
|
|
var field = {
|
|
context: context,
|
|
id: options.id,
|
|
labelId: options.labelId,
|
|
descriptionId: options.descriptionId,
|
|
root: null,
|
|
form: options.form || null,
|
|
dialog: options.dialog || null,
|
|
input: control,
|
|
control: control,
|
|
meta: options.meta || null,
|
|
validationMessageElement: null,
|
|
getValue: function () {
|
|
return valueFromRowEditControl(control);
|
|
},
|
|
setValue: function (value) {
|
|
if (
|
|
value !== null &&
|
|
typeof value !== "undefined" &&
|
|
typeof value === "object"
|
|
) {
|
|
throw new TypeError(
|
|
"field.setValue() accepts strings, numbers, booleans or null; serialize objects before setting the field value",
|
|
);
|
|
}
|
|
field.stopUsingSqliteDefault();
|
|
control.value = valueToEditText(value);
|
|
control.dataset.currentValueKind = rowEditValueKind(value);
|
|
},
|
|
getInitialValue: function () {
|
|
return initialValueFromRowEditControl(control);
|
|
},
|
|
hasChanged: function () {
|
|
return rowEditControlHasChanged(control);
|
|
},
|
|
clearValue: function () {
|
|
field.setValue(null);
|
|
},
|
|
isUsingSqliteDefault: function () {
|
|
return control.dataset.useSqliteDefault === "1";
|
|
},
|
|
useSqliteDefault: function () {
|
|
if (
|
|
context.defaultExpression === null ||
|
|
typeof context.defaultExpression === "undefined"
|
|
) {
|
|
return;
|
|
}
|
|
control.dataset.useSqliteDefault = "1";
|
|
control.disabled = true;
|
|
control.value = "";
|
|
control.dataset.currentValueKind = "null";
|
|
field.syncSqliteDefaultUi();
|
|
},
|
|
stopUsingSqliteDefault: function () {
|
|
if (control.dataset.useSqliteDefault !== "1") {
|
|
return;
|
|
}
|
|
control.dataset.useSqliteDefault = "0";
|
|
control.disabled = false;
|
|
field.syncSqliteDefaultUi();
|
|
},
|
|
syncSqliteDefaultUi: function () {},
|
|
markClean: function () {
|
|
markRowEditControlClean(control);
|
|
},
|
|
setValidity: function (message) {
|
|
message = message || "";
|
|
control.setCustomValidity(message);
|
|
if (message) {
|
|
control.setAttribute("aria-invalid", "true");
|
|
} else {
|
|
control.removeAttribute("aria-invalid");
|
|
}
|
|
var validationMessage = ensureColumnFieldValidationMessage(field);
|
|
if (validationMessage) {
|
|
validationMessage.textContent = message;
|
|
validationMessage.hidden = !message;
|
|
}
|
|
},
|
|
clearValidity: function () {
|
|
field.setValidity("");
|
|
},
|
|
};
|
|
field.markClean();
|
|
return field;
|
|
}
|
|
|
|
function ensureColumnFieldValidationMessage(field) {
|
|
if (field.validationMessageElement) {
|
|
return field.validationMessageElement;
|
|
}
|
|
if (!field.meta) {
|
|
return null;
|
|
}
|
|
var validationMessage = document.createElement("span");
|
|
validationMessage.id = field.id + "-validation-error";
|
|
validationMessage.className = "row-edit-field-validation-error";
|
|
validationMessage.hidden = true;
|
|
validationMessage.setAttribute("role", "alert");
|
|
field.meta.appendChild(validationMessage);
|
|
field.validationMessageElement = validationMessage;
|
|
return validationMessage;
|
|
}
|
|
|
|
function renderColumnField(pluginControl, fieldApi) {
|
|
if (!pluginControl || !pluginControl.render) {
|
|
return null;
|
|
}
|
|
var pluginWrap = document.createElement("div");
|
|
pluginWrap.className = "row-edit-plugin-control";
|
|
pluginWrap.dataset.pluginName = pluginControl.pluginName || "";
|
|
pluginWrap.dataset.column = fieldApi.context.column;
|
|
if (fieldApi.context.columnType && fieldApi.context.columnType.type) {
|
|
pluginWrap.dataset.columnType = fieldApi.context.columnType.type;
|
|
}
|
|
fieldApi.root = pluginWrap;
|
|
try {
|
|
var rendered = pluginControl.render(fieldApi);
|
|
if (rendered && rendered.nodeType) {
|
|
pluginWrap.appendChild(rendered);
|
|
}
|
|
} catch (error) {
|
|
console.error("Error rendering column form control", error);
|
|
return null;
|
|
}
|
|
pluginWrap._datasetteColumnField = pluginControl;
|
|
pluginWrap._datasetteColumnFormField = fieldApi;
|
|
return pluginWrap;
|
|
}
|
|
|
|
function validateJsonColumnField(field) {
|
|
var value = field.input.value;
|
|
if (value.trim() === "") {
|
|
field.clearValidity();
|
|
return true;
|
|
}
|
|
try {
|
|
JSON.parse(value);
|
|
field.clearValidity();
|
|
return true;
|
|
} catch (error) {
|
|
field.setValidity(
|
|
"Invalid JSON" + (error && error.message ? ": " + error.message : ""),
|
|
);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function registerBuiltinColumnFieldPlugins(manager) {
|
|
if (!manager || !manager.registerPlugin) {
|
|
return;
|
|
}
|
|
manager.registerPlugin("datasette-json-column", {
|
|
version: "1.0",
|
|
makeColumnField: function (context) {
|
|
if (!context.columnType || context.columnType.type !== "json") {
|
|
return;
|
|
}
|
|
return {
|
|
useTextarea: true,
|
|
render: function (field) {
|
|
field.input.addEventListener("input", function () {
|
|
validateJsonColumnField(field);
|
|
});
|
|
field.input.addEventListener("change", function () {
|
|
validateJsonColumnField(field);
|
|
});
|
|
validateJsonColumnField(field);
|
|
return field.input;
|
|
},
|
|
focus: function (field) {
|
|
field.input.focus();
|
|
},
|
|
};
|
|
},
|
|
});
|
|
}
|
|
|
|
function focusRowEditPluginControl(field) {
|
|
var pluginWrap = field.querySelector(".row-edit-plugin-control");
|
|
if (!pluginWrap) {
|
|
return false;
|
|
}
|
|
var pluginControl = pluginWrap._datasetteColumnField;
|
|
var fieldApi = pluginWrap._datasetteColumnFormField;
|
|
if (pluginControl && pluginControl.focus) {
|
|
try {
|
|
pluginControl.focus(fieldApi);
|
|
return true;
|
|
} catch (error) {
|
|
console.error("Error focusing column form control", error);
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function focusFirstRowEditControl(state, options) {
|
|
options = options || {};
|
|
var fields = state.fields.querySelectorAll(".row-edit-field");
|
|
for (var i = 0; i < fields.length; i += 1) {
|
|
var field = fields[i];
|
|
var control = field.querySelector(".row-edit-input");
|
|
if (!control) {
|
|
continue;
|
|
}
|
|
if (options.skipReadonly && (control.readOnly || control.disabled)) {
|
|
continue;
|
|
}
|
|
if (focusRowEditPluginControl(field)) {
|
|
return true;
|
|
}
|
|
control.focus();
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function destroyRowEditFields(state) {
|
|
if (!state || !state.fields) {
|
|
return;
|
|
}
|
|
state.fields
|
|
.querySelectorAll(".row-edit-plugin-control")
|
|
.forEach(function (pluginWrap) {
|
|
var pluginControl = pluginWrap._datasetteColumnField;
|
|
var fieldApi = pluginWrap._datasetteColumnFormField;
|
|
if (pluginControl && pluginControl.destroy) {
|
|
try {
|
|
pluginControl.destroy(fieldApi);
|
|
} catch (error) {
|
|
console.error("Error destroying column form control", error);
|
|
}
|
|
}
|
|
});
|
|
state.fields.innerHTML = "";
|
|
}
|
|
|
|
function createRowEditField(column, value, isPk, columnType, index, options) {
|
|
options = options || {};
|
|
var field = document.createElement("div");
|
|
field.className = "row-edit-field";
|
|
var defaultExpression = defaultExpressionForContext(
|
|
options.defaultExpression,
|
|
);
|
|
var hasDefaultExpression = defaultExpression !== null;
|
|
var useSqliteDefault = hasDefaultExpression && options.useSqliteDefault;
|
|
|
|
var fieldId = "row-edit-field-" + index;
|
|
var metaId = "row-edit-field-meta-" + index;
|
|
var labelId = "row-edit-field-label-" + index;
|
|
var label = document.createElement("label");
|
|
label.className = "row-edit-label";
|
|
label.id = labelId;
|
|
label.setAttribute("for", fieldId);
|
|
label.textContent = column;
|
|
|
|
var controlWrap = document.createElement("div");
|
|
controlWrap.className = "row-edit-control-wrap";
|
|
|
|
var context = columnFormControlContext(column, isPk, columnType, options);
|
|
var pluginControl = makeColumnField(options.manager, context);
|
|
var useTextarea =
|
|
(pluginControl && pluginControl.useTextarea === true) ||
|
|
shouldUseTextarea(value, columnType);
|
|
var control = useTextarea
|
|
? document.createElement("textarea")
|
|
: document.createElement("input");
|
|
control.className = "row-edit-input";
|
|
control.id = fieldId;
|
|
control.name = column;
|
|
control.value = valueToEditText(value);
|
|
control.setAttribute("aria-describedby", metaId);
|
|
control.dataset.initialValue = valueToEditText(value);
|
|
control.dataset.initialValueKind =
|
|
options.valueKind || rowEditValueKind(value);
|
|
control.dataset.primaryKey = isPk ? "1" : "0";
|
|
control.dataset.currentValueKind = control.dataset.initialValueKind;
|
|
if (hasDefaultExpression) {
|
|
control.dataset.useSqliteDefault = useSqliteDefault ? "1" : "0";
|
|
}
|
|
if (useSqliteDefault) {
|
|
control.disabled = true;
|
|
}
|
|
if (options.omitIfBlank) {
|
|
control.dataset.omitIfBlank = "1";
|
|
}
|
|
|
|
if (control.nodeName === "TEXTAREA") {
|
|
control.rows = Math.min(8, Math.max(3, control.value.split("\n").length));
|
|
} else {
|
|
control.type = "text";
|
|
}
|
|
|
|
if (isPk && options.primaryKeyReadonly !== false) {
|
|
control.readOnly = true;
|
|
}
|
|
|
|
var meta = document.createElement("span");
|
|
meta.id = metaId;
|
|
meta.className = "row-edit-field-meta";
|
|
if (options.autocompleteUrl) {
|
|
meta.classList.add("row-edit-field-meta-autocomplete");
|
|
meta.dataset.reserveSpace = "1";
|
|
}
|
|
var metaParts = [];
|
|
if (isPk) {
|
|
metaParts.push("Primary key");
|
|
}
|
|
if (options.notnull) {
|
|
metaParts.push("Required");
|
|
}
|
|
if (hasDefaultExpression && !useSqliteDefault) {
|
|
metaParts.push("SQLite default: " + defaultExpression);
|
|
}
|
|
if (value === null) {
|
|
metaParts.push("Current value: NULL");
|
|
control.placeholder = "NULL";
|
|
}
|
|
if (columnType && columnType.type) {
|
|
metaParts.push("Custom type: " + columnType.type);
|
|
}
|
|
meta.dataset.baseMeta = metaParts.join(" · ");
|
|
var baseMeta = document.createElement("span");
|
|
baseMeta.className = "row-edit-base-meta";
|
|
baseMeta.textContent = meta.dataset.baseMeta;
|
|
meta.appendChild(baseMeta);
|
|
if (options.autocompleteUrl) {
|
|
var foreignKeyLinkWrap = document.createElement("span");
|
|
foreignKeyLinkWrap.className = "row-edit-fk-link-wrap";
|
|
foreignKeyLinkWrap.hidden = true;
|
|
var foreignKeySeparator = document.createElement("span");
|
|
foreignKeySeparator.className = "row-edit-fk-separator";
|
|
foreignKeySeparator.textContent = meta.dataset.baseMeta ? " · " : "";
|
|
foreignKeySeparator.hidden = !meta.dataset.baseMeta;
|
|
foreignKeyLinkWrap.appendChild(foreignKeySeparator);
|
|
var foreignKeyPk = document.createElement("span");
|
|
foreignKeyPk.className = "row-edit-fk-pk";
|
|
foreignKeyLinkWrap.appendChild(foreignKeyPk);
|
|
foreignKeyLinkWrap.appendChild(document.createTextNode(" "));
|
|
var foreignKeyLink = document.createElement("a");
|
|
foreignKeyLink.className = "row-edit-fk-link";
|
|
foreignKeyLink.target = "_blank";
|
|
foreignKeyLink.rel = "noopener noreferrer";
|
|
foreignKeyLinkWrap.appendChild(foreignKeyLink);
|
|
meta.appendChild(foreignKeyLinkWrap);
|
|
updateRowEditFieldMetaHidden(meta);
|
|
}
|
|
var fieldApi = createColumnFieldApi({
|
|
id: fieldId,
|
|
labelId: labelId,
|
|
descriptionId: metaId,
|
|
control: control,
|
|
meta: meta,
|
|
input: control,
|
|
form: options.form || null,
|
|
dialog: options.dialog || null,
|
|
context: context,
|
|
});
|
|
field._datasetteColumnFormField = fieldApi;
|
|
var pluginControlElement = renderColumnField(pluginControl, fieldApi);
|
|
var controlElement =
|
|
pluginControlElement ||
|
|
rowEditControlElement(control, options.autocompleteUrl);
|
|
if (options.autocompleteUrl && !pluginControlElement) {
|
|
control.addEventListener("input", function () {
|
|
setForeignKeyMetaLink(meta, options.autocompleteUrl, null);
|
|
});
|
|
control.addEventListener("change", function () {
|
|
resolveForeignKeyMetaLink(control, options.autocompleteUrl, meta);
|
|
});
|
|
controlElement.addEventListener(
|
|
"datasette-autocomplete-select",
|
|
function (ev) {
|
|
setForeignKeyMetaLink(
|
|
meta,
|
|
options.autocompleteUrl,
|
|
ev.detail && ev.detail.row,
|
|
);
|
|
},
|
|
);
|
|
resolveForeignKeyMetaLink(control, options.autocompleteUrl, meta);
|
|
}
|
|
|
|
if (hasDefaultExpression) {
|
|
var defaultBlock = document.createElement("div");
|
|
defaultBlock.className = "row-edit-default";
|
|
defaultBlock.setAttribute("aria-describedby", metaId);
|
|
|
|
var defaultText = document.createElement("span");
|
|
defaultText.className = "row-edit-default-text";
|
|
defaultText.appendChild(document.createTextNode("default "));
|
|
var defaultCode = document.createElement("code");
|
|
defaultCode.className = "row-edit-default-code";
|
|
defaultCode.textContent = defaultExpression;
|
|
defaultText.appendChild(defaultCode);
|
|
|
|
var setValueButton = document.createElement("button");
|
|
setValueButton.type = "button";
|
|
setValueButton.className =
|
|
"row-edit-default-button row-edit-default-set-value";
|
|
setValueButton.textContent = "Set value";
|
|
setValueButton.setAttribute("aria-label", "Set value for " + column);
|
|
|
|
var customWrap = document.createElement("div");
|
|
customWrap.className = "row-edit-custom-value";
|
|
customWrap.hidden = true;
|
|
|
|
var useSqliteDefaultButton = document.createElement("button");
|
|
useSqliteDefaultButton.type = "button";
|
|
useSqliteDefaultButton.className = "row-edit-default-button";
|
|
useSqliteDefaultButton.textContent = "Use default";
|
|
useSqliteDefaultButton.setAttribute(
|
|
"aria-label",
|
|
"Use SQLite default for " + column,
|
|
);
|
|
|
|
setValueButton.addEventListener("click", function () {
|
|
fieldApi.stopUsingSqliteDefault();
|
|
control.focus();
|
|
});
|
|
|
|
useSqliteDefaultButton.addEventListener("click", function () {
|
|
fieldApi.useSqliteDefault();
|
|
setValueButton.focus();
|
|
});
|
|
|
|
defaultBlock.appendChild(defaultText);
|
|
defaultBlock.appendChild(setValueButton);
|
|
customWrap.appendChild(controlElement);
|
|
customWrap.appendChild(useSqliteDefaultButton);
|
|
controlWrap.appendChild(defaultBlock);
|
|
controlWrap.appendChild(customWrap);
|
|
fieldApi.syncSqliteDefaultUi = function () {
|
|
var usingDefault = fieldApi.isUsingSqliteDefault();
|
|
defaultBlock.hidden = !usingDefault;
|
|
customWrap.hidden = usingDefault;
|
|
};
|
|
fieldApi.syncSqliteDefaultUi();
|
|
} else {
|
|
controlWrap.appendChild(controlElement);
|
|
}
|
|
if (meta.textContent || options.autocompleteUrl) {
|
|
controlWrap.appendChild(meta);
|
|
}
|
|
field.appendChild(label);
|
|
field.appendChild(controlWrap);
|
|
return field;
|
|
}
|
|
|
|
function clearRowEditDialogError(state) {
|
|
state.error.hidden = true;
|
|
state.error.textContent = "";
|
|
}
|
|
|
|
function showRowEditDialogError(state, message) {
|
|
state.error.hidden = false;
|
|
state.error.textContent = message;
|
|
state.error.focus();
|
|
}
|
|
|
|
function updateRowEditDialogButtons(state) {
|
|
state.saveButton.disabled =
|
|
state.isLoading || state.isSaving || !state.hasLoaded;
|
|
state.cancelButton.disabled = state.isSaving;
|
|
var saveLabel = state.mode === "insert" ? "Insert row" : "Save";
|
|
state.saveButton.textContent = state.isSaving ? "Saving..." : saveLabel;
|
|
state.form.setAttribute(
|
|
"aria-busy",
|
|
state.isLoading || state.isSaving ? "true" : "false",
|
|
);
|
|
}
|
|
|
|
function setRowEditDialogLoading(state, isLoading) {
|
|
state.isLoading = isLoading;
|
|
state.loading.hidden = !isLoading;
|
|
updateRowEditDialogButtons(state);
|
|
}
|
|
|
|
function setRowEditDialogSaving(state, isSaving) {
|
|
state.isSaving = isSaving;
|
|
updateRowEditDialogButtons(state);
|
|
}
|
|
|
|
function valueFromRowEditControl(control) {
|
|
var value = control.value;
|
|
return valueFromRowEditText(
|
|
control.name,
|
|
value,
|
|
rowEditControlValueKind(control),
|
|
);
|
|
}
|
|
|
|
function valueFromRowEditText(name, value, initialValueKind) {
|
|
var trimmed = value.trim();
|
|
|
|
if (initialValueKind === "null" && value === "") {
|
|
return null;
|
|
}
|
|
if (initialValueKind === "number") {
|
|
if (trimmed === "") {
|
|
return null;
|
|
}
|
|
var numberValue = Number(trimmed);
|
|
if (Number.isNaN(numberValue)) {
|
|
throw new Error(name + " must be a number");
|
|
}
|
|
return numberValue;
|
|
}
|
|
if (initialValueKind === "boolean") {
|
|
if (/^(true|1|yes)$/i.test(trimmed)) {
|
|
return true;
|
|
}
|
|
if (/^(false|0|no)$/i.test(trimmed)) {
|
|
return false;
|
|
}
|
|
throw new Error(name + " must be true or false");
|
|
}
|
|
return value;
|
|
}
|
|
|
|
function initialValueFromRowEditControl(control) {
|
|
return valueFromRowEditText(
|
|
control.name,
|
|
control.dataset.initialValue || "",
|
|
control.dataset.initialValueKind || "string",
|
|
);
|
|
}
|
|
|
|
function rowEditControlValueKind(control) {
|
|
return (
|
|
control.dataset.currentValueKind ||
|
|
control.dataset.initialValueKind ||
|
|
"string"
|
|
);
|
|
}
|
|
|
|
function rowEditControlCleanValue(control) {
|
|
if (Object.prototype.hasOwnProperty.call(control.dataset, "cleanValue")) {
|
|
return control.dataset.cleanValue;
|
|
}
|
|
return control.dataset.initialValue || "";
|
|
}
|
|
|
|
function rowEditControlCleanValueKind(control) {
|
|
return (
|
|
control.dataset.cleanValueKind ||
|
|
control.dataset.initialValueKind ||
|
|
"string"
|
|
);
|
|
}
|
|
|
|
function rowEditControlCleanUsesSqliteDefault(control) {
|
|
if (
|
|
Object.prototype.hasOwnProperty.call(
|
|
control.dataset,
|
|
"cleanUseSqliteDefault",
|
|
)
|
|
) {
|
|
return control.dataset.cleanUseSqliteDefault === "1";
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function markRowEditControlClean(control) {
|
|
control.dataset.cleanValue = control.value;
|
|
control.dataset.cleanValueKind = rowEditControlValueKind(control);
|
|
control.dataset.cleanUseSqliteDefault =
|
|
control.dataset.useSqliteDefault === "1" ? "1" : "0";
|
|
}
|
|
|
|
function cleanValueFromRowEditControl(control) {
|
|
return valueFromRowEditText(
|
|
control.name,
|
|
rowEditControlCleanValue(control),
|
|
rowEditControlCleanValueKind(control),
|
|
);
|
|
}
|
|
|
|
function rowEditValuesMatch(left, right) {
|
|
if (left === right) {
|
|
return true;
|
|
}
|
|
if (left && right && typeof left === "object" && typeof right === "object") {
|
|
return JSON.stringify(left) === JSON.stringify(right);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function rowEditControlHasChanged(control) {
|
|
var usingSqliteDefault = control.dataset.useSqliteDefault === "1";
|
|
var cleanUsesSqliteDefault = rowEditControlCleanUsesSqliteDefault(control);
|
|
if (usingSqliteDefault || cleanUsesSqliteDefault) {
|
|
return usingSqliteDefault !== cleanUsesSqliteDefault;
|
|
}
|
|
if (
|
|
control.value === rowEditControlCleanValue(control) &&
|
|
rowEditControlValueKind(control) === rowEditControlCleanValueKind(control)
|
|
) {
|
|
return false;
|
|
}
|
|
try {
|
|
return !rowEditValuesMatch(
|
|
valueFromRowEditControl(control),
|
|
cleanValueFromRowEditControl(control),
|
|
);
|
|
} catch (_error) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
function collectRowFormValues(state) {
|
|
var values = {};
|
|
state.fields.querySelectorAll(".row-edit-input").forEach(function (control) {
|
|
if (
|
|
state.mode === "edit" &&
|
|
(control.readOnly || control.dataset.primaryKey === "1")
|
|
) {
|
|
return;
|
|
}
|
|
if (control.dataset.useSqliteDefault === "1") {
|
|
return;
|
|
}
|
|
if (control.dataset.omitIfBlank === "1" && control.value === "") {
|
|
return;
|
|
}
|
|
if (
|
|
state.mode === "edit" &&
|
|
control.value === (control.dataset.initialValue || "") &&
|
|
(control.dataset.currentValueKind ||
|
|
control.dataset.initialValueKind ||
|
|
"string") === (control.dataset.initialValueKind || "string")
|
|
) {
|
|
return;
|
|
}
|
|
var value = valueFromRowEditControl(control);
|
|
if (state.mode === "edit") {
|
|
try {
|
|
if (
|
|
rowEditValuesMatch(value, initialValueFromRowEditControl(control))
|
|
) {
|
|
return;
|
|
}
|
|
} catch (_error) {
|
|
// If the original value cannot be parsed using the field's current
|
|
// type, treat the field as changed and submit the corrected value.
|
|
}
|
|
}
|
|
values[control.name] = value;
|
|
});
|
|
return values;
|
|
}
|
|
|
|
function rowEditDialogHasChanges(state) {
|
|
if (!state || !state.hasLoaded || state.isLoading) {
|
|
return false;
|
|
}
|
|
var fields = state.fields.querySelectorAll(".row-edit-field");
|
|
for (var i = 0; i < fields.length; i += 1) {
|
|
var fieldApi = fields[i]._datasetteColumnFormField;
|
|
if (fieldApi && fieldApi.hasChanged && fieldApi.hasChanged()) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function confirmDiscardRowEditChanges(state) {
|
|
if (!rowEditDialogHasChanges(state)) {
|
|
return true;
|
|
}
|
|
var message =
|
|
state.mode === "insert"
|
|
? "Discard this new row?"
|
|
: "Discard unsaved changes to this row?";
|
|
return window.confirm(message);
|
|
}
|
|
|
|
function closeRowEditDialogIfConfirmed(state) {
|
|
if (!state || state.isSaving) {
|
|
return false;
|
|
}
|
|
if (!confirmDiscardRowEditChanges(state)) {
|
|
return false;
|
|
}
|
|
state.shouldRestoreFocus = true;
|
|
state.dialog.close();
|
|
return true;
|
|
}
|
|
|
|
function scheduleCloseRowEditDialogIfConfirmed(state) {
|
|
// Fix for an issue in Safari where hitting Esc would show
|
|
// the confirm() prompt asking if state should be discarded
|
|
// but the Esc key press would then cancel that dialog too.
|
|
// Wait for keyup, then move the confirm() to a fresh timer tick.
|
|
if (!state || state.isSaving || state.isClosePending) {
|
|
return false;
|
|
}
|
|
if (!rowEditDialogHasChanges(state)) {
|
|
state.shouldRestoreFocus = true;
|
|
state.dialog.close();
|
|
return true;
|
|
}
|
|
state.isClosePending = true;
|
|
var closeAfterKeyup = function () {
|
|
if (!state.isClosePending) {
|
|
return;
|
|
}
|
|
state.isClosePending = false;
|
|
closeRowEditDialogIfConfirmed(state);
|
|
};
|
|
var onKeyup = function (ev) {
|
|
if (ev.key !== "Escape") {
|
|
return;
|
|
}
|
|
document.removeEventListener("keyup", onKeyup, true);
|
|
setTimeout(closeAfterKeyup, 0);
|
|
};
|
|
document.addEventListener("keyup", onKeyup, true);
|
|
return true;
|
|
}
|
|
|
|
function findDataRowElement(root, rowId) {
|
|
var elements = root.querySelectorAll("[data-row]");
|
|
for (var i = 0; i < elements.length; i += 1) {
|
|
if (elements[i].getAttribute("data-row") === rowId) {
|
|
return elements[i];
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
async function fetchUpdatedRowElement(state) {
|
|
if (!state.currentFragmentUrl || !state.currentRowId) {
|
|
return null;
|
|
}
|
|
var response = await fetch(state.currentFragmentUrl, {
|
|
headers: {
|
|
Accept: "text/html",
|
|
},
|
|
});
|
|
var html = await response.text();
|
|
if (!response.ok) {
|
|
throw new Error("Could not refresh row: HTTP " + response.status);
|
|
}
|
|
var doc = new DOMParser().parseFromString(html, "text/html");
|
|
return findDataRowElement(doc, state.currentRowId);
|
|
}
|
|
|
|
function rowPathFromRowData(row, primaryKeys) {
|
|
if (!row) {
|
|
return null;
|
|
}
|
|
var keys = primaryKeys && primaryKeys.length ? primaryKeys : ["rowid"];
|
|
var bits = [];
|
|
for (var i = 0; i < keys.length; i += 1) {
|
|
var key = keys[i];
|
|
if (typeof row[key] === "undefined") {
|
|
return null;
|
|
}
|
|
bits.push(tildeEncode(row[key]));
|
|
}
|
|
return bits.join(",");
|
|
}
|
|
|
|
function addInsertedRowToPage(rowElement) {
|
|
var importedRow = document.importNode(rowElement, true);
|
|
var firstRow = document.querySelector("[data-row]");
|
|
if (firstRow && firstRow.parentNode) {
|
|
firstRow.parentNode.insertBefore(importedRow, firstRow);
|
|
} else {
|
|
var tbody = document.querySelector("table.rows-and-columns tbody");
|
|
if (!tbody) {
|
|
return null;
|
|
}
|
|
tbody.appendChild(importedRow);
|
|
}
|
|
var zeroResults = document.querySelector(".zero-results");
|
|
if (zeroResults) {
|
|
zeroResults.remove();
|
|
}
|
|
return importedRow;
|
|
}
|
|
|
|
async function saveRowEditDialog(state) {
|
|
if (state.isLoading || state.isSaving || !state.hasLoaded) {
|
|
return;
|
|
}
|
|
clearRowEditDialogError(state);
|
|
setRowEditDialogSaving(state, true);
|
|
|
|
try {
|
|
var url =
|
|
state.mode === "insert" ? state.currentInsertUrl : state.currentUpdateUrl;
|
|
if (!url) {
|
|
throw new Error(
|
|
state.mode === "insert"
|
|
? "Could not find the row insert URL"
|
|
: "Could not find the row update URL",
|
|
);
|
|
}
|
|
var formValues = collectRowFormValues(state);
|
|
if (state.mode === "edit" && !Object.keys(formValues).length) {
|
|
state.shouldRestoreFocus = true;
|
|
hideRowMutationStatus();
|
|
state.dialog.close();
|
|
return;
|
|
}
|
|
var payload =
|
|
state.mode === "insert"
|
|
? { row: formValues, return: true }
|
|
: { update: formValues, return: true };
|
|
var response = await fetch(url, {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
Accept: "application/json",
|
|
},
|
|
body: JSON.stringify(payload),
|
|
});
|
|
var data = null;
|
|
try {
|
|
data = await response.json();
|
|
} catch (_error) {
|
|
data = null;
|
|
}
|
|
if (!response.ok || (data && data.ok === false)) {
|
|
throw rowMutationRequestError(response, data);
|
|
}
|
|
|
|
if (state.mode === "insert") {
|
|
var insertData = tableInsertData() || {};
|
|
var insertedRowData =
|
|
data && data.rows && data.rows.length ? data.rows[0] : null;
|
|
var insertedRowId = rowPathFromRowData(
|
|
insertedRowData,
|
|
insertData.primaryKeys || [],
|
|
);
|
|
state.shouldRestoreFocus = false;
|
|
if (!insertedRowId) {
|
|
state.dialog.close();
|
|
var missingIdStatus = showRowMutationStatus(
|
|
state.manager,
|
|
"Inserted row. Refresh the page to see it.",
|
|
false,
|
|
);
|
|
missingIdStatus.focus();
|
|
return;
|
|
}
|
|
|
|
state.currentRowId = insertedRowId;
|
|
state.currentFragmentUrl = rowFragmentUrlById(insertedRowId);
|
|
var insertedRow = null;
|
|
try {
|
|
insertedRow = await fetchUpdatedRowElement(state);
|
|
} catch (_error) {
|
|
state.dialog.close();
|
|
var refreshFailedStatus = showRowMutationStatus(
|
|
state.manager,
|
|
"Inserted row, but could not refresh the table row. Refresh the page to see it.",
|
|
true,
|
|
);
|
|
refreshFailedStatus.focus();
|
|
return;
|
|
}
|
|
if (insertedRow) {
|
|
var insertedStatusMessage = insertedRowStatusMessage(
|
|
tildeDecode(insertedRowId),
|
|
rowTitleLabel(insertedRow),
|
|
);
|
|
var addedRow = addInsertedRowToPage(insertedRow);
|
|
state.dialog.close();
|
|
showRowMutationStatus(state.manager, insertedStatusMessage, false);
|
|
if (addedRow) {
|
|
var insertedFocusTarget =
|
|
addedRow.querySelector('button[data-row-action="edit"]') ||
|
|
addedRow;
|
|
insertedFocusTarget.focus();
|
|
}
|
|
} else {
|
|
state.dialog.close();
|
|
var filteredStatus = showRowMutationStatus(
|
|
state.manager,
|
|
"Inserted row. It does not match the current filters.",
|
|
false,
|
|
);
|
|
filteredStatus.focus();
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (isRowPage()) {
|
|
state.shouldRestoreFocus = false;
|
|
state.dialog.close();
|
|
location.reload();
|
|
return;
|
|
}
|
|
|
|
var updatedRow = await fetchUpdatedRowElement(state);
|
|
var focusTarget = null;
|
|
if (updatedRow && state.currentRow && document.contains(state.currentRow)) {
|
|
var importedRow = document.importNode(updatedRow, true);
|
|
state.currentRow.replaceWith(importedRow);
|
|
showRowMutationStatus(
|
|
state.manager,
|
|
state.currentPkPath
|
|
? "Updated row " + state.currentPkPath + "."
|
|
: "Updated row.",
|
|
false,
|
|
);
|
|
focusTarget =
|
|
importedRow.querySelector('button[data-row-action="edit"]') ||
|
|
importedRow;
|
|
} else if (state.currentRow && document.contains(state.currentRow)) {
|
|
focusTarget =
|
|
nextRowActionFocusTarget(state.currentRow, "edit") ||
|
|
ensureRowMutationStatus(state.manager);
|
|
state.currentRow.remove();
|
|
showRowMutationStatus(
|
|
state.manager,
|
|
state.currentPkPath
|
|
? "Updated row " +
|
|
state.currentPkPath +
|
|
". It no longer matches the current filters."
|
|
: "Updated row. It no longer matches the current filters.",
|
|
false,
|
|
);
|
|
}
|
|
|
|
state.shouldRestoreFocus = false;
|
|
state.dialog.close();
|
|
if (focusTarget && document.contains(focusTarget)) {
|
|
focusTarget.focus();
|
|
}
|
|
} catch (error) {
|
|
setRowEditDialogSaving(state, false);
|
|
showRowEditDialogError(state, error.message || "Could not save row");
|
|
}
|
|
}
|
|
|
|
function renderRowEditFields(state, data) {
|
|
var row = data.rows && data.rows.length ? data.rows[0] : null;
|
|
var columns = data.columns || (row ? Object.keys(row) : []);
|
|
var primaryKeys = data.primary_keys || [];
|
|
var columnTypes = data.column_types || {};
|
|
|
|
destroyRowEditFields(state);
|
|
columns.forEach(function (column, index) {
|
|
state.fields.appendChild(
|
|
createRowEditField(
|
|
column,
|
|
row ? row[column] : null,
|
|
primaryKeys.indexOf(column) !== -1,
|
|
columnTypes[column],
|
|
index,
|
|
{
|
|
autocompleteUrl: foreignKeyAutocompleteUrl(column),
|
|
dialog: state.dialog,
|
|
form: state.form,
|
|
manager: state.manager,
|
|
mode: state.mode,
|
|
primaryKeyReadonly: true,
|
|
},
|
|
),
|
|
);
|
|
});
|
|
|
|
state.hasLoaded = true;
|
|
updateRowEditDialogButtons(state);
|
|
if (!focusFirstRowEditControl(state, { skipReadonly: true })) {
|
|
focusFirstRowEditControl(state) || state.cancelButton.focus();
|
|
}
|
|
}
|
|
|
|
function renderRowInsertFields(state, data) {
|
|
var columns = data.columns || [];
|
|
|
|
destroyRowEditFields(state);
|
|
columns.forEach(function (column, index) {
|
|
state.fields.appendChild(
|
|
createRowEditField(
|
|
column.name,
|
|
"",
|
|
!!column.is_pk,
|
|
column.column_type,
|
|
index,
|
|
{
|
|
autocompleteUrl: foreignKeyAutocompleteUrl(column.name),
|
|
dialog: state.dialog,
|
|
form: state.form,
|
|
defaultExpression: column.default,
|
|
manager: state.manager,
|
|
mode: state.mode,
|
|
notnull: column.notnull,
|
|
primaryKeyReadonly: false,
|
|
sqliteType: column.sqlite_type,
|
|
useSqliteDefault: column.default !== null,
|
|
valueKind: column.value_kind,
|
|
},
|
|
),
|
|
);
|
|
});
|
|
|
|
if (!columns.length) {
|
|
var emptyMessage = document.createElement("p");
|
|
emptyMessage.className = "row-edit-empty";
|
|
emptyMessage.textContent = "This row will use the table defaults.";
|
|
state.fields.appendChild(emptyMessage);
|
|
}
|
|
|
|
state.hasLoaded = true;
|
|
updateRowEditDialogButtons(state);
|
|
var firstDefaultButton = state.fields.querySelector(
|
|
".row-edit-default-set-value",
|
|
);
|
|
if (firstDefaultButton) {
|
|
firstDefaultButton.focus();
|
|
} else {
|
|
focusFirstRowEditControl(state, { skipReadonly: true }) ||
|
|
state.saveButton.focus();
|
|
}
|
|
}
|
|
|
|
function setRowDialogTitle(title, text, codeText, labelText) {
|
|
title.textContent = "";
|
|
var action = document.createElement("span");
|
|
action.className = "row-dialog-action";
|
|
action.textContent = text;
|
|
title.appendChild(action);
|
|
if (!codeText) {
|
|
return;
|
|
}
|
|
title.appendChild(document.createTextNode(" "));
|
|
var code = document.createElement("code");
|
|
code.textContent = codeText;
|
|
title.appendChild(code);
|
|
if (labelText && labelText !== codeText) {
|
|
title.appendChild(document.createTextNode(" "));
|
|
var label = document.createElement("span");
|
|
label.className = "row-dialog-label";
|
|
label.textContent = labelText;
|
|
title.appendChild(label);
|
|
}
|
|
}
|
|
|
|
function ensureRowEditDialog(manager) {
|
|
if (rowEditDialogState) {
|
|
return rowEditDialogState;
|
|
}
|
|
if (!window.HTMLDialogElement) {
|
|
return null;
|
|
}
|
|
|
|
var dialog = document.createElement("dialog");
|
|
dialog.id = ROW_EDIT_DIALOG_ID;
|
|
dialog.className = "row-edit-dialog";
|
|
dialog.setAttribute("aria-labelledby", "row-edit-title");
|
|
dialog.innerHTML = `
|
|
<div class="modal-header">
|
|
<span class="modal-title" id="row-edit-title">Edit row</span>
|
|
</div>
|
|
<form class="row-edit-form" method="post">
|
|
<p class="row-edit-summary" id="row-edit-summary" hidden></p>
|
|
<p class="row-edit-loading" role="status" aria-live="polite">Loading row...</p>
|
|
<p class="row-edit-error" role="alert" tabindex="-1" hidden></p>
|
|
<div class="row-edit-fields"></div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-ghost row-edit-cancel">Cancel</button>
|
|
<button type="submit" class="btn btn-primary row-edit-save" disabled>Save</button>
|
|
</div>
|
|
</form>
|
|
`;
|
|
document.body.appendChild(dialog);
|
|
|
|
rowEditDialogState = {
|
|
dialog: dialog,
|
|
form: dialog.querySelector(".row-edit-form"),
|
|
title: dialog.querySelector(".modal-title"),
|
|
summary: dialog.querySelector(".row-edit-summary"),
|
|
loading: dialog.querySelector(".row-edit-loading"),
|
|
error: dialog.querySelector(".row-edit-error"),
|
|
fields: dialog.querySelector(".row-edit-fields"),
|
|
cancelButton: dialog.querySelector(".row-edit-cancel"),
|
|
saveButton: dialog.querySelector(".row-edit-save"),
|
|
currentButton: null,
|
|
currentRow: null,
|
|
currentRowId: null,
|
|
currentPkPath: null,
|
|
currentInsertUrl: null,
|
|
currentUpdateUrl: null,
|
|
currentFragmentUrl: null,
|
|
mode: "edit",
|
|
loadId: 0,
|
|
manager: manager,
|
|
isLoading: false,
|
|
isSaving: false,
|
|
isClosePending: false,
|
|
hasLoaded: false,
|
|
shouldRestoreFocus: true,
|
|
};
|
|
|
|
rowEditDialogState.form.addEventListener("submit", function (ev) {
|
|
ev.preventDefault();
|
|
saveRowEditDialog(rowEditDialogState);
|
|
});
|
|
|
|
rowEditDialogState.cancelButton.addEventListener("click", function () {
|
|
if (!rowEditDialogState.isSaving) {
|
|
rowEditDialogState.shouldRestoreFocus = true;
|
|
dialog.close();
|
|
}
|
|
});
|
|
|
|
dialog.addEventListener("click", function (ev) {
|
|
if (ev.target === dialog) {
|
|
closeRowEditDialogIfConfirmed(rowEditDialogState);
|
|
}
|
|
});
|
|
|
|
dialog.addEventListener("keydown", function (ev) {
|
|
if (ev.key !== "Escape") {
|
|
return;
|
|
}
|
|
ev.preventDefault();
|
|
scheduleCloseRowEditDialogIfConfirmed(rowEditDialogState);
|
|
});
|
|
|
|
dialog.addEventListener("cancel", function (ev) {
|
|
ev.preventDefault();
|
|
scheduleCloseRowEditDialogIfConfirmed(rowEditDialogState);
|
|
});
|
|
|
|
dialog.addEventListener("close", function () {
|
|
var state = rowEditDialogState;
|
|
state.loadId += 1;
|
|
state.isClosePending = false;
|
|
clearRowEditDialogError(state);
|
|
state.hasLoaded = false;
|
|
destroyRowEditFields(state);
|
|
setRowEditDialogLoading(state, false);
|
|
setRowEditDialogSaving(state, false);
|
|
if (
|
|
state.shouldRestoreFocus &&
|
|
state.currentButton &&
|
|
document.contains(state.currentButton)
|
|
) {
|
|
state.currentButton.focus();
|
|
}
|
|
});
|
|
|
|
return rowEditDialogState;
|
|
}
|
|
|
|
async function openRowEditDialog(button, manager) {
|
|
var row = rowElementForActionButton(button);
|
|
if (!row || !row.getAttribute("data-row")) {
|
|
return;
|
|
}
|
|
var state = ensureRowEditDialog(manager);
|
|
if (!state) {
|
|
return;
|
|
}
|
|
|
|
state.manager = manager;
|
|
state.mode = "edit";
|
|
state.currentButton = button;
|
|
state.currentRow = row;
|
|
state.currentRowId = row.getAttribute("data-row") || "";
|
|
state.currentPkPath = rowDisplayLabel(row);
|
|
state.currentInsertUrl = null;
|
|
state.currentUpdateUrl = rowUpdateUrl(row);
|
|
state.currentFragmentUrl = rowFragmentUrl(row);
|
|
if (state.currentUpdateUrl) {
|
|
state.form.action = new URL(
|
|
state.currentUpdateUrl,
|
|
location.href,
|
|
).toString();
|
|
} else {
|
|
state.form.removeAttribute("action");
|
|
}
|
|
state.shouldRestoreFocus = true;
|
|
state.hasLoaded = false;
|
|
state.loadId += 1;
|
|
var loadId = state.loadId;
|
|
|
|
clearRowEditDialogError(state);
|
|
setRowEditDialogLoading(state, true);
|
|
destroyRowEditFields(state);
|
|
state.dialog.removeAttribute("aria-describedby");
|
|
setRowDialogTitle(
|
|
state.title,
|
|
"Edit row",
|
|
state.currentPkPath || "this row",
|
|
rowTitleLabel(row),
|
|
);
|
|
state.summary.hidden = true;
|
|
state.summary.textContent = "";
|
|
|
|
if (!state.dialog.open) {
|
|
state.dialog.showModal();
|
|
}
|
|
state.cancelButton.focus();
|
|
|
|
try {
|
|
var response = await fetch(rowJsonUrl(row), {
|
|
headers: {
|
|
Accept: "application/json",
|
|
},
|
|
});
|
|
var data = await response.json();
|
|
if (loadId !== state.loadId) {
|
|
return;
|
|
}
|
|
if (!response.ok || data.ok === false) {
|
|
throw rowMutationRequestError(response, data);
|
|
}
|
|
setRowEditDialogLoading(state, false);
|
|
renderRowEditFields(state, data);
|
|
} catch (error) {
|
|
if (loadId !== state.loadId) {
|
|
return;
|
|
}
|
|
setRowEditDialogLoading(state, false);
|
|
showRowEditDialogError(state, error.message || "Could not load row");
|
|
state.cancelButton.focus();
|
|
}
|
|
}
|
|
|
|
function openRowInsertDialog(button, manager) {
|
|
var insertData = tableInsertData();
|
|
if (!insertData) {
|
|
return;
|
|
}
|
|
var state = ensureRowEditDialog(manager);
|
|
if (!state) {
|
|
return;
|
|
}
|
|
|
|
state.manager = manager;
|
|
state.mode = "insert";
|
|
state.currentButton = button;
|
|
state.currentRow = null;
|
|
state.currentRowId = null;
|
|
state.currentPkPath = null;
|
|
state.currentInsertUrl = tableInsertUrl();
|
|
state.currentUpdateUrl = null;
|
|
state.currentFragmentUrl = null;
|
|
state.shouldRestoreFocus = true;
|
|
state.hasLoaded = false;
|
|
state.loadId += 1;
|
|
|
|
if (state.currentInsertUrl) {
|
|
state.form.action = new URL(
|
|
state.currentInsertUrl,
|
|
location.href,
|
|
).toString();
|
|
} else {
|
|
state.form.removeAttribute("action");
|
|
}
|
|
|
|
clearRowEditDialogError(state);
|
|
setRowEditDialogLoading(state, false);
|
|
destroyRowEditFields(state);
|
|
state.dialog.removeAttribute("aria-describedby");
|
|
setRowDialogTitle(
|
|
state.title,
|
|
insertData.tableName
|
|
? "Insert row into " + insertData.tableName
|
|
: "Insert row",
|
|
);
|
|
state.summary.hidden = true;
|
|
state.summary.textContent = "";
|
|
|
|
if (!state.dialog.open) {
|
|
state.dialog.showModal();
|
|
}
|
|
renderRowInsertFields(state, insertData);
|
|
}
|
|
|
|
function initRowEditActions(manager) {
|
|
if (!window.fetch || !window.HTMLDialogElement) {
|
|
return;
|
|
}
|
|
document.addEventListener("click", function (ev) {
|
|
var button = ev.target.closest('button[data-row-action="edit"]');
|
|
if (!button) {
|
|
return;
|
|
}
|
|
ev.preventDefault();
|
|
openRowEditDialog(button, manager);
|
|
});
|
|
}
|
|
|
|
function initRowInsertActions(manager) {
|
|
if (!window.fetch || !window.HTMLDialogElement || !tableInsertData()) {
|
|
return;
|
|
}
|
|
document.addEventListener("click", function (ev) {
|
|
var button = ev.target.closest('button[data-table-action="insert-row"]');
|
|
if (!button) {
|
|
return;
|
|
}
|
|
ev.preventDefault();
|
|
openRowInsertDialog(button, manager);
|
|
});
|
|
}
|
|
|
|
document.addEventListener("datasette_init", function (evt) {
|
|
const { detail: manager } = evt;
|
|
|
|
registerBuiltinColumnFieldPlugins(manager);
|
|
initTableCreateActions(manager);
|
|
initTableAlterActions(manager);
|
|
initRowInsertActions(manager);
|
|
initRowEditActions(manager);
|
|
initRowDeleteActions(manager);
|
|
});
|