From b1ceade4980cb20f169ed293b909cdd19f915bb5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 7 Apr 2026 12:06:12 +0000 Subject: [PATCH 01/28] chore(deps): bump lodash from 4.17.23 to 4.18.1 Bumps [lodash](https://github.com/lodash/lodash) from 4.17.23 to 4.18.1. - [Release notes](https://github.com/lodash/lodash/releases) - [Commits](https://github.com/lodash/lodash/compare/4.17.23...4.18.1) --- updated-dependencies: - dependency-name: lodash dependency-version: 4.18.1 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 99efc83ff..c92c3c595 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15350,9 +15350,9 @@ } }, "node_modules/lodash": { - "version": "4.17.23", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", - "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", "dev": true, "license": "MIT" }, From 71bc70e7ad0a82fc3328e00b3a1109071c3a9370 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 7 Apr 2026 13:01:03 +0000 Subject: [PATCH 02/28] chore(deps-dev): bump handlebars from 4.7.8 to 4.7.9 Bumps [handlebars](https://github.com/handlebars-lang/handlebars.js) from 4.7.8 to 4.7.9. - [Release notes](https://github.com/handlebars-lang/handlebars.js/releases) - [Changelog](https://github.com/handlebars-lang/handlebars.js/blob/v4.7.9/release-notes.md) - [Commits](https://github.com/handlebars-lang/handlebars.js/compare/v4.7.8...v4.7.9) --- updated-dependencies: - dependency-name: handlebars dependency-version: 4.7.9 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index c92c3c595..512c959ee 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12592,9 +12592,9 @@ "license": "MIT" }, "node_modules/handlebars": { - "version": "4.7.8", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", - "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", + "version": "4.7.9", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz", + "integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==", "dev": true, "license": "MIT", "dependencies": { From 51f5ff31d9a1694283268663216ff7146c8df685 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 7 Apr 2026 13:12:42 +0000 Subject: [PATCH 03/28] chore(deps): bump lodash and @microsoft/api-extractor in /api Bumps [lodash](https://github.com/lodash/lodash) to 4.18.1 and updates ancestor dependency [@microsoft/api-extractor](https://github.com/microsoft/rushstack/tree/HEAD/apps/api-extractor). These dependencies need to be updated together. Updates `lodash` from 4.17.23 to 4.18.1 - [Release notes](https://github.com/lodash/lodash/releases) - [Commits](https://github.com/lodash/lodash/compare/4.17.23...4.18.1) Updates `@microsoft/api-extractor` from 7.52.8 to 7.58.1 - [Changelog](https://github.com/microsoft/rushstack/blob/main/apps/api-extractor/CHANGELOG.md) - [Commits](https://github.com/microsoft/rushstack/commits/@microsoft/api-extractor_v7.58.1/apps/api-extractor) --- updated-dependencies: - dependency-name: lodash dependency-version: 4.18.1 dependency-type: indirect - dependency-name: "@microsoft/api-extractor" dependency-version: 7.58.1 dependency-type: direct:development ... Signed-off-by: dependabot[bot] --- api/package-lock.json | 303 ++++++++++++++++++------------------------ 1 file changed, 129 insertions(+), 174 deletions(-) diff --git a/api/package-lock.json b/api/package-lock.json index 411708722..a31fe609c 100644 --- a/api/package-lock.json +++ b/api/package-lock.json @@ -28,29 +28,6 @@ "license": "MIT", "peer": true }, - "node_modules/@isaacs/balanced-match": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", - "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/@isaacs/brace-expansion": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz", - "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@isaacs/balanced-match": "^4.0.1" - }, - "engines": { - "node": "20 || >=22" - } - }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -98,54 +75,41 @@ } }, "node_modules/@microsoft/api-extractor": { - "version": "7.52.8", - "resolved": "https://registry.npmjs.org/@microsoft/api-extractor/-/api-extractor-7.52.8.tgz", - "integrity": "sha512-cszYIcjiNscDoMB1CIKZ3My61+JOhpERGlGr54i6bocvGLrcL/wo9o+RNXMBrb7XgLtKaizZWUpqRduQuHQLdg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@microsoft/api-extractor-model": "7.30.6", - "@microsoft/tsdoc": "~0.15.1", - "@microsoft/tsdoc-config": "~0.17.1", - "@rushstack/node-core-library": "5.13.1", - "@rushstack/rig-package": "0.5.3", - "@rushstack/terminal": "0.15.3", - "@rushstack/ts-command-line": "5.0.1", - "lodash": "~4.17.15", - "minimatch": "~3.0.3", + "version": "7.58.1", + "resolved": "https://registry.npmjs.org/@microsoft/api-extractor/-/api-extractor-7.58.1.tgz", + "integrity": "sha512-kF3GFME4lN22O5zbnXk2RP4y/4PDQdps0xKiYTipMYprkwCmmpsWLZt/N2Fkbil540cSLfJX0BW7LkHzgMVUYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@microsoft/api-extractor-model": "7.33.5", + "@microsoft/tsdoc": "~0.16.0", + "@microsoft/tsdoc-config": "~0.18.1", + "@rushstack/node-core-library": "5.21.0", + "@rushstack/rig-package": "0.7.2", + "@rushstack/terminal": "0.22.4", + "@rushstack/ts-command-line": "5.3.4", + "diff": "~8.0.2", + "lodash": "~4.18.1", + "minimatch": "10.2.3", "resolve": "~1.22.1", "semver": "~7.5.4", "source-map": "~0.6.1", - "typescript": "5.8.2" + "typescript": "5.9.3" }, "bin": { "api-extractor": "bin/api-extractor" } }, "node_modules/@microsoft/api-extractor-model": { - "version": "7.30.6", - "resolved": "https://registry.npmjs.org/@microsoft/api-extractor-model/-/api-extractor-model-7.30.6.tgz", - "integrity": "sha512-znmFn69wf/AIrwHya3fxX6uB5etSIn6vg4Q4RB/tb5VDDs1rqREc+AvMC/p19MUN13CZ7+V/8pkYPTj7q8tftg==", + "version": "7.33.5", + "resolved": "https://registry.npmjs.org/@microsoft/api-extractor-model/-/api-extractor-model-7.33.5.tgz", + "integrity": "sha512-Xh4dXuusndVQqVz4nEN9xOp0DyzsKxeD2FFJkSPg4arAjDSKPcy6cAc7CaeBPA7kF2wV1fuDlo2p/bNMpVr8yg==", "dev": true, "license": "MIT", "dependencies": { - "@microsoft/tsdoc": "~0.15.1", - "@microsoft/tsdoc-config": "~0.17.1", - "@rushstack/node-core-library": "5.13.1" - } - }, - "node_modules/@microsoft/api-extractor/node_modules/typescript": { - "version": "5.8.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz", - "integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" + "@microsoft/tsdoc": "~0.16.0", + "@microsoft/tsdoc-config": "~0.18.1", + "@rushstack/node-core-library": "5.21.0" } }, "node_modules/@microsoft/applicationinsights-channel-js": { @@ -238,21 +202,21 @@ } }, "node_modules/@microsoft/tsdoc": { - "version": "0.15.1", - "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.15.1.tgz", - "integrity": "sha512-4aErSrCR/On/e5G2hDP0wjooqDdauzEbIq8hIkIe5pXV0rtWJZvdCEKL0ykZxex+IxIwBp0eGeV48hQN07dXtw==", + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.16.0.tgz", + "integrity": "sha512-xgAyonlVVS+q7Vc7qLW0UrJU7rSFcETRWsqdXZtjzRU8dF+6CkozTK4V4y1LwOX7j8r/vHphjDeMeGI4tNGeGA==", "dev": true, "license": "MIT" }, "node_modules/@microsoft/tsdoc-config": { - "version": "0.17.1", - "resolved": "https://registry.npmjs.org/@microsoft/tsdoc-config/-/tsdoc-config-0.17.1.tgz", - "integrity": "sha512-UtjIFe0C6oYgTnad4q1QP4qXwLhe6tIpNTRStJ2RZEPIkqQPREAwE5spzVxsdn9UaEMUqhh0AqSx3X4nWAKXWw==", + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/@microsoft/tsdoc-config/-/tsdoc-config-0.18.1.tgz", + "integrity": "sha512-9brPoVdfN9k9g0dcWkFeA7IH9bbcttzDJlXvkf8b2OBzd5MueR1V2wkKBL0abn0otvmkHJC6aapBOTJDDeMCZg==", "dev": true, "license": "MIT", "dependencies": { - "@microsoft/tsdoc": "0.15.1", - "ajv": "~8.12.0", + "@microsoft/tsdoc": "0.16.0", + "ajv": "~8.18.0", "jju": "~1.4.0", "resolve": "~1.22.2" } @@ -306,13 +270,13 @@ "license": "MIT" }, "node_modules/@rushstack/node-core-library": { - "version": "5.13.1", - "resolved": "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-5.13.1.tgz", - "integrity": "sha512-5yXhzPFGEkVc9Fu92wsNJ9jlvdwz4RNb2bMso+/+TH0nMm1jDDDsOIf4l8GAkPxGuwPw5DH24RliWVfSPhlW/Q==", + "version": "5.21.0", + "resolved": "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-5.21.0.tgz", + "integrity": "sha512-LFzN+1lyWROit/P8Md6yxAth7lLYKn37oCKJHirEE2TQB25NDUM7bALf0ar+JAtwFfRCH+D+DGOA7DAzIi2r+g==", "dev": true, "license": "MIT", "dependencies": { - "ajv": "~8.13.0", + "ajv": "~8.18.0", "ajv-draft-04": "~1.0.0", "ajv-formats": "~3.0.1", "fs-extra": "~11.3.0", @@ -330,27 +294,25 @@ } } }, - "node_modules/@rushstack/node-core-library/node_modules/ajv": { - "version": "8.13.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.13.0.tgz", - "integrity": "sha512-PRA911Blj99jR5RMeTunVbNXMF6Lp4vZXnk5GQjcnUWUTsrXtekg/pnmFFI2u/I36Y/2bITGS30GZCXei6uNkA==", + "node_modules/@rushstack/problem-matcher": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@rushstack/problem-matcher/-/problem-matcher-0.2.1.tgz", + "integrity": "sha512-gulfhBs6n+I5b7DvjKRfhMGyUejtSgOHTclF/eONr8hcgF1APEDjhxIsfdUYYMzC3rvLwGluqLjbwCFZ8nxrog==", "dev": true, "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.4.1" + "peerDependencies": { + "@types/node": "*" }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, "node_modules/@rushstack/rig-package": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/@rushstack/rig-package/-/rig-package-0.5.3.tgz", - "integrity": "sha512-olzSSjYrvCNxUFZowevC3uz8gvKr3WTpHQ7BkpjtRpA3wK+T0ybep/SRUMfr195gBzJm5gaXw0ZMgjIyHqJUow==", + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@rushstack/rig-package/-/rig-package-0.7.2.tgz", + "integrity": "sha512-9XbFWuqMYcHUso4mnETfhGVUSaADBRj6HUAAEYk50nMPn8WRICmBuCphycQGNB3duIR6EEZX3Xj3SYc2XiP+9A==", "dev": true, "license": "MIT", "dependencies": { @@ -359,13 +321,14 @@ } }, "node_modules/@rushstack/terminal": { - "version": "0.15.3", - "resolved": "https://registry.npmjs.org/@rushstack/terminal/-/terminal-0.15.3.tgz", - "integrity": "sha512-DGJ0B2Vm69468kZCJkPj3AH5nN+nR9SPmC0rFHtzsS4lBQ7/dgOwtwVxYP7W9JPDMuRBkJ4KHmWKr036eJsj9g==", + "version": "0.22.4", + "resolved": "https://registry.npmjs.org/@rushstack/terminal/-/terminal-0.22.4.tgz", + "integrity": "sha512-fhtLjnXCc/4WleVbVl6aoc7jcWnU6yqjS1S8WoaNREG3ycu/viZ9R/9QM7Y/b4CDvcXoiDyMNIay7JMwBptM3g==", "dev": true, "license": "MIT", "dependencies": { - "@rushstack/node-core-library": "5.13.1", + "@rushstack/node-core-library": "5.21.0", + "@rushstack/problem-matcher": "0.2.1", "supports-color": "~8.1.1" }, "peerDependencies": { @@ -378,13 +341,13 @@ } }, "node_modules/@rushstack/ts-command-line": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@rushstack/ts-command-line/-/ts-command-line-5.0.1.tgz", - "integrity": "sha512-bsbUucn41UXrQK7wgM8CNM/jagBytEyJqXw/umtI8d68vFm1Jwxh1OtLrlW7uGZgjCWiiPH6ooUNa1aVsuVr3Q==", + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/@rushstack/ts-command-line/-/ts-command-line-5.3.4.tgz", + "integrity": "sha512-MLkVKVEN6/2clKTrjN2B2KqKCuPxRwnNsWY7a+FCAq2EMdkj10cM8YgiBSMeGFfzM0mDMzargpHNnNzaBi9Whg==", "dev": true, "license": "MIT", "dependencies": { - "@rushstack/terminal": "0.15.3", + "@rushstack/terminal": "0.22.4", "@types/argparse": "1.0.38", "argparse": "~1.0.9", "string-argv": "~0.3.1" @@ -444,16 +407,16 @@ } }, "node_modules/ajv": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", - "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "dev": true, "license": "MIT", "dependencies": { - "fast-deep-equal": "^3.1.1", + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" + "require-from-string": "^2.0.2" }, "funding": { "type": "github", @@ -530,21 +493,26 @@ } }, "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "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" + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } }, "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", "dev": true, "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" } }, "node_modules/color-convert": { @@ -574,13 +542,6 @@ "dev": true, "license": "MIT" }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, - "license": "MIT" - }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -613,6 +574,16 @@ "node": ">=0.10.0" } }, + "node_modules/diff": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.4.tgz", + "integrity": "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/discontinuous-range": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/discontinuous-range/-/discontinuous-range-1.0.0.tgz", @@ -720,6 +691,23 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/foreground-child": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", @@ -738,9 +726,9 @@ } }, "node_modules/fs-extra": { - "version": "11.3.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.0.tgz", - "integrity": "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==", + "version": "11.3.4", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.4.tgz", + "integrity": "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==", "dev": true, "license": "MIT", "dependencies": { @@ -786,22 +774,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/glob/node_modules/minimatch": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz", - "integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/brace-expansion": "^5.0.0" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -957,9 +929,9 @@ "license": "MIT" }, "node_modules/jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", "dev": true, "license": "MIT", "dependencies": { @@ -970,9 +942,9 @@ } }, "node_modules/lodash": { - "version": "4.17.23", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", - "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", "dev": true, "license": "MIT" }, @@ -987,16 +959,19 @@ } }, "node_modules/minimatch": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.8.tgz", - "integrity": "sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q==", + "version": "10.2.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.3.tgz", + "integrity": "sha512-Rwi3pnapEqirPSbWbrZaa6N3nmqq4Xer/2XooiOKyV3q12ML06f7MOuc5DVH8ONZIFhwIYQ3yzPH4nt7iWHaTg==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^1.1.7" + "brace-expansion": "^5.0.2" }, "engines": { - "node": "*" + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/minimist": { @@ -1104,16 +1079,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/railroad-diagrams": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz", @@ -1146,13 +1111,13 @@ } }, "node_modules/resolve": { - "version": "1.22.10", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", - "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", "dev": true, "license": "MIT", "dependencies": { - "is-core-module": "^2.16.0", + "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, @@ -1463,9 +1428,9 @@ "peer": true }, "node_modules/typescript": { - "version": "5.8.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", - "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", "bin": { @@ -1493,16 +1458,6 @@ "node": ">= 10.0.0" } }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, "node_modules/uuid": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", From 68e14e39751d1da3b346c1f38a08cb8bc23a20d8 Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Thu, 9 Apr 2026 13:40:19 +0000 Subject: [PATCH 04/28] docs: update CONTRIBUTING.md to include PR submission checklist and improve setup instructions --- CONTRIBUTING.md | 109 ++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 91 insertions(+), 18 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index eab16a554..b306cd46b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,11 +2,12 @@ Thank you for your interest in contributing to the **DocumentDB for VS Code** extension. This guide helps you set up your development environment and configure Visual Studio Code to effectively contribute to the extension. -The document consists of three sections: +The document consists of four sections: 1. [Branching Strategy](#1-branching-strategy) 2. [Machine Setup](#2-machine-setup) 3. [VS Code Configuration](#3-vs-code-configuration) +4. [PR Submission Checklist](#4-pr-submission-checklist) ## 1. Branching Strategy @@ -22,12 +23,10 @@ The repository follows a structured branching strategy to ensure smooth developm GitHub Actions are configured to perform automated checks on the repository. The intensity of these checks depends on the target branch: 1. **Push to `next`, `dev/*`, or `feature/*` branches**: - - Runs basic code quality checks and tests. - Skips resource-intensive jobs like integration tests and packaging to focus on code validation. 2. **Pull Requests to `main` or `next`**: - - Executes all jobs, including code checks, tests, and packaging. - Ensures complete validation before merging, including artifact generation. @@ -38,18 +37,21 @@ This setup ensures that contributions are thoroughly validated while optimizing ## 2. Machine Setup -Follow these instructions to configure your machine for JavaScript/TypeScript development using Windows Subsystem for Linux (WSL2) and Visual Studio Code. +> **Platform coverage:** The detailed setup instructions below are written for **Windows + WSL2**. Stub sections for [macOS](#22-macos-pending), [Windows (native)](#23-windows-native-pending), and [plain Linux](#24-linux-pending) are included but not yet filled in; Contributors on those platforms are warmly invited to submit a PR expanding those sections! -> This setup assumes you're using WSL2 on Windows. However, you can use a Linux or Windows setup exclusively if preferred. +--- -### 2.1. Install Ubuntu 22.\* on Windows +### 2.1. Windows + WSL2 _(documented)_ -- Install **Ubuntu 22.\*** from the Microsoft Store and launch it to configure your Linux user account. +Follow these instructions to configure your machine for JavaScript/TypeScript development using Windows Subsystem for Linux (WSL2) and Visual Studio Code. + +#### 2.1.1. Install Ubuntu 22.\* on Windows +- Install **Ubuntu 22.\*** from the Microsoft Store and launch it to configure your Linux user account. - Your development environment and tools will reside within `WSL2`. - VS Code integrates seamlessly with `WSL2` instances, enabling smooth development from your Windows machine. -### 2.2. Update Ubuntu Packages +#### 2.1.2. Update Ubuntu Packages Open your Ubuntu terminal and run: @@ -58,29 +60,48 @@ sudo apt update sudo apt upgrade ``` -### 2.3. Install Node.js with FNM (Fast Node Manager) +#### 2.1.3. Install Node.js with FNM (Fast Node Manager) + +`FNM` helps with installing and switching Node.js versions easily. This is useful for testing compatibility across different Node.js versions. -- `FNM` helps with installing and switching Node.js versions easily. This is useful for testing compatibility across different Node.js versions. +The minimum required versions are **Node.js 22.18.0** and **npm 10.0.0** (see `engines` in `package.json`). Run the following commands: ```bash curl -fsSL https://fnm.vercel.app/install | bash source ~/.bashrc -fnm install 22 -fnm use 22 -fnm default 22 -node --version +fnm install 22.18.0 +fnm use 22.18.0 +fnm default 22.18.0 +node --version # should print v22.18.0 or later +npm --version # should print 10.x or later ``` -### 2.4. Install TypeScript Globally (optional) - -You can install TypeScript globally: +#### 2.1.4. Install TypeScript Globally (optional) ```bash npm install -g typescript ``` +--- + +### 2.2. macOS _(pending)_ + +> **Help wanted!** If you develop on macOS, please consider contributing setup instructions for this section. The general flow (install Node.js via a version manager such as `nvm` or `fnm`, clone the repo, `npm install && npm run build`) should be very similar to the WSL2 path above. + +--- + +### 2.3. Windows (native) _(pending)_ + +> **Help wanted!** If you develop on Windows without WSL2, please consider contributing setup instructions for this section. + +--- + +### 2.4. Linux _(pending)_ + +> **Help wanted!** If you develop on Linux natively, please consider contributing setup instructions for this section. The WSL2 Ubuntu steps above should translate almost verbatim. + ## 3. VS Code Configuration This section explains how to clone the **DocumentDB for VS Code** repository and set up Visual Studio Code for development and debugging. @@ -90,7 +111,6 @@ This section explains how to clone the **DocumentDB for VS Code** repository and 1. Ensure you have completed the [Machine Setup](#2-machine-setup) steps. 2. Fork or directly clone the official repository: - - [DocumentDB for VS Code (vscode-documentdb)](https://github.com/microsoft/vscode-documentdb) - Open your **WSL2** terminal and clone the repository: @@ -124,6 +144,59 @@ code . - Select `Launch Extension (webpack)`. - Press `F5`. +## 4. PR Submission Checklist + +Before opening or marking a pull request as ready for review, **all of the following steps must pass locally**. The same checks run in CI, so catching failures locally saves time. + +### 4.1. Localization + +If you added, changed, or removed any user-facing string (anything passed to `vscode.l10n.t()`), regenerate the localization bundle: + +```bash +npm run l10n +``` + +Commit any changes to the `l10n/` folder together with your code changes. + +### 4.2. Formatting + +Run Prettier to ensure all files meet the project's formatting standards: + +```bash +npm run prettier-fix +``` + +Commit any files that Prettier reformats. + +### 4.3. Linting + +Run ESLint and fix all reported issues before submitting: + +```bash +npm run lint +``` + +### 4.4. Package Verification + +Verify the extension can be packaged successfully without errors: + +```bash +npm run package +``` + +This step catches webpack bundling issues and missing assets that unit tests alone won't surface. + +--- + +> **Summary — run these four commands before every PR:** +> +> ```bash +> npm run l10n +> npm run prettier-fix +> npm run lint +> npm run package +> ``` + ## You're Ready to Contribute! 🎉 You've now successfully set up your development environment and are ready to contribute to **DocumentDB for VS Code**. We appreciate your contributions! From e9ce511b6564c88ffdce2874309a2ca10452df84 Mon Sep 17 00:00:00 2001 From: bgaeddert Date: Thu, 2 Apr 2026 06:06:39 -0500 Subject: [PATCH 05/28] feat: add Copy Reference context menu for databases and collections Adds a "Copy Reference" right-click option to database and collection nodes in the sidebar. Database copies the db name, collection copies db.collection dot notation. --- package.json | 32 ++++++++++++++++++ src/commands/copyReference/copyReference.ts | 36 +++++++++++++++++++++ src/documentdb/ClustersExtension.ts | 10 ++++++ 3 files changed, 78 insertions(+) create mode 100644 src/commands/copyReference/copyReference.ts diff --git a/package.json b/package.json index ed394f432..dba596e7b 100644 --- a/package.json +++ b/package.json @@ -533,6 +533,18 @@ "category": "DocumentDB", "command": "vscode-documentdb.command.pasteCollection", "title": "Paste Collection…" + }, + { + "//": "Copy Database Reference", + "category": "DocumentDB", + "command": "vscode-documentdb.command.copyDatabaseReference", + "title": "Copy Reference" + }, + { + "//": "Copy Collection Reference", + "category": "DocumentDB", + "command": "vscode-documentdb.command.copyCollectionReference", + "title": "Copy Reference" } ], "submenus": [ @@ -864,6 +876,18 @@ "command": "vscode-documentdb.command.pasteCollection", "when": "view =~ /connectionsView|discoveryView|azure(ResourceGroups|FocusView)/ && viewItem =~ /\\btreeitem_collection\\b/i && viewItem =~ /\\bexperience_(documentDB|mongoRU)\\b/i", "group": "3@4" + }, + { + "//": "[Database] Copy Reference", + "command": "vscode-documentdb.command.copyDatabaseReference", + "when": "view =~ /connectionsView|discoveryView|azure(ResourceGroups|FocusView)/ && viewItem =~ /\\btreeitem_database\\b/i && viewItem =~ /\\bexperience_(documentDB|mongoRU)\\b/i", + "group": "6@1" + }, + { + "//": "[Collection] Copy Reference", + "command": "vscode-documentdb.command.copyCollectionReference", + "when": "view =~ /connectionsView|discoveryView|azure(ResourceGroups|FocusView)/ && viewItem =~ /\\btreeitem_collection\\b/i && viewItem =~ /\\bexperience_(documentDB|mongoRU)\\b/i", + "group": "6@1" } ], "explorer/context": [], @@ -928,6 +952,14 @@ "command": "vscode-documentdb.command.azureResourcesView.addConnectionToConnectionsView", "when": "never" }, + { + "command": "vscode-documentdb.command.copyDatabaseReference", + "when": "never" + }, + { + "command": "vscode-documentdb.command.copyCollectionReference", + "when": "never" + }, { "command": "vscode-documentdb.command.copyConnectionString", "when": "never" diff --git a/src/commands/copyReference/copyReference.ts b/src/commands/copyReference/copyReference.ts new file mode 100644 index 000000000..1579d456b --- /dev/null +++ b/src/commands/copyReference/copyReference.ts @@ -0,0 +1,36 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { type IActionContext } from '@microsoft/vscode-azext-utils'; +import * as l10n from '@vscode/l10n'; +import * as vscode from 'vscode'; +import { type CollectionItem } from '../../tree/documentdb/CollectionItem'; +import { type DatabaseItem } from '../../tree/documentdb/DatabaseItem'; + +export async function copyDatabaseReference( + _context: IActionContext, + node: DatabaseItem, +): Promise { + if (!node) { + throw new Error(l10n.t('No node selected.')); + } + + const reference = node.databaseInfo.name; + await vscode.env.clipboard.writeText(reference); + void vscode.window.showInformationMessage(l10n.t('The reference has been copied to the clipboard')); +} + +export async function copyCollectionReference( + _context: IActionContext, + node: CollectionItem, +): Promise { + if (!node) { + throw new Error(l10n.t('No node selected.')); + } + + const reference = `${node.databaseInfo.name}.${node.collectionInfo.name}`; + await vscode.env.clipboard.writeText(reference); + void vscode.window.showInformationMessage(l10n.t('The reference has been copied to the clipboard')); +} diff --git a/src/documentdb/ClustersExtension.ts b/src/documentdb/ClustersExtension.ts index 59f9422e8..655402f69 100644 --- a/src/documentdb/ClustersExtension.ts +++ b/src/documentdb/ClustersExtension.ts @@ -29,6 +29,7 @@ import { renameConnection } from '../commands/connections-view/renameConnection/ import { renameFolder } from '../commands/connections-view/renameFolder/renameFolder'; import { copyCollection } from '../commands/copyCollection/copyCollection'; import { copyAzureConnectionString } from '../commands/copyConnectionString/copyConnectionString'; +import { copyCollectionReference, copyDatabaseReference } from '../commands/copyReference/copyReference'; import { createCollection } from '../commands/createCollection/createCollection'; import { createAzureDatabase } from '../commands/createDatabase/createDatabase'; import { createMongoDocument } from '../commands/createDocument/createDocument'; @@ -387,6 +388,15 @@ export class ClustersExtension implements vscode.Disposable { withTreeNodeCommandCorrelation(deleteAzureDatabase), ); + registerCommandWithTreeNodeUnwrapping( + 'vscode-documentdb.command.copyDatabaseReference', + withTreeNodeCommandCorrelation(copyDatabaseReference), + ); + registerCommandWithTreeNodeUnwrapping( + 'vscode-documentdb.command.copyCollectionReference', + withTreeNodeCommandCorrelation(copyCollectionReference), + ); + registerCommandWithTreeNodeUnwrapping( 'vscode-documentdb.command.hideIndex', withTreeNodeCommandCorrelation(hideIndex), From 094a2dc0d1cfe76da6f544dd19e507e635cafe3e Mon Sep 17 00:00:00 2001 From: bgaeddert Date: Thu, 2 Apr 2026 06:43:15 -0500 Subject: [PATCH 06/28] fix: update l10n bundle with new localization key for Copy Reference --- l10n/bundle.l10n.json | 1 + 1 file changed, 1 insertion(+) diff --git a/l10n/bundle.l10n.json b/l10n/bundle.l10n.json index a4493f78a..7b834da67 100644 --- a/l10n/bundle.l10n.json +++ b/l10n/bundle.l10n.json @@ -986,6 +986,7 @@ "The name must be between {0} and {1} characters.": "The name must be between {0} and {1} characters.", "The output window may contain additional information.": "The output window may contain additional information.", "The process exited prematurely.": "The process exited prematurely.", + "The reference has been copied to the clipboard": "The reference has been copied to the clipboard", "The selected authentication method is not supported.": "The selected authentication method is not supported.", "The selected connection has been removed.": "The selected connection has been removed.", "The selected folder has been removed.": "The selected folder has been removed.", From 34c144bf9372f71fada685d5f2ffd39d5db4009c Mon Sep 17 00:00:00 2001 From: bgaeddert Date: Thu, 2 Apr 2026 07:27:07 -0500 Subject: [PATCH 07/28] style: fix prettier formatting in copyReference.ts --- src/commands/copyReference/copyReference.ts | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/commands/copyReference/copyReference.ts b/src/commands/copyReference/copyReference.ts index 1579d456b..3dfb555fe 100644 --- a/src/commands/copyReference/copyReference.ts +++ b/src/commands/copyReference/copyReference.ts @@ -9,10 +9,7 @@ import * as vscode from 'vscode'; import { type CollectionItem } from '../../tree/documentdb/CollectionItem'; import { type DatabaseItem } from '../../tree/documentdb/DatabaseItem'; -export async function copyDatabaseReference( - _context: IActionContext, - node: DatabaseItem, -): Promise { +export async function copyDatabaseReference(_context: IActionContext, node: DatabaseItem): Promise { if (!node) { throw new Error(l10n.t('No node selected.')); } @@ -22,10 +19,7 @@ export async function copyDatabaseReference( void vscode.window.showInformationMessage(l10n.t('The reference has been copied to the clipboard')); } -export async function copyCollectionReference( - _context: IActionContext, - node: CollectionItem, -): Promise { +export async function copyCollectionReference(_context: IActionContext, node: CollectionItem): Promise { if (!node) { throw new Error(l10n.t('No node selected.')); } From 32e6d3ebb0fda984e8ff9a53141c063cc8438aa1 Mon Sep 17 00:00:00 2001 From: bgaeddert Date: Thu, 2 Apr 2026 07:48:51 -0500 Subject: [PATCH 08/28] refactor: consolidate copy reference into shared helper and differentiate titles Extracts shared logic into copyReferenceInternal helper. Renames command titles to "Copy Database Reference" and "Copy Collection Reference" to avoid duplicate labels in VS Code command/keybinding lists. --- l10n/bundle.l10n.json | 2 +- package.json | 4 ++-- src/commands/copyReference/copyReference.ts | 21 +++++++++++---------- 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/l10n/bundle.l10n.json b/l10n/bundle.l10n.json index 7b834da67..69599aa20 100644 --- a/l10n/bundle.l10n.json +++ b/l10n/bundle.l10n.json @@ -986,7 +986,7 @@ "The name must be between {0} and {1} characters.": "The name must be between {0} and {1} characters.", "The output window may contain additional information.": "The output window may contain additional information.", "The process exited prematurely.": "The process exited prematurely.", - "The reference has been copied to the clipboard": "The reference has been copied to the clipboard", + "The reference has been copied to the clipboard.": "The reference has been copied to the clipboard.", "The selected authentication method is not supported.": "The selected authentication method is not supported.", "The selected connection has been removed.": "The selected connection has been removed.", "The selected folder has been removed.": "The selected folder has been removed.", diff --git a/package.json b/package.json index dba596e7b..6788fb90e 100644 --- a/package.json +++ b/package.json @@ -538,13 +538,13 @@ "//": "Copy Database Reference", "category": "DocumentDB", "command": "vscode-documentdb.command.copyDatabaseReference", - "title": "Copy Reference" + "title": "Copy Database Reference" }, { "//": "Copy Collection Reference", "category": "DocumentDB", "command": "vscode-documentdb.command.copyCollectionReference", - "title": "Copy Reference" + "title": "Copy Collection Reference" } ], "submenus": [ diff --git a/src/commands/copyReference/copyReference.ts b/src/commands/copyReference/copyReference.ts index 3dfb555fe..0a8180887 100644 --- a/src/commands/copyReference/copyReference.ts +++ b/src/commands/copyReference/copyReference.ts @@ -9,22 +9,23 @@ import * as vscode from 'vscode'; import { type CollectionItem } from '../../tree/documentdb/CollectionItem'; import { type DatabaseItem } from '../../tree/documentdb/DatabaseItem'; -export async function copyDatabaseReference(_context: IActionContext, node: DatabaseItem): Promise { +async function copyReferenceInternal( + node: T | undefined, + getReference: (node: T) => string, +): Promise { if (!node) { throw new Error(l10n.t('No node selected.')); } - const reference = node.databaseInfo.name; + const reference = getReference(node); await vscode.env.clipboard.writeText(reference); - void vscode.window.showInformationMessage(l10n.t('The reference has been copied to the clipboard')); + void vscode.window.showInformationMessage(l10n.t('The reference has been copied to the clipboard.')); } -export async function copyCollectionReference(_context: IActionContext, node: CollectionItem): Promise { - if (!node) { - throw new Error(l10n.t('No node selected.')); - } +export async function copyDatabaseReference(_context: IActionContext, node: DatabaseItem): Promise { + await copyReferenceInternal(node, (n) => n.databaseInfo.name); +} - const reference = `${node.databaseInfo.name}.${node.collectionInfo.name}`; - await vscode.env.clipboard.writeText(reference); - void vscode.window.showInformationMessage(l10n.t('The reference has been copied to the clipboard')); +export async function copyCollectionReference(_context: IActionContext, node: CollectionItem): Promise { + await copyReferenceInternal(node, (n) => `${n.databaseInfo.name}.${n.collectionInfo.name}`); } From 57888b4e69c8669d991541109843817b9755b7c4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 15 Apr 2026 20:18:57 +0000 Subject: [PATCH 09/28] chore(deps): bump follow-redirects from 1.15.11 to 1.16.0 Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.15.11 to 1.16.0. - [Release notes](https://github.com/follow-redirects/follow-redirects/releases) - [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.15.11...v1.16.0) --- updated-dependencies: - dependency-name: follow-redirects dependency-version: 1.16.0 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 512c959ee..0061ad01f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12031,9 +12031,9 @@ "license": "ISC" }, "node_modules/follow-redirects": { - "version": "1.15.11", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", - "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", "dev": true, "funding": [ { From 0b888990ec51448846c87fadd207243e13035829 Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Thu, 16 Apr 2026 10:57:49 +0200 Subject: [PATCH 10/28] faet: new skill for external PR reviews --- .github/skills/review-external-pr/SKILL.md | 101 +++++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 .github/skills/review-external-pr/SKILL.md diff --git a/.github/skills/review-external-pr/SKILL.md b/.github/skills/review-external-pr/SKILL.md new file mode 100644 index 000000000..6a4cc134b --- /dev/null +++ b/.github/skills/review-external-pr/SKILL.md @@ -0,0 +1,101 @@ +--- +name: review-external-pr +description: Prepare an external contributor's PR for maintainer review by redirecting it into a dedicated review branch, then merging and creating a new finalization PR targeting next. Use when triaging/reviewing contributor PRs, merging external PRs with maintainer changes, or setting up a review workflow for incoming community contributions. +--- + +# Review External PR Workflow + +Redirects an external contributor's PR into a `reviews/` staging branch so a maintainer can inspect, add changes, then merge everything into `next` cleanly. + +## When to Use + +- An external contributor opened a PR targeting `next` and you want to add changes before merging +- You want to formally review and finalize a community contribution +- You want the contributor to get proper merge credit while still controlling what lands in `next` + +## Workflow Steps + +### 1. Gather PR Info + +```bash +gh pr view --json title,author,headRefName,baseRefName,body +``` + +Note the **PR number**, **title**, and **author login** — you'll need them for branch naming and PR descriptions. + +### 2. Create the Review Branch + +Branch naming format: `reviews/-original-pr-` + +```bash +git fetch origin +git checkout -b reviews/-original-pr- origin/next +git push origin reviews/-original-pr- +``` + +Example: `reviews/copy-reference-original-pr-545` + +### 3. Retarget the Contributor's PR + +> ⚠️ **Known issue**: `gh pr edit --base` may emit a deprecation warning about Projects (classic). This is a cosmetic warning only — the base branch change succeeds regardless. Verify with `gh pr view --json baseRefName`. + +```bash +gh pr edit --base reviews/-original-pr- +``` + +Verify: + +```bash +gh pr view --json baseRefName +``` + +### 4. Merge the Contributor's PR + +Once the base is updated and the PR is ready: + +```bash +gh pr merge --squash +``` + +Or approve + merge via the GitHub UI to trigger any required status checks. + +### 5. Create the Finalization PR + +Pull the merged review branch, then open a new PR from it to `next`: + +```bash +git checkout reviews/-original-pr- +git pull origin reviews/-original-pr- +``` + +Create the PR: + +```bash +gh pr create \ + --base next \ + --head reviews/-original-pr- \ + --title " [reviewed]" \ + --body "This PR finalizes the review of the contribution originally submitted by @ in #. + +Original PR: " +``` + +### 6. Comment on the Original PR + +Go back to the contributor's original (now merged) PR and leave a comment linking to the finalization PR: + +```bash +gh pr comment \ + --body "Thank you for the contribution! The review is continuing in # where maintainer changes will be finalized before merging to \`next\`." +``` + +## Summary + +| Step | Action | Result | +| ---- | ------------------------------------------ | ----------------------------------------- | +| 1 | Gather PR info | Know PR number, title, author | +| 2 | Create `reviews/...` branch off `next` | Staging branch ready | +| 3 | Retarget contributor's PR to review branch | Their diff is scoped to review branch | +| 4 | Merge contributor's PR | Contributor gets merge credit | +| 5 | Create finalization PR to `next` | Maintainer controls what lands in `next` | +| 6 | Comment on original PR with link to new PR | Contributor is informed, thread is linked | From 1a596eb0312072a5d54cdc5103433c5670f83a3f Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Thu, 16 Apr 2026 12:52:28 +0200 Subject: [PATCH 11/28] feat: implement unified Copy Reference command for databases, collections, and indexes --- l10n/bundle.l10n.json | 11 +- package.json | 32 ++- src/commands/copyReference/copyReference.ts | 211 ++++++++++++++++++-- src/documentdb/ClustersExtension.ts | 10 +- 4 files changed, 225 insertions(+), 39 deletions(-) diff --git a/l10n/bundle.l10n.json b/l10n/bundle.l10n.json index 69599aa20..dd19183d3 100644 --- a/l10n/bundle.l10n.json +++ b/l10n/bundle.l10n.json @@ -282,12 +282,15 @@ "Connection: {connectionName}": "Connection: {connectionName}", "Connections have moved": "Connections have moved", "Continue": "Continue", + "Copied to clipboard": "Copied to clipboard", "Copy \"{sourceCollection}\" from \"{sourceDatabase}\" to \"{targetDatabase}/{targetCollection}\"": "Copy \"{sourceCollection}\" from \"{sourceDatabase}\" to \"{targetDatabase}/{targetCollection}\"", "Copy index definitions from source collection?": "Copy index definitions from source collection?", "Copy index definitions from source to target collection.": "Copy index definitions from source to target collection.", "Copy Indexes: {yesNoValue}": "Copy Indexes: {yesNoValue}", "Copy only documents without recreating indexes.": "Copy only documents without recreating indexes.", "Copy operation cancelled.": "Copy operation cancelled.", + "Copy Reference: {0}": "Copy Reference: {0}", + "Copy Reference: {0}.{1}": "Copy Reference: {0}.{1}", "Copy with password": "Copy with password", "Copy without password": "Copy without password", "Copy-and-Merge": "Copy-and-Merge", @@ -639,6 +642,7 @@ "JSON View": "JSON View", "Keep-alive timeout exceeded": "Keep-alive timeout exceeded", "Keep-alive timeout exceeded: stream has been running for {0} seconds (limit: {1} seconds)": "Keep-alive timeout exceeded: stream has been running for {0} seconds (limit: {1} seconds)", + "Key Definition": "Key Definition", "Keys Examined": "Keys Examined", "Large Collection Copy Operation": "Large Collection Copy Operation", "Learn more": "Learn more", @@ -695,6 +699,8 @@ "Move to top level": "Move to top level", "Moved {0} item(s) to \"{1}\".": "Moved {0} item(s) to \"{1}\".", "N/A": "N/A", + "Name": "Name", + "Namespace": "Namespace", "New Connection": "New Connection", "New connection has been added to your DocumentDB Connections.": "New connection has been added to your DocumentDB Connections.", "New connection has been added.": "New connection has been added.", @@ -789,6 +795,7 @@ "Project": "Project", "Project: Specify which fields to include or exclude": "Project: Specify which fields to include or exclude", "Provider \"{0}\" does not have resource type \"{1}\".": "Provider \"{0}\" does not have resource type \"{1}\".", + "Qualified Name": "Qualified Name", "Query Efficiency Analysis": "Query Efficiency Analysis", "Query Execution Failed": "Query Execution Failed", "Query generation failed": "Query generation failed", @@ -849,6 +856,7 @@ "See output for more details.": "See output for more details.", "Select {0}": "Select {0}", "Select {mongoExecutableFileName}": "Select {mongoExecutableFileName}", + "Select a format to copy": "Select a format to copy", "Select a location for new resources.": "Select a location for new resources.", "Select a tenant for Microsoft Entra ID authentication": "Select a tenant for Microsoft Entra ID authentication", "Select a workspace folder": "Select a workspace folder", @@ -874,6 +882,8 @@ "SHARD_MERGE · {0} shards": "SHARD_MERGE · {0} shards", "SHARD_MERGE · {0} shards · {1} docs · {2}ms": "SHARD_MERGE · {0} shards · {1} docs · {2}ms", "Shard: {0}": "Shard: {0}", + "Shell Command": "Shell Command", + "Shell Reference": "Shell Reference", "Show Output": "Show Output", "Show Stage Details": "Show Stage Details", "Sign in to additional accounts or authenticate with other tenants to see more options.": "Sign in to additional accounts or authenticate with other tenants to see more options.", @@ -986,7 +996,6 @@ "The name must be between {0} and {1} characters.": "The name must be between {0} and {1} characters.", "The output window may contain additional information.": "The output window may contain additional information.", "The process exited prematurely.": "The process exited prematurely.", - "The reference has been copied to the clipboard.": "The reference has been copied to the clipboard.", "The selected authentication method is not supported.": "The selected authentication method is not supported.", "The selected connection has been removed.": "The selected connection has been removed.", "The selected folder has been removed.": "The selected folder has been removed.", diff --git a/package.json b/package.json index 6788fb90e..ba6272d11 100644 --- a/package.json +++ b/package.json @@ -535,16 +535,10 @@ "title": "Paste Collection…" }, { - "//": "Copy Database Reference", + "//": "Copy Reference", "category": "DocumentDB", - "command": "vscode-documentdb.command.copyDatabaseReference", - "title": "Copy Database Reference" - }, - { - "//": "Copy Collection Reference", - "category": "DocumentDB", - "command": "vscode-documentdb.command.copyCollectionReference", - "title": "Copy Collection Reference" + "command": "vscode-documentdb.command.copyReference", + "title": "Copy Reference…" } ], "submenus": [ @@ -879,15 +873,21 @@ }, { "//": "[Database] Copy Reference", - "command": "vscode-documentdb.command.copyDatabaseReference", + "command": "vscode-documentdb.command.copyReference", "when": "view =~ /connectionsView|discoveryView|azure(ResourceGroups|FocusView)/ && viewItem =~ /\\btreeitem_database\\b/i && viewItem =~ /\\bexperience_(documentDB|mongoRU)\\b/i", - "group": "6@1" + "group": "yheAlmostLastGroup@1" }, { "//": "[Collection] Copy Reference", - "command": "vscode-documentdb.command.copyCollectionReference", + "command": "vscode-documentdb.command.copyReference", "when": "view =~ /connectionsView|discoveryView|azure(ResourceGroups|FocusView)/ && viewItem =~ /\\btreeitem_collection\\b/i && viewItem =~ /\\bexperience_(documentDB|mongoRU)\\b/i", - "group": "6@1" + "group": "yheAlmostLastGroup@1" + }, + { + "//": "[Index] Copy Reference", + "command": "vscode-documentdb.command.copyReference", + "when": "view =~ /connectionsView|discoveryView|azure(ResourceGroups|FocusView)/ && viewItem =~ /\\btreeitem_index\\b/i && viewItem =~ /\\bexperience_(documentDB|mongoRU)\\b/i", + "group": "yheAlmostLastGroup@1" } ], "explorer/context": [], @@ -953,11 +953,7 @@ "when": "never" }, { - "command": "vscode-documentdb.command.copyDatabaseReference", - "when": "never" - }, - { - "command": "vscode-documentdb.command.copyCollectionReference", + "command": "vscode-documentdb.command.copyReference", "when": "never" }, { diff --git a/src/commands/copyReference/copyReference.ts b/src/commands/copyReference/copyReference.ts index 0a8180887..a9cfbae98 100644 --- a/src/commands/copyReference/copyReference.ts +++ b/src/commands/copyReference/copyReference.ts @@ -6,26 +6,211 @@ import { type IActionContext } from '@microsoft/vscode-azext-utils'; import * as l10n from '@vscode/l10n'; import * as vscode from 'vscode'; -import { type CollectionItem } from '../../tree/documentdb/CollectionItem'; +import { CollectionItem } from '../../tree/documentdb/CollectionItem'; import { type DatabaseItem } from '../../tree/documentdb/DatabaseItem'; +import { IndexItem } from '../../tree/documentdb/IndexItem'; -async function copyReferenceInternal( - node: T | undefined, - getReference: (node: T) => string, +interface CopyReferenceOption extends vscode.QuickPickItem { + value: string; +} + +function formatIndexKey(key: Record): string { + const entries = Object.entries(key) + .map(([field, order]) => `${field}: ${order}`) + .join(', '); + return `{ ${entries} }`; +} + +/** + * Returns true if a name requires quoting (cannot be used in dot-notation). + * A name is safe for dot-notation only if it matches a valid JS identifier. + */ +function needsQuoting(name: string): boolean { + return !/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name); +} + +function escapeDoubleQuotes(value: string): string { + return value.replace(/\\/g, '\\\\').replace(/"/g, '\\"'); +} + +function shellCollectionRef(collName: string): string { + return needsQuoting(collName) ? `db.getCollection("${escapeDoubleQuotes(collName)}")` : `db.${collName}`; +} + +function getClusterHost(connectionString: string | undefined): string | undefined { + if (!connectionString) { + return undefined; + } + + try { + const url = new URL(connectionString); + return url.host; + } catch { + return undefined; + } +} + +function getDatabaseOptions(node: DatabaseItem): CopyReferenceOption[] { + const dbName = node.databaseInfo.name; + const host = getClusterHost(node.cluster.connectionString); + + const options: CopyReferenceOption[] = [ + { + label: l10n.t('Name'), + detail: dbName, + value: dbName, + }, + { + label: l10n.t('Shell Command'), + detail: `use ${dbName}`, + value: `use ${dbName}`, + }, + ]; + + if (host) { + options.push({ + label: l10n.t('Qualified Name'), + detail: `${host}/${dbName}`, + value: `${host}/${dbName}`, + }); + } + + return options; +} + +function getCollectionOptions(node: CollectionItem): CopyReferenceOption[] { + const dbName = node.databaseInfo.name; + const collName = node.collectionInfo.name; + const quoted = needsQuoting(collName) || needsQuoting(dbName); + const escapedCollName = escapeDoubleQuotes(collName); + + const options: CopyReferenceOption[] = [ + { + label: l10n.t('Name'), + detail: collName, + value: collName, + }, + ]; + + if (!quoted) { + options.push({ + label: l10n.t('Namespace'), + detail: `${dbName}.${collName}`, + value: `${dbName}.${collName}`, + }); + } + + if (!quoted) { + options.push({ + label: l10n.t('Shell Reference'), + detail: `db.${collName}`, + value: `db.${collName}`, + }); + } + + options.push({ + label: l10n.t('Shell Command'), + detail: `db.getCollection("${escapedCollName}")`, + value: `db.getCollection("${escapedCollName}")`, + }); + + return options; +} + +function getIndexOptions(node: IndexItem): CopyReferenceOption[] { + const indexName = node.indexInfo.name; + const collName = node.collectionInfo.name; + + const options: CopyReferenceOption[] = [ + { + label: l10n.t('Name'), + detail: indexName, + value: indexName, + }, + ]; + + if (node.indexInfo.key) { + const keyDef = formatIndexKey(node.indexInfo.key); + + options.push({ + label: l10n.t('Key Definition'), + detail: keyDef, + value: keyDef, + }); + + const collRef = shellCollectionRef(collName); + const escapedIndexName = escapeDoubleQuotes(indexName); + + options.push({ + label: l10n.t('Shell Command'), + detail: `${collRef}.getIndexes().find(i => i.name === "${escapedIndexName}")`, + value: `${collRef}.getIndexes().find(i => i.name === "${escapedIndexName}")`, + }); + } + + return options; +} + +function getOptionsForNode(node: DatabaseItem | CollectionItem | IndexItem): { + title: string; + options: CopyReferenceOption[]; +} { + if (node instanceof IndexItem) { + return { + title: l10n.t('Copy Reference: {0}', node.indexInfo.name), + options: getIndexOptions(node), + }; + } + + if (node instanceof CollectionItem) { + return { + title: l10n.t('Copy Reference: {0}.{1}', node.databaseInfo.name, node.collectionInfo.name), + options: getCollectionOptions(node), + }; + } + + return { + title: l10n.t('Copy Reference: {0}', node.databaseInfo.name), + options: getDatabaseOptions(node), + }; +} + +export async function copyReference( + _context: IActionContext, + node: DatabaseItem | CollectionItem | IndexItem, ): Promise { if (!node) { throw new Error(l10n.t('No node selected.')); } - const reference = getReference(node); - await vscode.env.clipboard.writeText(reference); - void vscode.window.showInformationMessage(l10n.t('The reference has been copied to the clipboard.')); -} + const { title, options } = getOptionsForNode(node); -export async function copyDatabaseReference(_context: IActionContext, node: DatabaseItem): Promise { - await copyReferenceInternal(node, (n) => n.databaseInfo.name); -} + const picker = vscode.window.createQuickPick(); + picker.title = title; + picker.placeholder = l10n.t('Select a format to copy'); + picker.items = options; + picker.matchOnDetail = true; + + // Workaround: setting sortByLabel to false via the options object is not available + // in the createQuickPick API, but we can suppress persistence by not setting a value + // for the quickpick's value property, which avoids reordering. + + const result = await new Promise((resolve) => { + picker.onDidAccept(() => { + resolve(picker.selectedItems[0] as CopyReferenceOption | undefined); + picker.dispose(); + }); + picker.onDidHide(() => { + resolve(undefined); + picker.dispose(); + }); + picker.show(); + }); + + if (!result) { + return; + } -export async function copyCollectionReference(_context: IActionContext, node: CollectionItem): Promise { - await copyReferenceInternal(node, (n) => `${n.databaseInfo.name}.${n.collectionInfo.name}`); + await vscode.env.clipboard.writeText(result.value); + void vscode.window.showInformationMessage(l10n.t('Copied to clipboard')); } diff --git a/src/documentdb/ClustersExtension.ts b/src/documentdb/ClustersExtension.ts index 655402f69..6e7108f2d 100644 --- a/src/documentdb/ClustersExtension.ts +++ b/src/documentdb/ClustersExtension.ts @@ -29,7 +29,7 @@ import { renameConnection } from '../commands/connections-view/renameConnection/ import { renameFolder } from '../commands/connections-view/renameFolder/renameFolder'; import { copyCollection } from '../commands/copyCollection/copyCollection'; import { copyAzureConnectionString } from '../commands/copyConnectionString/copyConnectionString'; -import { copyCollectionReference, copyDatabaseReference } from '../commands/copyReference/copyReference'; +import { copyReference } from '../commands/copyReference/copyReference'; import { createCollection } from '../commands/createCollection/createCollection'; import { createAzureDatabase } from '../commands/createDatabase/createDatabase'; import { createMongoDocument } from '../commands/createDocument/createDocument'; @@ -389,12 +389,8 @@ export class ClustersExtension implements vscode.Disposable { ); registerCommandWithTreeNodeUnwrapping( - 'vscode-documentdb.command.copyDatabaseReference', - withTreeNodeCommandCorrelation(copyDatabaseReference), - ); - registerCommandWithTreeNodeUnwrapping( - 'vscode-documentdb.command.copyCollectionReference', - withTreeNodeCommandCorrelation(copyCollectionReference), + 'vscode-documentdb.command.copyReference', + withTreeNodeCommandCorrelation(copyReference), ); registerCommandWithTreeNodeUnwrapping( From 3a4e6e2704e98cf4b1c0d5a5311d1242181ed7f5 Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Thu, 16 Apr 2026 13:03:56 +0200 Subject: [PATCH 12/28] chore: simplified quickpick / wizard code --- l10n/bundle.l10n.json | 1 - src/commands/copyReference/copyReference.ts | 115 +++++--------------- 2 files changed, 26 insertions(+), 90 deletions(-) diff --git a/l10n/bundle.l10n.json b/l10n/bundle.l10n.json index dd19183d3..ee9aae2b7 100644 --- a/l10n/bundle.l10n.json +++ b/l10n/bundle.l10n.json @@ -856,7 +856,6 @@ "See output for more details.": "See output for more details.", "Select {0}": "Select {0}", "Select {mongoExecutableFileName}": "Select {mongoExecutableFileName}", - "Select a format to copy": "Select a format to copy", "Select a location for new resources.": "Select a location for new resources.", "Select a tenant for Microsoft Entra ID authentication": "Select a tenant for Microsoft Entra ID authentication", "Select a workspace folder": "Select a workspace folder", diff --git a/src/commands/copyReference/copyReference.ts b/src/commands/copyReference/copyReference.ts index a9cfbae98..39b7e74d6 100644 --- a/src/commands/copyReference/copyReference.ts +++ b/src/commands/copyReference/copyReference.ts @@ -10,8 +10,11 @@ import { CollectionItem } from '../../tree/documentdb/CollectionItem'; import { type DatabaseItem } from '../../tree/documentdb/DatabaseItem'; import { IndexItem } from '../../tree/documentdb/IndexItem'; -interface CopyReferenceOption extends vscode.QuickPickItem { - value: string; +interface CopyReferenceOption { + id: string; + label: string; + detail: string; + alwaysShow: true; } function formatIndexKey(key: Record): string { @@ -50,29 +53,18 @@ function getClusterHost(connectionString: string | undefined): string | undefine } } +function opt(label: string, value: string): CopyReferenceOption { + return { id: value, label, detail: value, alwaysShow: true }; +} + function getDatabaseOptions(node: DatabaseItem): CopyReferenceOption[] { const dbName = node.databaseInfo.name; const host = getClusterHost(node.cluster.connectionString); - const options: CopyReferenceOption[] = [ - { - label: l10n.t('Name'), - detail: dbName, - value: dbName, - }, - { - label: l10n.t('Shell Command'), - detail: `use ${dbName}`, - value: `use ${dbName}`, - }, - ]; + const options: CopyReferenceOption[] = [opt(l10n.t('Name'), dbName), opt(l10n.t('Shell Command'), `use ${dbName}`)]; if (host) { - options.push({ - label: l10n.t('Qualified Name'), - detail: `${host}/${dbName}`, - value: `${host}/${dbName}`, - }); + options.push(opt(l10n.t('Qualified Name'), `${host}/${dbName}`)); } return options; @@ -84,35 +76,14 @@ function getCollectionOptions(node: CollectionItem): CopyReferenceOption[] { const quoted = needsQuoting(collName) || needsQuoting(dbName); const escapedCollName = escapeDoubleQuotes(collName); - const options: CopyReferenceOption[] = [ - { - label: l10n.t('Name'), - detail: collName, - value: collName, - }, - ]; - - if (!quoted) { - options.push({ - label: l10n.t('Namespace'), - detail: `${dbName}.${collName}`, - value: `${dbName}.${collName}`, - }); - } + const options: CopyReferenceOption[] = [opt(l10n.t('Name'), collName)]; if (!quoted) { - options.push({ - label: l10n.t('Shell Reference'), - detail: `db.${collName}`, - value: `db.${collName}`, - }); + options.push(opt(l10n.t('Namespace'), `${dbName}.${collName}`)); + options.push(opt(l10n.t('Shell Reference'), `db.${collName}`)); } - options.push({ - label: l10n.t('Shell Command'), - detail: `db.getCollection("${escapedCollName}")`, - value: `db.getCollection("${escapedCollName}")`, - }); + options.push(opt(l10n.t('Shell Command'), `db.getCollection("${escapedCollName}")`)); return options; } @@ -121,31 +92,17 @@ function getIndexOptions(node: IndexItem): CopyReferenceOption[] { const indexName = node.indexInfo.name; const collName = node.collectionInfo.name; - const options: CopyReferenceOption[] = [ - { - label: l10n.t('Name'), - detail: indexName, - value: indexName, - }, - ]; + const options: CopyReferenceOption[] = [opt(l10n.t('Name'), indexName)]; if (node.indexInfo.key) { const keyDef = formatIndexKey(node.indexInfo.key); - - options.push({ - label: l10n.t('Key Definition'), - detail: keyDef, - value: keyDef, - }); - const collRef = shellCollectionRef(collName); const escapedIndexName = escapeDoubleQuotes(indexName); - options.push({ - label: l10n.t('Shell Command'), - detail: `${collRef}.getIndexes().find(i => i.name === "${escapedIndexName}")`, - value: `${collRef}.getIndexes().find(i => i.name === "${escapedIndexName}")`, - }); + options.push(opt(l10n.t('Key Definition'), keyDef)); + options.push( + opt(l10n.t('Shell Command'), `${collRef}.getIndexes().find(i => i.name === "${escapedIndexName}")`), + ); } return options; @@ -176,7 +133,7 @@ function getOptionsForNode(node: DatabaseItem | CollectionItem | IndexItem): { } export async function copyReference( - _context: IActionContext, + context: IActionContext, node: DatabaseItem | CollectionItem | IndexItem, ): Promise { if (!node) { @@ -185,32 +142,12 @@ export async function copyReference( const { title, options } = getOptionsForNode(node); - const picker = vscode.window.createQuickPick(); - picker.title = title; - picker.placeholder = l10n.t('Select a format to copy'); - picker.items = options; - picker.matchOnDetail = true; - - // Workaround: setting sortByLabel to false via the options object is not available - // in the createQuickPick API, but we can suppress persistence by not setting a value - // for the quickpick's value property, which avoids reordering. - - const result = await new Promise((resolve) => { - picker.onDidAccept(() => { - resolve(picker.selectedItems[0] as CopyReferenceOption | undefined); - picker.dispose(); - }); - picker.onDidHide(() => { - resolve(undefined); - picker.dispose(); - }); - picker.show(); + const picked = await context.ui.showQuickPick(options, { + placeHolder: title, + stepName: 'copyReference', + suppressPersistence: true, }); - if (!result) { - return; - } - - await vscode.env.clipboard.writeText(result.value); + await vscode.env.clipboard.writeText(picked.id); void vscode.window.showInformationMessage(l10n.t('Copied to clipboard')); } From 4a1b7f623c822f26c74a369dffc818ce04e3d84f Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Tue, 14 Apr 2026 13:10:47 +0200 Subject: [PATCH 13/28] Feature: Add selected authentication method and connection user to connection models and tooltips --- .../ConnectionsBranchDataProvider.ts | 2 + .../connections-view/DocumentDBClusterItem.ts | 92 ++++++++++++++++--- src/tree/connections-view/FolderItem.ts | 2 + .../LocalEmulators/LocalEmulatorsItem.ts | 2 + .../models/ConnectionClusterModel.ts | 13 +++ 5 files changed, 97 insertions(+), 14 deletions(-) diff --git a/src/tree/connections-view/ConnectionsBranchDataProvider.ts b/src/tree/connections-view/ConnectionsBranchDataProvider.ts index 052054c35..359122a9e 100644 --- a/src/tree/connections-view/ConnectionsBranchDataProvider.ts +++ b/src/tree/connections-view/ConnectionsBranchDataProvider.ts @@ -150,6 +150,8 @@ export class ConnectionsBranchDataProvider extends BaseExtendedTreeDataProvider< dbExperience: DocumentDBExperience, connectionString: connection.secrets.connectionString, emulatorConfiguration: connection.properties.emulatorConfiguration, + selectedAuthMethod: connection.properties.selectedAuthMethod, + connectionUser: connection.secrets.nativeAuthConfig?.connectionUser, }; ext.outputChannel.trace( diff --git a/src/tree/connections-view/DocumentDBClusterItem.ts b/src/tree/connections-view/DocumentDBClusterItem.ts index 34bcbbf69..c6ebacdb2 100644 --- a/src/tree/connections-view/DocumentDBClusterItem.ts +++ b/src/tree/connections-view/DocumentDBClusterItem.ts @@ -13,7 +13,13 @@ import * as l10n from '@vscode/l10n'; import * as vscode from 'vscode'; import { nonNullProp } from '../../utils/nonNull'; -import { authMethodFromString, AuthMethodId, authMethodsFromString } from '../../documentdb/auth/AuthMethod'; +import { + authMethodFromString, + AuthMethodId, + authMethodsFromString, + getAuthMethod, + isSupportedAuthMethod, +} from '../../documentdb/auth/AuthMethod'; import { ClustersClient } from '../../documentdb/ClustersClient'; import { CredentialCache } from '../../documentdb/CredentialCache'; import { DocumentDBConnectionString } from '../../documentdb/utils/DocumentDBConnectionString'; @@ -30,6 +36,14 @@ import { type TreeCluster } from '../models/BaseClusterModel'; import { type TreeElementWithStorageId } from '../TreeElementWithStorageId'; import { type ConnectionClusterModel } from './models/ConnectionClusterModel'; +/** + * Escapes markdown special characters so user-provided text is always rendered + * as plain text rather than being interpreted as markdown formatting or links. + */ +function escapeMarkdown(text: string): string { + return text.replace(/[\\`*_{}[\]()#+\-.!|~]/g, '\\$&'); +} + export class DocumentDBClusterItem extends ClusterItemBase implements TreeElementWithStorageId { public override readonly cluster: TreeCluster; @@ -309,19 +323,12 @@ export class DocumentDBClusterItem extends ClusterItemBase 0) { + md.appendMarkdown(`**${l10n.t('Host')}:** ${hosts.join(', ')}\n\n`); + } + + // Auth method + const authMethodId = this.cluster.selectedAuthMethod; + if (authMethodId) { + const authLabel = isSupportedAuthMethod(authMethodId) ? getAuthMethod(authMethodId).label : authMethodId; + md.appendMarkdown(`**${l10n.t('Auth')}:** ${authLabel}\n\n`); + + if (authMethodId === AuthMethodId.NativeAuth && this.cluster.connectionUser) { + md.appendMarkdown(`**${l10n.t('User')}:** ${escapeMarkdown(this.cluster.connectionUser)}\n\n`); + } + } + + // Emulator security notice + if (this.cluster.emulatorConfiguration?.isEmulator) { + if (this.cluster.emulatorConfiguration.disableEmulatorSecurity) { + md.appendMarkdown(`⚠️ **${l10n.t('Security')}:** ${l10n.t('TLS/SSL Disabled')}\n\n`); + } else { + md.appendMarkdown(`✅ **${l10n.t('Security')}:** ${l10n.t('TLS/SSL Enabled')}\n\n`); + } + } + + return md; + } + + /** + * Extracts the host(s) from the connection string for display in the tooltip. + * Returns an empty array if the connection string is unavailable or unparseable. + */ + private getHosts(): string[] { + if (!this.cluster.connectionString) { + return []; + } + try { + return new DocumentDBConnectionString(this.cluster.connectionString).hosts ?? []; + } catch { + return []; + } + } } diff --git a/src/tree/connections-view/FolderItem.ts b/src/tree/connections-view/FolderItem.ts index 398f3a6bb..795fc1022 100644 --- a/src/tree/connections-view/FolderItem.ts +++ b/src/tree/connections-view/FolderItem.ts @@ -107,6 +107,8 @@ export class FolderItem implements TreeElement, TreeElementWithContextValue { dbExperience: DocumentDBExperience, connectionString: child?.secrets?.connectionString ?? undefined, emulatorConfiguration: child.properties.emulatorConfiguration, + selectedAuthMethod: child.properties.selectedAuthMethod, + connectionUser: child.secrets?.nativeAuthConfig?.connectionUser, }; ext.outputChannel.trace( diff --git a/src/tree/connections-view/LocalEmulators/LocalEmulatorsItem.ts b/src/tree/connections-view/LocalEmulators/LocalEmulatorsItem.ts index d4fe00261..20fb14d4b 100644 --- a/src/tree/connections-view/LocalEmulators/LocalEmulatorsItem.ts +++ b/src/tree/connections-view/LocalEmulators/LocalEmulatorsItem.ts @@ -69,6 +69,8 @@ export class LocalEmulatorsItem implements TreeElement, TreeElementWithContextVa dbExperience: DocumentDBExperience, connectionString: connection.secrets.connectionString, emulatorConfiguration: emulatorConfiguration, + selectedAuthMethod: connection.properties.selectedAuthMethod, + connectionUser: connection.secrets.nativeAuthConfig?.connectionUser, }; ext.outputChannel.trace( diff --git a/src/tree/connections-view/models/ConnectionClusterModel.ts b/src/tree/connections-view/models/ConnectionClusterModel.ts index 127f7334e..2b4fc24ea 100644 --- a/src/tree/connections-view/models/ConnectionClusterModel.ts +++ b/src/tree/connections-view/models/ConnectionClusterModel.ts @@ -27,4 +27,17 @@ export interface ConnectionClusterModel extends BaseClusterModel { * Present when this connection represents a local emulator instance. */ emulatorConfiguration?: EmulatorConfiguration; + + /** + * The selected authentication method ID (e.g. 'NativeAuth', 'MicrosoftEntraID'). + * Populated from storage when the tree item is built, used for tooltip display. + */ + selectedAuthMethod?: string; + + /** + * The connection username for native (SCRAM) authentication. + * Populated from storage when the tree item is built, used for tooltip display. + * Never contains a password. + */ + connectionUser?: string; } From d0754ee41f56e5636d59d9a4ef900ed03c5eee58 Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Thu, 16 Apr 2026 15:04:57 +0200 Subject: [PATCH 14/28] feat: add formatSize utility for formatting byte sizes --- src/utils/formatSize.ts | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 src/utils/formatSize.ts diff --git a/src/utils/formatSize.ts b/src/utils/formatSize.ts new file mode 100644 index 000000000..2767466df --- /dev/null +++ b/src/utils/formatSize.ts @@ -0,0 +1,30 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/** + * Formats a byte size for display. + * Uses binary units (KB, MB, GB, TB) with up to 1 decimal place. + * + * @param bytes The size in bytes to format + * @returns Formatted string representation (e.g., "1.2 MB", "500 B") + */ +export function formatSize(bytes: number): string { + if (bytes < 1024) { + return `${bytes} B`; + } + + const units = ['KB', 'MB', 'GB', 'TB']; + let value = bytes; + let unitIndex = -1; + + while (value >= 1024 && unitIndex < units.length - 1) { + value /= 1024; + unitIndex++; + } + + // Use up to 1 decimal place, drop trailing ".0" + const formatted = value % 1 === 0 ? value.toFixed(0) : value.toFixed(1); + return `${formatted} ${units[unitIndex]}`; +} From 99ffa650c1b294a78bf3858083c7731a9c460a0d Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Thu, 16 Apr 2026 15:05:42 +0200 Subject: [PATCH 15/28] feat: add database-level tooltip with size on disk Show the database size on disk in both the tree item description and a markdown tooltip. Size data comes from listDatabases() which already populates DatabaseItemModel.sizeOnDisk at no extra cost. --- src/tree/documentdb/DatabaseItem.ts | 31 +++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/src/tree/documentdb/DatabaseItem.ts b/src/tree/documentdb/DatabaseItem.ts index e3b14580e..768877445 100644 --- a/src/tree/documentdb/DatabaseItem.ts +++ b/src/tree/documentdb/DatabaseItem.ts @@ -8,12 +8,21 @@ import * as l10n from '@vscode/l10n'; import * as vscode from 'vscode'; import { ClustersClient, type DatabaseItemModel } from '../../documentdb/ClustersClient'; import { type Experience } from '../../DocumentDBExperiences'; +import { formatSize } from '../../utils/formatSize'; import { type BaseClusterModel, type TreeCluster } from '../models/BaseClusterModel'; import { type TreeElement } from '../TreeElement'; import { type TreeElementWithContextValue } from '../TreeElementWithContextValue'; import { type TreeElementWithExperience } from '../TreeElementWithExperience'; import { CollectionItem } from './CollectionItem'; +/** + * Escapes markdown special characters so user-provided text is always rendered + * as plain text rather than being interpreted as markdown formatting or links. + */ +function escapeMarkdown(text: string): string { + return text.replace(/[\\`*_{}[\]()#+\-.!|~]/g, '\\$&'); +} + export class DatabaseItem implements TreeElement, TreeElementWithExperience, TreeElementWithContextValue { public readonly id: string; public readonly experience: Experience; @@ -62,12 +71,34 @@ export class DatabaseItem implements TreeElement, TreeElementWithExperience, Tre } getTreeItem(): vscode.TreeItem { + // Show size on disk as the description (e.g., "1.2 MB") + const description = + typeof this.databaseInfo.sizeOnDisk === 'number' ? formatSize(this.databaseInfo.sizeOnDisk) : undefined; + return { id: this.id, contextValue: this.contextValue, label: this.databaseInfo.name, + description, + tooltip: this.buildTooltip(), iconPath: new vscode.ThemeIcon('database'), // TODO: create our own icon here, this one's shape can change collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, }; } + + /** + * Builds a markdown tooltip showing the database name and size on disk. + */ + private buildTooltip(): vscode.MarkdownString { + const md = new vscode.MarkdownString(); + md.isTrusted = false; + + md.appendMarkdown(`### ${escapeMarkdown(this.databaseInfo.name)}\n\n`); + + if (typeof this.databaseInfo.sizeOnDisk === 'number') { + md.appendMarkdown(`**${l10n.t('Size on Disk')}:** ${formatSize(this.databaseInfo.sizeOnDisk)}\n\n`); + } + + return md; + } } From 9f2fdcc3811f2a5e17b8cdb125f6c023f0426aa3 Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Thu, 16 Apr 2026 15:06:28 +0200 Subject: [PATCH 16/28] feat: add collection-level tooltip with type and document count Show the collection type (collection/view/timeseries) as a badge and document count in a markdown tooltip. Also display the parent database name for context. All data is already available from listCollections() and the async document count loader. --- src/tree/documentdb/CollectionItem.ts | 36 +++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/src/tree/documentdb/CollectionItem.ts b/src/tree/documentdb/CollectionItem.ts index 48dc6d35a..415238cc4 100644 --- a/src/tree/documentdb/CollectionItem.ts +++ b/src/tree/documentdb/CollectionItem.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { createContextValue } from '@microsoft/vscode-azext-utils'; +import * as l10n from '@vscode/l10n'; import * as vscode from 'vscode'; import { ClustersClient, type CollectionItemModel, type DatabaseItemModel } from '../../documentdb/ClustersClient'; import { type Experience } from '../../DocumentDBExperiences'; @@ -16,6 +17,14 @@ import { type TreeElementWithExperience } from '../TreeElementWithExperience'; import { DocumentsItem } from './DocumentsItem'; import { IndexesItem } from './IndexesItem'; +/** + * Escapes markdown special characters so user-provided text is always rendered + * as plain text rather than being interpreted as markdown formatting or links. + */ +function escapeMarkdown(text: string): string { + return text.replace(/[\\`*_{}[\]()#+\-.!|~]/g, '\\$&'); +} + export class CollectionItem implements TreeElement, TreeElementWithExperience, TreeElementWithContextValue { public readonly id: string; public readonly experience: Experience; @@ -98,8 +107,35 @@ export class CollectionItem implements TreeElement, TreeElementWithExperience, T contextValue: this.contextValue, label: this.collectionInfo.name, description, + tooltip: this.buildTooltip(), iconPath: new vscode.ThemeIcon('folder-library'), collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, }; } + + /** + * Builds a markdown tooltip showing the collection name, type, and document count. + */ + private buildTooltip(): vscode.MarkdownString { + const md = new vscode.MarkdownString(); + md.isTrusted = false; + + md.appendMarkdown(`### ${escapeMarkdown(this.collectionInfo.name)}\n\n`); + + // Type badge (collection, view, timeseries) + const collectionType = this.collectionInfo.type ?? 'collection'; + md.appendMarkdown(`\`${collectionType}\`\n\n`); + + md.appendMarkdown('---\n\n'); + + // Database context + md.appendMarkdown(`**${l10n.t('Database')}:** ${escapeMarkdown(this.databaseInfo.name)}\n\n`); + + // Document count + if (typeof this.documentCount === 'number') { + md.appendMarkdown(`**${l10n.t('Documents')}:** ${formatDocumentCount(this.documentCount)}\n\n`); + } + + return md; + } } From 4bc014d49af2cd495608c6b478adc5e861fc7c67 Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Thu, 16 Apr 2026 15:09:41 +0200 Subject: [PATCH 17/28] chore: update l10n bundle for tooltip strings --- l10n/bundle.l10n.json | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/l10n/bundle.l10n.json b/l10n/bundle.l10n.json index a4493f78a..2449a3937 100644 --- a/l10n/bundle.l10n.json +++ b/l10n/bundle.l10n.json @@ -118,11 +118,9 @@ "▶️ Run Command": "▶️ Run Command", "► Task '{taskName}' starting...": "► Task '{taskName}' starting...", "○ Task '{taskName}' initializing...": "○ Task '{taskName}' initializing...", - "⚠️ **Security:** TLS/SSL Disabled": "⚠️ **Security:** TLS/SSL Disabled", "⚠️ existing collection": "⚠️ existing collection", "⚠ TLS/SSL Disabled": "⚠ TLS/SSL Disabled", "⚠️ Warning: This will modify the existing collection. Documents with matching _id values will be handled based on your conflict resolution setting.": "⚠️ Warning: This will modify the existing collection. Documents with matching _id values will be handled based on your conflict resolution setting.", - "✅ **Security:** TLS/SSL Enabled": "✅ **Security:** TLS/SSL Enabled", "✓ Task '{taskName}' completed successfully. {message}": "✓ Task '{taskName}' completed successfully. {message}", "$(add) Create...": "$(add) Create...", "$(arrow-left) Go Back": "$(arrow-left) Go Back", @@ -184,6 +182,7 @@ "Are you sure?": "Are you sure?", "Ask Copilot to generate the query for you": "Ask Copilot to generate the query for you", "Attempting to authenticate with \"{cluster}\"…": "Attempting to authenticate with \"{cluster}\"…", + "Auth": "Auth", "Authenticate to connect with your DocumentDB cluster": "Authenticate to connect with your DocumentDB cluster", "Authenticate to Connect with Your DocumentDB Cluster": "Authenticate to Connect with Your DocumentDB Cluster", "Authenticate using a username and password": "Authenticate using a username and password", @@ -326,6 +325,7 @@ "Credentials updated successfully.": "Credentials updated successfully.", "Data shown was correct": "Data shown was correct", "Data shown was incorrect": "Data shown was incorrect", + "Database": "Database", "database \"{0}\"": "database \"{0}\"", "Database name cannot be longer than 64 characters.": "Database name cannot be longer than 64 characters.", "Database name cannot contain any of the following characters: \"{0}{1}\"": "Database name cannot contain any of the following characters: \"{0}{1}\"", @@ -548,6 +548,7 @@ "Hide Index…": "Hide Index…", "Hiding index…": "Hiding index…", "HIGH PRIORITY": "HIGH PRIORITY", + "Host": "Host", "How do you want to connect?": "How do you want to connect?", "How should conflicts be handled during the copy operation?": "How should conflicts be handled during the copy operation?", "How would you rate Query Insights?": "How would you rate Query Insights?", @@ -846,6 +847,7 @@ "Save to the database": "Save to the database", "Saving \"{path}\" will update the entity \"{name}\" to the cloud.": "Saving \"{path}\" will update the entity \"{name}\" to the cloud.", "Saving credentials for \"{clusterName}\"…": "Saving credentials for \"{clusterName}\"…", + "Security": "Security", "See output for more details.": "See output for more details.", "Select {0}": "Select {0}", "Select {mongoExecutableFileName}": "Select {mongoExecutableFileName}", @@ -886,6 +888,7 @@ "Signed in to tenant \"{0}\"": "Signed in to tenant \"{0}\"", "Signing out programmatically is not supported. You must sign out by selecting the account in the Accounts menu and choosing Sign Out.": "Signing out programmatically is not supported. You must sign out by selecting the account in the Accounts menu and choosing Sign Out.", "Simulated failure at step {0} for testing purposes": "Simulated failure at step {0} for testing purposes", + "Size on Disk": "Size on Disk", "Skip": "Skip", "Skip and Log (continue)": "Skip and Log (continue)", "Skip for now": "Skip for now", @@ -1014,6 +1017,8 @@ "This will also delete {0}.": "This will also delete {0}.", "This will prevent the query planner from using this index.": "This will prevent the query planner from using this index.", "Timed out trying to execute the Mongo script. To use a longer timeout, modify the VS Code 'mongo.shell.timeout' setting.": "Timed out trying to execute the Mongo script. To use a longer timeout, modify the VS Code 'mongo.shell.timeout' setting.", + "TLS/SSL Disabled": "TLS/SSL Disabled", + "TLS/SSL Enabled": "TLS/SSL Enabled", "To connect to Azure resources, you need to sign in to Azure accounts.": "To connect to Azure resources, you need to sign in to Azure accounts.", "TODO: Share the steps needed to reliably reproduce the problem. Please include actual and expected results.": "TODO: Share the steps needed to reliably reproduce the problem. Please include actual and expected results.", "Too many arguments. Expecting 0 or 1 argument(s) to {constructorCall}": "Too many arguments. Expecting 0 or 1 argument(s) to {constructorCall}", @@ -1064,6 +1069,7 @@ "URL handling aborted. Connection was unsuccessful or the specified database/collection does not exist.": "URL handling aborted. Connection was unsuccessful or the specified database/collection does not exist.", "Use anyway": "Use anyway", "Use projection to return only necessary fields. This reduces network transfer and memory usage, especially important for documents with large embedded arrays or binary data.": "Use projection to return only necessary fields. This reduces network transfer and memory usage, especially important for documents with large embedded arrays or binary data.", + "User": "User", "Username and Password": "Username and Password", "Username cannot be empty": "Username cannot be empty", "Username contains characters that cannot be safely encoded.": "Username contains characters that cannot be safely encoded.", From b4975f424d3f9454ed26a025d54d681e9e96be02 Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Thu, 16 Apr 2026 15:28:00 +0200 Subject: [PATCH 18/28] fix: drop sizeOnDisk from database tooltip, capitalize type badges DocumentDB returns sizeOnDisk as 0, so remove it from the database tooltip and description. Add a 'Database' badge to the database tooltip for consistency. Capitalize the collection type badge (Collection, View, Timeseries). --- l10n/bundle.l10n.json | 1 - src/tree/documentdb/CollectionItem.ts | 5 +++-- src/tree/documentdb/DatabaseItem.ts | 12 ++---------- 3 files changed, 5 insertions(+), 13 deletions(-) diff --git a/l10n/bundle.l10n.json b/l10n/bundle.l10n.json index 2449a3937..4fc0f9cc1 100644 --- a/l10n/bundle.l10n.json +++ b/l10n/bundle.l10n.json @@ -888,7 +888,6 @@ "Signed in to tenant \"{0}\"": "Signed in to tenant \"{0}\"", "Signing out programmatically is not supported. You must sign out by selecting the account in the Accounts menu and choosing Sign Out.": "Signing out programmatically is not supported. You must sign out by selecting the account in the Accounts menu and choosing Sign Out.", "Simulated failure at step {0} for testing purposes": "Simulated failure at step {0} for testing purposes", - "Size on Disk": "Size on Disk", "Skip": "Skip", "Skip and Log (continue)": "Skip and Log (continue)", "Skip for now": "Skip for now", diff --git a/src/tree/documentdb/CollectionItem.ts b/src/tree/documentdb/CollectionItem.ts index 415238cc4..c5e79b427 100644 --- a/src/tree/documentdb/CollectionItem.ts +++ b/src/tree/documentdb/CollectionItem.ts @@ -122,9 +122,10 @@ export class CollectionItem implements TreeElement, TreeElementWithExperience, T md.appendMarkdown(`### ${escapeMarkdown(this.collectionInfo.name)}\n\n`); - // Type badge (collection, view, timeseries) + // Type badge (Collection, View, Timeseries) const collectionType = this.collectionInfo.type ?? 'collection'; - md.appendMarkdown(`\`${collectionType}\`\n\n`); + const capitalizedType = collectionType.charAt(0).toUpperCase() + collectionType.slice(1); + md.appendMarkdown(`\`${capitalizedType}\`\n\n`); md.appendMarkdown('---\n\n'); diff --git a/src/tree/documentdb/DatabaseItem.ts b/src/tree/documentdb/DatabaseItem.ts index 768877445..c6a8eef47 100644 --- a/src/tree/documentdb/DatabaseItem.ts +++ b/src/tree/documentdb/DatabaseItem.ts @@ -8,7 +8,6 @@ import * as l10n from '@vscode/l10n'; import * as vscode from 'vscode'; import { ClustersClient, type DatabaseItemModel } from '../../documentdb/ClustersClient'; import { type Experience } from '../../DocumentDBExperiences'; -import { formatSize } from '../../utils/formatSize'; import { type BaseClusterModel, type TreeCluster } from '../models/BaseClusterModel'; import { type TreeElement } from '../TreeElement'; import { type TreeElementWithContextValue } from '../TreeElementWithContextValue'; @@ -71,15 +70,10 @@ export class DatabaseItem implements TreeElement, TreeElementWithExperience, Tre } getTreeItem(): vscode.TreeItem { - // Show size on disk as the description (e.g., "1.2 MB") - const description = - typeof this.databaseInfo.sizeOnDisk === 'number' ? formatSize(this.databaseInfo.sizeOnDisk) : undefined; - return { id: this.id, contextValue: this.contextValue, label: this.databaseInfo.name, - description, tooltip: this.buildTooltip(), iconPath: new vscode.ThemeIcon('database'), // TODO: create our own icon here, this one's shape can change collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, @@ -87,7 +81,7 @@ export class DatabaseItem implements TreeElement, TreeElementWithExperience, Tre } /** - * Builds a markdown tooltip showing the database name and size on disk. + * Builds a markdown tooltip showing the database name. */ private buildTooltip(): vscode.MarkdownString { const md = new vscode.MarkdownString(); @@ -95,9 +89,7 @@ export class DatabaseItem implements TreeElement, TreeElementWithExperience, Tre md.appendMarkdown(`### ${escapeMarkdown(this.databaseInfo.name)}\n\n`); - if (typeof this.databaseInfo.sizeOnDisk === 'number') { - md.appendMarkdown(`**${l10n.t('Size on Disk')}:** ${formatSize(this.databaseInfo.sizeOnDisk)}\n\n`); - } + md.appendMarkdown(`\`${l10n.t('Database')}\`\n\n`); return md; } From 41a1b57c6dd2ad2877fbae0f33a304239d0d4111 Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Thu, 16 Apr 2026 15:32:28 +0200 Subject: [PATCH 19/28] chore: remove unused formatSize utility --- src/utils/formatSize.ts | 30 ------------------------------ 1 file changed, 30 deletions(-) delete mode 100644 src/utils/formatSize.ts diff --git a/src/utils/formatSize.ts b/src/utils/formatSize.ts deleted file mode 100644 index 2767466df..000000000 --- a/src/utils/formatSize.ts +++ /dev/null @@ -1,30 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -/** - * Formats a byte size for display. - * Uses binary units (KB, MB, GB, TB) with up to 1 decimal place. - * - * @param bytes The size in bytes to format - * @returns Formatted string representation (e.g., "1.2 MB", "500 B") - */ -export function formatSize(bytes: number): string { - if (bytes < 1024) { - return `${bytes} B`; - } - - const units = ['KB', 'MB', 'GB', 'TB']; - let value = bytes; - let unitIndex = -1; - - while (value >= 1024 && unitIndex < units.length - 1) { - value /= 1024; - unitIndex++; - } - - // Use up to 1 decimal place, drop trailing ".0" - const formatted = value % 1 === 0 ? value.toFixed(0) : value.toFixed(1); - return `${formatted} ${units[unitIndex]}`; -} From aeb996096ea35fd76652a64ad144913d8f4988c2 Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Thu, 16 Apr 2026 16:14:14 +0200 Subject: [PATCH 20/28] fix: escape hosts and auth label in cluster tooltip markdown Escape each host and the auth-method label through escapeMarkdown() before appending them as markdown, preventing user-controlled values from rendering unexpected formatting or links. --- src/tree/connections-view/DocumentDBClusterItem.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/tree/connections-view/DocumentDBClusterItem.ts b/src/tree/connections-view/DocumentDBClusterItem.ts index c6ebacdb2..e08264dfb 100644 --- a/src/tree/connections-view/DocumentDBClusterItem.ts +++ b/src/tree/connections-view/DocumentDBClusterItem.ts @@ -359,14 +359,15 @@ export class DocumentDBClusterItem extends ClusterItemBase 0) { - md.appendMarkdown(`**${l10n.t('Host')}:** ${hosts.join(', ')}\n\n`); + const escapedHosts = hosts.map((host) => escapeMarkdown(host)); + md.appendMarkdown(`**${l10n.t('Host')}:** ${escapedHosts.join(', ')}\n\n`); } // Auth method const authMethodId = this.cluster.selectedAuthMethod; if (authMethodId) { const authLabel = isSupportedAuthMethod(authMethodId) ? getAuthMethod(authMethodId).label : authMethodId; - md.appendMarkdown(`**${l10n.t('Auth')}:** ${authLabel}\n\n`); + md.appendMarkdown(`**${l10n.t('Auth')}:** ${escapeMarkdown(authLabel)}\n\n`); if (authMethodId === AuthMethodId.NativeAuth && this.cluster.connectionUser) { md.appendMarkdown(`**${l10n.t('User')}:** ${escapeMarkdown(this.cluster.connectionUser)}\n\n`); From 9ee1629eb8c9b4c2aee7462a1d69dd771c843d51 Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Thu, 16 Apr 2026 16:19:48 +0200 Subject: [PATCH 21/28] fix: resolve unsafe enum comparison lint error in tooltip Guard the AuthMethodId.NativeAuth comparison with isSupportedAuthMethod() to satisfy the @typescript-eslint/no-unsafe-enum-comparison rule. --- src/tree/connections-view/DocumentDBClusterItem.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/tree/connections-view/DocumentDBClusterItem.ts b/src/tree/connections-view/DocumentDBClusterItem.ts index e08264dfb..9bd7d2f06 100644 --- a/src/tree/connections-view/DocumentDBClusterItem.ts +++ b/src/tree/connections-view/DocumentDBClusterItem.ts @@ -366,10 +366,11 @@ export class DocumentDBClusterItem extends ClusterItemBase Date: Mon, 19 Jan 2026 19:08:01 -0700 Subject: [PATCH 22/28] Added functionality for URL decoding of password and suggesting to fix it for the user --- .../connections-view/DocumentDBClusterItem.ts | 83 +++++++++++++++++-- 1 file changed, 75 insertions(+), 8 deletions(-) diff --git a/src/tree/connections-view/DocumentDBClusterItem.ts b/src/tree/connections-view/DocumentDBClusterItem.ts index 9bd7d2f06..5605f119a 100644 --- a/src/tree/connections-view/DocumentDBClusterItem.ts +++ b/src/tree/connections-view/DocumentDBClusterItem.ts @@ -249,17 +249,84 @@ export class DocumentDBClusterItem extends ClusterItemBase Date: Tue, 21 Apr 2026 13:34:30 +0200 Subject: [PATCH 23/28] chore: update l10n bundle after adding URL-encoded password strings --- l10n/bundle.l10n.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/l10n/bundle.l10n.json b/l10n/bundle.l10n.json index 22bf10345..f97d921d3 100644 --- a/l10n/bundle.l10n.json +++ b/l10n/bundle.l10n.json @@ -267,6 +267,7 @@ "Conflict Resolution: {strategyName}": "Conflict Resolution: {strategyName}", "Connect to a database": "Connect to a database", "Connected to \"{name}\"": "Connected to \"{name}\"", + "Connected to the cluster \"{cluster}\" using decoded password.": "Connected to the cluster \"{cluster}\" using decoded password.", "Connected to the cluster \"{cluster}\".": "Connected to the cluster \"{cluster}\".", "Connecting to \"{cluster}\"…": "Connecting to \"{cluster}\"…", "Connecting to the cluster as \"{username}\"…": "Connecting to the cluster as \"{username}\"…", @@ -841,6 +842,8 @@ "Resource group \"{0}\" already exists in subscription \"{1}\".": "Resource group \"{0}\" already exists in subscription \"{1}\".", "Results found": "Results found", "Retry": "Retry", + "Retry Error: {error}": "Retry Error: {error}", + "Retry with Decoded Password": "Retry with Decoded Password", "Reusing active connection for \"{cluster}\".": "Reusing active connection for \"{cluster}\".", "Revisit connection details and try again.": "Revisit connection details and try again.", "Role assignment \"{0}\" created for the {2} resource \"{1}\".": "Role assignment \"{0}\" created for the {2} resource \"{1}\".", @@ -1132,6 +1135,7 @@ "Your Cluster": "Your Cluster", "Your database stores documents with embedded fields, allowing for hierarchical data organization.": "Your database stores documents with embedded fields, allowing for hierarchical data organization.", "Your feedback helps us improve Query Insights. Tell us what could be better:": "Your feedback helps us improve Query Insights. Tell us what could be better:", + "Your password appears to be URL-encoded.": "Your password appears to be URL-encoded.", "Your positive feedback helps us understand what works well in Query Insights. Tell us more:": "Your positive feedback helps us understand what works well in Query Insights. Tell us more:", "Your query is performing well. You can still use the AI-powered analysis to get a detailed explanation of the query execution, review the indexing, and explore if further optimizations are possible.": "Your query is performing well. You can still use the AI-powered analysis to get a detailed explanation of the query execution, review the indexing, and explore if further optimizations are possible.", "Your VS Code window must be reloaded to perform this action.": "Your VS Code window must be reloaded to perform this action." From be405884b8621b6be57e9ee91fd1323747d77be9 Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Tue, 21 Apr 2026 15:00:35 +0200 Subject: [PATCH 24/28] feat: implement URL-encoded password handling with user confirmation for retries --- src/documentdb/auth/urlEncodedPassword.ts | 124 ++++++++++++++++++ .../connections-view/DocumentDBClusterItem.ts | 55 +++----- 2 files changed, 143 insertions(+), 36 deletions(-) create mode 100644 src/documentdb/auth/urlEncodedPassword.ts diff --git a/src/documentdb/auth/urlEncodedPassword.ts b/src/documentdb/auth/urlEncodedPassword.ts new file mode 100644 index 000000000..3fddc0dc6 --- /dev/null +++ b/src/documentdb/auth/urlEncodedPassword.ts @@ -0,0 +1,124 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { type IActionContext } from '@microsoft/vscode-azext-utils'; +import * as l10n from '@vscode/l10n'; +import * as vscode from 'vscode'; + +/** + * Helpers for detecting and recovering from URL-encoded passwords. + * + * Users sometimes paste a password copied from a connection-string URL and + * forget to URL-decode it. The server then rejects the credentials because + * the actual password characters differ from the encoded form (e.g. "p%40ss" + * vs. "p@ss"). Rather than silently retrying — which could trip brute-force + * lockouts on the server side — we show a one-time prompt and let the user + * decide whether to retry with the decoded value. + */ + +/** Matches a `%XX` percent-encoded byte. */ +const URL_ENCODED_BYTE_PATTERN = /%[0-9A-Fa-f]{2}/; + +/** + * Returns the decoded password if `password` contains `%XX` sequences and + * `decodeURIComponent` produces a different, non-empty string; otherwise + * returns `undefined`. Intended as a hint — callers must never auto-apply + * the decoded value without user consent. + */ +export function tryDecodeUrlEncodedPassword(password: string | undefined): string | undefined { + if (!password || !URL_ENCODED_BYTE_PATTERN.test(password)) { + return undefined; + } + + let decoded: string; + try { + decoded = decodeURIComponent(password); + } catch { + return undefined; + } + + if (decoded.length === 0 || decoded === password) { + return undefined; + } + + return decoded; +} + +/** + * Telemetry property keys reported by {@link showConnectionFailedAndMaybeOfferDecodedRetry}. + * No password material is ever recorded — only boolean indicators about the flow. + */ +export const UrlEncodedPasswordTelemetry = { + /** `true` when the original password looked URL-encoded and a decoded variant was available. */ + Detected: 'urlDecodePasswordDetected', + /** `true` when the user was shown the "Retry with decoded password" button. */ + Offered: 'urlDecodePasswordOffered', + /** `true` when the user clicked the retry button. */ + Accepted: 'urlDecodePasswordAccepted', +} as const; + +export interface ShowConnectionFailedOptions { + /** Display name of the cluster shown in the error dialog. */ + readonly clusterName: string; + /** The password that was used for the failed connection attempt (may be undefined for non-password auth). */ + readonly password: string | undefined; + /** Whether the failed attempt used password-based (native) auth. Retry is only offered for native auth. */ + readonly isNativeAuth: boolean; + /** The error returned by the failed connection attempt. */ + readonly originalError: unknown; + /** Action context used to record non-sensitive telemetry about the retry flow. */ + readonly context: IActionContext; +} + +export interface ShowConnectionFailedResult { + /** + * Populated with the decoded password only when the user explicitly chose to retry. + * Callers must mask this value via `context.valuesToMask` before use. + */ + readonly decodedPassword?: string; +} + +/** + * Shows the "Failed to connect" modal. If the password looks URL-encoded, adds a + * one-time "Retry with decoded password" button. Records telemetry about whether + * the hint was offered and accepted. + */ +export async function showConnectionFailedAndMaybeOfferDecodedRetry( + options: ShowConnectionFailedOptions, +): Promise { + const { clusterName, password, isNativeAuth, originalError, context } = options; + + const decodedPassword = isNativeAuth ? tryDecodeUrlEncodedPassword(password) : undefined; + const canOfferRetry = decodedPassword !== undefined; + + context.telemetry.properties[UrlEncodedPasswordTelemetry.Detected] = canOfferRetry ? 'true' : 'false'; + context.telemetry.properties[UrlEncodedPasswordTelemetry.Offered] = canOfferRetry ? 'true' : 'false'; + + const errorMessage = originalError instanceof Error ? originalError.message : String(originalError); + + let detail = + l10n.t('Revisit connection details and try again.') + + '\n\n' + + l10n.t('Error: {error}', { error: errorMessage }); + + const retryButton = l10n.t('Retry with Decoded Password'); + const buttons: string[] = []; + + if (canOfferRetry) { + detail += '\n\n' + l10n.t('Your password appears to be URL-encoded.'); + buttons.push(retryButton); + } + + const selected = await vscode.window.showErrorMessage( + l10n.t('Failed to connect to "{cluster}"', { cluster: clusterName }), + { modal: true, detail }, + ...buttons, + ); + + const accepted = canOfferRetry && selected === retryButton; + context.telemetry.properties[UrlEncodedPasswordTelemetry.Accepted] = accepted ? 'true' : 'false'; + + return { decodedPassword: accepted ? decodedPassword : undefined }; +} diff --git a/src/tree/connections-view/DocumentDBClusterItem.ts b/src/tree/connections-view/DocumentDBClusterItem.ts index 5605f119a..9d2a9dc34 100644 --- a/src/tree/connections-view/DocumentDBClusterItem.ts +++ b/src/tree/connections-view/DocumentDBClusterItem.ts @@ -20,6 +20,7 @@ import { getAuthMethod, isSupportedAuthMethod, } from '../../documentdb/auth/AuthMethod'; +import { showConnectionFailedAndMaybeOfferDecodedRetry } from '../../documentdb/auth/urlEncodedPassword'; import { ClustersClient } from '../../documentdb/ClustersClient'; import { CredentialCache } from '../../documentdb/CredentialCache'; import { DocumentDBConnectionString } from '../../documentdb/utils/DocumentDBConnectionString'; @@ -249,45 +250,25 @@ export class DocumentDBClusterItem extends ClusterItemBase Date: Tue, 21 Apr 2026 15:08:07 +0200 Subject: [PATCH 25/28] feat: enhance URL-encoded password handling with user prompts for retries and saving credentials --- l10n/bundle.l10n.json | 6 ++-- src/documentdb/auth/urlEncodedPassword.ts | 8 ++++-- .../connections-view/DocumentDBClusterItem.ts | 28 ++++++++++++++----- 3 files changed, 31 insertions(+), 11 deletions(-) diff --git a/l10n/bundle.l10n.json b/l10n/bundle.l10n.json index f97d921d3..1ca46859a 100644 --- a/l10n/bundle.l10n.json +++ b/l10n/bundle.l10n.json @@ -122,6 +122,7 @@ "⚠ TLS/SSL Disabled": "⚠ TLS/SSL Disabled", "⚠️ Warning: This will modify the existing collection. Documents with matching _id values will be handled based on your conflict resolution setting.": "⚠️ Warning: This will modify the existing collection. Documents with matching _id values will be handled based on your conflict resolution setting.", "✓ Task '{taskName}' completed successfully. {message}": "✓ Task '{taskName}' completed successfully. {message}", + "💡 Your password appears to contain URL-encoded characters (e.g. %40 instead of @). This often happens when copying a password from a connection string URL. Would you like to retry with the decoded version?": "💡 Your password appears to contain URL-encoded characters (e.g. %40 instead of @). This often happens when copying a password from a connection string URL. Would you like to retry with the decoded version?", "$(add) Create...": "$(add) Create...", "$(arrow-left) Go Back": "$(arrow-left) Go Back", "$(check) Success": "$(check) Success", @@ -266,6 +267,7 @@ "Configuring tenant filtering…": "Configuring tenant filtering…", "Conflict Resolution: {strategyName}": "Conflict Resolution: {strategyName}", "Connect to a database": "Connect to a database", + "Connected to \"{cluster}\" using the decoded password. Would you like to update your saved credentials?": "Connected to \"{cluster}\" using the decoded password. Would you like to update your saved credentials?", "Connected to \"{name}\"": "Connected to \"{name}\"", "Connected to the cluster \"{cluster}\" using decoded password.": "Connected to the cluster \"{cluster}\" using decoded password.", "Connected to the cluster \"{cluster}\".": "Connected to the cluster \"{cluster}\".", @@ -843,7 +845,6 @@ "Results found": "Results found", "Retry": "Retry", "Retry Error: {error}": "Retry Error: {error}", - "Retry with Decoded Password": "Retry with Decoded Password", "Reusing active connection for \"{cluster}\".": "Reusing active connection for \"{cluster}\".", "Revisit connection details and try again.": "Revisit connection details and try again.", "Role assignment \"{0}\" created for the {2} resource \"{1}\".": "Role assignment \"{0}\" created for the {2} resource \"{1}\".", @@ -1038,6 +1039,7 @@ "Transforming Stage 2 response to UI format": "Transforming Stage 2 response to UI format", "Tree View": "Tree View", "Try again": "Try again", + "Try with Decoded Password": "Try with Decoded Password", "Type \"it\" for more": "Type \"it\" for more", "Unable to connect to the local database instance. Make sure it is started correctly. See {link} for tips.": "Unable to connect to the local database instance. Make sure it is started correctly. See {link} for tips.", "Unable to connect to the local instance. Make sure it is started correctly. See {link} for tips.": "Unable to connect to the local instance. Make sure it is started correctly. See {link} for tips.", @@ -1075,6 +1077,7 @@ "Update Azure Account Extension to at least version \"{0}\"...": "Update Azure Account Extension to at least version \"{0}\"...", "Update cluster credentials": "Update cluster credentials", "Update Connection String": "Update Connection String", + "Update Saved Password": "Update Saved Password", "Updated entity \"{name}\".": "Updated entity \"{name}\".", "Upload": "Upload", "URL handling aborted. Connection was unsuccessful or the specified database/collection does not exist.": "URL handling aborted. Connection was unsuccessful or the specified database/collection does not exist.", @@ -1135,7 +1138,6 @@ "Your Cluster": "Your Cluster", "Your database stores documents with embedded fields, allowing for hierarchical data organization.": "Your database stores documents with embedded fields, allowing for hierarchical data organization.", "Your feedback helps us improve Query Insights. Tell us what could be better:": "Your feedback helps us improve Query Insights. Tell us what could be better:", - "Your password appears to be URL-encoded.": "Your password appears to be URL-encoded.", "Your positive feedback helps us understand what works well in Query Insights. Tell us more:": "Your positive feedback helps us understand what works well in Query Insights. Tell us more:", "Your query is performing well. You can still use the AI-powered analysis to get a detailed explanation of the query execution, review the indexing, and explore if further optimizations are possible.": "Your query is performing well. You can still use the AI-powered analysis to get a detailed explanation of the query execution, review the indexing, and explore if further optimizations are possible.", "Your VS Code window must be reloaded to perform this action.": "Your VS Code window must be reloaded to perform this action." diff --git a/src/documentdb/auth/urlEncodedPassword.ts b/src/documentdb/auth/urlEncodedPassword.ts index 3fddc0dc6..c842e49fe 100644 --- a/src/documentdb/auth/urlEncodedPassword.ts +++ b/src/documentdb/auth/urlEncodedPassword.ts @@ -103,11 +103,15 @@ export async function showConnectionFailedAndMaybeOfferDecodedRetry( '\n\n' + l10n.t('Error: {error}', { error: errorMessage }); - const retryButton = l10n.t('Retry with Decoded Password'); + const retryButton = l10n.t('Try with Decoded Password'); const buttons: string[] = []; if (canOfferRetry) { - detail += '\n\n' + l10n.t('Your password appears to be URL-encoded.'); + detail += + '\n\n' + + l10n.t( + '💡 Your password appears to contain URL-encoded characters (e.g. %40 instead of @). This often happens when copying a password from a connection string URL. Would you like to retry with the decoded version?', + ); buttons.push(retryButton); } diff --git a/src/tree/connections-view/DocumentDBClusterItem.ts b/src/tree/connections-view/DocumentDBClusterItem.ts index 9d2a9dc34..912e65b30 100644 --- a/src/tree/connections-view/DocumentDBClusterItem.ts +++ b/src/tree/connections-view/DocumentDBClusterItem.ts @@ -278,19 +278,33 @@ export class DocumentDBClusterItem extends ClusterItemBase Date: Tue, 21 Apr 2026 16:06:50 +0200 Subject: [PATCH 26/28] feat: add error handling for saving updated credentials and implement tests for URL-encoded password decoding --- l10n/bundle.l10n.json | 1 + .../auth/urlEncodedPassword.test.ts | 204 ++++++++++++++++++ .../connections-view/DocumentDBClusterItem.ts | 60 ++++-- 3 files changed, 242 insertions(+), 23 deletions(-) create mode 100644 src/documentdb/auth/urlEncodedPassword.test.ts diff --git a/l10n/bundle.l10n.json b/l10n/bundle.l10n.json index 1ca46859a..dedb2266a 100644 --- a/l10n/bundle.l10n.json +++ b/l10n/bundle.l10n.json @@ -518,6 +518,7 @@ "Failed to save credentials: {0}": "Failed to save credentials: {0}", "Failed to save credentials: connection not found in storage.": "Failed to save credentials: connection not found in storage.", "Failed to save credentials.": "Failed to save credentials.", + "Failed to save updated credentials: {error}": "Failed to save updated credentials: {error}", "Failed to sign in to tenant {0}: {1}": "Failed to sign in to tenant {0}: {1}", "Failed to start a session: {0}": "Failed to start a session: {0}", "Failed to start a transaction with the provided session: {0}": "Failed to start a transaction with the provided session: {0}", diff --git a/src/documentdb/auth/urlEncodedPassword.test.ts b/src/documentdb/auth/urlEncodedPassword.test.ts new file mode 100644 index 000000000..bb711e604 --- /dev/null +++ b/src/documentdb/auth/urlEncodedPassword.test.ts @@ -0,0 +1,204 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { type IActionContext } from '@microsoft/vscode-azext-utils'; +import * as vscode from 'vscode'; +import { + showConnectionFailedAndMaybeOfferDecodedRetry, + tryDecodeUrlEncodedPassword, + UrlEncodedPasswordTelemetry, +} from './urlEncodedPassword'; + +function createMockContext(): IActionContext { + return { + telemetry: { properties: {}, measurements: {} }, + errorHandling: { issueProperties: {} }, + ui: {} as IActionContext['ui'], + valuesToMask: [], + } as unknown as IActionContext; +} + +describe('tryDecodeUrlEncodedPassword', () => { + it('returns undefined for undefined input', () => { + expect(tryDecodeUrlEncodedPassword(undefined)).toBeUndefined(); + }); + + it('returns undefined for empty string', () => { + expect(tryDecodeUrlEncodedPassword('')).toBeUndefined(); + }); + + it('returns undefined when password has no percent-encoded sequences', () => { + expect(tryDecodeUrlEncodedPassword('plainPassword123')).toBeUndefined(); + }); + + it('returns undefined when password contains % but not valid encoding', () => { + // e.g. "100%" — the % is not followed by two hex digits + expect(tryDecodeUrlEncodedPassword('100%')).toBeUndefined(); + }); + + it('returns undefined when password contains %XX that is not valid UTF-8', () => { + // %C3 alone is an incomplete UTF-8 sequence; decodeURIComponent should throw + expect(tryDecodeUrlEncodedPassword('%C3')).toBeUndefined(); + }); + + it('returns decoded password for %40 (@)', () => { + expect(tryDecodeUrlEncodedPassword('p%40ss')).toBe('p@ss'); + }); + + it('returns decoded password for multiple encoded characters', () => { + expect(tryDecodeUrlEncodedPassword('p%40ss%21w%23rd')).toBe('p@ss!w#rd'); + }); + + it('returns decoded password for %20 (space)', () => { + expect(tryDecodeUrlEncodedPassword('my%20password')).toBe('my password'); + }); + + it('returns undefined when decoding produces the same string', () => { + // %30 decodes to "0", so "abc%30" decodes to "abc0" + // But a password like "%41" decodes to "A", which differs. + // A password that is already decoded with no encoded chars won't match the pattern. + // Let's use a case where decoded === original: not possible with valid %XX that differs. + // Actually if someone has "%25" it decodes to "%", so that's always different. + // This edge case is hard to trigger with valid encoding, so skip pure equality test. + }); + + it('handles case-insensitive hex digits', () => { + expect(tryDecodeUrlEncodedPassword('p%2Fss')).toBe('p/ss'); + expect(tryDecodeUrlEncodedPassword('p%2fss')).toBe('p/ss'); + }); +}); + +describe('showConnectionFailedAndMaybeOfferDecodedRetry', () => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const showErrorMessage: jest.Mock = vscode.window.showErrorMessage as unknown as jest.Mock; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('shows error dialog without retry button when password is not URL-encoded', async () => { + const context = createMockContext(); + + showErrorMessage.mockResolvedValueOnce(undefined); + + const result = await showConnectionFailedAndMaybeOfferDecodedRetry({ + clusterName: 'test-cluster', + password: 'plainPassword', + isNativeAuth: true, + originalError: new Error('auth failed'), + context, + }); + + expect(result.decodedPassword).toBeUndefined(); + expect(context.telemetry.properties[UrlEncodedPasswordTelemetry.Detected]).toBe('false'); + expect(context.telemetry.properties[UrlEncodedPasswordTelemetry.Offered]).toBe('false'); + // The dialog should have been called with no extra buttons + expect(showErrorMessage).toHaveBeenCalledWith( + expect.stringContaining('test-cluster'), + expect.objectContaining({ modal: true }), + ); + }); + + it('shows error dialog without retry button when auth is not native', async () => { + const context = createMockContext(); + + showErrorMessage.mockResolvedValueOnce(undefined); + + const result = await showConnectionFailedAndMaybeOfferDecodedRetry({ + clusterName: 'test-cluster', + password: 'p%40ss', // URL-encoded but non-native auth + isNativeAuth: false, + originalError: new Error('auth failed'), + context, + }); + + expect(result.decodedPassword).toBeUndefined(); + expect(context.telemetry.properties[UrlEncodedPasswordTelemetry.Detected]).toBe('false'); + }); + + it('offers retry button when password is URL-encoded and auth is native', async () => { + const context = createMockContext(); + + showErrorMessage.mockResolvedValueOnce(undefined); + + const result = await showConnectionFailedAndMaybeOfferDecodedRetry({ + clusterName: 'test-cluster', + password: 'p%40ss', + isNativeAuth: true, + originalError: new Error('auth failed'), + context, + }); + + expect(result.decodedPassword).toBeUndefined(); // user didn't click retry + expect(context.telemetry.properties[UrlEncodedPasswordTelemetry.Detected]).toBe('true'); + expect(context.telemetry.properties[UrlEncodedPasswordTelemetry.Offered]).toBe('true'); + expect(context.telemetry.properties[UrlEncodedPasswordTelemetry.Accepted]).toBe('false'); + // Should have been called with the retry button + expect(showErrorMessage).toHaveBeenCalledWith( + expect.stringContaining('test-cluster'), + expect.objectContaining({ modal: true }), + expect.stringContaining('Decoded Password'), + ); + }); + + it('returns decoded password when user clicks retry button', async () => { + const context = createMockContext(); + + // Simulate user clicking the retry button + showErrorMessage.mockImplementation((_msg: string, _opts: unknown, ...buttons: string[]) => + Promise.resolve(buttons[0]), + ); + + const result = await showConnectionFailedAndMaybeOfferDecodedRetry({ + clusterName: 'test-cluster', + password: 'p%40ss', + isNativeAuth: true, + originalError: new Error('auth failed'), + context, + }); + + expect(result.decodedPassword).toBe('p@ss'); + expect(context.telemetry.properties[UrlEncodedPasswordTelemetry.Accepted]).toBe('true'); + }); + + it('handles non-Error original error', async () => { + const context = createMockContext(); + + showErrorMessage.mockResolvedValueOnce(undefined); + + await showConnectionFailedAndMaybeOfferDecodedRetry({ + clusterName: 'test-cluster', + password: undefined, + isNativeAuth: true, + originalError: 'string error', + context, + }); + + // Should not throw; the dialog detail should contain the stringified error + expect(showErrorMessage).toHaveBeenCalledWith( + expect.any(String), + expect.objectContaining({ + detail: expect.stringContaining('string error'), + }), + ); + }); + + it('shows error dialog without retry button when password is undefined', async () => { + const context = createMockContext(); + + showErrorMessage.mockResolvedValueOnce(undefined); + + const result = await showConnectionFailedAndMaybeOfferDecodedRetry({ + clusterName: 'test-cluster', + password: undefined, + isNativeAuth: true, + originalError: new Error('auth failed'), + context, + }); + + expect(result.decodedPassword).toBeUndefined(); + expect(context.telemetry.properties[UrlEncodedPasswordTelemetry.Detected]).toBe('false'); + }); +}); diff --git a/src/tree/connections-view/DocumentDBClusterItem.ts b/src/tree/connections-view/DocumentDBClusterItem.ts index 912e65b30..53383a6db 100644 --- a/src/tree/connections-view/DocumentDBClusterItem.ts +++ b/src/tree/connections-view/DocumentDBClusterItem.ts @@ -275,9 +275,29 @@ export class DocumentDBClusterItem extends ClusterItemBase Date: Tue, 21 Apr 2026 16:23:42 +0200 Subject: [PATCH 27/28] chore: version bump to `0.7.4` --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0061ad01f..e7ffe32c6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "vscode-documentdb", - "version": "0.7.3", + "version": "0.7.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "vscode-documentdb", - "version": "0.7.3", + "version": "0.7.4", "license": "SEE LICENSE IN LICENSE.md", "dependencies": { "@azure/arm-compute": "^22.4.0", diff --git a/package.json b/package.json index ba6272d11..edbb64e8a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "vscode-documentdb", - "version": "0.7.3", + "version": "0.7.4", "releaseNotesUrl": "https://github.com/microsoft/vscode-documentdb/discussions/489", "aiKey": "0c6ae279ed8443289764825290e4f9e2-1a736e7c-1324-4338-be46-fc2a58ae4d14-7255", "publisher": "ms-azuretools", From c312e17f10fdb05768011263e0158424aa4fdc8c Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Tue, 21 Apr 2026 16:41:37 +0200 Subject: [PATCH 28/28] feat: add release notes for v0.7.4 with new features, improvements, and dependency updates --- CHANGELOG.md | 16 +++++++++++++ docs/index.md | 2 +- docs/release-notes/0.7.md | 49 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 66 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 11d4160eb..e5c6a89b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,21 @@ # Change Log +## 0.7.4 + +### New Features & Improvements + +- **URL-Encoded Password Detection**: When a connection attempt fails and the password contains URL-encoded characters, the extension now offers a "Retry with Decoded Password" option. If the retry succeeds, the decoded password can be saved. [#444](https://github.com/microsoft/vscode-documentdb/issues/444), [#594](https://github.com/microsoft/vscode-documentdb/pull/594) +- **Rich Markdown Tooltips**: Cluster, database, and collection tree items in the Connections view now show rich markdown tooltips on hover, displaying useful details (host, auth method, document count, storage size) without requiring expansion or connection. [#579](https://github.com/microsoft/vscode-documentdb/issues/579), [#588](https://github.com/microsoft/vscode-documentdb/pull/588) +- **Copy Reference Context Menu**: Adds a "Copy Reference…" right-click option to database, collection, and index nodes with a QuickPick format picker. Databases offer name, shell command (`use dbName`), or qualified name; collections offer name, namespace, shell reference, or `db.getCollection()` form; indexes offer name, key definition, or shell command. Names with special characters automatically use safe escaping. [#545](https://github.com/microsoft/vscode-documentdb/pull/545), [#587](https://github.com/microsoft/vscode-documentdb/pull/587) + +### Documentation + +- **Improved CONTRIBUTING.md**: Adds a PR submission checklist, corrects Node/npm version requirements, and adds multi-platform setup stubs. [#565](https://github.com/microsoft/vscode-documentdb/pull/565) + +### Dependencies + +- **Dependency Updates**: Bumps `handlebars` (4.7.8 → 4.7.9), `lodash` (4.17.23 → 4.18.1), `lodash` and `@microsoft/api-extractor` in `/api`, and `follow-redirects` (1.15.11 → 1.16.0). [#552](https://github.com/microsoft/vscode-documentdb/pull/552), [#556](https://github.com/microsoft/vscode-documentdb/pull/556), [#558](https://github.com/microsoft/vscode-documentdb/pull/558), [#586](https://github.com/microsoft/vscode-documentdb/pull/586) + ## 0.7.3 ### New Features diff --git a/docs/index.md b/docs/index.md index 113de0914..47dfbbb2f 100644 --- a/docs/index.md +++ b/docs/index.md @@ -65,7 +65,7 @@ The User Manual provides guidance on using DocumentDB for VS Code. It contains d Explore the history of updates and improvements to the DocumentDB for VS Code extension. Each release brings new features, enhancements, and fixes to improve your experience. -- [0.7](./release-notes/0.7), [0.7.2](./release-notes/0.7#patch-release-v072), [0.7.3](./release-notes/0.7#patch-release-v073) +- [0.7](./release-notes/0.7), [0.7.2](./release-notes/0.7#patch-release-v072), [0.7.3](./release-notes/0.7#patch-release-v073), [0.7.4](./release-notes/0.7#patch-release-v074) - [0.6](./release-notes/0.6), [0.6.1](./release-notes/0.6#patch-release-v061), [0.6.2](./release-notes/0.6#patch-release-v062), [0.6.3](./release-notes/0.6#patch-release-v063) - [0.5](./release-notes/0.5), [0.5.1](./release-notes/0.5#patch-release-v051), [0.5.2](./release-notes/0.5#patch-release-v052) - [0.4](./release-notes/0.4), [0.4.1](./release-notes/0.4#patch-release-v041) diff --git a/docs/release-notes/0.7.md b/docs/release-notes/0.7.md index 1ebe3d2d6..5aeb3746f 100644 --- a/docs/release-notes/0.7.md +++ b/docs/release-notes/0.7.md @@ -270,3 +270,52 @@ Internal telemetry for connection and discovery actions has been improved to cap See the full changelog entry for this release: ➡️ [CHANGELOG.md#073](https://github.com/microsoft/vscode-documentdb/blob/main/CHANGELOG.md#073) + +--- + +## Patch Release v0.7.4 + +This patch adds URL-encoded password detection, rich markdown tooltips across all tree item levels, a new "Copy Reference" context menu, and several dependency updates. + +### What's Changed in v0.7.4 + +#### 💠 **URL-Encoded Password Detection** ([#444](https://github.com/microsoft/vscode-documentdb/issues/444), [#594](https://github.com/microsoft/vscode-documentdb/pull/594)) + +Connecting with a URL-encoded password no longer results in a cryptic authentication failure. When a connection attempt fails and the password contains URL-encoded characters (`%XX` sequences), the extension now offers a **"Retry with Decoded Password"** option in the error dialog. If the retry succeeds, the decoded password can be saved — no manual editing required. + +Thanks to **[@vaibh123540](https://github.com/vaibh123540)** for the original contribution in [#454](https://github.com/microsoft/vscode-documentdb/pull/454)! + +#### 💠 **Rich Markdown Tooltips** ([#579](https://github.com/microsoft/vscode-documentdb/issues/579), [#588](https://github.com/microsoft/vscode-documentdb/pull/588)) + +Hovering over tree items in the Connections view now reveals rich markdown tooltips at **cluster**, **database**, and **collection** levels — giving you useful details at a glance without expanding or connecting. + +- **Cluster tooltips** show the display name, hostname, authentication method, and TLS status. +- **Database tooltips** display the database name and estimated storage size. +- **Collection tooltips** show estimated document count and storage size. + +All data comes from information already fetched during tree expansion, so no extra API calls are needed. + +#### 💠 **Copy Reference Context Menu** ([#545](https://github.com/microsoft/vscode-documentdb/pull/545), [#587](https://github.com/microsoft/vscode-documentdb/pull/587)) + +A new **"Copy Reference…"** right-click option is available on **database**, **collection**, and **index** nodes. Instead of copying a single fixed format, a QuickPick lets you choose the most useful representation: + +- **Database**: Name, Shell Command (`use dbName`), Qualified Name (`host/dbName`) +- **Collection**: Name, Namespace (`db.coll`), Shell Reference (`db.coll`), Shell Command (`db.getCollection("coll")`) +- **Index**: Name, Key Definition (`{ field: 1 }`), Shell Command (`db.coll.getIndexes().find(...)`) + +Names containing special characters (spaces, parentheses, etc.) automatically use the safe `db.getCollection("...")` form, and dot-notation options are hidden when they would produce broken syntax. + +Thanks to **[@bgaeddert](https://github.com/bgaeddert)** for the original contribution in [#545](https://github.com/microsoft/vscode-documentdb/pull/545)! + +#### 💠 **Improved CONTRIBUTING.md** ([#565](https://github.com/microsoft/vscode-documentdb/pull/565)) + +The contributing guide now includes a **PR submission checklist** (localization, formatting, linting), corrected Node/npm version requirements, and multi-platform setup stubs — making it easier for new contributors to get started. + +#### 💠 **Dependency Updates** ([#552](https://github.com/microsoft/vscode-documentdb/pull/552), [#556](https://github.com/microsoft/vscode-documentdb/pull/556), [#558](https://github.com/microsoft/vscode-documentdb/pull/558), [#586](https://github.com/microsoft/vscode-documentdb/pull/586)) + +Updated `handlebars` (4.7.8 → 4.7.9), `lodash` (4.17.23 → 4.18.1), `lodash` and `@microsoft/api-extractor` in `/api`, and `follow-redirects` (1.15.11 → 1.16.0) to their latest versions. + +### Changelog + +See the full changelog entry for this release: +➡️ [CHANGELOG.md#074](https://github.com/microsoft/vscode-documentdb/blob/main/CHANGELOG.md#074)