Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
# Change Log

## [Unreleased]

### Added

- 🔍 **Cell viewer dialog**: "Expand Cell" in the context menu opens a full-value viewer with copy for long text
- ↕ **Row expand/collapse**: Context menu action toggles multiline rows and persists row height per tab

### Changed

- 📐 **Table layout defaults**: Tables now derive width from default column sizes with multiline cell previews and scroll
- 🖱️ **Resizing UX**: Column/row resizing uses pointer events, auto-scroll near edges, and updates table width
- 🧰 **View state restores**: Column widths and row heights now refresh table width, multiline styling, and overflow indicators on load


## [0.4.2] - 2025-12-16

### Changed
Expand Down
280 changes: 280 additions & 0 deletions media/context-menu.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,14 @@ function createContextMenuElement() {
<span class="icon">📋</span>
<span>Copy Cell</span>
</div>
<div class="context-menu-item context-menu-item-expand" data-action="expand-cell" style="display: none;">
<span class="icon">🔍</span>
<span>Expand Cell</span>
</div>
<div class="context-menu-item context-menu-item-row" data-action="toggle-row-multiline">
<span class="icon">↕</span>
<span>Expand Row</span>
</div>
<div class="context-menu-item context-menu-item-json" data-action="view-json" style="display: none;">
<span class="icon">{}</span>
<span>View JSON</span>
Expand Down Expand Up @@ -301,6 +309,46 @@ function showContextMenuAt(x, y) {
}
}

// Show/hide expand-cell action for long text values
const expandItem = contextMenu.querySelector('[data-action="expand-cell"]');
if (expandItem) {
const canExpand = !!currentCell && !hasBlob;
if (!canExpand) {
expandItem.style.display = "none";
} else {
const raw = getCellRawValue(currentCell);
const value = raw && typeof raw.value === "string" ? raw.value : "";
const cellContent = currentCell.querySelector(".cell-content");
const hasOverflow =
cellContent &&
cellContent instanceof HTMLElement &&
cellContent.scrollWidth > cellContent.clientWidth + 2;
const shouldShow =
raw.truncated ||
value.length > 80 ||
value.includes("\n") ||
!!hasOverflow;
expandItem.style.display = shouldShow ? "flex" : "none";
}
}

// Show row expand/collapse action
const rowToggleItem = contextMenu.querySelector(
'[data-action="toggle-row-multiline"]'
);
if (rowToggleItem) {
if (!currentRow) {
rowToggleItem.style.display = "none";
} else {
rowToggleItem.style.display = "flex";
const isMultiline = currentRow.classList.contains("row-multiline");
const label = rowToggleItem.querySelector("span:last-child");
if (label) {
label.textContent = isMultiline ? "Collapse Row" : "Expand Row";
}
}
}

// Position the context menu
contextMenu.style.left = x + "px";
contextMenu.style.top = y + "px";
Expand Down Expand Up @@ -410,6 +458,12 @@ function executeContextMenuAction(action) {
case "copy-cell":
copyCellValue();
break;
case "expand-cell":
expandCellValue();
break;
case "toggle-row-multiline":
toggleRowMultiline();
break;
case "view-json":
viewCellAsJson();
break;
Expand Down Expand Up @@ -462,6 +516,151 @@ function copyCellValue() {
copyToClipboard(cellValue, "Cell value copied");
}

/**
* Expand current cell value in a dialog.
*/
function expandCellValue() {
if (!currentCell) {
return;
}

const cellContent = currentCell.querySelector(".cell-content");
if (cellContent && cellContent.getAttribute("data-blob") === "true") {
if (typeof showError === "function") {
showError("Cell is a blob.");
}
return;
}

const raw = getCellRawValue(currentCell);
const isNull =
cellContent &&
cellContent.querySelector("em") &&
(cellContent.textContent || "").trim() === "NULL";
const value = isNull ? "NULL" : raw.value || "";

const columnName =
currentCell.getAttribute("data-column-name") ||
currentCell.dataset.columnName ||
"";
const title = columnName ? `Cell Viewer — ${columnName}` : "Cell Viewer";

showCellViewerDialog({
title,
value,
truncated: raw.truncated,
isNull,
});
}

/**
* Toggle a row between single-line and multiline display.
*/
function toggleRowMultiline() {
if (!currentRow) {
return;
}
const row = currentRow;
const rowIndex = row.getAttribute("data-row-index");
if (!rowIndex) {
return;
}
const table = row.closest(".data-table");
const tableWrapper = row.closest(".enhanced-table-wrapper");
const defaultHeight =
typeof window.TABLE_ROW_HEIGHT_DEFAULT === "number"
? window.TABLE_ROW_HEIGHT_DEFAULT
: 28;
const isMultiline = row.classList.contains("row-multiline");
if (isMultiline) {
row.style.height = "";
row.style.minHeight = "";

if (typeof window.updateRowMultilineForRow === "function") {
requestAnimationFrame(() => {
window.updateRowMultilineForRow(row);
});
}

if (tableWrapper && typeof window.setTabViewState === "function") {
const tabKey =
tableWrapper.getAttribute("data-table") || tableWrapper.dataset.table;
if (tabKey) {
const patch = { rowHeights: { [rowIndex]: null } };
window.setTabViewState(tabKey, patch, {
renderTabs: false,
renderSidebar: false,
});
}
}

if (tableWrapper && typeof window.refreshVirtualTable === "function") {
window.refreshVirtualTable(tableWrapper);
}
if (table && typeof window.scheduleCellOverflowIndicators === "function") {
window.scheduleCellOverflowIndicators(table);
}
return;
}

const targetHeight = getExpandedRowHeight(row, defaultHeight);

row.style.height = `${targetHeight}px`;
row.style.minHeight = `${targetHeight}px`;

if (typeof window.updateRowMultilineForRow === "function") {
window.updateRowMultilineForRow(row, targetHeight);
}

if (tableWrapper && typeof window.setTabViewState === "function") {
const tabKey =
tableWrapper.getAttribute("data-table") || tableWrapper.dataset.table;
if (tabKey) {
const patch = { rowHeights: { [rowIndex]: targetHeight } };
window.setTabViewState(tabKey, patch, {
renderTabs: false,
renderSidebar: false,
});
}
}

if (tableWrapper && typeof window.refreshVirtualTable === "function") {
window.refreshVirtualTable(tableWrapper);
}
if (table && typeof window.scheduleCellOverflowIndicators === "function") {
window.scheduleCellOverflowIndicators(table);
}
}

function getExpandedRowHeight(row, minHeight) {
const lineHeight =
typeof window.TABLE_CELL_LINE_HEIGHT === "number"
? window.TABLE_CELL_LINE_HEIGHT
: 18;
const verticalPadding =
typeof window.TABLE_CELL_VERTICAL_PADDING === "number"
? window.TABLE_CELL_VERTICAL_PADDING
: 24;

let maxScrollHeight = 0;
row
.querySelectorAll(".cell-content:not(.cell-content-blob)")
.forEach((content) => {
if (!(content instanceof HTMLElement)) {
return;
}
maxScrollHeight = Math.max(maxScrollHeight, content.scrollHeight || 0);
});

if (!maxScrollHeight) {
return minHeight;
}

const lines = Math.max(1, Math.ceil(maxScrollHeight / lineHeight));
const height = verticalPadding + lines * lineHeight;
return Math.max(minHeight, height);
}

/**
* Copy entire row data to clipboard
*/
Expand Down Expand Up @@ -1522,6 +1721,87 @@ function showJsonViewerDialog(opts) {
copyBtn.focus();
}

/**
* Show a cell viewer dialog for long text.
* @param {{ title: string, value: string, truncated: boolean, isNull: boolean }} opts
*/
function showCellViewerDialog(opts) {
const overlay = document.createElement("div");
overlay.className = "confirm-dialog-overlay";

const dialog = document.createElement("div");
dialog.className = "confirm-dialog cell-viewer-dialog";

const titleEl = document.createElement("h3");
titleEl.className = "confirm-dialog-title";
titleEl.textContent = opts.title || "Cell Viewer";

const infoEl = document.createElement("div");
infoEl.className = "confirm-dialog-table-info";
const infoParts = [];
if (opts.isNull) {
infoParts.push("NULL value");
} else if (!opts.value) {
infoParts.push("Empty string");
} else {
infoParts.push(`${opts.value.length.toLocaleString()} characters`);
}
if (opts.truncated) {
infoParts.push("truncated in table view");
}
infoEl.textContent = infoParts.join(" • ");

const contentEl = document.createElement("pre");
contentEl.className = "confirm-dialog-row-data cell-viewer-content";
contentEl.textContent = opts.value;

const buttonsEl = document.createElement("div");
buttonsEl.className = "confirm-dialog-buttons";

const closeBtn = document.createElement("button");
closeBtn.className = "secondary-button";
closeBtn.textContent = "Close";

const copyBtn = document.createElement("button");
copyBtn.className = "primary-button";
copyBtn.textContent = "Copy";

buttonsEl.appendChild(closeBtn);
buttonsEl.appendChild(copyBtn);

dialog.appendChild(titleEl);
dialog.appendChild(infoEl);
dialog.appendChild(contentEl);
dialog.appendChild(buttonsEl);
overlay.appendChild(dialog);

const close = () => {
overlay.remove();
};

closeBtn.addEventListener("click", close);
overlay.addEventListener("click", (e) => {
if (e.target === overlay) {
close();
}
});

const handleEscape = (e) => {
if (e.key === "Escape") {
close();
document.removeEventListener("keydown", handleEscape);
}
};
document.addEventListener("keydown", handleEscape);

copyBtn.addEventListener("click", () => {
copyToClipboard(opts.value, "Cell value copied");
});

document.body.appendChild(overlay);
copyBtn.focus();
}

/**
* Get column header text
* @param {HTMLTableHeaderCellElement} header - Table header element
Expand Down
10 changes: 10 additions & 0 deletions media/css/30-components/confirm-dialog.css
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,16 @@
.json-viewer-dialog .confirm-dialog-row-data {
max-height: 60vh;
}
.cell-viewer-dialog {
max-width: 960px;
width: 95%;
max-height: 85vh;
}
.cell-viewer-dialog .cell-viewer-content {
max-height: 60vh;
overflow: auto;
white-space: pre;
}

.blob-viewer-dialog {
max-width: 980px;
Expand Down
27 changes: 25 additions & 2 deletions media/css/30-components/tables.css
Original file line number Diff line number Diff line change
Expand Up @@ -41,15 +41,22 @@
}

.data-table {
/* Allow tables to grow wider than the container so column resizing can expand horizontally. */
width: max-content;
/* Keep initial table width constrained; columns can still expand on user resize. */
width: 100%;
min-width: 100%;
border-collapse: collapse;
background: var(--vscode-editor-background);
border: 1px solid var(--vscode-panel-border);
font-family: var(--vscode-editor-font-family);
font-size: 0.9em;
table-layout: fixed;
--table-col-default-width: 220px;
--cell-line-height: 18px;
--cell-visible-lines: 3;
}
.data-table col {
width: var(--table-col-default-width);
max-width: var(--table-col-default-width);
}
.data-table th,
.data-table td {
Expand Down Expand Up @@ -93,6 +100,22 @@
white-space: nowrap;
position: relative;
}
.data-table tr.row-multiline td {
white-space: normal;
vertical-align: top;
}
.data-table .cell-content:not(.cell-content-blob) {
display: block;
width: 100%;
max-width: 100%;
line-height: var(--cell-line-height, 18px);
max-height: calc(var(--cell-visible-lines, 3) * var(--cell-line-height, 18px));
overflow-x: hidden;
overflow-y: scroll;
white-space: pre-wrap;
word-break: break-word;
scrollbar-width: thin;
}
.data-table td em {
color: var(--vscode-descriptionForeground);
font-style: italic;
Expand Down
Loading
Loading