diff --git a/datasette/static/app.css b/datasette/static/app.css index f9ebe5ac..49f070b1 100644 --- a/datasette/static/app.css +++ b/datasette/static/app.css @@ -1760,7 +1760,7 @@ dialog.table-create-dialog { border-radius: var(--modal-border-radius, 0.75rem); padding: 0; margin: auto; - width: min(760px, calc(100vw - 32px)); + width: min(980px, calc(100vw - 32px)); max-width: 95vw; max-height: min(780px, calc(100vh - 32px)); box-shadow: var(--modal-shadow, 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)); @@ -1839,7 +1839,7 @@ dialog.table-create-dialog::backdrop { } .table-create-label, -.table-create-columns-heading { +.table-create-column-headings { color: var(--ink); font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 0.82rem; @@ -1876,10 +1876,12 @@ dialog.table-create-dialog::backdrop { color: var(--muted); } +.table-create-foreign-key-target option, .table-create-custom-column-type option { color: var(--ink); } +.table-create-foreign-key-target option[value=""], .table-create-custom-column-type option[value=""] { color: var(--muted); } @@ -1898,40 +1900,130 @@ dialog.table-create-dialog::backdrop { gap: 10px; } -.table-create-columns-heading { - font-weight: 600; -} - .table-create-column-list { display: grid; gap: 8px; } -.table-create-column-row { +.table-create-column-headings, +.table-create-column-main { display: grid; - grid-template-columns: minmax(140px, 1.2fr) minmax(7.5rem, 0.7fr) minmax(12rem, 1fr) minmax(3.5rem, max-content) 32px; + grid-template-columns: minmax(140px, 1.2fr) minmax(7.5rem, 0.7fr) max-content 32px; align-items: center; gap: 8px; min-width: 0; - position: relative; } -.table-create-primary-key { - display: inline-flex; - align-items: center; - gap: 6px; - color: var(--ink); - font-size: 0.85rem; +.table-create-column-row { + display: grid; + gap: 8px; min-width: 0; - white-space: nowrap; - justify-self: center; } -.table-create-primary-key-input { +.table-create-column-headings { + color: var(--muted); + font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; + font-size: 0.72rem; + padding: 0 1px; +} + +.table-create-column-details { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); + align-items: start; + gap: 12px 16px; + padding: 12px; + border-left: 3px solid var(--rule); + background: #f8fafc; +} + +.table-create-column-details[hidden] { + display: none; +} + +.table-create-detail-field { + display: grid; + gap: 4px; + min-width: 0; +} + +.table-create-detail-label { + color: var(--muted); + font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; + font-size: 0.72rem; +} + +.table-create-detail-help { + color: var(--muted); + font-size: 0.82rem; + line-height: 1.35; margin: 0; } -.table-create-remove-column { +.table-create-detail-check { + display: inline-flex; + align-items: flex-start; + gap: 8px; + color: var(--ink); + font-size: 0.85rem; + line-height: 1.35; + min-width: 0; + white-space: normal; +} + +.table-create-primary-key, +.table-create-foreign-key-field { + grid-column: 1 / -1; +} + +.table-create-detail-check input { + flex: 0 0 auto; + margin: 0.15rem 0 0; +} + +.table-create-detail-check span { + min-width: 0; + overflow-wrap: break-word; +} + +.table-create-move-controls { + display: grid; + grid-template-columns: repeat(4, 32px); + gap: 4px; + justify-content: start; +} + +.table-create-more-options { + appearance: none; + border: 0; + background: transparent; + color: var(--accent); + cursor: pointer; + font: inherit; + font-size: 0.85rem; + justify-self: start; + padding: 0; + grid-column: 1 / -1; + text-align: left; +} + +.table-create-more-options:hover, +.table-create-more-options:focus { + text-decoration: underline; +} + +.table-create-more-options:focus { + outline: 3px solid rgba(26, 86, 219, 0.12); + outline-offset: 2px; +} + +.table-create-more-options:disabled { + color: var(--muted); + cursor: default; + text-decoration: none; +} + +.table-create-icon-button { appearance: none; border: 1px solid rgba(74, 85, 104, 0.24); background: transparent; @@ -1945,17 +2037,17 @@ dialog.table-create-dialog::backdrop { padding: 0; } -.table-create-remove-column:hover, -.table-create-remove-column:focus { +.table-create-icon-button:hover, +.table-create-icon-button:focus { background: rgba(74, 85, 104, 0.07); } -.table-create-remove-column:focus { +.table-create-icon-button:focus { outline: 3px solid #b3d4ff; outline-offset: 1px; } -.table-create-remove-column svg { +.table-create-icon-button svg { display: block; } @@ -1967,11 +2059,19 @@ dialog.table-create-dialog::backdrop { background: #fff; color: var(--accent); cursor: pointer; + display: inline-flex; + align-items: center; + gap: 6px; font: inherit; font-size: 0.85rem; padding: 7px 10px; } +.table-create-add-column svg { + display: block; + flex: 0 0 auto; +} + .table-create-add-column:hover, .table-create-add-column:focus { background: #f8fafc; @@ -2027,7 +2127,7 @@ dialog.table-create-dialog::backdrop { .table-create-dialog .btn:disabled, .table-create-add-column:disabled, -.table-create-remove-column:disabled { +.table-create-icon-button:disabled { opacity: 0.55; cursor: not-allowed; } @@ -2198,7 +2298,7 @@ dialog.table-alter-dialog::backdrop { .table-alter-column-headings, .table-alter-column-main { display: grid; - grid-template-columns: minmax(140px, 1.2fr) minmax(7.5rem, 0.7fr) minmax(12rem, 1fr) max-content 32px; + grid-template-columns: minmax(140px, 1.2fr) minmax(7.5rem, 0.7fr) max-content 32px; align-items: center; gap: 8px; min-width: 0; @@ -2384,11 +2484,19 @@ dialog.table-alter-dialog::backdrop { background: #fff; color: var(--accent); cursor: pointer; + display: inline-flex; + align-items: center; + gap: 6px; font: inherit; font-size: 0.85rem; padding: 7px 10px; } +.table-alter-add-column svg { + display: block; + flex: 0 0 auto; +} + .table-alter-add-column:hover, .table-alter-add-column:focus { background: #f8fafc; @@ -2504,21 +2612,16 @@ dialog.table-alter-dialog::backdrop { justify-self: end; } - .table-alter-custom-column-type { - grid-column: 1 / 3; - grid-row: 2; - } - .table-alter-move-controls { grid-column: 1; - grid-row: 3; + grid-row: 2; justify-self: start; } .table-alter-more-options { align-self: center; grid-column: 2 / 4; - grid-row: 3; + grid-row: 2; } .table-alter-column-details { @@ -2700,13 +2803,50 @@ dialog.table-alter-dialog::backdrop { padding-top: 0; } + .table-create-column-headings { + display: none; + } + .table-create-column-row { - grid-template-columns: minmax(0, 1fr) 8.5rem 3.5rem 32px; + padding-bottom: 8px; + border-bottom: 1px solid var(--rule); + } + + .table-create-column-main { + grid-template-columns: minmax(0, 1fr) minmax(7.5rem, 0.8fr) 32px; align-items: end; } - .table-create-custom-column-type { - grid-column: 1 / -1; + .table-create-column-name { + grid-column: 1; + grid-row: 1; + } + + .table-create-column-type { + grid-column: 2; + grid-row: 1; + } + + .table-create-remove-column { + grid-column: 3; + grid-row: 1; + justify-self: end; + } + + .table-create-move-controls { + grid-column: 1; + grid-row: 2; + justify-self: start; + } + + .table-create-more-options { + align-self: center; + grid-column: 2 / 4; + grid-row: 2; + } + + .table-create-column-details { + grid-template-columns: 1fr; } .table-create-dialog .modal-footer { diff --git a/datasette/static/edit-tools.js b/datasette/static/edit-tools.js index cb964365..5a9d0962 100644 --- a/datasette/static/edit-tools.js +++ b/datasette/static/edit-tools.js @@ -64,12 +64,220 @@ function sqliteColumnTypeLabel(type) { if (type === "float") { return "floating point number"; } + if (type === "real") { + return "floating point number"; + } if (type === "blob") { return "blob - binary data"; } return 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: '', + up: '', + down: '', + bottom: + '', + remove: + '', +}; + +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 || []; @@ -93,15 +301,205 @@ function tableCreateCustomTypeAppliesToSqliteType(option, sqliteType) { ); } +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 refreshTableCreateForeignKeyControls(state) { + tableCreateDialogRows(state).forEach(function (row, index) { + if (index > 0) { + syncTableCreateForeignKeyOptions(row, state); + } + }); +} + +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); + } + } + }); + 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 ""; } - var columns = []; - state.columnList - .querySelectorAll(".table-create-column-row") - .forEach(function (row) { - columns.push({ + 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: @@ -111,11 +509,14 @@ function tableCreateDialogSignature(state) { } ).value || "", pk: row.querySelector(".table-create-primary-key-input").checked, - }); - }); - return JSON.stringify({ - table: state.tableName.value, - columns: columns, + foreignKey: + ( + row.querySelector(".table-create-foreign-key-target") || { + value: "", + } + ).value || "", + }; + }), }); } @@ -151,42 +552,28 @@ function setTableCreateDialogSaving(state, isSaving) { .forEach(function (control) { control.disabled = isSaving; }); + if (!isSaving) { + updateTableCreateColumnRules(state); + } + updateTableCreateMoveButtons(state); } function tableCreateSelectTypeValue(select, type) { var options = tableCreateColumnTypes(); - 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; + populateSqliteColumnTypeSelect(select, type, options); } function updateTableCreateCustomColumnTypePlaceholder(select) { - select.classList.toggle("table-create-input-placeholder", !select.value); + updateSelectPlaceholder(select, "table-create-input-placeholder"); } function createTableCustomColumnTypeSelect() { var options = tableCreateCustomColumnTypes(); - var select = document.createElement("select"); - select.className = "table-create-input table-create-custom-column-type"; - 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); - }); - updateTableCreateCustomColumnTypePlaceholder(select); - return select; + return createCustomColumnTypeSelect( + options, + "table-create-input table-create-custom-column-type", + "table-create-input-placeholder", + ); } function syncTableCreateCustomTypeForSqliteType(row) { @@ -209,6 +596,19 @@ function createTableColumnRow(state, column) { 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"; @@ -228,40 +628,88 @@ function createTableColumnRow(state, column) { typeSelect.setAttribute("aria-label", "Column type"); tableCreateSelectTypeValue(typeSelect, column && column.type); - var customTypeSelect = createTableCustomColumnTypeSelect(); - if (column && column.customType) { - customTypeSelect.value = column.customType; + 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"; + customTypeSelect = createTableCustomColumnTypeSelect(); + customTypeSelect.id = customTypeId; + customTypeSelect.value = column && column.customType ? column.customType : ""; + updateTableCreateCustomColumnTypePlaceholder(customTypeSelect); + customTypeField.appendChild(customTypeLabel); + customTypeField.appendChild(customTypeSelect); } - updateTableCreateCustomColumnTypePlaceholder(customTypeSelect); var pkLabel = document.createElement("label"); - pkLabel.className = "table-create-primary-key"; + 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"); - pkText.textContent = "PK"; - pkText.title = "Primary key"; + 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 removeButton = document.createElement("button"); - removeButton.type = "button"; - removeButton.className = "table-create-remove-column"; - removeButton.setAttribute("aria-label", "Remove column"); - removeButton.title = "Remove column"; - removeButton.innerHTML = - ''; + 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); - row.appendChild(nameLabel); - row.appendChild(nameInput); - row.appendChild(typeSelect); - if (tableCreateCustomColumnTypes().length) { - row.appendChild(customTypeSelect); + 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); } - row.appendChild(pkLabel); - row.appendChild(removeButton); + details.appendChild(foreignKeyField); + details.appendChild(pkLabel); + row.appendChild(main); + row.appendChild(details); removeButton.addEventListener("click", function () { if (state.isSaving) { @@ -269,6 +717,7 @@ function createTableColumnRow(state, column) { } row.remove(); clearTableCreateDialogError(state); + updateTableCreateColumnRules(state); var nextInput = state.columnList.querySelector(".table-create-column-name"); if (nextInput) { nextInput.focus(); @@ -283,21 +732,94 @@ function createTableColumnRow(state, column) { typeSelect.addEventListener("change", function () { clearTableCreateDialogError(state); syncTableCreateCustomTypeForSqliteType(row); + syncTableCreateForeignKeyOptions(row, state); }); - customTypeSelect.addEventListener("change", function () { - clearTableCreateDialogError(state); - updateTableCreateCustomColumnTypePlaceholder(customTypeSelect); - var option = tableCreateCustomColumnType(customTypeSelect.value); - if ( - option && - option.fixedSqliteType && - tableCreateColumnTypes().indexOf(option.fixedSqliteType) !== -1 - ) { - typeSelect.value = option.fixedSqliteType; - } - }); + if (customTypeSelect) { + customTypeSelect.addEventListener("change", function () { + clearTableCreateDialogError(state); + updateTableCreateCustomColumnTypePlaceholder(customTypeSelect); + var option = tableCreateCustomColumnType(customTypeSelect.value); + if ( + option && + option.fixedSqliteType && + tableCreateColumnTypes().indexOf(option.fixedSqliteType) !== -1 + ) { + typeSelect.value = option.fixedSqliteType; + syncTableCreateForeignKeyOptions(row, state); + } + }); + } pkInput.addEventListener("change", function () { clearTableCreateDialogError(state); + updateTableCreateColumnRules(state); + }); + foreignKeySelect.addEventListener("change", function () { + foreignKeySelect.dataset.selectedKey = foreignKeySelect.value; + clearTableCreateDialogError(state); + updateTableCreateCustomColumnTypePlaceholder(foreignKeySelect); + }); + + 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; @@ -306,6 +828,7 @@ function createTableColumnRow(state, column) { function addTableCreateColumn(state, column) { var row = createTableColumnRow(state, column || { type: "text" }); state.columnList.appendChild(row); + updateTableCreateColumnRules(state); return row; } @@ -323,6 +846,7 @@ function resetTableCreateDialog(state) { type: "text", primaryKey: false, }); + updateTableCreateColumnRules(state); state.initialSignature = tableCreateDialogSignature(state); } @@ -332,16 +856,32 @@ function collectTableCreatePayload(state) { columns: [], }; var primaryKeys = []; - state.columnList - .querySelectorAll(".table-create-column-row") - .forEach(function (row) { - var name = row.querySelector(".table-create-column-name").value.trim(); - var type = row.querySelector(".table-create-column-type").value; - payload.columns.push({ name: name, type: type }); - if (row.querySelector(".table-create-primary-key-input").checked) { - primaryKeys.push(name); - } - }); + 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) { @@ -352,21 +892,19 @@ function collectTableCreatePayload(state) { function collectTableCreateColumnTypeAssignments(state) { var assignments = []; - state.columnList - .querySelectorAll(".table-create-column-row") - .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, - }); + 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; } @@ -604,9 +1142,14 @@ function ensureTableCreateDialog(manager) {
-
Columns
+
- +