diff --git a/CHANGELOG.md b/CHANGELOG.md
index af5ca8a48..3b047d439 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,27 +9,31 @@
### Improvements
-- **Estimated Document Count**: Shows an estimated document count for collections in the tree view. [#170](https://github.com/microsoft/vscode-documentdb/pull/170)
-- **Copy Connection String with Password**: Adds an option to include the password when copying a connection string. [#436](https://github.com/microsoft/vscode-documentdb/pull/436)
-- **Release Notes Notification**: Prompts users to view release notes after upgrading to a new major or minor version. [#487](https://github.com/microsoft/vscode-documentdb/pull/487)
- **Accessibility**: Improves screen reader announcements, keyboard navigation, and ARIA labeling across Query Insights and document editing. [#374](https://github.com/microsoft/vscode-documentdb/issues/374), [#375](https://github.com/microsoft/vscode-documentdb/issues/375), [#377](https://github.com/microsoft/vscode-documentdb/issues/377), [#378](https://github.com/microsoft/vscode-documentdb/issues/378), [#379](https://github.com/microsoft/vscode-documentdb/issues/379), [#380](https://github.com/microsoft/vscode-documentdb/issues/380), [#381](https://github.com/microsoft/vscode-documentdb/issues/381), [#384](https://github.com/microsoft/vscode-documentdb/issues/384), [#385](https://github.com/microsoft/vscode-documentdb/issues/385)
-- **Alphabetical Collection Sorting**: Sorts collections alphabetically in the tree view. Thanks to [@VanitasBlade](https://github.com/VanitasBlade). [#456](https://github.com/microsoft/vscode-documentdb/issues/456), [#465](https://github.com/microsoft/vscode-documentdb/pull/465)
-- **Query Insights Prompt Hardening**: Updates the Query Insights model/prompt and adds additional prompt-injection mitigations. [#468](https://github.com/microsoft/vscode-documentdb/pull/468)
-- **Connection String Validation**: Trims and validates connection string input to avoid empty values. [#467](https://github.com/microsoft/vscode-documentdb/pull/467)
+- **Alphabetical Collection Sorting**: Sorts collections alphabetically in the tree view. [#456](https://github.com/microsoft/vscode-documentdb/issues/456), [#465](https://github.com/microsoft/vscode-documentdb/pull/465)
+- **Cancellable Imports**: Import operations can now be cancelled. [#496](https://github.com/microsoft/vscode-documentdb/pull/496)
- **Collection Paste Feedback**: Refreshes collection metadata after paste and improves error reporting for failed writes. [#482](https://github.com/microsoft/vscode-documentdb/pull/482), [#484](https://github.com/microsoft/vscode-documentdb/pull/484)
+- **Collection Paste Validation and Input Trimming Consistency**: Fixes inconsistent trimming/validation of user input. [#493](https://github.com/microsoft/vscode-documentdb/pull/493)
+- **Connection String Validation**: Trims and validates connection string input to avoid empty values. [#467](https://github.com/microsoft/vscode-documentdb/pull/467)
+- **Copy Connection String with Password**: Adds an option to include the password when copying a connection string. [#436](https://github.com/microsoft/vscode-documentdb/pull/436)
+- **Estimated Document Count**: Shows an estimated document count for collections in the tree view. [#170](https://github.com/microsoft/vscode-documentdb/pull/170)
+- **Import/Export Feedback**: Improves user feedback and error handling for import/export operations. [#495](https://github.com/microsoft/vscode-documentdb/pull/495)
+- **Query Insights Prompt Hardening**: Updates the Query Insights model/prompt and adds additional prompt-injection mitigations. [#468](https://github.com/microsoft/vscode-documentdb/pull/468)
+- **Release Notes Notification**: Prompts users to view release notes after upgrading to a new major or minor version. [#487](https://github.com/microsoft/vscode-documentdb/pull/487)
### Fixes
+- **Azure Resources View Expansion**: Fixes cluster expansion failures in the Azure Resources view by deriving resource group information from resource IDs. [#480](https://github.com/microsoft/vscode-documentdb/pull/480)
- **Dark Theme Rendering**: Fixes unreadable text in some dark themes by respecting theme colors. [#457](https://github.com/microsoft/vscode-documentdb/issues/457)
-- **Query Insights Markdown Rendering**: Restricts AI output formatting to avoid malformed markdown rendering. [#428](https://github.com/microsoft/vscode-documentdb/issues/428)
+- **Import from Discovery View**: Fixes document import for Azure Cosmos DB for MongoDB (RU) discovery when connection metadata is not yet cached. [#368](https://github.com/microsoft/vscode-documentdb/issues/368), [#479](https://github.com/microsoft/vscode-documentdb/pull/479)
- **Invalid Query JSON**: Shows a clear error when query JSON fails to parse instead of silently using empty objects. [#458](https://github.com/microsoft/vscode-documentdb/issues/458), [#471](https://github.com/microsoft/vscode-documentdb/pull/471)
- **Keyboard Paste Shortcuts**: Restores Ctrl+V/Cmd+V in the Query Editor and Document View by pinning Monaco to 0.52.2. [#435](https://github.com/microsoft/vscode-documentdb/issues/435), [#470](https://github.com/microsoft/vscode-documentdb/pull/470)
-- **Import from Discovery View**: Fixes document import for Azure Cosmos DB for MongoDB (RU) discovery when connection metadata is not yet cached. [#368](https://github.com/microsoft/vscode-documentdb/issues/368), [#479](https://github.com/microsoft/vscode-documentdb/pull/479)
-- **Azure Resources View Expansion**: Fixes cluster expansion failures in the Azure Resources view by deriving resource group information from resource IDs. [#480](https://github.com/microsoft/vscode-documentdb/pull/480)
+- **Query Insights Markdown Rendering**: Restricts AI output formatting to avoid malformed markdown rendering. [#428](https://github.com/microsoft/vscode-documentdb/issues/428)
### Security
- **Dependency Updates**: Updates `qs` and `express` to address security vulnerabilities. [#434](https://github.com/microsoft/vscode-documentdb/pull/434)
+- **Webpack Update**: Bumps `webpack` from 5.103.0 to 5.105.0. [#494](https://github.com/microsoft/vscode-documentdb/pull/494)
## 0.6.3
diff --git a/docs/release-notes/0.7.md b/docs/release-notes/0.7.md
index f40d892ae..0be7fb0b5 100644
--- a/docs/release-notes/0.7.md
+++ b/docs/release-notes/0.7.md
@@ -14,7 +14,7 @@ It includes **Lightweight Data Migration** for collection copying, adds **Folder
We're introducing **Lightweight Data Migration** (also known as "**Collection Copy-and-Paste**"). This feature enables you to copy collections and paste them into new or existing collections, across different databases or even different servers - all directly within VS Code.
-

+
Designed for **smaller-to-medium datasets** that can be streamed through your local machine, this feature simplifies moving data between environments. It's useful for:
@@ -73,9 +73,9 @@ Accessibility is a core value for us, and this release includes improvements to
**Screen Reader Enhancements**
-- **Search Results Announcements** ([#384](https://github.com/microsoft/vscode-documentdb/issues/384)): Screen readers now properly announce search result counts and "No Results Found" messages in the Collection View
- **AI Analysis Status** ([#380](https://github.com/microsoft/vscode-documentdb/issues/380)): Screen readers now announce the "AI is analyzing..." status message instead of generic "Document" text in Query Insights
- **Rating Button Context** ([#381](https://github.com/microsoft/vscode-documentdb/issues/381)): Added proper grouping labels so screen readers announce "How would you rate AI insights?" when focusing on like/dislike buttons
+- **Search Results Announcements** ([#384](https://github.com/microsoft/vscode-documentdb/issues/384)): Screen readers now properly announce search result counts and "No Results Found" messages in the Collection View
**Keyboard Navigation Improvements**
@@ -84,10 +84,10 @@ Accessibility is a core value for us, and this release includes improvements to
**ARIA Labeling Fixes**
-- **Query Field Labels** ([#379](https://github.com/microsoft/vscode-documentdb/issues/379)): Added proper visual labels for query fields with programmatic names that correctly reflect field purpose
- **Button Naming** ([#378](https://github.com/microsoft/vscode-documentdb/issues/378)): Next, Previous, and Close buttons in the Tips section now have proper accessible names
-- **Spin Button Labels** ([#377](https://github.com/microsoft/vscode-documentdb/issues/377)): Skip and Limit spin buttons now have appropriate accessible names for assistive technologies
- **Label-in-Name Compliance** ([#374](https://github.com/microsoft/vscode-documentdb/issues/374)): Refresh and Validate buttons now have accessible names that include their visual labels, ensuring compatibility with voice control
+- **Query Field Labels** ([#379](https://github.com/microsoft/vscode-documentdb/issues/379)): Added proper visual labels for query fields with programmatic names that correctly reflect field purpose
+- **Spin Button Labels** ([#377](https://github.com/microsoft/vscode-documentdb/issues/377)): Skip and Limit spin buttons now have appropriate accessible names for assistive technologies
**User Impact**
@@ -101,43 +101,54 @@ We're grateful for the contributions from our open-source community! This releas
Collections within DocumentDB databases are now displayed in alphabetical order, making it much easier to find the collection you're looking for. This improvement enhances usability when working with databases that contain many collections.
-**Contributed by [@VanitasBlade](https://github.com/VanitasBlade)**, fixing issue [#456](https://github.com/microsoft/vscode-documentdb/issues/456) originally reported by [@majelbstoat](https://github.com/majelbstoat).
+**Contributed by [@VanitasBlade](https://github.com/VanitasBlade)**, fixing issue [#456](https://github.com/microsoft/vscode-documentdb/issues/456) originally reported by [@majelbstoat](https://github.com/majelbstoat) and [@MattParkerDev](https://github.com/MattParkerDev).
### 2️⃣ Bug Reports from the Community
+- **Collection paste validation & input trimming consistency**: Reported by [@DhruvC-Affine](https://github.com/DhruvC-Affine); fixed in [#493](https://github.com/microsoft/vscode-documentdb/pull/493). The user spotted that pasting a collection didn't validate the input correctly, but this uncovered a wider issue with lack of consistency when user input was trimmed. This report helped to improve the quality overall.
+- **Dark theme visibility**: Reported by [@majelbstoat](https://github.com/majelbstoat) ([#457](https://github.com/microsoft/vscode-documentdb/issues/457)); fixed in [Dark Theme Support](#dark-theme-support).
- **Invalid query JSON handling**: Reported by [@majelbstoat](https://github.com/majelbstoat) ([#458](https://github.com/microsoft/vscode-documentdb/issues/458)); fixed in [Query Parsing Error Handling](#query-parsing-error-handling).
+- **Improved Import/Export Feedback**: Reported by [@MattParkerDev](https://github.com/MattParkerDev); fixed in [#495](https://github.com/microsoft/vscode-documentdb/pull/495). The user reported that no output was displayed for import success or failure. This has been fixed to provide clear feedback.
- **Keyboard paste shortcuts**: Reported by [@cveld](https://github.com/cveld) and [@markjbrown](https://github.com/markjbrown) ([#435](https://github.com/microsoft/vscode-documentdb/issues/435)); fixed in [Monaco Editor Keyboard Paste](#monaco-editor-keyboard-paste).
## Additional Improvements
-### Estimated Document Count Display
+### Cancellable Import Operations
-Collections in the tree view now display an estimated document count, helping you assess collection sizes without running explicit count queries. This makes it easier to understand your data at a glance.
+Long-running import operations can now be cancelled. A cancel button is available in the progress notification, allowing you to stop the import process if needed. [#496](https://github.com/microsoft/vscode-documentdb/pull/496)
-### Query Insights Performance Enhancement ([#468](https://github.com/microsoft/vscode-documentdb/pull/468))
+### Connection String Handling ([#467](https://github.com/microsoft/vscode-documentdb/pull/467))
-The Query Insights feature has been improved with an updated AI model (GPT-4o) and refined prompt architecture. Key improvements include:
+Connection string inputs are now automatically trimmed and validated. This prevents issues when pasting connection strings that may contain leading or trailing whitespace, ensuring successful connections every time.
-- **Faster Response Times**: The GPT-4o model delivers query analysis results more quickly than previous models
-- **Enhanced Security**: Restructured prompt components with explicit delimiters and security instructions to protect against prompt injection attacks
-- **Better Reliability**: Improved prompt structure ensures more consistent and accurate recommendations
+### Copy Connection String with Password ([#436](https://github.com/microsoft/vscode-documentdb/pull/436))
-These changes make the Query Insights feature faster and more reliable for analyzing query performance.
+When copying connection details to the clipboard, you can now choose whether to include the password. Previously, it wasn't possible to copy connection strings with passwords included—this option provides the flexibility needed for different security contexts.
-### Connection String Handling ([#467](https://github.com/microsoft/vscode-documentdb/pull/467))
+### Estimated Document Count Display
-Connection string inputs are now automatically trimmed and validated. This prevents issues when pasting connection strings that may contain leading or trailing whitespace, ensuring successful connections every time.
+Collections in the tree view now display an estimated document count, helping you assess collection sizes without running explicit count queries. This makes it easier to understand your data at a glance.
+
+### Improved AI Response Formatting ([#428](https://github.com/microsoft/vscode-documentdb/issues/428))
+
+Fixed markdown formatting issues in AI-generated responses from the Query Insights feature. The extension now restricts formatting options to prevent malformed output, ensuring recommendations are always readable and properly rendered.
### Improved Data Migration Feedback
Two enhancements improve the collection paste experience:
- **Accurate Count Updates** ([#482](https://github.com/microsoft/vscode-documentdb/pull/482)): Document count estimates now refresh correctly after paste operations, even when some documents fail to insert
-- **Complete Error Logging** ([#484](https://github.com/microsoft/vscode-documentdb/pull/484)): All write errors, including unique index violations, are now properly logged. Failed operations automatically open the output log to ensure you don't miss important error details
+- **Complete Error Logging** ([#484](https://github.com/microsoft/vscode-documentdb/pull/484)): All write errors, including unique index violations, are now properly logged. A "Show Output" button is included in error notifications, allowing you to quickly access detailed logs when an operation fails.
-### Copy Connection String with Password ([#436](https://github.com/microsoft/vscode-documentdb/pull/436))
+### Query Insights Performance Enhancement ([#468](https://github.com/microsoft/vscode-documentdb/pull/468))
-When copying connection details to the clipboard, you can now choose whether to include the password. Previously, it wasn't possible to copy connection strings with passwords included—this option provides the flexibility needed for different security contexts.
+The Query Insights feature has been improved with an updated AI model (GPT-4o) and refined prompt architecture. Key improvements include:
+
+- **Faster Response Times**: The GPT-4o model delivers query analysis results more quickly than previous models
+- **Enhanced Security**: Restructured prompt components with explicit delimiters and security instructions to protect against prompt injection attacks
+- **Better Reliability**: Improved prompt structure ensures more consistent and accurate recommendations
+
+These changes make the Query Insights feature faster and more reliable for analyzing query performance.
### Release Notes Notification on Upgrade ([#487](https://github.com/microsoft/vscode-documentdb/pull/487))
@@ -149,25 +160,21 @@ The notification appears when the Connections View becomes visible, and lets you
- Choose Remind Me Later to see it again next session
- Ignore the notification for that version
-### Improved AI Response Formatting ([#428](https://github.com/microsoft/vscode-documentdb/issues/428))
-
-Fixed markdown formatting issues in AI-generated responses from the Query Insights feature. The extension now restricts formatting options to prevent malformed output, ensuring recommendations are always readable and properly rendered.
-
## Key Fixes
-### Dark Theme Support ([#457](https://github.com/microsoft/vscode-documentdb/issues/457))
+### Azure Resources View Connectivity ([#480](https://github.com/microsoft/vscode-documentdb/pull/480))
-Resolved UI rendering issues that affected users of dark themes like Nord. Text in certain controls was dark-on-dark and invisible, making parts of the interface unusable. All UI elements now properly respect theme colors for better visibility in both light and dark modes.
+Resolved connection failures when expanding Azure DocumentDB clusters in the Azure Resources view. Previously, undefined resource group metadata caused cryptic API errors. The extension now correctly extracts resource group information from resource IDs, ensuring reliable connectivity to vCore and Azure Cosmos DB for MongoDB (RU) clusters.
-### Security Dependency Updates ([#434](https://github.com/microsoft/vscode-documentdb/pull/434))
+### Dark Theme Support ([#457](https://github.com/microsoft/vscode-documentdb/issues/457))
-Updated `qs` and `express` dependencies to address security vulnerabilities, ensuring the extension maintains high security standards.
+
-### Query Parsing Error Handling ([#471](https://github.com/microsoft/vscode-documentdb/pull/471))
+Resolved UI rendering issues that affected users of dark themes like Nord. Text in certain controls was dark-on-dark and invisible, making parts of the interface unusable. All UI elements now properly respect theme colors for better visibility in both light and dark modes. **Reported by [@majelbstoat](https://github.com/majelbstoat)** in [#457](https://github.com/microsoft/vscode-documentdb/issues/457).
-
+### Document Import from Discovery View ([#479](https://github.com/microsoft/vscode-documentdb/pull/479))
-Fixed an issue where invalid JSON in query fields (filter, projection, sort) was silently replaced with empty objects, making it difficult to understand why queries weren't working as expected. Parse failures now display clear error dialogs with helpful JSON syntax examples. **Reported by [@majelbstoat](https://github.com/majelbstoat)** in [#458](https://github.com/microsoft/vscode-documentdb/issues/458).
+Fixed document import failures when using the Azure Service Discovery View with Azure Cosmos DB for MongoDB (RU) resources. The extension now properly retrieves connection strings even when metadata cache hasn't fully populated, ensuring reliable imports from discovered resources. Resolves [#368](https://github.com/microsoft/vscode-documentdb/issues/368).
### Monaco Editor Keyboard Paste ([#470](https://github.com/microsoft/vscode-documentdb/pull/470))
@@ -175,13 +182,15 @@ Fixed an issue where invalid JSON in query fields (filter, projection, sort) was
Restored keyboard paste functionality (`Ctrl+V` / `Cmd+V`) in the Query Editor and Document View. A regression in Monaco Editor 0.53.x/0.54.x broke keyboard shortcuts for paste operations. Downgrading to Monaco 0.52.2 resolves this issue. Context menu paste always worked, but keyboard shortcuts are now functional again. **Reported by [@cveld](https://github.com/cveld)** in [#435](https://github.com/microsoft/vscode-documentdb/issues/435).
-### Document Import from Discovery View ([#479](https://github.com/microsoft/vscode-documentdb/pull/479))
+### Query Parsing Error Handling ([#471](https://github.com/microsoft/vscode-documentdb/pull/471))
-Fixed document import failures when using the Azure Service Discovery View with Azure Cosmos DB for MongoDB (RU) resources. The extension now properly retrieves connection strings even when metadata cache hasn't fully populated, ensuring reliable imports from discovered resources. Resolves [#368](https://github.com/microsoft/vscode-documentdb/issues/368).
+
-### Azure Resources View Connectivity ([#480](https://github.com/microsoft/vscode-documentdb/pull/480))
+Fixed an issue where invalid JSON in query fields (filter, projection, sort) was silently replaced with empty objects, making it difficult to understand why queries weren't working as expected. Parse failures now display clear error dialogs with helpful JSON syntax examples. **Reported by [@majelbstoat](https://github.com/majelbstoat)** in [#458](https://github.com/microsoft/vscode-documentdb/issues/458).
-Resolved connection failures when expanding Azure DocumentDB clusters in the Azure Resources view. Previously, undefined resource group metadata caused cryptic API errors. The extension now correctly extracts resource group information from resource IDs, ensuring reliable connectivity to vCore and Azure Cosmos DB for MongoDB (RU) clusters.
+### Security Dependency Updates ([#434](https://github.com/microsoft/vscode-documentdb/pull/434), [#494](https://github.com/microsoft/vscode-documentdb/pull/494))
+
+Updated `qs`, `express` and `webpack` dependencies to address security vulnerabilities, ensuring the extension maintains high security standards.
## Changelog
diff --git a/docs/release-notes/images/0.7.0_copy_and_paste.png b/docs/release-notes/images/0.7.0_copy_and_paste.png
new file mode 100644
index 000000000..c2fb8571a
Binary files /dev/null and b/docs/release-notes/images/0.7.0_copy_and_paste.png differ
diff --git a/l10n/bundle.l10n.json b/l10n/bundle.l10n.json
index 97e0ff714..b7abaf527 100644
--- a/l10n/bundle.l10n.json
+++ b/l10n/bundle.l10n.json
@@ -86,6 +86,7 @@
"{0} connections": "{0} connections",
"{0} created": "{0} created",
"{0} failed: {1}": "{0} failed: {1}",
+ "{0} file(s) were ignored because they do not match the \"*.json\" pattern.": "{0} file(s) were ignored because they do not match the \"*.json\" pattern.",
"{0} inserted": "{0} inserted",
"{0} item(s) already exist in the destination. Check the Output panel for details.": "{0} item(s) already exist in the destination. Check the Output panel for details.",
"{0} processed": "{0} processed",
@@ -123,6 +124,7 @@
"$(error) Failure": "$(error) Failure",
"$(info) Some storage accounts were filtered because of their sku. Learn more...": "$(info) Some storage accounts were filtered because of their sku. Learn more...",
"$(keyboard) Manually enter error": "$(keyboard) Manually enter error",
+ "$(output) Show Output": "$(output) Show Output",
"$(pass) Signed in": "$(pass) Signed in",
"$(plus) Create new {0}...": "$(plus) Create new {0}...",
"$(plus) Create new resource group": "$(plus) Create new resource group",
@@ -209,6 +211,7 @@
"Back to account selection": "Back to account selection",
"Back to tenant selection": "Back to tenant selection",
"Browse to {mongoExecutableFileName}": "Browse to {mongoExecutableFileName}",
+ "Bulk write error during import into \"{0}.{1}\": {2} document(s) inserted.": "Bulk write error during import into \"{0}.{1}\": {2} document(s) inserted.",
"Cancel": "Cancel",
"Cancel this operation": "Cancel this operation",
"Cannot {0}": "Cannot {0}",
@@ -399,6 +402,7 @@
"Error dropping index: {error}": "Error dropping index: {error}",
"Error exporting documents: {error}": "Error exporting documents: {error}",
"Error generating query": "Error generating query",
+ "Error inserting documents into \"{0}.{1}\": {2}": "Error inserting documents into \"{0}.{1}\": {2}",
"Error modifying index: {error}": "Error modifying index: {error}",
"Error opening the document view": "Error opening the document view",
"Error running process: ": "Error running process: ",
@@ -411,7 +415,7 @@
"Error while running the query": "Error while running the query",
"Error: {0}": "Error: {0}",
"Error: {error}": "Error: {error}",
- "Errors found in document {path}. Please fix these.": "Errors found in document {path}. Please fix these.",
+ "Errors found in file \"{path}\". Please fix these:": "Errors found in file \"{path}\". Please fix these:",
"Examined-to-Returned Ratio": "Examined-to-Returned Ratio",
"Excellent": "Excellent",
"Execute the find query": "Execute the find query",
@@ -431,12 +435,13 @@
"Explain(count) completed [{durationMs}ms]": "Explain(count) completed [{durationMs}ms]",
"Explain(find) completed [{durationMs}ms]": "Explain(find) completed [{durationMs}ms]",
"Export": "Export",
+ "Export complete. Exported document count: {documentCount}": "Export complete. Exported document count: {documentCount}",
"Export Current Query Results…": "Export Current Query Results…",
"Export Entire Collection…": "Export Entire Collection…",
"Export Execution Plan Details": "Export Execution Plan Details",
+ "Export operation was canceled after {0} document(s).": "Export operation was canceled after {0} document(s).",
"Export Optimization Opportunities": "Export Optimization Opportunities",
"Exported document count: {documentCount}": "Exported document count: {documentCount}",
- "Exporting data to: {filePath}": "Exporting data to: {filePath}",
"Exporting documents": "Exporting documents",
"Exporting…": "Exporting…",
"Extension dependency with id \"{0}\" must be updated.": "Extension dependency with id \"{0}\" must be updated.",
@@ -462,7 +467,7 @@
"Failed to drop index.": "Failed to drop index.",
"Failed to end session: {0}": "Failed to end session: {0}",
"Failed to ensure the target collection exists.": "Failed to ensure the target collection exists.",
- "Failed to export documents. Please see the output for details.": "Failed to export documents. Please see the output for details.",
+ "Failed to export documents.": "Failed to export documents.",
"Failed to extract cluster credentials from the selected node.": "Failed to extract cluster credentials from the selected node.",
"Failed to extract the connection string from the selected account.": "Failed to extract the connection string from the selected account.",
"Failed to find commandId on generic tree item.": "Failed to find commandId on generic tree item.",
@@ -545,8 +550,13 @@
"Ignore": "Ignore",
"Ignoring the following files that do not match the \"*.json\" file name pattern:": "Ignoring the following files that do not match the \"*.json\" file name pattern:",
"Import": "Import",
+ "Import canceled. Inserted {0} document(s) before cancellation.": "Import canceled. Inserted {0} document(s) before cancellation.",
"Import completed with errors.": "Import completed with errors.",
+ "Import failed with unexpected error: {0}": "Import failed with unexpected error: {0}",
+ "Import failed: document buffer is not initialized.": "Import failed: document buffer is not initialized.",
+ "Import failed: unsupported node type.": "Import failed: unsupported node type.",
"Import From JSON…": "Import From JSON…",
+ "Import operation was canceled after {0} document(s).": "Import operation was canceled after {0} document(s).",
"Import successful.": "Import successful.",
"IMPORTANT: Please be sure to remove any private information before submitting.": "IMPORTANT: Please be sure to remove any private information before submitting.",
"Imported: {name} (imported on {date})": "Imported: {name} (imported on {date})",
@@ -582,7 +592,7 @@
"Info from the webview: ": "Info from the webview: ",
"Information was confusing": "Information was confusing",
"Initializing task...": "Initializing task...",
- "Inserted {0} document(s). See output for more details.": "Inserted {0} document(s). See output for more details.",
+ "Inserted {0} document(s).": "Inserted {0} document(s).",
"Install Azure Account Extension...": "Install Azure Account Extension...",
"Internal error: connectionString must be defined.": "Internal error: connectionString must be defined.",
"Internal error: connectionString, port, and api must be defined.": "Internal error: connectionString, port, and api must be defined.",
@@ -635,6 +645,7 @@
"Limit": "Limit",
"Limit Returned Fields": "Limit Returned Fields",
"Load More...": "Load More...",
+ "Loaded {0} document(s) from \"{1}\"": "Loaded {0} document(s) from \"{1}\"",
"Loading \"{0}\"...": "Loading \"{0}\"...",
"Loading Azure Accounts Used for Service Discovery…": "Loading Azure Accounts Used for Service Discovery…",
"Loading cluster details for \"{cluster}\"": "Loading cluster details for \"{cluster}\"",
@@ -734,6 +745,8 @@
"Optimizing the index on {0} can improve query performance by better matching the query pattern.": "Optimizing the index on {0} can improve query performance by better matching the query pattern.",
"Overwrite existing documents": "Overwrite existing documents",
"Overwrite existing documents that share the same _id; other write errors will abort the operation.": "Overwrite existing documents that share the same _id; other write errors will abort the operation.",
+ "Parsing file {0}: {1}": "Parsing file {0}: {1}",
+ "Password cannot be empty": "Password cannot be empty",
"Password for {username_at_resource}": "Password for {username_at_resource}",
"Paste Collection": "Paste Collection",
"Pasting…": "Pasting…",
@@ -819,6 +832,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}\"…",
+ "See output for more details.": "See output for more details.",
"Select {0}": "Select {0}",
"Select {mongoExecutableFileName}": "Select {mongoExecutableFileName}",
"Select a location for new resources.": "Select a location for new resources.",
@@ -846,6 +860,7 @@
"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}",
+ "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.",
"Sign in to additional accounts or authenticate with other tenants to see more subscriptions.": "Sign in to additional accounts or authenticate with other tenants to see more subscriptions.",
@@ -875,6 +890,8 @@
"Starting Azure account management wizard": "Starting Azure account management wizard",
"Starting Azure sign-in process…": "Starting Azure sign-in process…",
"Starting executable: \"{command}\"": "Starting executable: \"{command}\"",
+ "Starting export to: {filePath}": "Starting export to: {filePath}",
+ "Starting import of {0} file(s) into collection \"{1}\"": "Starting import of {0} file(s) into collection \"{1}\"",
"Starting sign-in to tenant: {0}": "Starting sign-in to tenant: {0}",
"Starts with mongodb:// or mongodb+srv://": "Starts with mongodb:// or mongodb+srv://",
"Stopping {0}": "Stopping {0}",
@@ -986,6 +1003,7 @@
"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}",
+ "Total documents to import: {0}": "Total documents to import: {0}",
"Total time taken to execute the query on the server": "Total time taken to execute the query on the server",
"Transforming Stage 2 response to UI format": "Transforming Stage 2 response to UI format",
"Tree View": "Tree View",
@@ -1044,6 +1062,7 @@
"Validating source collection...": "Validating source collection...",
"Verifying folder can be deleted…": "Verifying folder can be deleted…",
"Verifying move operation…": "Verifying move operation…",
+ "View conflict details in the Output panel": "View conflict details in the Output panel",
"View in Marketplace": "View in Marketplace",
"View Raw Execution Stats": "View Raw Execution Stats",
"View Raw Explain Output": "View Raw Explain Output",
@@ -1061,6 +1080,7 @@
"Working...": "Working...",
"Working…": "Working…",
"Would you like to open the Collection View?": "Would you like to open the Collection View?",
+ "Write error: {0}": "Write error: {0}",
"Write operation failed: {0}": "Write operation failed: {0}",
"writing batch...": "writing batch...",
"Yes": "Yes",
diff --git a/package-lock.json b/package-lock.json
index f79724f7a..f6ebba692 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -99,7 +99,7 @@
"ts-node": "~10.9.2",
"typescript": "~5.9.3",
"typescript-eslint": "~8.47.0",
- "webpack": "~5.103.0",
+ "webpack": "~5.105.0",
"webpack-bundle-analyzer": "~5.0.0",
"webpack-cli": "~6.0.1",
"webpack-dev-server": "~5.2.2"
@@ -8665,9 +8665,9 @@
"license": "MIT"
},
"node_modules/baseline-browser-mapping": {
- "version": "2.8.31",
- "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.31.tgz",
- "integrity": "sha512-a28v2eWrrRWPpJSzxc+mKwm0ZtVx/G8SepdQZDArnXYU/XS+IF6mp8aB/4E+hH1tyGCoDo3KlUCdlSxGDsRkAw==",
+ "version": "2.9.19",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz",
+ "integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==",
"dev": true,
"license": "Apache-2.0",
"bin": {
@@ -8910,9 +8910,9 @@
"license": "ISC"
},
"node_modules/browserslist": {
- "version": "4.28.0",
- "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.0.tgz",
- "integrity": "sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ==",
+ "version": "4.28.1",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz",
+ "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==",
"dev": true,
"funding": [
{
@@ -8930,11 +8930,11 @@
],
"license": "MIT",
"dependencies": {
- "baseline-browser-mapping": "^2.8.25",
- "caniuse-lite": "^1.0.30001754",
- "electron-to-chromium": "^1.5.249",
+ "baseline-browser-mapping": "^2.9.0",
+ "caniuse-lite": "^1.0.30001759",
+ "electron-to-chromium": "^1.5.263",
"node-releases": "^2.0.27",
- "update-browserslist-db": "^1.1.4"
+ "update-browserslist-db": "^1.2.0"
},
"bin": {
"browserslist": "cli.js"
@@ -9182,9 +9182,9 @@
}
},
"node_modules/caniuse-lite": {
- "version": "1.0.30001757",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001757.tgz",
- "integrity": "sha512-r0nnL/I28Zi/yjk1el6ilj27tKcdjLsNqAOZr0yVjWPrSQyHgKI2INaEWw21bAQSv2LXRt1XuCS/GomNpWOxsQ==",
+ "version": "1.0.30001769",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001769.tgz",
+ "integrity": "sha512-BCfFL1sHijQlBGWBMuJyhZUhzo7wer5sVj9hqekB/7xn0Ypy+pER/edCYQm4exbXj4WiySGp40P8UuTh6w1srg==",
"dev": true,
"funding": [
{
@@ -10432,9 +10432,9 @@
"license": "MIT"
},
"node_modules/electron-to-chromium": {
- "version": "1.5.260",
- "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.260.tgz",
- "integrity": "sha512-ov8rBoOBhVawpzdre+Cmz4FB+y66Eqrk6Gwqd8NGxuhv99GQ8XqMAr351KEkOt7gukXWDg6gJWEMKgL2RLMPtA==",
+ "version": "1.5.286",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz",
+ "integrity": "sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==",
"dev": true,
"license": "ISC"
},
@@ -10528,14 +10528,14 @@
}
},
"node_modules/enhanced-resolve": {
- "version": "5.18.3",
- "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz",
- "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==",
+ "version": "5.19.0",
+ "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.19.0.tgz",
+ "integrity": "sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==",
"dev": true,
"license": "MIT",
"dependencies": {
"graceful-fs": "^4.2.4",
- "tapable": "^2.2.0"
+ "tapable": "^2.3.0"
},
"engines": {
"node": ">=10.13.0"
@@ -10690,9 +10690,9 @@
}
},
"node_modules/es-module-lexer": {
- "version": "1.7.0",
- "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
- "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==",
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz",
+ "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==",
"dev": true,
"license": "MIT"
},
@@ -19792,9 +19792,9 @@
}
},
"node_modules/terser-webpack-plugin": {
- "version": "5.3.14",
- "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.14.tgz",
- "integrity": "sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==",
+ "version": "5.3.16",
+ "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.16.tgz",
+ "integrity": "sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -20910,9 +20910,9 @@
}
},
"node_modules/update-browserslist-db": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz",
- "integrity": "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==",
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
+ "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
"dev": true,
"funding": [
{
@@ -21197,9 +21197,9 @@
}
},
"node_modules/watchpack": {
- "version": "2.4.4",
- "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz",
- "integrity": "sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==",
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz",
+ "integrity": "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -21237,9 +21237,9 @@
}
},
"node_modules/webpack": {
- "version": "5.103.0",
- "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.103.0.tgz",
- "integrity": "sha512-HU1JOuV1OavsZ+mfigY0j8d1TgQgbZ6M+J75zDkpEAwYeXjWSqrGJtgnPblJjd/mAyTNQ7ygw0MiKOn6etz8yw==",
+ "version": "5.105.0",
+ "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.105.0.tgz",
+ "integrity": "sha512-gX/dMkRQc7QOMzgTe6KsYFM7DxeIONQSui1s0n/0xht36HvrgbxtM1xBlgx596NbpHuQU8P7QpKwrZYwUX48nw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -21251,10 +21251,10 @@
"@webassemblyjs/wasm-parser": "^1.14.1",
"acorn": "^8.15.0",
"acorn-import-phases": "^1.0.3",
- "browserslist": "^4.26.3",
+ "browserslist": "^4.28.1",
"chrome-trace-event": "^1.0.2",
- "enhanced-resolve": "^5.17.3",
- "es-module-lexer": "^1.2.1",
+ "enhanced-resolve": "^5.19.0",
+ "es-module-lexer": "^2.0.0",
"eslint-scope": "5.1.1",
"events": "^3.2.0",
"glob-to-regexp": "^0.4.1",
@@ -21265,8 +21265,8 @@
"neo-async": "^2.6.2",
"schema-utils": "^4.3.3",
"tapable": "^2.3.0",
- "terser-webpack-plugin": "^5.3.11",
- "watchpack": "^2.4.4",
+ "terser-webpack-plugin": "^5.3.16",
+ "watchpack": "^2.5.1",
"webpack-sources": "^3.3.3"
},
"bin": {
diff --git a/package.json b/package.json
index 1ca0eeaba..3f5ded3fa 100644
--- a/package.json
+++ b/package.json
@@ -141,7 +141,7 @@
"ts-node": "~10.9.2",
"typescript": "~5.9.3",
"typescript-eslint": "~8.47.0",
- "webpack": "~5.103.0",
+ "webpack": "~5.105.0",
"webpack-bundle-analyzer": "~5.0.0",
"webpack-cli": "~6.0.1",
"webpack-dev-server": "~5.2.2"
diff --git a/src/commands/addDiscoveryRegistry/PromptRegistryStep.ts b/src/commands/addDiscoveryRegistry/PromptRegistryStep.ts
index 3c53c3c18..650b66bfb 100644
--- a/src/commands/addDiscoveryRegistry/PromptRegistryStep.ts
+++ b/src/commands/addDiscoveryRegistry/PromptRegistryStep.ts
@@ -30,7 +30,7 @@ export class PromptRegistryStep extends AzureWizardPromptStep a.label.localeCompare(b.label));
+ .sort((a, b) => a.label.localeCompare(b.label, undefined, { numeric: true }));
if (promptItems.length === 0) {
promptItems.push({
diff --git a/src/commands/chooseDataMigrationExtension/chooseDataMigrationExtension.ts b/src/commands/chooseDataMigrationExtension/chooseDataMigrationExtension.ts
index f4ba7f02e..11f124326 100644
--- a/src/commands/chooseDataMigrationExtension/chooseDataMigrationExtension.ts
+++ b/src/commands/chooseDataMigrationExtension/chooseDataMigrationExtension.ts
@@ -26,7 +26,7 @@ export async function chooseDataMigrationExtension(context: IActionContext, node
alwaysShow: true,
}))
// Sort alphabetically
- .sort((a, b) => a.label.localeCompare(b.label));
+ .sort((a, b) => a.label.localeCompare(b.label, undefined, { numeric: true }));
const commonItems = [
// {
diff --git a/src/commands/connections-view/deleteFolder/VerifyNoConflictsStep.test.ts b/src/commands/connections-view/deleteFolder/VerifyNoConflictsStep.test.ts
new file mode 100644
index 000000000..b1d9a1ca5
--- /dev/null
+++ b/src/commands/connections-view/deleteFolder/VerifyNoConflictsStep.test.ts
@@ -0,0 +1,252 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the MIT License. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+import { UserCancelledError } from '@microsoft/vscode-azext-utils';
+import { ConnectionType, ItemType } from '../../../services/connectionStorageService';
+import { type DeleteFolderWizardContext } from './DeleteFolderWizardContext';
+import { VerifyNoConflictsStep } from './VerifyNoConflictsStep';
+
+// Mock ConnectionStorageService
+const mockGetChildren = jest.fn();
+jest.mock('../../../services/connectionStorageService', () => ({
+ ConnectionStorageService: {
+ getChildren: (...args: unknown[]) => mockGetChildren(...args),
+ },
+ ConnectionType: {
+ Clusters: 'clusters',
+ Emulators: 'emulators',
+ },
+ ItemType: {
+ Connection: 'connection',
+ Folder: 'folder',
+ },
+}));
+
+// Mock TaskService
+const mockFindConflictingTasksForConnections = jest.fn<
+ Array<{ taskId: string; taskName: string; taskType: string }>,
+ [string[]]
+>(() => []);
+jest.mock('../../../services/taskService/taskService', () => ({
+ TaskService: {
+ findConflictingTasksForConnections: (connectionIds: string[]) =>
+ mockFindConflictingTasksForConnections(connectionIds),
+ },
+}));
+
+// Mock verificationUtils
+const mockEnumerateConnectionsInFolder = jest.fn, [string, string]>();
+const mockLogTaskConflicts = jest.fn();
+jest.mock('../verificationUtils', () => ({
+ VerificationCompleteError: class VerificationCompleteError extends Error {
+ constructor() {
+ super('Conflict verification completed successfully');
+ this.name = 'VerificationCompleteError';
+ }
+ },
+ findConflictingTasks: jest.requireActual('../verificationUtils').findConflictingTasks,
+ enumerateConnectionsInFolder: (...args: unknown[]) =>
+ mockEnumerateConnectionsInFolder(...(args as [string, string])),
+ logTaskConflicts: (...args: unknown[]) => mockLogTaskConflicts(...args),
+}));
+
+// Mock extensionVariables
+const mockAppendLog = jest.fn();
+const mockShow = jest.fn();
+jest.mock('../../../extensionVariables', () => ({
+ ext: {
+ outputChannel: {
+ get appendLog() {
+ return mockAppendLog;
+ },
+ get show() {
+ return mockShow;
+ },
+ },
+ },
+}));
+
+// Mock vscode l10n
+jest.mock('@vscode/l10n', () => ({
+ t: jest.fn((str: string) => str),
+}));
+
+// Helper to create mock context
+function createMockContext(overrides: Partial = {}): DeleteFolderWizardContext {
+ return {
+ telemetry: { properties: {}, measurements: {} },
+ errorHandling: { issueProperties: {} },
+ valuesToMask: [],
+ ui: {
+ showQuickPick: jest.fn(),
+ showInputBox: jest.fn(),
+ showWarningMessage: jest.fn(),
+ onDidFinishPrompt: jest.fn(),
+ showOpenDialog: jest.fn(),
+ showWorkspaceFolderPick: jest.fn(),
+ },
+ folderItem: overrides.folderItem ?? {
+ storageId: 'folder-1',
+ name: 'Test Folder',
+ },
+ connectionType: overrides.connectionType ?? ConnectionType.Clusters,
+ foldersToDelete: 0,
+ connectionsToDelete: 0,
+ conflictingTasks: [],
+ ...overrides,
+ } as DeleteFolderWizardContext;
+}
+
+describe('VerifyNoConflictsStep (deleteFolder)', () => {
+ let step: VerifyNoConflictsStep;
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ step = new VerifyNoConflictsStep();
+ mockGetChildren.mockResolvedValue([]);
+ mockEnumerateConnectionsInFolder.mockResolvedValue([]);
+ mockFindConflictingTasksForConnections.mockReturnValue([]);
+ });
+
+ describe('shouldPrompt', () => {
+ it('should always return true', () => {
+ expect(step.shouldPrompt()).toBe(true);
+ });
+ });
+
+ describe('prompt - no conflicts', () => {
+ it('should proceed without error when no conflicts exist', async () => {
+ const context = createMockContext();
+
+ (context.ui.showQuickPick as jest.Mock).mockImplementation(async (itemsPromise: Promise) => {
+ await itemsPromise;
+ return { data: 'exit' };
+ });
+
+ // Should complete without error (VerificationCompleteError is caught internally)
+ await expect(step.prompt(context)).resolves.not.toThrow();
+ });
+
+ it('should count descendants correctly', async () => {
+ const context = createMockContext();
+
+ // Simulate folder with 2 connections and 1 subfolder containing 1 connection
+ mockGetChildren
+ .mockResolvedValueOnce([
+ { id: 'conn-1', properties: { type: ItemType.Connection } },
+ { id: 'conn-2', properties: { type: ItemType.Connection } },
+ { id: 'subfolder-1', properties: { type: ItemType.Folder } },
+ ])
+ .mockResolvedValueOnce([{ id: 'conn-3', properties: { type: ItemType.Connection } }]);
+
+ (context.ui.showQuickPick as jest.Mock).mockImplementation(async (itemsPromise: Promise) => {
+ await itemsPromise;
+ return { data: 'exit' };
+ });
+
+ await step.prompt(context);
+
+ expect(context.foldersToDelete).toBe(2); // 1 subfolder + 1 for the folder itself
+ expect(context.connectionsToDelete).toBe(3);
+ });
+ });
+
+ describe('prompt - task conflicts', () => {
+ it('should throw UserCancelledError when user selects cancel', async () => {
+ const context = createMockContext();
+
+ mockEnumerateConnectionsInFolder.mockResolvedValue(['conn-1']);
+ mockFindConflictingTasksForConnections.mockReturnValue([
+ { taskId: 'task-1', taskName: 'Copy Task', taskType: 'copy-paste' },
+ ]);
+
+ (context.ui.showQuickPick as jest.Mock).mockImplementation(async (itemsPromise: Promise) => {
+ await itemsPromise;
+ return { data: 'exit' };
+ });
+
+ await expect(step.prompt(context)).rejects.toThrow(UserCancelledError);
+ expect(context.conflictingTasks).toHaveLength(1);
+ });
+
+ it('should offer show-output and cancel options for task conflicts', async () => {
+ const context = createMockContext();
+
+ mockEnumerateConnectionsInFolder.mockResolvedValue(['conn-1']);
+ mockFindConflictingTasksForConnections.mockReturnValue([
+ { taskId: 'task-1', taskName: 'Copy Task', taskType: 'copy-paste' },
+ ]);
+
+ let capturedOptions: Array<{ data: string }> = [];
+ (context.ui.showQuickPick as jest.Mock).mockImplementation(
+ async (itemsPromise: Promise>) => {
+ capturedOptions = await itemsPromise;
+ return { data: 'exit' };
+ },
+ );
+
+ try {
+ await step.prompt(context);
+ } catch {
+ // Expected
+ }
+
+ expect(capturedOptions).toHaveLength(2);
+ expect(capturedOptions[0].data).toBe('show-output');
+ expect(capturedOptions[1].data).toBe('exit');
+ });
+
+ it('should show output and re-prompt when user selects show-output', async () => {
+ const context = createMockContext();
+
+ mockEnumerateConnectionsInFolder.mockResolvedValue(['conn-1']);
+ mockFindConflictingTasksForConnections.mockReturnValue([
+ { taskId: 'task-1', taskName: 'Copy Task', taskType: 'copy-paste' },
+ ]);
+
+ let callCount = 0;
+ (context.ui.showQuickPick as jest.Mock).mockImplementation(async (itemsPromise: Promise) => {
+ await itemsPromise;
+ callCount++;
+ if (callCount === 1) {
+ return { data: 'show-output' };
+ }
+ return { data: 'exit' };
+ });
+
+ await expect(step.prompt(context)).rejects.toThrow(UserCancelledError);
+
+ // show-output should have caused re-prompt (2 calls total)
+ expect(callCount).toBe(2);
+ // outputChannel.show() should have been called
+ expect(mockShow).toHaveBeenCalled();
+ });
+
+ it('should log task conflict details', async () => {
+ const context = createMockContext();
+
+ mockEnumerateConnectionsInFolder.mockResolvedValue(['conn-1']);
+ mockFindConflictingTasksForConnections.mockReturnValue([
+ { taskId: 'task-1', taskName: 'Copy Task', taskType: 'copy-paste' },
+ ]);
+
+ (context.ui.showQuickPick as jest.Mock).mockImplementation(async (itemsPromise: Promise) => {
+ await itemsPromise;
+ return { data: 'exit' };
+ });
+
+ try {
+ await step.prompt(context);
+ } catch {
+ // Expected
+ }
+
+ expect(mockLogTaskConflicts).toHaveBeenCalledWith(
+ expect.any(String),
+ expect.arrayContaining([expect.objectContaining({ taskId: 'task-1' })]),
+ );
+ });
+ });
+});
diff --git a/src/commands/connections-view/deleteFolder/VerifyNoConflictsStep.ts b/src/commands/connections-view/deleteFolder/VerifyNoConflictsStep.ts
index 7d99f8fc9..4986a0153 100644
--- a/src/commands/connections-view/deleteFolder/VerifyNoConflictsStep.ts
+++ b/src/commands/connections-view/deleteFolder/VerifyNoConflictsStep.ts
@@ -5,6 +5,7 @@
import { AzureWizardPromptStep, UserCancelledError, type IAzureQuickPickItem } from '@microsoft/vscode-azext-utils';
import * as l10n from '@vscode/l10n';
+import { ext } from '../../../extensionVariables';
import { ConnectionStorageService, ItemType } from '../../../services/connectionStorageService';
import {
enumerateConnectionsInFolder,
@@ -14,7 +15,7 @@ import {
} from '../verificationUtils';
import { type DeleteFolderWizardContext } from './DeleteFolderWizardContext';
-type ConflictAction = 'exit';
+type ConflictAction = 'exit' | 'show-output';
/**
* Step to verify the folder can be deleted and count items to be deleted.
@@ -28,16 +29,21 @@ type ConflictAction = 'exit';
export class VerifyNoConflictsStep extends AzureWizardPromptStep {
public async prompt(context: DeleteFolderWizardContext): Promise {
try {
- // Use QuickPick with loading state while verifying
- const result = await context.ui.showQuickPick(this.verifyAndCount(context), {
- placeHolder: l10n.t('Verifying folder can be deleted…'),
- loadingPlaceHolder: l10n.t('Analyzing folder contents…'),
- suppressPersistence: true,
- });
+ // Loop to allow re-prompting after "Show Output"
+ while (true) {
+ const result = await context.ui.showQuickPick(this.verifyAndCount(context), {
+ placeHolder: l10n.t('Verifying folder can be deleted…'),
+ loadingPlaceHolder: l10n.t('Analyzing folder contents…'),
+ suppressPersistence: true,
+ });
- // User selected an action (only shown when conflicts exist)
- if (result.data === 'exit') {
- throw new UserCancelledError();
+ // User selected an action (only shown when conflicts exist)
+ if (result.data === 'show-output') {
+ ext.outputChannel.show();
+ continue; // Re-prompt after showing output
+ } else if (result.data === 'exit') {
+ throw new UserCancelledError();
+ }
}
} catch (error) {
if (error instanceof VerificationCompleteError) {
@@ -81,8 +87,13 @@ export class VerifyNoConflictsStep extends AzureWizardPromptStep a.label.localeCompare(b.label)); // Alphabetical sort
+ .sort((a, b) => a.label.localeCompare(b.label, undefined, { numeric: true })); // Alphabetical sort
// Determine if ALL source items are currently at root level
const allItemsAtRoot = context.itemsToMove.every((item) => item.properties.parentId === undefined);
diff --git a/src/commands/connections-view/moveItems/VerifyNoConflictsStep.test.ts b/src/commands/connections-view/moveItems/VerifyNoConflictsStep.test.ts
index 9d20cf823..827b97aed 100644
--- a/src/commands/connections-view/moveItems/VerifyNoConflictsStep.test.ts
+++ b/src/commands/connections-view/moveItems/VerifyNoConflictsStep.test.ts
@@ -240,7 +240,7 @@ describe('VerifyNoConflictsStep', () => {
(context.ui.showQuickPick as jest.Mock).mockImplementation(
async (itemsPromise: Promise[]>) => {
const items = await itemsPromise;
- expect(items).toHaveLength(2); // 'back' and 'exit' options
+ expect(items).toHaveLength(3); // 'show-output', 'back' and 'exit' options
return { data: 'back' };
},
);
@@ -294,9 +294,8 @@ describe('VerifyNoConflictsStep', () => {
// Expected to throw
}
- // Verify output channel was used
+ // Verify output channel was used for logging (but not auto-shown)
expect(mockAppendLog).toHaveBeenCalled();
- expect(mockShow).toHaveBeenCalled();
// Verify conflict names were logged
const logCalls = mockAppendLog.mock.calls.map((call) => call[0]);
@@ -382,6 +381,35 @@ describe('VerifyNoConflictsStep', () => {
await expect(step.prompt(context)).resolves.not.toThrow();
expect(mockIsNameDuplicateInParent).not.toHaveBeenCalled();
});
+
+ it('should show output and re-prompt when user selects show-output for naming conflicts', async () => {
+ const context = createMockContext({
+ itemsToMove: [createMockConnectionItem({ name: 'Conflicting Item' })],
+ targetFolderId: 'target-folder',
+ });
+
+ // Simulate conflict
+ mockIsNameDuplicateInParent.mockResolvedValue(true);
+
+ let callCount = 0;
+ (context.ui.showQuickPick as jest.Mock).mockImplementation(
+ async (itemsPromise: Promise[]>) => {
+ await itemsPromise;
+ callCount++;
+ if (callCount === 1) {
+ return { data: 'show-output' }; // First call: show output
+ }
+ return { data: 'exit' }; // Second call: cancel
+ },
+ );
+
+ await expect(step.prompt(context)).rejects.toThrow(UserCancelledError);
+
+ // show-output should have caused re-prompt (2 calls total)
+ expect(callCount).toBe(2);
+ // outputChannel.show() should have been called
+ expect(mockShow).toHaveBeenCalled();
+ });
});
describe('task conflict detection', () => {
@@ -518,9 +546,10 @@ describe('VerifyNoConflictsStep', () => {
// Expected
}
- // Task conflicts should only have cancel option (no go back)
- expect(capturedOptions).toHaveLength(1);
- expect(capturedOptions[0].data).toBe('exit');
+ // Task conflicts should have show-output and cancel options (no go back)
+ expect(capturedOptions).toHaveLength(2);
+ expect(capturedOptions[0].data).toBe('show-output');
+ expect(capturedOptions[1].data).toBe('exit');
});
it('should log task conflict details to output channel', async () => {
@@ -555,5 +584,36 @@ describe('VerifyNoConflictsStep', () => {
expect.arrayContaining([expect.objectContaining({ taskId: 'task-1' })]),
);
});
+
+ it('should show output and re-prompt when user selects show-output for task conflicts', async () => {
+ const context = createMockContext({
+ itemsToMove: [createMockConnectionItem({ id: 'conn-1', name: 'Connection 1' })],
+ });
+
+ mockFindConflictingTasksForConnections.mockReturnValue([
+ { taskId: 'task-1', taskName: 'Copy Task', taskType: 'copy-paste' },
+ ]);
+
+ let callCount = 0;
+ const mockShowQuickPick = jest.fn().mockImplementation(async (itemsPromise: Promise) => {
+ await itemsPromise;
+ callCount++;
+ if (callCount === 1) {
+ return { data: 'show-output' }; // First call: show output
+ }
+ return { data: 'exit' }; // Second call: cancel
+ });
+ context.ui = {
+ ...context.ui,
+ showQuickPick: mockShowQuickPick,
+ } as unknown as typeof context.ui;
+
+ await expect(step.prompt(context)).rejects.toThrow(UserCancelledError);
+
+ // show-output should have caused re-prompt (2 calls total)
+ expect(callCount).toBe(2);
+ // outputChannel.show() should have been called
+ expect(mockShow).toHaveBeenCalled();
+ });
});
});
diff --git a/src/commands/connections-view/moveItems/VerifyNoConflictsStep.ts b/src/commands/connections-view/moveItems/VerifyNoConflictsStep.ts
index 00ce04f62..d5619b143 100644
--- a/src/commands/connections-view/moveItems/VerifyNoConflictsStep.ts
+++ b/src/commands/connections-view/moveItems/VerifyNoConflictsStep.ts
@@ -20,7 +20,7 @@ import {
} from '../verificationUtils';
import { type MoveItemsWizardContext } from './MoveItemsWizardContext';
-type ConflictAction = 'back' | 'exit';
+type ConflictAction = 'back' | 'exit' | 'show-output';
/**
* Step to verify the move operation can proceed safely.
@@ -33,20 +33,25 @@ type ConflictAction = 'back' | 'exit';
export class VerifyNoConflictsStep extends AzureWizardPromptStep {
public async prompt(context: MoveItemsWizardContext): Promise {
try {
- // Use QuickPick with loading state while checking for conflicts
- const result = await context.ui.showQuickPick(this.verifyNoConflicts(context), {
- placeHolder: l10n.t('Verifying move operation…'),
- loadingPlaceHolder: l10n.t('Checking for conflicts…'),
- suppressPersistence: true,
- });
-
- // User selected an action (only shown when conflicts exist)
- if (result.data === 'back') {
- context.targetFolderId = undefined;
- context.targetFolderPath = undefined;
- throw new GoBackError();
- } else {
- throw new UserCancelledError();
+ // Loop to allow re-prompting after "Show Output"
+ while (true) {
+ const result = await context.ui.showQuickPick(this.verifyNoConflicts(context), {
+ placeHolder: l10n.t('Verifying move operation…'),
+ loadingPlaceHolder: l10n.t('Checking for conflicts…'),
+ suppressPersistence: true,
+ });
+
+ // User selected an action (only shown when conflicts exist)
+ if (result.data === 'show-output') {
+ ext.outputChannel.show();
+ continue; // Re-prompt after showing output
+ } else if (result.data === 'back') {
+ context.targetFolderId = undefined;
+ context.targetFolderPath = undefined;
+ throw new GoBackError();
+ } else {
+ throw new UserCancelledError();
+ }
}
} catch (error) {
if (error instanceof VerificationCompleteError) {
@@ -109,8 +114,13 @@ export class VerifyNoConflictsStep extends AzureWizardPromptStep {
+ name = name.trim();
+
if (name.length === 0) {
return l10n.t('Collection name is required.');
}
diff --git a/src/commands/createDatabase/DatabaseNameStep.ts b/src/commands/createDatabase/DatabaseNameStep.ts
index abd0a6f06..33c0876f9 100644
--- a/src/commands/createDatabase/DatabaseNameStep.ts
+++ b/src/commands/createDatabase/DatabaseNameStep.ts
@@ -60,6 +60,8 @@ export class DatabaseNameStep extends AzureWizardPromptStep {
+ name = name.trim();
+
if (name.length === 0) {
return l10n.t('Database name is required.');
}
diff --git a/src/commands/exportDocuments/exportDocuments.ts b/src/commands/exportDocuments/exportDocuments.ts
index 31edc0b3d..0f2fd7bb2 100644
--- a/src/commands/exportDocuments/exportDocuments.ts
+++ b/src/commands/exportDocuments/exportDocuments.ts
@@ -56,7 +56,7 @@ export async function exportQueryResults(
);
const filePath = targetUri.fsPath; // Convert `vscode.Uri` to a regular file path
- ext.outputChannel.appendLog(l10n.t('Exporting data to: {filePath}', { filePath }));
+ ext.outputChannel.info(l10n.t('Starting export to: {filePath}', { filePath }));
let documentCount = 0;
@@ -78,7 +78,7 @@ export async function exportQueryResults(
actionContext.telemetry.measurements.documentCount = documentCount;
});
- ext.outputChannel.appendLog(l10n.t('Exported document count: {documentCount}', { documentCount }));
+ ext.outputChannel.info(l10n.t('Export complete. Exported document count: {documentCount}', { documentCount }));
}
async function runExportWithProgressAndDescription(
@@ -99,14 +99,21 @@ async function runExportWithProgressAndDescription(
try {
await exportFunction(progress, cancellationToken);
} catch (error) {
- vscode.window.showErrorMessage(
- l10n.t('Failed to export documents. Please see the output for details.'),
- );
- ext.outputChannel.appendLog(
+ ext.outputChannel.error(
l10n.t('Error exporting documents: {error}', {
error: parseError(error).message,
}),
);
+
+ void vscode.window
+ .showErrorMessage(l10n.t('Failed to export documents.'), l10n.t('Show Output'))
+ .then((choice) => {
+ if (choice === l10n.t('Show Output')) {
+ ext.outputChannel.show();
+ }
+ });
+
+ throw error;
}
progress.report({ increment: 100 }); // Complete the progress bar
},
@@ -125,52 +132,51 @@ async function exportDocumentsToFile(
let documentCount = 0;
- try {
- // Start the JSON array
- let buffer = '[\n';
-
- for await (const doc of documentStream) {
- if (cancellationToken.isCancellationRequested) {
- // Cancel the operation
- documentStreamAbortController.abort();
- await vscode.workspace.fs.delete(vscode.Uri.file(filePath)); // Clean up the file if canceled
- vscode.window.showWarningMessage(l10n.t('The export operation was canceled.'));
- return documentCount;
- }
-
- documentCount += 1;
- const docString = EJSON.stringify(doc, undefined, 4);
-
- // Progress reporting for every 100 documents
- if (documentCount % 100 === 0) {
- progress.report({ message: l10n.t('{documentCount} documents exported…', { documentCount }) });
+ // Start the JSON array
+ let buffer = '[\n';
+
+ for await (const doc of documentStream) {
+ if (cancellationToken.isCancellationRequested) {
+ // Cancel the operation
+ documentStreamAbortController.abort();
+ try {
+ await vscode.workspace.fs.delete(vscode.Uri.file(filePath));
+ } catch {
+ // Ignore errors if the file doesn't exist yet (canceled before first write)
}
+ ext.outputChannel.warn(l10n.t('Export operation was canceled after {0} document(s).', documentCount));
+ void vscode.window.showWarningMessage(l10n.t('The export operation was canceled.'));
+ return documentCount;
+ }
- // Prepare buffer for writing
- buffer += buffer.length > 2 ? ',\n' : ''; // Add a comma and newline for non-first documents
- buffer += docString;
+ documentCount += 1;
+ const docString = EJSON.stringify(doc, undefined, 4);
- if (buffer.length > bufferLimit) {
- await appendToFile(filePath, buffer);
- buffer = ''; // Clear the buffer after writing
- }
+ // Progress reporting for every 100 documents
+ if (documentCount % 100 === 0) {
+ ext.outputChannel.trace(l10n.t('{documentCount} documents exported…', { documentCount }));
+ progress.report({ message: l10n.t('{documentCount} documents exported…', { documentCount }) });
}
- // Final buffer flush after the loop
- if (buffer.length > 0) {
+ // Prepare buffer for writing
+ buffer += buffer.length > 2 ? ',\n' : ''; // Add a comma and newline for non-first documents
+ buffer += docString;
+
+ if (buffer.length > bufferLimit) {
await appendToFile(filePath, buffer);
+ buffer = ''; // Clear the buffer after writing
}
+ }
- await appendToFile(filePath, '\n]'); // End the JSON array
-
- vscode.window.showInformationMessage(l10n.t('Exported document count: {documentCount}', { documentCount }));
- } catch (error) {
- vscode.window.showErrorMessage(
- l10n.t('Error exporting documents: {error}', { error: parseError(error).message }),
- );
- throw error; // Re-throw the error to be caught by the outer error handler
+ // Final buffer flush after the loop
+ if (buffer.length > 0) {
+ await appendToFile(filePath, buffer);
}
+ await appendToFile(filePath, '\n]'); // End the JSON array
+
+ void vscode.window.showInformationMessage(l10n.t('Exported document count: {documentCount}', { documentCount }));
+
return documentCount;
}
diff --git a/src/commands/importDocuments/importDocuments.ts b/src/commands/importDocuments/importDocuments.ts
index c3a589d95..82b323216 100644
--- a/src/commands/importDocuments/importDocuments.ts
+++ b/src/commands/importDocuments/importDocuments.ts
@@ -46,11 +46,21 @@ export async function importDocuments(
});
if (ignoredUris.length) {
- ext.outputChannel.appendLog(
+ ext.outputChannel.warn(
l10n.t('Ignoring the following files that do not match the "*.json" file name pattern:'),
);
- ignoredUris.forEach((uri) => ext.outputChannel.appendLog(`${uri.fsPath}`));
- ext.outputChannel.show();
+ ignoredUris.forEach((uri) => ext.outputChannel.warn(` ${uri.fsPath}`));
+
+ void vscode.window
+ .showWarningMessage(
+ l10n.t('{0} file(s) were ignored because they do not match the "*.json" pattern.', ignoredUris.length),
+ l10n.t('Show Output'),
+ )
+ .then((choice) => {
+ if (choice === l10n.t('Show Output')) {
+ ext.outputChannel.show();
+ }
+ });
}
if (!selectedItem) {
@@ -80,10 +90,19 @@ export async function importDocumentsWithProgress(selectedItem: CollectionItem,
{
location: vscode.ProgressLocation.Notification,
title: l10n.t('Importing documents…'),
+ cancellable: true,
},
- async (progress) => {
+ async (progress, cancellationToken) => {
progress.report({ increment: 0, message: l10n.t('Loading documents…') });
+ ext.outputChannel.info(
+ l10n.t(
+ 'Starting import of {0} file(s) into collection "{1}"',
+ uris.length,
+ selectedItem.collectionInfo.name,
+ ),
+ );
+
const countUri = uris.length;
const incrementUri = 25 / (countUri || 1);
const documents: unknown[] = [];
@@ -96,24 +115,32 @@ export async function importDocumentsWithProgress(selectedItem: CollectionItem,
message: l10n.t('Loading document {num} of {countUri}', { num: i + 1, countUri }),
});
+ ext.outputChannel.trace(l10n.t('Parsing file {0}: {1}', i + 1, uris[i].fsPath));
+
const result = await parseAndValidateFile(selectedItem, uris[i]);
// Note to future maintainers: the validation can return 0 valid documents and still have errors.
if (result.errors && result.errors.length) {
- ext.outputChannel.appendLog(
- l10n.t('Errors found in document {path}. Please fix these.', { path: uris[i].path }),
+ ext.outputChannel.error(
+ l10n.t('Errors found in file "{path}". Please fix these:', { path: uris[i].path }),
);
- ext.outputChannel.appendLog(result.errors.join('\n'));
- ext.outputChannel.show();
+ for (const err of result.errors) {
+ ext.outputChannel.error(` ${err}`);
+ }
hasErrors = true;
}
if (result.documents && result.documents.length > 0) {
+ ext.outputChannel.trace(
+ l10n.t('Loaded {0} document(s) from "{1}"', result.documents.length, uris[i].path),
+ );
documents.push(...result.documents);
}
}
+ ext.outputChannel.info(l10n.t('Total documents to import: {0}', documents.length));
+
const countDocuments = documents.length;
const incrementDocuments = 75 / (countDocuments || 1);
let count = 0;
@@ -143,7 +170,15 @@ export async function importDocumentsWithProgress(selectedItem: CollectionItem,
}
}
+ let wasCancelled = false;
+
for (let i = 0; i < countDocuments; i++) {
+ if (cancellationToken.isCancellationRequested) {
+ wasCancelled = true;
+ ext.outputChannel.warn(l10n.t('Import operation was canceled after {0} document(s).', count));
+ break;
+ }
+
progress.report({
increment: incrementDocuments,
message: l10n.t('Importing document {num} of {countDocuments}', {
@@ -161,7 +196,7 @@ export async function importDocumentsWithProgress(selectedItem: CollectionItem,
}
// Do insertion for the last batch for bulk insertion
- if (buffer && buffer.getStats().documentCount > 0) {
+ if (!wasCancelled && buffer && buffer.getStats().documentCount > 0) {
const lastBatchFlushResult = await insertDocument(selectedItem, undefined, buffer);
count += lastBatchFlushResult.count;
@@ -171,16 +206,41 @@ export async function importDocumentsWithProgress(selectedItem: CollectionItem,
// let's make sure we reach 100% progress, useful in case of errors etc.
progress.report({ increment: 100, message: l10n.t('Finished importing') });
- return (
- (hasErrors ? l10n.t('Import completed with errors.') : l10n.t('Import successful.')) +
- ' ' +
- l10n.t('Inserted {0} document(s). See output for more details.', count)
- );
+ return { hasErrors, count, wasCancelled };
},
);
- // We should not use await here, otherwise the node status will not be updated until the message is closed
- vscode.window.showInformationMessage(result);
+ if (result.wasCancelled) {
+ const message = l10n.t('Import canceled. Inserted {0} document(s) before cancellation.', result.count);
+ ext.outputChannel.warn(message);
+ void vscode.window.showWarningMessage(message, l10n.t('Show Output')).then((choice) => {
+ if (choice === l10n.t('Show Output')) {
+ ext.outputChannel.show();
+ }
+ });
+ return;
+ }
+
+ const message =
+ (result.hasErrors ? l10n.t('Import completed with errors.') : l10n.t('Import successful.')) +
+ ' ' +
+ l10n.t('Inserted {0} document(s).', result.count);
+
+ if (result.hasErrors) {
+ ext.outputChannel.warn(message);
+
+ void vscode.window
+ .showWarningMessage(message + ' ' + l10n.t('See output for more details.'), l10n.t('Show Output'))
+ .then((choice) => {
+ if (choice === l10n.t('Show Output')) {
+ ext.outputChannel.show();
+ }
+ });
+ } else {
+ ext.outputChannel.info(message);
+ // We should not use await here, otherwise the node status will not be updated until the message is closed
+ void vscode.window.showInformationMessage(message);
+ }
}
async function askForDocuments(context: IActionContext): Promise {
@@ -257,6 +317,7 @@ async function insertDocument(
try {
// Check for valid buffer
if (!buffer) {
+ ext.outputChannel.error(l10n.t('Import failed: document buffer is not initialized.'));
return { count: 0, errorOccurred: true };
}
@@ -266,8 +327,11 @@ async function insertDocument(
}
// Should only reach here if node is neither CollectionItem nor CosmosDBContainerResourceItem
+ ext.outputChannel.error(l10n.t('Import failed: unsupported node type.'));
return { count: 0, errorOccurred: true };
- } catch {
+ } catch (error) {
+ const errorMessage = error instanceof Error ? error.message : String(error);
+ ext.outputChannel.error(l10n.t('Import failed with unexpected error: {0}', errorMessage));
return { count: 0, errorOccurred: true };
}
}
@@ -316,12 +380,44 @@ async function insertDocumentWithBufferIntoCluster(
if (isBulkWriteError(error)) {
// Handle MongoDB bulk write errors
// It could be a partial failure, so we need to check the result
+ ext.outputChannel.warn(
+ l10n.t(
+ 'Bulk write error during import into "{0}.{1}": {2} document(s) inserted.',
+ databaseName,
+ collectionName,
+ error.result.insertedCount,
+ ),
+ );
+ if (error.writeErrors) {
+ const errors = Array.isArray(error.writeErrors) ? error.writeErrors : [error.writeErrors];
+ for (const writeError of errors) {
+ // Extract error message from WriteError object
+ // WriteError objects have errmsg property, other errors may have message
+ let errorMsg = '';
+ if (typeof writeError === 'object' && writeError !== null) {
+ if (typeof (writeError as unknown as { errmsg?: string }).errmsg === 'string') {
+ errorMsg = (writeError as unknown as { errmsg: string }).errmsg;
+ } else if (typeof (writeError as unknown as { message?: string }).message === 'string') {
+ errorMsg = (writeError as unknown as { message: string }).message;
+ } else {
+ errorMsg = JSON.stringify(writeError);
+ }
+ } else {
+ errorMsg = JSON.stringify(writeError);
+ }
+ ext.outputChannel.error(' ' + l10n.t('Write error: {0}', errorMsg));
+ }
+ }
return {
count: error.result.insertedCount,
errorOccurred: true,
};
} else {
// Handle other errors
+ const errorMessage = error instanceof Error ? error.message : String(error);
+ ext.outputChannel.error(
+ l10n.t('Error inserting documents into "{0}.{1}": {2}', databaseName, collectionName, errorMessage),
+ );
return {
count: 0,
errorOccurred: true,
diff --git a/src/commands/newConnection/PromptPasswordStep.ts b/src/commands/newConnection/PromptPasswordStep.ts
index 872e9a44e..330cdcbfd 100644
--- a/src/commands/newConnection/PromptPasswordStep.ts
+++ b/src/commands/newConnection/PromptPasswordStep.ts
@@ -21,6 +21,13 @@ export class PromptPasswordStep extends AzureWizardPromptStep this.validateInput(context, password),
+ // eslint-disable-next-line @typescript-eslint/require-await
+ asyncValidationTask: async (password?: string) => {
+ if (!password || password.length === 0) {
+ return l10n.t('Password cannot be empty');
+ }
+ return undefined;
+ },
});
context.valuesToMask.push(password);
@@ -36,7 +43,7 @@ export class PromptPasswordStep extends AzureWizardPromptStep a.label.localeCompare(b.label));
+ .sort((a, b) => a.label.localeCompare(b.label, undefined, { numeric: true }));
const selectedItem = await context.ui.showQuickPick(
[
diff --git a/src/commands/newConnection/PromptTenantStep.ts b/src/commands/newConnection/PromptTenantStep.ts
index 853f82b0e..e0ce37bae 100644
--- a/src/commands/newConnection/PromptTenantStep.ts
+++ b/src/commands/newConnection/PromptTenantStep.ts
@@ -129,7 +129,7 @@ export class PromptTenantStep extends AzureWizardPromptStep {
+ if (!username || username.trim().length === 0) {
+ return l10n.t('Username cannot be empty');
+ }
+ return undefined;
+ },
});
context.userName = username.trim();
diff --git a/src/commands/newLocalConnection/mongo-ru/PromptMongoRUEmulatorConnectionStringStep.ts b/src/commands/newLocalConnection/mongo-ru/PromptMongoRUEmulatorConnectionStringStep.ts
index 6cee8eb90..f1dc91f81 100644
--- a/src/commands/newLocalConnection/mongo-ru/PromptMongoRUEmulatorConnectionStringStep.ts
+++ b/src/commands/newLocalConnection/mongo-ru/PromptMongoRUEmulatorConnectionStringStep.ts
@@ -34,6 +34,14 @@ export class PromptMongoRUEmulatorConnectionStringStep extends AzureWizardPrompt
//eslint-disable-next-line @typescript-eslint/require-await
private async validateConnectionString(connectionString: string): Promise {
+ connectionString = connectionString ? connectionString.trim() : '';
+
+ if (connectionString.length === 0) {
+ return l10n.t('Invalid Connection String: {error}', {
+ error: l10n.t('Connection string cannot be empty.'),
+ });
+ }
+
try {
new DocumentDBConnectionString(connectionString);
} catch (error) {
diff --git a/src/commands/pasteCollection/PromptNewCollectionNameStep.ts b/src/commands/pasteCollection/PromptNewCollectionNameStep.ts
index d1b613774..c28d937c6 100644
--- a/src/commands/pasteCollection/PromptNewCollectionNameStep.ts
+++ b/src/commands/pasteCollection/PromptNewCollectionNameStep.ts
@@ -153,6 +153,8 @@ export class PromptNewCollectionNameStep extends AzureWizardPromptStep {
+ name = name.trim();
+
if (name.length === 0) {
return l10n.t('Collection name is required.');
}
diff --git a/src/commands/updateConnectionString/ConnectionStringStep.ts b/src/commands/updateConnectionString/ConnectionStringStep.ts
index 4cb451655..245132b86 100644
--- a/src/commands/updateConnectionString/ConnectionStringStep.ts
+++ b/src/commands/updateConnectionString/ConnectionStringStep.ts
@@ -28,8 +28,9 @@ export class ConnectionStringStep extends AzureWizardPromptStep {
+ if (!username || username.trim().length === 0) {
+ return l10n.t('Username cannot be empty');
+ }
+ return undefined;
+ },
});
const trimmedUsername = username.trim();
diff --git a/src/documentdb/wizards/authenticate/ProvidePasswordStep.ts b/src/documentdb/wizards/authenticate/ProvidePasswordStep.ts
index b5e405fa6..d55a20374 100644
--- a/src/documentdb/wizards/authenticate/ProvidePasswordStep.ts
+++ b/src/documentdb/wizards/authenticate/ProvidePasswordStep.ts
@@ -24,15 +24,13 @@ export class ProvidePasswordStep extends AzureWizardPromptStep {
+ if (!username || username.trim().length === 0) {
+ return l10n.t('Username cannot be empty');
+ }
+ return undefined;
+ },
});
const trimmedUsername = username.trim();
diff --git a/src/plugins/api-shared/azure/credentialsManagement/AccountTenantsStep.ts b/src/plugins/api-shared/azure/credentialsManagement/AccountTenantsStep.ts
index 6ad884fbd..4441c6894 100644
--- a/src/plugins/api-shared/azure/credentialsManagement/AccountTenantsStep.ts
+++ b/src/plugins/api-shared/azure/credentialsManagement/AccountTenantsStep.ts
@@ -64,7 +64,7 @@ export class AccountTenantsStep extends AzureWizardPromptStep {
const aName = a.tenant.displayName ?? a.tenant.tenantId ?? '';
const bName = b.tenant.displayName ?? b.tenant.tenantId ?? '';
- return aName.localeCompare(bName);
+ return aName.localeCompare(bName, undefined, { numeric: true });
});
const tenantItems: TenantQuickPickItem[] = sortedTenants.map(({ tenant, isSignedIn }) => ({
diff --git a/src/plugins/api-shared/azure/credentialsManagement/SelectAccountStep.ts b/src/plugins/api-shared/azure/credentialsManagement/SelectAccountStep.ts
index 377093bad..dae504da3 100644
--- a/src/plugins/api-shared/azure/credentialsManagement/SelectAccountStep.ts
+++ b/src/plugins/api-shared/azure/credentialsManagement/SelectAccountStep.ts
@@ -166,7 +166,9 @@ export class SelectAccountStep extends AzureWizardPromptStep a.account.label.localeCompare(b.account.label));
+ return Array.from(accountMap.values()).sort((a, b) =>
+ a.account.label.localeCompare(b.account.label, undefined, { numeric: true }),
+ );
} catch (error) {
ext.outputChannel.error(
l10n.t(
diff --git a/src/plugins/api-shared/azure/subscriptionFiltering/FilterSubscriptionSubStep.ts b/src/plugins/api-shared/azure/subscriptionFiltering/FilterSubscriptionSubStep.ts
index 3d8d889de..961409cfd 100644
--- a/src/plugins/api-shared/azure/subscriptionFiltering/FilterSubscriptionSubStep.ts
+++ b/src/plugins/api-shared/azure/subscriptionFiltering/FilterSubscriptionSubStep.ts
@@ -87,7 +87,7 @@ export class FilterSubscriptionSubStep extends AzureWizardPromptStep a.label.localeCompare(b.label));
+ .sort((a, b) => a.label.localeCompare(b.label, undefined, { numeric: true }));
const selectedItems = await context.ui.showQuickPick(subscriptionItems, {
stepName: 'filterSubscriptions',
diff --git a/src/plugins/api-shared/azure/subscriptionFiltering/InitializeFilteringStep.ts b/src/plugins/api-shared/azure/subscriptionFiltering/InitializeFilteringStep.ts
index ebefde738..dc31f4cdd 100644
--- a/src/plugins/api-shared/azure/subscriptionFiltering/InitializeFilteringStep.ts
+++ b/src/plugins/api-shared/azure/subscriptionFiltering/InitializeFilteringStep.ts
@@ -82,7 +82,7 @@ export class InitializeFilteringStep extends AzureWizardPromptStep a.label.localeCompare(b.label));
+ .sort((a, b) => a.label.localeCompare(b.label, undefined, { numeric: true }));
// Add edit entry at the top
return [
diff --git a/src/plugins/service-azure-mongo-ru/discovery-tree/AzureMongoRUServiceRootItem.ts b/src/plugins/service-azure-mongo-ru/discovery-tree/AzureMongoRUServiceRootItem.ts
index 7a38c8884..a1b3f2c0d 100644
--- a/src/plugins/service-azure-mongo-ru/discovery-tree/AzureMongoRUServiceRootItem.ts
+++ b/src/plugins/service-azure-mongo-ru/discovery-tree/AzureMongoRUServiceRootItem.ts
@@ -85,7 +85,7 @@ export class AzureMongoRUServiceRootItem
return (
subscriptions
// sort by name
- .sort((a, b) => a.name.localeCompare(b.name))
+ .sort((a, b) => a.name.localeCompare(b.name, undefined, { numeric: true }))
// map to AzureMongoRUSubscriptionItem
.map((sub) => {
return new AzureMongoRUSubscriptionItem(
diff --git a/src/plugins/service-azure-mongo-ru/discovery-tree/AzureMongoRUSubscriptionItem.ts b/src/plugins/service-azure-mongo-ru/discovery-tree/AzureMongoRUSubscriptionItem.ts
index fb654343b..bb0d67df4 100644
--- a/src/plugins/service-azure-mongo-ru/discovery-tree/AzureMongoRUSubscriptionItem.ts
+++ b/src/plugins/service-azure-mongo-ru/discovery-tree/AzureMongoRUSubscriptionItem.ts
@@ -59,7 +59,7 @@ export class AzureMongoRUSubscriptionItem implements TreeElement, TreeElementWit
context.telemetry.measurements.discoveryLoadTimeMs = Date.now() - startTime;
return accounts
- .sort((a, b) => (a.name || '').localeCompare(b.name || ''))
+ .sort((a, b) => (a.name || '').localeCompare(b.name || '', undefined, { numeric: true }))
.map((account) => {
const resourceId = nonNullProp(account, 'id', 'account.id', 'AzureMongoRUSubscriptionItem.ts');
diff --git a/src/plugins/service-azure-mongo-ru/discovery-wizard/SelectRUClusterStep.ts b/src/plugins/service-azure-mongo-ru/discovery-wizard/SelectRUClusterStep.ts
index 8b000b8fd..eafc61033 100644
--- a/src/plugins/service-azure-mongo-ru/discovery-wizard/SelectRUClusterStep.ts
+++ b/src/plugins/service-azure-mongo-ru/discovery-wizard/SelectRUClusterStep.ts
@@ -50,7 +50,7 @@ export class SelectRUClusterStep extends AzureWizardPromptStep a.label.localeCompare(b.label));
+ .sort((a, b) => a.label.localeCompare(b.label, undefined, { numeric: true }));
return promptItems;
};
diff --git a/src/plugins/service-azure-mongo-vcore/discovery-tree/AzureServiceRootItem.ts b/src/plugins/service-azure-mongo-vcore/discovery-tree/AzureServiceRootItem.ts
index a84bc9af6..ffd242318 100644
--- a/src/plugins/service-azure-mongo-vcore/discovery-tree/AzureServiceRootItem.ts
+++ b/src/plugins/service-azure-mongo-vcore/discovery-tree/AzureServiceRootItem.ts
@@ -84,7 +84,7 @@ export class AzureServiceRootItem implements TreeElement, TreeElementWithContext
return (
subscriptions
// sort by name
- .sort((a, b) => a.name.localeCompare(b.name))
+ .sort((a, b) => a.name.localeCompare(b.name, undefined, { numeric: true }))
// map to AzureSubscriptionItem
.map((sub) => {
return new AzureSubscriptionItem(
diff --git a/src/plugins/service-azure-mongo-vcore/discovery-tree/AzureSubscriptionItem.ts b/src/plugins/service-azure-mongo-vcore/discovery-tree/AzureSubscriptionItem.ts
index 7957a190b..d5ccc7560 100644
--- a/src/plugins/service-azure-mongo-vcore/discovery-tree/AzureSubscriptionItem.ts
+++ b/src/plugins/service-azure-mongo-vcore/discovery-tree/AzureSubscriptionItem.ts
@@ -61,7 +61,7 @@ export class AzureSubscriptionItem implements TreeElement, TreeElementWithContex
context.telemetry.measurements.discoveryLoadTimeMs = Date.now() - startTime;
return accounts
- .sort((a, b) => (a.name || '').localeCompare(b.name || ''))
+ .sort((a, b) => (a.name || '').localeCompare(b.name || '', undefined, { numeric: true }))
.map((account) => {
const resourceId = nonNullProp(account, 'id', 'account.id', 'AzureSubscriptionItem.ts');
diff --git a/src/plugins/service-azure-mongo-vcore/discovery-wizard/SelectClusterStep.ts b/src/plugins/service-azure-mongo-vcore/discovery-wizard/SelectClusterStep.ts
index 8377e3ed3..c42a81be3 100644
--- a/src/plugins/service-azure-mongo-vcore/discovery-wizard/SelectClusterStep.ts
+++ b/src/plugins/service-azure-mongo-vcore/discovery-wizard/SelectClusterStep.ts
@@ -45,7 +45,7 @@ export class SelectClusterStep extends AzureWizardPromptStep a.label.localeCompare(b.label));
+ .sort((a, b) => a.label.localeCompare(b.label, undefined, { numeric: true }));
return promptItems;
};
diff --git a/src/plugins/service-azure-vm/discovery-tree/AzureServiceRootItem.ts b/src/plugins/service-azure-vm/discovery-tree/AzureServiceRootItem.ts
index db8aad92b..2e413bd70 100644
--- a/src/plugins/service-azure-vm/discovery-tree/AzureServiceRootItem.ts
+++ b/src/plugins/service-azure-vm/discovery-tree/AzureServiceRootItem.ts
@@ -83,7 +83,7 @@ export class AzureServiceRootItem implements TreeElement, TreeElementWithContext
return (
subscriptions
// sort by name
- .sort((a, b) => a.name.localeCompare(b.name))
+ .sort((a, b) => a.name.localeCompare(b.name, undefined, { numeric: true }))
// map to AzureSubscriptionItem
.map((sub) => {
return new AzureSubscriptionItem(
diff --git a/src/plugins/service-azure-vm/discovery-tree/AzureSubscriptionItem.ts b/src/plugins/service-azure-vm/discovery-tree/AzureSubscriptionItem.ts
index bc0329d7d..a193e5413 100644
--- a/src/plugins/service-azure-vm/discovery-tree/AzureSubscriptionItem.ts
+++ b/src/plugins/service-azure-vm/discovery-tree/AzureSubscriptionItem.ts
@@ -144,7 +144,9 @@ export class AzureSubscriptionItem implements TreeElement, TreeElementWithContex
context.telemetry.measurements.discoveryResourcesCount = vmItems.length;
context.telemetry.measurements.discoveryLoadTimeMs = Date.now() - startTime;
- return vmItems.sort((a, b) => a.cluster.name.localeCompare(b.cluster.name));
+ return vmItems.sort((a, b) =>
+ a.cluster.name.localeCompare(b.cluster.name, undefined, { numeric: true }),
+ );
},
);
}
diff --git a/src/plugins/service-azure-vm/discovery-wizard/SelectVMStep.ts b/src/plugins/service-azure-vm/discovery-wizard/SelectVMStep.ts
index f18d2a7d4..39248efa5 100644
--- a/src/plugins/service-azure-vm/discovery-wizard/SelectVMStep.ts
+++ b/src/plugins/service-azure-vm/discovery-wizard/SelectVMStep.ts
@@ -124,7 +124,7 @@ export class SelectVMStep extends AzureWizardPromptStep a.label.localeCompare(b.label));
+ return taggedVms.sort((a, b) => a.label.localeCompare(b.label, undefined, { numeric: true }));
};
const selectedVMItem = await context.ui.showQuickPick(getVMQuickPickItems(), {
diff --git a/src/services/taskService/README.md b/src/services/taskService/README.md
index 9d88dfc18..f096feb2b 100644
--- a/src/services/taskService/README.md
+++ b/src/services/taskService/README.md
@@ -582,6 +582,21 @@ try {
}
```
+### Task Failure Notifications
+
+When a task transitions to `Failed`, the framework handles failure reporting centrally:
+
+1. **Error logging**: The `Task` base class automatically logs the error to the output channel via `ext.outputChannel.error()` — task authors do NOT need to do this manually.
+2. **User notification**: The `TaskProgressReportingService` automatically shows an error notification with a **"Show Output"** button, allowing the user to view error details on demand.
+3. **No auto-show**: The output channel is NOT opened automatically — this avoids disrupting the user's workflow. The user can choose to view it via the button.
+
+Task authors do **not** need to:
+
+- Call `ext.outputChannel.show()` on failure
+- Show their own error notifications via `vscode.window.showErrorMessage()`
+
+Both are handled by the framework. If a task needs custom failure behavior (e.g., logging additional context), it can handle errors in `doWork()` before re-throwing.
+
### Error Classification
The `StreamingDocumentWriter` classifies errors for retry decisions:
diff --git a/src/services/taskService/UI/taskProgressReportingService.ts b/src/services/taskService/UI/taskProgressReportingService.ts
index d1bd15386..6db374fc3 100644
--- a/src/services/taskService/UI/taskProgressReportingService.ts
+++ b/src/services/taskService/UI/taskProgressReportingService.ts
@@ -4,6 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
+import { ext } from '../../../extensionVariables';
import { isTerminalState, TaskState, type Task, type TaskService } from '../taskService';
/**
@@ -321,13 +322,20 @@ class TaskProgressReportingServiceImpl implements TaskProgressReportingService {
void vscode.window.showInformationMessage(vscode.l10n.t('{0} was stopped', task.name));
break;
case TaskState.Failed:
- void vscode.window.showErrorMessage(
- vscode.l10n.t(
- '{0} failed: {1}',
- task.name,
- status.error instanceof Error ? status.error.message : 'Unknown error',
- ),
- );
+ void vscode.window
+ .showErrorMessage(
+ vscode.l10n.t(
+ '{0} failed: {1}',
+ task.name,
+ status.error instanceof Error ? status.error.message : 'Unknown error',
+ ),
+ vscode.l10n.t('Show Output'),
+ )
+ .then((choice) => {
+ if (choice === vscode.l10n.t('Show Output')) {
+ ext.outputChannel.show();
+ }
+ });
break;
}
}
diff --git a/src/services/taskService/data-api/writers/StreamingDocumentWriter.ts b/src/services/taskService/data-api/writers/StreamingDocumentWriter.ts
index aa3577e33..4cf1500fc 100644
--- a/src/services/taskService/data-api/writers/StreamingDocumentWriter.ts
+++ b/src/services/taskService/data-api/writers/StreamingDocumentWriter.ts
@@ -826,7 +826,6 @@ export abstract class StreamingDocumentWriter {
);
ext.outputChannel.error(vscode.l10n.t('[StreamingWriter] Partial progress: {0}', statsError.getStatsString()));
- ext.outputChannel.show();
throw statsError;
}
diff --git a/src/services/taskService/taskService.test.ts b/src/services/taskService/taskService.test.ts
index 62be0f170..663e6a0c8 100644
--- a/src/services/taskService/taskService.test.ts
+++ b/src/services/taskService/taskService.test.ts
@@ -31,6 +31,9 @@ jest.mock('@microsoft/vscode-azext-utils', () => ({
properties: {},
measurements: {},
},
+ errorHandling: {
+ suppressDisplay: false,
+ },
};
return await callback(mockContext);
},
diff --git a/src/services/taskService/taskService.ts b/src/services/taskService/taskService.ts
index 700a3a1aa..71d242aae 100644
--- a/src/services/taskService/taskService.ts
+++ b/src/services/taskService/taskService.ts
@@ -236,7 +236,6 @@ export abstract class Task {
message: `${msg}${detail}`.trim(),
}),
);
- ext.outputChannel.show();
}
}
}
@@ -341,6 +340,10 @@ export abstract class Task {
this.updateStatus(TaskState.Completed, vscode.l10n.t('Task completed successfully'), 100);
}
} catch (error) {
+ // Suppress the default error notification from callWithTelemetryAndErrorHandling
+ // because TaskProgressReportingService shows its own notification with a "Show Output" button
+ context.errorHandling.suppressDisplay = true;
+
// Add error information to telemetry
context.telemetry.properties.task_error = error instanceof Error ? error.message : 'Unknown error';
diff --git a/src/tree/connections-view/ConnectionsBranchDataProvider.ts b/src/tree/connections-view/ConnectionsBranchDataProvider.ts
index 8e1a07673..052054c35 100644
--- a/src/tree/connections-view/ConnectionsBranchDataProvider.ts
+++ b/src/tree/connections-view/ConnectionsBranchDataProvider.ts
@@ -160,10 +160,10 @@ export class ConnectionsBranchDataProvider extends BaseExtendedTreeDataProvider<
});
// Sort folders alphabetically by name
- clusterFolderItems.sort((a, b) => a.name.localeCompare(b.name));
+ clusterFolderItems.sort((a, b) => a.name.localeCompare(b.name, undefined, { numeric: true }));
// Sort connections alphabetically by name
- clusterItems.sort((a, b) => a.cluster.name.localeCompare(b.cluster.name));
+ clusterItems.sort((a, b) => a.cluster.name.localeCompare(b.cluster.name, undefined, { numeric: true }));
// Show "New Connection" only if there are no cluster folders or connections
// (don't count the LocalEmulatorsItem - it's always shown)
diff --git a/src/tree/connections-view/FolderItem.ts b/src/tree/connections-view/FolderItem.ts
index 4d7c8898f..398f3a6bb 100644
--- a/src/tree/connections-view/FolderItem.ts
+++ b/src/tree/connections-view/FolderItem.ts
@@ -121,14 +121,14 @@ export class FolderItem implements TreeElement, TreeElementWithContextValue {
folderElements.sort((a, b) => {
const aName = (a as FolderItem).name;
const bName = (b as FolderItem).name;
- return aName.localeCompare(bName);
+ return aName.localeCompare(bName, undefined, { numeric: true });
});
// Sort connections alphabetically by name
connectionElements.sort((a, b) => {
const aName = (a as DocumentDBClusterItem).cluster.name;
const bName = (b as DocumentDBClusterItem).cluster.name;
- return aName.localeCompare(bName);
+ return aName.localeCompare(bName, undefined, { numeric: true });
});
// Return folders first, then connections
diff --git a/src/tree/connections-view/LocalEmulators/LocalEmulatorsItem.ts b/src/tree/connections-view/LocalEmulators/LocalEmulatorsItem.ts
index f3e8f44c3..d4fe00261 100644
--- a/src/tree/connections-view/LocalEmulators/LocalEmulatorsItem.ts
+++ b/src/tree/connections-view/LocalEmulators/LocalEmulatorsItem.ts
@@ -79,10 +79,10 @@ export class LocalEmulatorsItem implements TreeElement, TreeElementWithContextVa
});
// Sort folders alphabetically by name
- folderItems.sort((a, b) => a.name.localeCompare(b.name));
+ folderItems.sort((a, b) => a.name.localeCompare(b.name, undefined, { numeric: true }));
// Sort connections alphabetically by name
- connectionItems.sort((a, b) => a.cluster.name.localeCompare(b.cluster.name));
+ connectionItems.sort((a, b) => a.cluster.name.localeCompare(b.cluster.name, undefined, { numeric: true }));
// Show "New Local Connection" only if there are no folders or connections
const hasItems = folderItems.length > 0 || connectionItems.length > 0;
diff --git a/src/tree/discovery-view/DiscoveryBranchDataProvider.ts b/src/tree/discovery-view/DiscoveryBranchDataProvider.ts
index e904261c4..c16ef28e5 100644
--- a/src/tree/discovery-view/DiscoveryBranchDataProvider.ts
+++ b/src/tree/discovery-view/DiscoveryBranchDataProvider.ts
@@ -147,7 +147,7 @@ export class DiscoveryBranchDataProvider extends BaseExtendedTreeDataProvider a.id?.localeCompare(b.id ?? '') ?? 0);
+ return rootItems.sort((a, b) => a.id?.localeCompare(b.id ?? '', undefined, { numeric: true }) ?? 0);
}
/**
diff --git a/src/tree/documentdb/ClusterItemBase.ts b/src/tree/documentdb/ClusterItemBase.ts
index 71f2a9813..e62f16c0c 100644
--- a/src/tree/documentdb/ClusterItemBase.ts
+++ b/src/tree/documentdb/ClusterItemBase.ts
@@ -159,6 +159,9 @@ export abstract class ClusterItemBase a.name.localeCompare(b.name, undefined, { numeric: true }));
+
// Map the databases to DatabaseItem elements
return databases.map((database) => new DatabaseItem(this.cluster, database));
});
diff --git a/src/tree/documentdb/DatabaseItem.ts b/src/tree/documentdb/DatabaseItem.ts
index 51844a521..e3b14580e 100644
--- a/src/tree/documentdb/DatabaseItem.ts
+++ b/src/tree/documentdb/DatabaseItem.ts
@@ -49,6 +49,9 @@ export class DatabaseItem implements TreeElement, TreeElementWithExperience, Tre
];
}
+ // Sort collections alphabetically by name
+ collections.sort((a, b) => a.name.localeCompare(b.name, undefined, { numeric: true }));
+
return collections.map((collection) => {
const collectionItem = new CollectionItem(this.cluster, this.databaseInfo, collection);
// Start loading document count in background (fire-and-forget)
diff --git a/src/tree/documentdb/IndexesItem.ts b/src/tree/documentdb/IndexesItem.ts
index 89c8af9fa..086b9e1b6 100644
--- a/src/tree/documentdb/IndexesItem.ts
+++ b/src/tree/documentdb/IndexesItem.ts
@@ -48,7 +48,7 @@ export class IndexesItem implements TreeElement, TreeElementWithExperience, Tree
}
// Sort indexes by name
- indexes.sort((a, b) => a.name.localeCompare(b.name));
+ indexes.sort((a, b) => a.name.localeCompare(b.name, undefined, { numeric: true }));
return indexes.map((index) => {
return new IndexItem(this.cluster, this.databaseInfo, this.collectionInfo, index);