From 15bd72e02b1972e6039d9c570663ea35c61bbc7e Mon Sep 17 00:00:00 2001 From: "exe.dev user" Date: Thu, 5 Mar 2026 03:22:35 +0000 Subject: [PATCH] Add Set Where command New modal-based command that bulk-sets cell values conditionally: - Match Column: autocomplete column name picker - Match Values: textarea with one value per line - Set Value: the value to write Scans all rows; where the Match Column cell equals any Match Value, sets the cell at the cursor's current column to Set Value. Modal supports full keyboard navigation: Tab between fields, Enter in Set Value confirms, Escape closes, arrow keys for autocomplete selection. Co-authored-by: Shelley --- app_test.go | 4 +- commands.go | 1 + frontend/index.html | 18 ++++ frontend/src/main.js | 213 +++++++++++++++++++++++++++++++++++++++++ frontend/src/style.css | 91 ++++++++++++++++++ 5 files changed, 325 insertions(+), 2 deletions(-) diff --git a/app_test.go b/app_test.go index d566601..0781425 100644 --- a/app_test.go +++ b/app_test.go @@ -98,8 +98,8 @@ func TestParseCSVString(t *testing.T) { func TestGetCommands(t *testing.T) { reg := NewCommandRegistry() cmds := reg.GetCommands() - if len(cmds) != 17 { - t.Errorf("expected 12 commands, got %d", len(cmds)) + if len(cmds) != 18 { + t.Errorf("expected 18 commands, got %d", len(cmds)) } // Check that all have IDs for _, cmd := range cmds { diff --git a/commands.go b/commands.go index 806a458..d79dbf2 100644 --- a/commands.go +++ b/commands.go @@ -34,6 +34,7 @@ func (c *CommandRegistry) GetCommands() []Command { {ID: "sort-desc", Name: "Sort Z-A", Shortcut: ""}, {ID: "sort-advanced", Name: "Sort Advanced", Shortcut: ""}, {ID: "match-cell", Name: "Match Cell", Shortcut: "Cmd+M"}, + {ID: "set-where", Name: "Set Where", Shortcut: ""}, {ID: "delete-row", Name: "Delete Row", Shortcut: "Cmd+Backspace"}, } } diff --git a/frontend/index.html b/frontend/index.html index ad7ed40..1050183 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -39,6 +39,24 @@ + diff --git a/frontend/src/main.js b/frontend/src/main.js index 8e3e3b1..38d0bb8 100644 --- a/frontend/src/main.js +++ b/frontend/src/main.js @@ -32,6 +32,13 @@ const sortInput = $('#sort-columns-input'); const sortAutocomplete = $('#sort-autocomplete'); const sortConfirmBtn = $('#sort-confirm-btn'); const sortCancelBtn = $('#sort-cancel-btn'); +const swModal = $('#set-where-modal'); +const swMatchCol = $('#sw-match-col'); +const swColAutocomplete = $('#sw-col-autocomplete'); +const swMatchValues = $('#sw-match-values'); +const swSetValue = $('#sw-set-value'); +const swConfirmBtn = $('#sw-confirm-btn'); +const swCancelBtn = $('#sw-cancel-btn'); // Default column width const DEFAULT_COL_WIDTH = 120; @@ -669,6 +676,7 @@ async function loadCommands() { { ID: 'sort-desc', Name: 'Sort Z-A', Shortcut: '' }, { ID: 'sort-advanced', Name: 'Sort Advanced', Shortcut: '' }, { ID: 'match-cell', Name: 'Match Cell', Shortcut: 'Cmd+M' }, + { ID: 'set-where', Name: 'Set Where', Shortcut: '' }, { ID: 'delete-row', Name: 'Delete Row', Shortcut: 'Cmd+Backspace' }, ]; } @@ -755,6 +763,7 @@ commandInput.addEventListener('keydown', (e) => { overlay.addEventListener('click', () => { if (!commandPalette.classList.contains('hidden')) closeCommandPalette(); if (!sortModal.classList.contains('hidden')) closeSortAdvanced(); + if (!swModal.classList.contains('hidden')) closeSetWhere(); }); // ===== Command execution ===== @@ -776,6 +785,7 @@ async function executeCommand(id) { case 'sort-desc': doSort(false); break; case 'sort-advanced': openSortAdvanced(); break; case 'match-cell': doMatchCell(); break; + case 'set-where': openSetWhere(); break; case 'delete-row': doDeleteRow(); break; } } @@ -1235,6 +1245,209 @@ sortInput.addEventListener('keydown', (e) => { sortConfirmBtn.addEventListener('click', confirmSortAdvanced); sortCancelBtn.addEventListener('click', closeSortAdvanced); +// ===== Set Where Modal ===== +let swAcIndex = -1; + +function openSetWhere() { + swModal.classList.remove('hidden'); + overlay.classList.remove('hidden'); + swMatchCol.value = ''; + swMatchValues.value = ''; + swSetValue.value = ''; + hideSwAutocomplete(); + swMatchCol.focus(); +} + +function closeSetWhere() { + swModal.classList.add('hidden'); + overlay.classList.add('hidden'); + swMatchCol.value = ''; + swMatchValues.value = ''; + swSetValue.value = ''; + hideSwAutocomplete(); +} + +function hideSwAutocomplete() { + swColAutocomplete.classList.add('hidden'); + swColAutocomplete.innerHTML = ''; + swAcIndex = -1; +} + +function updateSwAutocomplete() { + const text = swMatchCol.value.trim().toLowerCase(); + const matches = state.headers + .filter(h => h.toLowerCase().includes(text)) + .sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase())); + + if (matches.length === 0 || (matches.length === 1 && matches[0].toLowerCase() === text)) { + hideSwAutocomplete(); + return; + } + + swColAutocomplete.innerHTML = ''; + swColAutocomplete.classList.remove('hidden'); + swAcIndex = -1; + + matches.forEach((name) => { + const div = document.createElement('div'); + div.className = 'ac-item'; + div.textContent = name; + div.addEventListener('mousedown', (e) => { + e.preventDefault(); + swMatchCol.value = name; + hideSwAutocomplete(); + swMatchValues.focus(); + }); + swColAutocomplete.appendChild(div); + }); +} + +function confirmSetWhere() { + const colName = swMatchCol.value.trim(); + const matchColIdx = state.headers.findIndex( + h => h.toLowerCase() === colName.toLowerCase() + ); + if (matchColIdx === -1) { + setStatus(`Unknown column: "${colName}"`); + return; + } + + const matchVals = new Set( + swMatchValues.value.split('\n').map(s => s.trim()).filter(s => s.length > 0) + ); + if (matchVals.size === 0) { + setStatus('No match values specified'); + return; + } + + const setValue = swSetValue.value; + const targetCol = state.cursor.col; + let count = 0; + + for (let r = 0; r < state.rows.length; r++) { + const cellVal = state.rows[r][matchColIdx] || ''; + if (matchVals.has(cellVal)) { + setCellValue(r, targetCol, setValue); + count++; + } + } + + closeSetWhere(); + render(); + const targetColName = state.headers[targetCol] || colLabel(targetCol); + setStatus(`Set ${count} cell(s) in ${targetColName}`); +} + +swMatchCol.addEventListener('input', updateSwAutocomplete); +swMatchCol.addEventListener('focus', updateSwAutocomplete); +swMatchCol.addEventListener('blur', () => { + setTimeout(hideSwAutocomplete, 150); +}); + +swMatchCol.addEventListener('keydown', (e) => { + const items = swColAutocomplete.querySelectorAll('.ac-item'); + + if (e.key === 'Escape') { + e.preventDefault(); + e.stopPropagation(); + if (!swColAutocomplete.classList.contains('hidden')) { + hideSwAutocomplete(); + } else { + closeSetWhere(); + } + return; + } + + if (e.key === 'Enter') { + e.preventDefault(); + e.stopPropagation(); + if (swAcIndex >= 0 && items[swAcIndex]) { + swMatchCol.value = items[swAcIndex].textContent; + hideSwAutocomplete(); + swMatchValues.focus(); + } else { + // Move focus to match values + swMatchValues.focus(); + } + return; + } + + if (e.key === 'ArrowDown' && items.length > 0) { + e.preventDefault(); + if (swAcIndex >= 0) items[swAcIndex]?.classList.remove('active'); + swAcIndex = (swAcIndex + 1) % items.length; + items[swAcIndex]?.classList.add('active'); + items[swAcIndex]?.scrollIntoView({ block: 'nearest' }); + return; + } + + if (e.key === 'ArrowUp' && items.length > 0) { + e.preventDefault(); + if (swAcIndex >= 0) items[swAcIndex]?.classList.remove('active'); + swAcIndex = (swAcIndex - 1 + items.length) % items.length; + items[swAcIndex]?.classList.add('active'); + items[swAcIndex]?.scrollIntoView({ block: 'nearest' }); + return; + } + + if (e.key === 'Tab') { + e.preventDefault(); + if (swAcIndex >= 0 && items[swAcIndex]) { + swMatchCol.value = items[swAcIndex].textContent; + hideSwAutocomplete(); + } + swMatchValues.focus(); + return; + } + + e.stopPropagation(); +}); + +swMatchValues.addEventListener('keydown', (e) => { + if (e.key === 'Escape') { + e.preventDefault(); + e.stopPropagation(); + closeSetWhere(); + return; + } + // Tab to next field + if (e.key === 'Tab' && !e.shiftKey) { + e.preventDefault(); + swSetValue.focus(); + return; + } + if (e.key === 'Tab' && e.shiftKey) { + e.preventDefault(); + swMatchCol.focus(); + return; + } + e.stopPropagation(); +}); + +swSetValue.addEventListener('keydown', (e) => { + if (e.key === 'Escape') { + e.preventDefault(); + e.stopPropagation(); + closeSetWhere(); + return; + } + if (e.key === 'Enter') { + e.preventDefault(); + e.stopPropagation(); + confirmSetWhere(); + return; + } + if (e.key === 'Tab' && e.shiftKey) { + e.preventDefault(); + swMatchValues.focus(); + return; + } + e.stopPropagation(); +}); + +swConfirmBtn.addEventListener('click', confirmSetWhere); +swCancelBtn.addEventListener('click', closeSetWhere); + // ===== File loading ===== async function loadFile(filePath) { try { diff --git a/frontend/src/style.css b/frontend/src/style.css index ea72bad..1e4eeda 100644 --- a/frontend/src/style.css +++ b/frontend/src/style.css @@ -391,6 +391,97 @@ td.cursor-cell { background: #1a54a9; } +/* Set Where modal */ +#set-where-modal { + position: fixed; + top: 80px; + left: 50%; + transform: translateX(-50%); + width: 460px; + background: #fff; + border: 1px solid var(--border); + border-radius: 8px; + box-shadow: 0 8px 32px rgba(0,0,0,0.2); + z-index: 100; + overflow: visible; +} + +#set-where-modal.hidden { + display: none; +} + +.sw-input-wrap { + position: relative; +} + +#sw-match-col { + width: 100%; + padding: 8px 10px; + font-size: 14px; + font-family: inherit; + border: 1px solid var(--border); + border-radius: 4px; + outline: none; + box-sizing: border-box; +} + +#sw-match-col:focus { + border-color: var(--selected-border); + box-shadow: 0 0 0 2px rgba(34,102,204,0.15); +} + +#sw-col-autocomplete { + position: absolute; + left: 0; + right: 0; + top: 100%; + margin-top: 2px; + background: #fff; + border: 1px solid var(--border); + border-radius: 4px; + box-shadow: 0 4px 12px rgba(0,0,0,0.12); + max-height: 180px; + overflow-y: auto; + z-index: 110; +} + +#sw-col-autocomplete.hidden { + display: none; +} + +#sw-match-values { + width: 100%; + padding: 8px 10px; + font-size: 14px; + font-family: inherit; + border: 1px solid var(--border); + border-radius: 4px; + outline: none; + resize: vertical; + box-sizing: border-box; +} + +#sw-match-values:focus { + border-color: var(--selected-border); + box-shadow: 0 0 0 2px rgba(34,102,204,0.15); +} + +#sw-set-value { + width: 100%; + padding: 8px 10px; + font-size: 14px; + font-family: inherit; + border: 1px solid var(--border); + border-radius: 4px; + outline: none; + box-sizing: border-box; +} + +#sw-set-value:focus { + border-color: var(--selected-border); + box-shadow: 0 0 0 2px rgba(34,102,204,0.15); +} + /* Drag over style */ #table-container.drag-over { background: var(--selected-bg);