From 466849373cc8e0c8054f3d5277b0c166ac241748 Mon Sep 17 00:00:00 2001 From: Kun Ren Date: Fri, 25 Sep 2026 01:46:19 +0800 Subject: [PATCH 1/5] chore(deps): upgrade AG Grid Community to 36.2.0 --- package-lock.json | 286 +++++++++++++++++++++++++++++++++++++++------- package.json | 5 +- 2 files changed, 246 insertions(+), 45 deletions(-) diff --git a/package-lock.json b/package-lock.json index d4df3f5eb..498d2448b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "3.0.0-rc.0", "license": "SEE LICENSE IN LICENSE", "dependencies": { - "ag-grid-community": "^35.2.1", + "ag-grid-community": "^36.2.0", "cheerio": "1.0.0-rc.12", "crypto": "^1.0.1", "ejs": "^3.1.10", @@ -44,9 +44,10 @@ "eslint": "^7.32.0", "eslint-plugin-jsdoc": "^35.5.1", "git-cliff": "^2.12.0", + "glob": "^13.0.6", "mocha": "^11.7.5", "sinon": "^15.2.0", - "typescript": "^4.9.5" + "typescript": "~5.8.3" }, "engines": { "vscode": "^1.110.0" @@ -1460,6 +1461,52 @@ "dev": true, "license": "MIT" }, + "node_modules/@vscode/test-cli/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@vscode/test-cli/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@vscode/test-cli/node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/@vscode/test-electron": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@vscode/test-electron/-/test-electron-3.1.0.tgz", @@ -1500,20 +1547,27 @@ } }, "node_modules/ag-charts-types": { - "version": "13.2.1", - "resolved": "https://registry.npmjs.org/ag-charts-types/-/ag-charts-types-13.2.1.tgz", - "integrity": "sha512-r7veb3QqJtIKlXmeUsLR4/oDPwmHxFI2tmbZra/203mdaz3uwQUrrgYNg628nrK+7L2YxXnwGc6L05tWjLLjNQ==", + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/ag-charts-types/-/ag-charts-types-14.2.0.tgz", + "integrity": "sha512-4pDV6/4zbzPN7t607Gr1o6DO9RhmPafQGr+hIpTJBaerbaMoC+WOO3v/wkxFbvCNrHKOexEKSfNTH7PaAR4UzA==", "license": "MIT" }, "node_modules/ag-grid-community": { - "version": "35.2.1", - "resolved": "https://registry.npmjs.org/ag-grid-community/-/ag-grid-community-35.2.1.tgz", - "integrity": "sha512-ycmGI+1EbUT7i3eg/Kgi1owwnkdHXRufo10Xm6cfSsVPM3TMpvlbLgi28KIPt9DGHZWHq9fOBn7nxMNdv1Yaow==", + "version": "36.2.0", + "resolved": "https://registry.npmjs.org/ag-grid-community/-/ag-grid-community-36.2.0.tgz", + "integrity": "sha512-tK0Toj7t0fV/OMLPnDc0xlYKAqDbnCT3iJodYSn2TJ541caU8jPdYwWhMdW+AP2+pUNwFiJVgc04eJNfvBPsWw==", "license": "MIT", "dependencies": { - "ag-charts-types": "13.2.1" + "ag-charts-types": "14.2.0", + "ag-stack": "36.2.0" } }, + "node_modules/ag-stack": { + "version": "36.2.0", + "resolved": "https://registry.npmjs.org/ag-stack/-/ag-stack-36.2.0.tgz", + "integrity": "sha512-f3XT2htFs9dUPcHni2z9ak69QNta5ODEpooHuuNvbjN98JkokpEqRQ9YLQHQJFdCcgi9Ybs/j3zkkvmNRamIPg==", + "license": "MIT" + }, "node_modules/agent-base": { "version": "7.1.4", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", @@ -3190,22 +3244,18 @@ ] }, "node_modules/glob": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", - "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" }, - "bin": { - "glob": "dist/esm/bin.mjs" + "engines": { + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -3224,6 +3274,45 @@ "node": ">= 6" } }, + "node_modules/glob/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/globals": { "version": "13.24.0", "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", @@ -3929,11 +4018,14 @@ } }, "node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "version": "11.5.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.3.tgz", + "integrity": "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==", "dev": true, - "license": "ISC" + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } }, "node_modules/make-dir": { "version": "4.0.0", @@ -4100,6 +4192,28 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/mocha/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/mocha/node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -4110,6 +4224,30 @@ "node": ">=8" } }, + "node_modules/mocha/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/mocha/node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/mocha/node_modules/readdirp": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", @@ -4562,17 +4700,17 @@ } }, "node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" }, "engines": { - "node": ">=16 || 14 >=14.18" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -5341,19 +5479,51 @@ "node": "18 || 20 || >=22" } }, - "node_modules/test-exclude/node_modules/brace-expansion": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", - "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "node_modules/test-exclude/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "dev": true, - "license": "MIT", + "license": "ISC", "dependencies": { - "balanced-match": "^4.0.2" + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/test-exclude/node_modules/glob/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" }, "engines": { - "node": "18 || 20 || >=22" + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/test-exclude/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, "node_modules/test-exclude/node_modules/minimatch": { "version": "10.2.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", @@ -5370,6 +5540,36 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/test-exclude/node_modules/minimatch/node_modules/brace-expansion": { + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/test-exclude/node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -5456,9 +5656,9 @@ } }, "node_modules/typescript": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", - "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "dev": true, "license": "Apache-2.0", "bin": { @@ -5466,7 +5666,7 @@ "tsserver": "bin/tsserver" }, "engines": { - "node": ">=4.2.0" + "node": ">=14.17" } }, "node_modules/undici-types": { diff --git a/package.json b/package.json index 3110cd3ab..ac7f39930 100644 --- a/package.json +++ b/package.json @@ -2034,12 +2034,13 @@ "eslint": "^7.32.0", "eslint-plugin-jsdoc": "^35.5.1", "git-cliff": "^2.12.0", + "glob": "^13.0.6", "mocha": "^11.7.5", "sinon": "^15.2.0", - "typescript": "^4.9.5" + "typescript": "~5.8.3" }, "dependencies": { - "ag-grid-community": "^35.2.1", + "ag-grid-community": "^36.2.0", "cheerio": "1.0.0-rc.12", "crypto": "^1.0.1", "ejs": "^3.1.10", From 3d02e8e43c91b54cc738e3141f8e39264085b0f6 Mon Sep 17 00:00:00 2001 From: Kun Ren Date: Fri, 25 Sep 2026 01:46:53 +0800 Subject: [PATCH 2/5] feat(dataview): add viewer controls and persistent fit-width sizing --- CHANGELOG.md | 3 + README.md | 9 ++ src/dataViewer.ts | 249 ++++++++++++++++++++++++++++++ src/dataViewerColumnPanel.ts | 133 +++++++--------- src/session.ts | 208 ++++++------------------- src/test/suite/dataViewer.test.ts | 235 ++++++++++++++++++++++++++++ 6 files changed, 599 insertions(+), 238 deletions(-) create mode 100644 src/dataViewer.ts create mode 100644 src/test/suite/dataViewer.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 53d51f801..3fbab2b5e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,12 +4,15 @@ ### Bug Fixes +* fix(dataview): fit columns to the viewer width by default and preserve fit-width sizing when scrolling, resizing, or refreshing the viewer +* fix(dataview): use supported grid and row-selection APIs in file-backed tables, initialize the grid after the document is ready, and preserve date and integer64 handling in both viewer modes * fix(rstudioapi): resolve emulation issues and viewer routing * fix(liveshare): resolve activation errors, file reading bugs, and add hooks for sess compatibility * fix(workspace): fix code submission delays when the workspace contains many or large objects ### Features +* feat(dataview): upgrade AG Grid Community to 36.2.0, add searchable column pinning, inline filters and clear-all controls, row counts, saved view settings, full-value tooltips, and column sizing controls * feat(sess): migrate session watcher to WebSockets/JSON-RPC 2.0 * feat: implement rstudioapi::showPrompt() and rstudioapi::askForPassword() for sess package * feat: evaluate params from YAML header in Rmd files before running code diff --git a/README.md b/README.md index 6053ecf64..a56cf776e 100644 --- a/README.md +++ b/README.md @@ -87,6 +87,15 @@ Please consult the relevant installation wiki pages for your OS ([Windows](https * Full support of [Remote Development](https://code.visualstudio.com/docs/remote/remote-overview) via [SSH](https://code.visualstudio.com/docs/remote/ssh), [Containers](https://code.visualstudio.com/docs/remote/containers) and [WSL](https://code.visualstudio.com/docs/remote/wsl). +## Exploring data + +Call `View(x)` to open a table. The viewer loads rows on demand and applies column filters and sorting in R across the full table. + +* **Columns** searches column names, shows or hides matching columns, and pins columns to either edge. Drag entries to reorder them. Row names stay pinned on the left. +* **Filters** shows filter inputs below the headers. Press Enter to apply text and number filters; use the filter button for ranges, missing values, or combined conditions. **Clear filters** removes every column filter, including filters on hidden columns. The toolbar shows the matching and total row counts. +* Columns fit the available width by default and adapt when the viewer is resized. **Size to content** sizes the currently displayed cells, up to 480 pixels, and keeps those widths; **Fit width** switches back to filling the viewer. Manually resized columns retain their widths while the remaining flexible columns share the space. Hover over a cell to see its full value. +* Repeated `View()` calls for the same object retain the viewer's sizing mode, column layout, sorting, filters, and page size when the column names, order, and types are unchanged. Refreshed data starts on the first page. **Reset view** restores fit-width sizing, the default layout, filters, sorting, and configured page size. + ## Questions, issues, feature requests, and contributions * If you have a question about accomplishing something in general with the extension, please [ask on Stack Overflow](https://stackoverflow.com/questions/tagged/visual-studio-code+r). diff --git a/src/dataViewer.ts b/src/dataViewer.ts new file mode 100644 index 000000000..2c55da697 --- /dev/null +++ b/src/dataViewer.ts @@ -0,0 +1,249 @@ +'use strict'; + +export function getDataViewerStyle(): string { + return ` + body { + display: flex; flex-direction: column; overflow: hidden; + font: var(--vscode-font-size, 13px) var(--vscode-font-family, sans-serif); + } + #gridContainer { position: relative; flex: 1; min-height: 0; } + #viewerToolbar { + display: flex; align-items: center; flex-wrap: wrap; gap: 6px; + padding: 6px 8px; border-bottom: 1px solid var(--vscode-panel-border); + background: var(--vscode-editor-background); color: var(--vscode-foreground); + font: var(--vscode-font-size, 13px) var(--vscode-font-family, sans-serif); + } + #viewerToolbar button { + padding: 4px 8px; border: 1px solid transparent; border-radius: 3px; + background: var(--vscode-button-secondaryBackground); color: var(--vscode-button-secondaryForeground); + font: inherit; cursor: pointer; + } + #viewerToolbar button:hover { background: var(--vscode-button-secondaryHoverBackground); } + #viewerToolbar button[aria-pressed="true"] { border-color: var(--vscode-focusBorder); } + #viewerToolbar button:disabled { opacity: 0.5; cursor: default; } + #viewerToolbar button:focus-visible, #columnPanel :focus-visible { + outline: 1px solid var(--vscode-focusBorder); outline-offset: 2px; + } + #viewerRowCount { margin-left: auto; font-size: 12px; color: var(--vscode-descriptionForeground); } + .dataview-na { color: var(--vscode-descriptionForeground); font-style: italic; opacity: 0.75; } + `; +} + +export function getDataViewerToolbarHtml(): string { + return ` +
+ + + + + + + +
+ `; +} + +// Shared by the on-demand and file-backed viewers. Kept independent of VS Code so +// column conversion and state restoration can also be exercised outside a webview. +export function getDataViewerScript(): string { + return ` + let viewerSchema; + let viewerFloatingFilters = false; + let viewerInitialState; + let viewerDefaultPageSize; + let viewerSizingMode = 'fit'; + const emptyCellRenderer = () => ''; + const naCellRenderer = () => { + const element = document.createElement('span'); + element.className = 'dataview-na'; + element.textContent = 'NA'; + return element; + }; + + function getAgTheme() { + const light = document.body.classList.contains('vscode-light') || + document.body.classList.contains('vscode-high-contrast-light'); + return window.agGrid.themeBalham.withPart(light + ? window.agGrid.colorSchemeLight : window.agGrid.colorSchemeDark); + } + + function prepareViewerColumns(columns) { + // R uses positional field IDs. Never restore a filter onto a renamed, + // reordered or differently typed column after a repeated View() call. + viewerSchema = JSON.stringify(columns.map(column => [ + column.field, column.headerName, column.type, column.cellDataType, + column.filter, column.sortable, column.headerTooltip + ])); + const saved = vscode.getState?.(); + viewerInitialState = saved?.schema === viewerSchema ? saved.gridState : undefined; + viewerFloatingFilters = saved?.schema === viewerSchema && saved.floatingFilters === true; + viewerSizingMode = saved?.schema === viewerSchema && saved.sizingMode === 'content' ? 'content' : 'fit'; + if (viewerInitialState && !saved.sizingMode) { + // Older views saved fixed content widths. Let the new default flex + // sizing take over without losing their filters or column layout. + viewerInitialState = { ...viewerInitialState, partialColumnState: true }; + delete viewerInitialState.columnSizing; + } + const bigintFields = []; + for (const column of columns) { + column.cellRendererSelector = params => { + if (params.data == null) { + return { component: emptyCellRenderer }; + } + return params.value == null ? { component: naCellRenderer } : undefined; + }; + if (column.type === 'dateColumn' || column.type === 'datetimeColumn') { + column.cellDataType = column.type === 'dateColumn' ? 'dateString' : 'dateTimeString'; + column.filter = 'agDateColumnFilter'; + column.filterParams = { browserDatePicker: true }; + column.initialWidth = 200; + } else if (column.type === 'bigintColumn') { + column.cellDataType = 'bigint'; + column.filter = 'agBigIntColumnFilter'; + bigintFields.push(column.field); + } else if (column.type === 'numericColumn') { + column.cellDataType = 'number'; + } else if (column.type === 'booleanColumn') { + column.cellDataType = 'boolean'; + column.filterParams = { buttons: ['reset'] }; + } + if (column.field === '0') { + column.pinned = 'left'; + column.lockPinned = true; + column.lockPosition = 'left'; + column.lockVisible = true; + column.floatingFilter = false; + column.suppressSizeToFit = true; + column.flex = 0; + } + if (column.type !== 'numericColumn') { + delete column.type; + } + } + return bigintFields; + } + + function prepareViewerRows(rows, bigintFields) { + for (const row of rows) { + for (const field of bigintFields) { + if (row[field] != null) { + row[field] = BigInt(row[field]); + } + } + } + return rows; + } + + function saveViewerState() { + if (!gridApi || !viewerSchema) { + return; + } + const state = gridApi.getState(); + // A refreshed object can have fewer rows. Preserve page size, but start + // at the first page instead of restoring an invalid page/scroll offset. + const gridState = { + version: state.version, + columnOrder: state.columnOrder, + columnSizing: state.columnSizing, + columnPinning: state.columnPinning, + columnVisibility: state.columnVisibility, + sort: state.sort, + filter: state.filter, + pagination: { pageSize: state.pagination?.pageSize } + }; + vscode.setState?.({ + schema: viewerSchema, gridState, floatingFilters: viewerFloatingFilters, + sizingMode: viewerSizingMode + }); + } + + function getViewerGridOptions(pageSize) { + viewerDefaultPageSize = pageSize > 0 ? pageSize : 500; + const restoredPageSize = viewerInitialState?.pagination?.pageSize; + return { + theme: getAgTheme(), + initialState: viewerInitialState, + defaultColDef: { + sortable: true, resizable: true, filter: true, cellDataType: false, + floatingFilter: viewerFloatingFilters, + initialWidth: 100, minWidth: 50, + initialFlex: viewerSizingMode === 'fit' ? 1 : 0, + filterParams: { buttons: ['reset', 'apply'] }, + // v36.2's tooltip API displays the complete formatted cell value. + tooltip: true + }, + // Flex owns fit-width sizing. Content sizing is an explicit toolbar + // action so scrolling or resizing cannot replace the fitted widths. + pagination: pageSize > 0, + paginationPageSize: viewerDefaultPageSize, + paginationPageSizeSelector: [...new Set([ + 20, 50, 100, viewerDefaultPageSize, restoredPageSize + ].filter(size => Number.isInteger(size) && size > 0))].sort((a, b) => a - b), + enableCellTextSelection: true, + ensureDomOrder: true, + tooltipShowDelay: 300, + onStateUpdated: saveViewerState + }; + } + + function updateViewerRowCount(filtered, total) { + const format = value => new Intl.NumberFormat().format(value); + document.querySelector('#viewerRowCount').textContent = filtered === total + ? format(total) + ' rows' : format(filtered) + ' of ' + format(total) + ' rows'; + } + + function updateViewerSizingButtons() { + document.querySelector('#viewerFit').setAttribute('aria-pressed', String(viewerSizingMode === 'fit')); + document.querySelector('#viewerAutoSize').setAttribute('aria-pressed', String(viewerSizingMode === 'content')); + } + + function setViewerSizingMode(mode) { + viewerSizingMode = mode; + gridApi.applyColumnState({ + state: gridApi.getColumns().map(column => ({ + colId: column.getColId(), + flex: mode === 'fit' && column.getColId() !== '0' ? 1 : null + })) + }); + if (mode === 'content') { + gridApi.autoSizeAllColumns({ defaultMaxWidth: 480 }); + } + updateViewerSizingButtons(); + saveViewerState(); + } + + function initializeViewerToolbar() { + const filters = document.querySelector('#viewerFilters'); + const clear = document.querySelector('#viewerClearFilters'); + const updateFilters = () => { + filters.setAttribute('aria-pressed', String(viewerFloatingFilters)); + clear.disabled = Object.keys(gridApi.getFilterModel()).length === 0; + }; + const setFloatingFilters = visible => { + viewerFloatingFilters = visible; + gridApi.setGridOption('defaultColDef', { + ...gridApi.getGridOption('defaultColDef'), floatingFilter: visible + }); + updateFilters(); + saveViewerState(); + }; + filters.addEventListener('click', () => setFloatingFilters(!viewerFloatingFilters)); + clear.addEventListener('click', () => gridApi.setFilterModel(null)); + document.querySelector('#viewerAutoSize').addEventListener('click', () => setViewerSizingMode('content')); + document.querySelector('#viewerFit').addEventListener('click', () => setViewerSizingMode('fit')); + document.querySelector('#viewerReset').addEventListener('click', () => { + gridApi.setFilterModel(null); + gridApi.resetColumnState(); + gridApi.setGridOption('paginationPageSize', viewerDefaultPageSize); + gridApi.paginationGoToFirstPage(); + setFloatingFilters(false); + setViewerSizingMode('fit'); + }); + gridApi.addEventListener('filterChanged', updateFilters); + window.addEventListener('pagehide', saveViewerState); + updateFilters(); + updateViewerSizingButtons(); + initializeColumnPanel(); + } + `; +} diff --git a/src/dataViewerColumnPanel.ts b/src/dataViewerColumnPanel.ts index 898d9f46b..41e50f967 100644 --- a/src/dataViewerColumnPanel.ts +++ b/src/dataViewerColumnPanel.ts @@ -2,76 +2,13 @@ export function getDataViewerColumnPanelStyle(): string { return ` - #myGrid { - --column-panel-toggle-space: 34px; - } - - /* Reserve menu space in the last header without adding an empty data column. */ - #myGrid .ag-header-cell.ag-column-last { - padding-right: calc(var(--ag-cell-horizontal-padding) + var(--column-panel-toggle-space)); - } - - #myGrid .ag-header-cell.ag-column-last .ag-header-cell-resize { - right: calc(var(--column-panel-toggle-space) - 3px); - } - - #columnPanelToggle { - flex: 0 0 calc(var(--column-panel-toggle-space) - 6px); - align-self: center; - width: calc(var(--column-panel-toggle-space) - 6px); - height: 28px; - margin: 0 3px; - padding: 0; - border: 0; - background: transparent; - color: var(--vscode-foreground); - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - } - - #columnPanelToggle:hover { - background-color: var(--vscode-toolbar-hoverBackground); - } - - #columnPanelToggle[hidden] { - display: none; - } - - #columnPanelToggle span, - #columnPanelToggle span::before, - #columnPanelToggle span::after { - display: block; - width: 14px; - height: 1.5px; - background-color: currentColor; - content: ''; - } - - #columnPanelToggle span { - position: relative; - } - - #columnPanelToggle span::before { - position: absolute; - top: -5px; - left: 0; - } - - #columnPanelToggle span::after { - position: absolute; - top: 5px; - left: 0; - } - #columnPanel { position: absolute; top: 0; right: 0; z-index: 30; display: none; - width: min(320px, 42vw); + width: min(380px, 100%); height: 100%; box-sizing: border-box; border-left: 1px solid var(--vscode-panel-border); @@ -103,6 +40,19 @@ export function getDataViewerColumnPanelStyle(): string { line-height: 1; } + #columnPanelSearch { + margin: 8px 12px; padding: 5px; box-sizing: border-box; min-width: 0; + color: var(--vscode-input-foreground); background: var(--vscode-input-background); + border: 1px solid var(--vscode-input-border, transparent); + } + + .column-panel-item[hidden] { display: none; } + .column-panel-pin { + margin-left: auto; max-width: 85px; padding: 2px; + color: var(--vscode-dropdown-foreground); background: var(--vscode-dropdown-background); + border: 1px solid var(--vscode-dropdown-border); + } + #columnPanelActions { display: flex; gap: 8px; @@ -168,12 +118,12 @@ export function getDataViewerColumnPanelStyle(): string { export function getDataViewerColumnPanelHtml(): string { return ` -
Columns
+
@@ -192,31 +142,38 @@ export function getDataViewerColumnPanelScript(): string { const selectAll = document.querySelector('#columnSelectAll'); const deselectAll = document.querySelector('#columnDeselectAll'); const list = document.querySelector('#columnPanelList'); - const header = document.querySelector('#myGrid .ag-header'); - if (!gridApi || !panel || !toggle || !close || !selectAll || !deselectAll || !list || !header) { + const search = document.querySelector('#columnPanelSearch'); + if (!gridApi || !panel || !toggle || !close || !selectAll || !deselectAll || !list || !search) { return; } - // Reserve space within the header's flex layout, without narrowing the rows. - header.append(toggle); - toggle.hidden = false; - function getSelectableColumns() { return gridApi.getAllGridColumns().filter(column => column.getColId() !== '0'); } + function matchingColumns() { + const query = search.value.toLocaleLowerCase(); + return getSelectableColumns().filter(column => + String(column.getColDef().headerName ?? column.getColId()).toLocaleLowerCase().includes(query)); + } + function updateVisibility() { if (!panel.classList.contains('visible')) { return; } + const matches = new Set(matchingColumns().map(column => column.getColId())); let visibleCount = 0; for (const item of list.children) { - const visible = gridApi.getColumn(item.dataset.colId).isVisible(); - item.querySelector('input').checked = visible; - visibleCount += visible ? 1 : 0; + const column = gridApi.getColumn(item.dataset.colId); + item.hidden = !matches.has(item.dataset.colId); + item.querySelector('input').checked = column.isVisible(); + item.querySelector('select').value = column.getPinned() || ''; + visibleCount += !item.hidden && column.isVisible() ? 1 : 0; } - selectAll.disabled = visibleCount === list.children.length; + selectAll.disabled = visibleCount === matches.size; deselectAll.disabled = visibleCount === 0; + selectAll.textContent = search.value ? 'Show matching' : 'Select all'; + deselectAll.textContent = search.value ? 'Hide matching' : 'Deselect all'; } function renderColumns() { @@ -240,7 +197,16 @@ export function getDataViewerColumnPanelScript(): string { label.textContent = name; label.title = name; - item.append(checkbox, label); + const pin = document.createElement('select'); + pin.className = 'column-panel-pin'; + pin.setAttribute('aria-label', 'Pin ' + name); + for (const [value, text] of [['', 'Unpinned'], ['left', 'Pin left'], ['right', 'Pin right']]) { + const option = document.createElement('option'); + option.value = value; + option.textContent = text; + pin.append(option); + } + item.append(checkbox, label, pin); list.append(item); } @@ -249,10 +215,15 @@ export function getDataViewerColumnPanelScript(): string { } function setAllVisible(visible) { - gridApi.setColumnsVisible(getSelectableColumns().map(column => column.getColId()), visible); + gridApi.setColumnsVisible(matchingColumns().map(column => column.getColId()), visible); } gridApi.addEventListener('columnVisible', updateVisibility); + gridApi.addEventListener('columnPinned', updateVisibility); + gridApi.addEventListener('columnsReset', () => { + if (panel.classList.contains('visible')) { renderColumns(); } + }); + search.addEventListener('input', updateVisibility); gridApi.addEventListener('columnMoved', event => { if (event.finished !== false && panel.classList.contains('visible')) { renderColumns(); @@ -264,6 +235,7 @@ export function getDataViewerColumnPanelScript(): string { toggle.setAttribute('aria-expanded', String(visible)); if (visible) { renderColumns(); + search.focus(); } }); close.addEventListener('click', () => { @@ -271,12 +243,17 @@ export function getDataViewerColumnPanelScript(): string { toggle.setAttribute('aria-expanded', 'false'); toggle.focus(); }); + panel.addEventListener('keydown', event => { + if (event.key === 'Escape') { close.click(); } + }); selectAll.addEventListener('click', () => setAllVisible(true)); deselectAll.addEventListener('click', () => setAllVisible(false)); list.addEventListener('change', event => { const checkbox = event.target; if (checkbox.matches('input[type="checkbox"]')) { gridApi.setColumnsVisible([checkbox.closest('.column-panel-item').dataset.colId], checkbox.checked); + } else if (checkbox.matches('select')) { + gridApi.setColumnsPinned([checkbox.closest('.column-panel-item').dataset.colId], checkbox.value || null); } }); @@ -292,7 +269,7 @@ export function getDataViewerColumnPanelScript(): string { list.addEventListener('dragstart', event => { const item = event.target.closest('.column-panel-item'); - if (!item) { + if (!item || event.target.closest('input, select')) { return; } draggedItem = item; diff --git a/src/session.ts b/src/session.ts index 4a64cbbad..a4545cc32 100644 --- a/src/session.ts +++ b/src/session.ts @@ -17,6 +17,7 @@ import { extensionContext, homeExtDir, rWorkspace, globalRHelp, globalPlotManage import { resolveBackend, CommonPlotManager } from './plotViewer'; import { showWebView } from './webViewer'; +import { getDataViewerScript, getDataViewerStyle, getDataViewerToolbarHtml } from './dataViewer'; import { getDataViewerColumnPanelHtml, getDataViewerColumnPanelScript, getDataViewerColumnPanelStyle } from './dataViewerColumnPanel'; export interface SessionInfo { @@ -855,7 +856,6 @@ export async function getTableHtml(webview: Webview, file: string | undefined, t #gridContainer { position: relative; - height: 100%; } #fetchStatus { @@ -933,6 +933,7 @@ export async function getTableHtml(webview: Webview, file: string | undefined, t opacity: 0.75; } ${getDataViewerColumnPanelStyle()} + ${getDataViewerStyle()} + ${getDataViewerToolbarHtml()}
${getDataViewerColumnPanelHtml()} @@ -1463,76 +1387,36 @@ export async function getTableHtml(webview: Webview, file: string | undefined, t [class*="vscode"] .text-right { text-align: right; } + ${getDataViewerStyle()} + ${getDataViewerColumnPanelStyle()} -
+ ${getDataViewerToolbarHtml()} +
+
+ ${getDataViewerColumnPanelHtml()} +
`; diff --git a/src/test/suite/dataViewer.test.ts b/src/test/suite/dataViewer.test.ts new file mode 100644 index 000000000..c15e8f868 --- /dev/null +++ b/src/test/suite/dataViewer.test.ts @@ -0,0 +1,235 @@ +import * as assert from 'assert'; +import * as vm from 'vm'; +import type { ColDef, ColumnState, GridOptions, GridState } from 'ag-grid-community'; +import { getDataViewerScript } from '../../dataViewer'; + +interface SavedViewer { + schema: string; + gridState: GridState; + floatingFilters: boolean; + sizingMode?: 'fit' | 'content'; +} + +interface ViewerScript { + prepareViewerColumns(columns: ColDef[]): string[]; + prepareViewerRows(rows: Record[], fields: string[]): Record[]; + getViewerGridOptions(pageSize: number): GridOptions; + saveViewerState(): void; + getAgTheme(): unknown; + setViewerSizingMode(mode: 'fit' | 'content'): void; +} + +function viewerScript(saved?: SavedViewer, gridState: GridState = {}, light = false): { + script: ViewerScript; + stored: () => SavedViewer | undefined; + appliedColumns: () => ColumnState[]; + contentSizingCalls: () => number; +} { + let stored: SavedViewer | undefined; + let appliedColumns: ColumnState[] = []; + let contentSizingCalls = 0; + const script = vm.runInNewContext(`${getDataViewerScript()} + ({ prepareViewerColumns, prepareViewerRows, getViewerGridOptions, saveViewerState, getAgTheme, setViewerSizingMode })`, { + vscode: { + getState: () => saved, + setState: (value: SavedViewer) => { stored = value; }, + }, + gridApi: { + getState: () => gridState, + getColumns: () => ['0', '1', '2'].map(colId => ({ getColId: () => colId })), + applyColumnState: ({ state }: { state: ColumnState[] }) => { appliedColumns = state; }, + autoSizeAllColumns: () => { contentSizingCalls++; }, + }, + document: { + body: { classList: { contains: (name: string) => light && name === 'vscode-high-contrast-light' } }, + createElement: () => ({ className: '', textContent: '' }), + querySelector: () => ({ setAttribute: () => undefined }), + }, + window: { + agGrid: { + themeBalham: { withPart: (scheme: unknown) => scheme }, + colorSchemeLight: 'light', colorSchemeDark: 'dark', + }, + }, + }) as ViewerScript; + return { + script, stored: () => stored, + appliedColumns: () => appliedColumns, contentSizingCalls: () => contentSizingCalls, + }; +} + +const columns = (): ColDef[] => [ + { field: '0', headerName: ' ', type: 'numericColumn', filter: false, sortable: false }, + { field: '1', headerName: 'Name', type: 'textColumn', filter: 'agTextColumnFilter' }, + { field: '2', headerName: 'Value', type: 'numericColumn', filter: 'agNumberColumnFilter' }, +]; + +suite('Data viewer', () => { + test('defaults to flexible widths and prevents content sizing on scroll and resize', () => { + const viewer = viewerScript(); + viewer.script.prepareViewerColumns(columns()); + const options = viewer.script.getViewerGridOptions(500); + assert.strictEqual(options.defaultColDef?.initialFlex, 1); + assert.strictEqual(options.autoSizeStrategy, undefined); + assert.strictEqual(viewer.contentSizingCalls(), 0); + }); + + test('content sizing runs only on request and switching back to fit restores flex', () => { + const viewer = viewerScript(); + viewer.script.prepareViewerColumns(columns()); + viewer.script.getViewerGridOptions(500); + viewer.script.setViewerSizingMode('content'); + assert.ok(viewer.appliedColumns().every(column => column.flex === null)); + assert.strictEqual(viewer.contentSizingCalls(), 1); + assert.strictEqual(viewer.stored()?.sizingMode, 'content'); + + viewer.script.setViewerSizingMode('fit'); + assert.strictEqual(viewer.appliedColumns()[0].flex, null); + assert.ok(viewer.appliedColumns().slice(1).every(column => column.flex === 1)); + assert.strictEqual(viewer.contentSizingCalls(), 1); + assert.strictEqual(viewer.stored()?.sizingMode, 'fit'); + }); + + test('old saved content widths do not disable the new default fit mode', () => { + const first = viewerScript(undefined, { + columnSizing: { columnSizingModel: [{ colId: '1', width: 70 }, { colId: '2', width: 80 }] }, + columnVisibility: { hiddenColIds: ['2'] }, + }); + first.script.prepareViewerColumns(columns()); + first.script.saveViewerState(); + const saved = first.stored(); + assert.ok(saved); + delete saved.sizingMode; + const next = viewerScript(saved).script; + next.prepareViewerColumns(columns()); + const options = next.getViewerGridOptions(500); + assert.strictEqual(options.defaultColDef?.initialFlex, 1); + assert.strictEqual(options.initialState?.columnSizing, undefined); + assert.strictEqual(options.initialState?.partialColumnState, true); + assert.strictEqual(options.initialState?.columnVisibility, saved.gridState.columnVisibility); + assert.ok(saved.gridState.columnSizing, 'restoring must not mutate the saved view'); + }); + + test('restores the chosen sizing mode and its column widths or flex values', () => { + for (const mode of ['fit', 'content'] as const) { + const first = viewerScript(undefined, { + columnSizing: { columnSizingModel: [{ colId: '1', flex: mode === 'fit' ? 1 : undefined, width: 120 }] }, + }); + first.script.prepareViewerColumns(columns()); + first.script.setViewerSizingMode(mode); + const saved = first.stored(); + const next = viewerScript(saved).script; + next.prepareViewerColumns(columns()); + const options = next.getViewerGridOptions(500); + assert.strictEqual(options.defaultColDef?.initialFlex, mode === 'fit' ? 1 : 0); + assert.strictEqual(options.initialState?.columnSizing, saved?.gridState.columnSizing); + } + }); + + test('restores sort, filters and layout only for the same R schema', () => { + const state: GridState = { + columnVisibility: { hiddenColIds: ['2'] }, + sort: { sortModel: [{ colId: '2', sort: 'desc' }] }, + filter: { filterModel: { '1': { filterType: 'text', type: 'contains', filter: 'apple' } } }, + }; + const first = viewerScript(undefined, state); + first.script.prepareViewerColumns(columns()); + first.script.saveViewerState(); + const saved = first.stored(); + assert.ok(saved); + saved.floatingFilters = true; + + const next = viewerScript(saved).script; + next.prepareViewerColumns(columns()); + const options = next.getViewerGridOptions(500); + assert.strictEqual(options.initialState?.filter, state.filter); + assert.strictEqual(options.initialState?.sort, state.sort); + assert.strictEqual(options.initialState?.columnVisibility, state.columnVisibility); + assert.strictEqual(options.defaultColDef?.floatingFilter, true); + + const changes: ColDef[][] = [ + columns().map(column => column.field === '1' ? { ...column, headerName: 'Another variable' } : column), + columns().map(column => column.field === '2' ? { ...column, type: 'textColumn' } : column), + columns().reverse(), + columns().slice(0, 2), + ]; + for (const changed of changes) { + const refreshed: ViewerScript = viewerScript(saved).script; + refreshed.prepareViewerColumns(changed); + assert.strictEqual(refreshed.getViewerGridOptions(500).initialState, undefined); + assert.strictEqual(refreshed.getViewerGridOptions(500).defaultColDef?.floatingFilter, false); + } + }); + + test('refresh keeps page size without restoring row positions or selected rows', () => { + const viewer = viewerScript(undefined, { + pagination: { page: 12, pageSize: 200 }, + scroll: { top: 5000, left: 0 }, + rowSelection: ['42'], + }); + viewer.script.prepareViewerColumns(columns()); + viewer.script.saveViewerState(); + const state = viewer.stored()?.gridState; + assert.strictEqual(state?.pagination?.pageSize, 200); + assert.strictEqual(state?.pagination?.page, undefined); + assert.strictEqual(state?.scroll, undefined); + assert.strictEqual(state?.rowSelection, undefined); + }); + + test('uses native date, datetime, boolean and bigint types in both viewer modes', () => { + const defs: ColDef[] = [ + ...columns(), + { field: '3', type: 'dateColumn' }, + { field: '4', type: 'datetimeColumn' }, + { field: '5', type: 'booleanColumn' }, + { field: '6', type: 'bigintColumn' }, + { field: '7', type: 'textColumn', filter: false, sortable: false }, + ]; + const script = viewerScript().script; + assert.deepStrictEqual(Array.from(script.prepareViewerColumns(defs)), ['6']); + assert.strictEqual(defs[2].cellDataType, 'number'); + assert.strictEqual(defs[3].cellDataType, 'dateString'); + assert.strictEqual(defs[4].cellDataType, 'dateTimeString'); + assert.strictEqual(defs[4].filter, 'agDateColumnFilter'); + assert.strictEqual(defs[5].cellDataType, 'boolean'); + assert.strictEqual(defs[6].cellDataType, 'bigint'); + assert.strictEqual(defs[6].filter, 'agBigIntColumnFilter'); + assert.strictEqual(defs[7].filter, false); + assert.strictEqual(defs[7].sortable, false); + assert.strictEqual(defs[0].pinned, 'left'); + assert.strictEqual(defs[0].lockVisible, true); + }); + + test('keeps integer64 precision and missing values when loading rows', () => { + const script = viewerScript().script; + const rows = script.prepareViewerRows([ + { '1': '9007199254740993', '2': '001' }, + { '1': '-9223372036854775807', '2': '' }, + { '1': null }, + ], ['1']); + assert.strictEqual(rows[0]['1'], 9007199254740993n); + assert.strictEqual(rows[1]['1'], -9223372036854775807n); + assert.strictEqual(rows[2]['1'], null); + assert.strictEqual(rows[0]['2'], '001'); + assert.strictEqual(rows[1]['2'], ''); + }); + + test('page-size choices are positive, sorted and unique, including restored sizes', () => { + for (const pageSize of [0, 20, 50, 75, 500]) { + const first = viewerScript(undefined, { pagination: { pageSize: 200 } }); + first.script.prepareViewerColumns(columns()); + first.script.saveViewerState(); + const script = viewerScript(first.stored()).script; + script.prepareViewerColumns(columns()); + const options = script.getViewerGridOptions(pageSize); + const expected = [...new Set([20, 50, 100, pageSize || 500, 200])].sort((a, b) => a - b); + assert.deepStrictEqual(Array.from(options.paginationPageSizeSelector as number[]), expected); + assert.strictEqual(options.pagination, pageSize > 0); + } + }); + + test('high contrast light uses the light grid theme', () => { + assert.strictEqual(viewerScript(undefined, {}, true).script.getAgTheme(), 'light'); + assert.strictEqual(viewerScript().script.getAgTheme(), 'dark'); + }); +}); From eb5ada703681090d36040efde7ae0637f0801e34 Mon Sep 17 00:00:00 2001 From: Kun Ren Date: Fri, 25 Sep 2026 12:49:12 +0800 Subject: [PATCH 3/5] fix(dataview): preserve column order when toggling filters --- src/dataViewer.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/dataViewer.ts b/src/dataViewer.ts index 2c55da697..89f4a32d3 100644 --- a/src/dataViewer.ts +++ b/src/dataViewer.ts @@ -163,6 +163,9 @@ export function getDataViewerScript(): string { return { theme: getAgTheme(), initialState: viewerInitialState, + // Updating defaultColDef for floating filters rebuilds the columns. + // Keep the user's order until they explicitly reset the view. + maintainColumnOrder: true, defaultColDef: { sortable: true, resizable: true, filter: true, cellDataType: false, floatingFilter: viewerFloatingFilters, From c7586ec52db2d27f9d8eb777aa1f7acb48b16397 Mon Sep 17 00:00:00 2001 From: Kun Ren Date: Sat, 26 Sep 2026 12:39:37 +0800 Subject: [PATCH 4/5] fix(dataview): mark saved column state as partial --- src/dataViewer.ts | 2 ++ src/test/suite/dataViewer.test.ts | 1 + 2 files changed, 3 insertions(+) diff --git a/src/dataViewer.ts b/src/dataViewer.ts index 89f4a32d3..f02576871 100644 --- a/src/dataViewer.ts +++ b/src/dataViewer.ts @@ -143,6 +143,8 @@ export function getDataViewerScript(): string { // at the first page instead of restoring an invalid page/scroll offset. const gridState = { version: state.version, + // We persist only selected column-state sections for initialState. + partialColumnState: true, columnOrder: state.columnOrder, columnSizing: state.columnSizing, columnPinning: state.columnPinning, diff --git a/src/test/suite/dataViewer.test.ts b/src/test/suite/dataViewer.test.ts index c15e8f868..9bae4fa57 100644 --- a/src/test/suite/dataViewer.test.ts +++ b/src/test/suite/dataViewer.test.ts @@ -100,6 +100,7 @@ suite('Data viewer', () => { const saved = first.stored(); assert.ok(saved); delete saved.sizingMode; + delete saved.gridState.partialColumnState; const next = viewerScript(saved).script; next.prepareViewerColumns(columns()); const options = next.getViewerGridOptions(500); From e2033d5340dffe3cc5dbe1be01ff2b8386deb7f6 Mon Sep 17 00:00:00 2001 From: Kun Ren Date: Sat, 26 Sep 2026 23:43:41 +0800 Subject: [PATCH 5/5] fix(dataview): preserve row-name widths across refreshes --- src/dataViewer.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/dataViewer.ts b/src/dataViewer.ts index f02576871..b34b1b6e5 100644 --- a/src/dataViewer.ts +++ b/src/dataViewer.ts @@ -114,7 +114,7 @@ export function getDataViewerScript(): string { column.lockVisible = true; column.floatingFilter = false; column.suppressSizeToFit = true; - column.flex = 0; + column.initialFlex = 0; } if (column.type !== 'numericColumn') { delete column.type; @@ -239,6 +239,14 @@ export function getDataViewerScript(): string { document.querySelector('#viewerReset').addEventListener('click', () => { gridApi.setFilterModel(null); gridApi.resetColumnState(); + const rowNameColumn = gridApi.getColumn('0'); + if (rowNameColumn) { + // AG Grid skips width restoration for flex: 0. Clear flex when + // restoring the fixed row-name column's initial width. + gridApi.applyColumnState({ state: [{ + colId: '0', flex: null, width: rowNameColumn.getColDef().initialWidth + }] }); + } gridApi.setGridOption('paginationPageSize', viewerDefaultPageSize); gridApi.paginationGoToFirstPage(); setFloatingFilters(false);