Skip to content

Add "Copy As" submenu to Notebook Query Result context menus - #22395

Draft
Lewis Sanchez (lewis-sanchez) wants to merge 17 commits into
mainfrom
lewissanchez/editData/copy-as-submenu
Draft

Add "Copy As" submenu to Notebook Query Result context menus#22395
Lewis Sanchez (lewis-sanchez) wants to merge 17 commits into
mainfrom
lewissanchez/editData/copy-as-submenu

Conversation

@lewis-sanchez

@lewis-sanchez Lewis Sanchez (lewis-sanchez) commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Description

This PR fixes #22209

This PR adds a sub-context menu for "Copy As" commands, so that they are similar to the ones present in the query results grid for regular query editors. There are a total of 4 "Copy As" commands that are being added:

  1. Copy as CSV
  2. Copy as JSON
  3. Copy as INSERT INTO
  4. Copy as IN clause

Because the iframe where the query results grid is rendered doesn't have a way to communicate back to SQL Tools Service, the copying logic had to be implemented client side, otherwise we most likely could have reused the same copying functionality used by the regular query results grid.

The new context menu options appear in the results grid that appears after a query is executed in a SQL notebook:
image

Result Grid and Notebook Result Grid

Result Grid:

Copy as CSV

image
John,2003-11-09,john.smith@example.com
Mariah,2004-01-30,mariah.jones@example.com
John,2003-11-09,john.smith@example.com
Mariah,2004-01-30,mariah.jones@example.com
John,2003-11-09,john.smith@example.com
Mariah,2004-01-30,mariah.jones@example.com
image

Copy as JSON

[
  {
    "FirstName": "John",
    "DateOfBirth": null,
    "Email": "john.smith@example.com"
  },
  {
    "FirstName": "Mariah",
    "DateOfBirth": null,
    "Email": "mariah.jones@example.com"
  },
  {
    "FirstName": "John",
    "DateOfBirth": null,
    "Email": "john.smith@example.com"
  },
  {
    "FirstName": "Mariah",
    "DateOfBirth": null,
    "Email": "mariah.jones@example.com"
  },
  {
    "FirstName": null,
    "DateOfBirth": "2005-01-01T00:00:00",
    "Email": null
  },
  {
    "FirstName": null,
    "DateOfBirth": "2005-01-02T00:00:00",
    "Email": null
  }
]

Copy as INSERT INTO

image
INSERT INTO TableName (FirstName, DateOfBirth, Email)
VALUES
    ('John', '2003-11-09', 'john.smith@example.com'),
    ('Mariah', '2004-01-30', 'mariah.jones@example.com'),
    ('John', '2003-11-09', 'john.smith@example.com'),
    ('Mariah', '2004-01-30', 'mariah.jones@example.com'),
    ('John', '2003-11-09', 'john.smith@example.com'),
    ('Mariah', '2004-01-30', 'mariah.jones@example.com');
image
INSERT INTO TableName (FirstName, DateOfBirth, Email)
VALUES
    ('John', NULL, NULL),
    ('Mariah', NULL, NULL),
    (NULL, '2005-01-01', NULL),
    (NULL, '2005-01-02', NULL),
    (NULL, NULL, 'j.oneil@example.com'),
    (NULL, NULL, 'Stu.Brad@example.com');
image
INSERT INTO TableName (FirstName, DateOfBirth, Email)
VALUES
    (NULL, '2005-01-01', NULL),
    (NULL, '2005-01-02', NULL),
    ('Jacob', NULL, NULL),
    ('Stu', NULL, NULL),
    (NULL, NULL, 'john.smith@example.com'),
    (NULL, NULL, 'mariah.jones@example.com');

Copy as IN Clause

image image image
IN
(
    'Cindy',
    'Mariah',
    'Jim'
)

Notebook Result Grid:

Copy as CSV

image
John,2003-11-09,john.smith@example.com
Mariah,2004-01-30,mariah.jones@example.com
John,2003-11-09,john.smith@example.com
Mariah,2004-01-30,mariah.jones@example.com
John,2003-11-09,john.smith@example.com
Mariah,2004-01-30,mariah.jones@example.com

Copy as JSON

image
[
  {
    "FirstName": "John",
    "DateOfBirth": null,
    "Email": "john.smith@example.com"
  },
  {
    "FirstName": "Mariah",
    "DateOfBirth": null,
    "Email": "mariah.jones@example.com"
  },
  {
    "FirstName": "John",
    "DateOfBirth": null,
    "Email": "john.smith@example.com"
  },
  {
    "FirstName": "Mariah",
    "DateOfBirth": null,
    "Email": "mariah.jones@example.com"
  },
  {
    "FirstName": null,
    "DateOfBirth": "2005-01-01",
    "Email": null
  },
  {
    "FirstName": null,
    "DateOfBirth": "2005-01-02",
    "Email": null
  }
]

Copy as INSERT INTO

image
INSERT INTO TableName (FirstName, DateOfBirth, Email)
VALUES
    ('John', '2003-11-09', 'john.smith@example.com'),
    ('Mariah', '2004-01-30', 'mariah.jones@example.com'),
    ('John', '2003-11-09', 'john.smith@example.com'),
    ('Mariah', '2004-01-30', 'mariah.jones@example.com'),
    ('John', '2003-11-09', 'john.smith@example.com'),
    ('Mariah', '2004-01-30', 'mariah.jones@example.com');
image
INSERT INTO TableName (FirstName, DateOfBirth, Email)
VALUES
    ('John', NULL, NULL),
    ('Mariah', NULL, NULL),
    (NULL, '2005-01-01', NULL),
    (NULL, '2005-01-02', NULL),
    (NULL, NULL, 'j.oneil@example.com'),
    (NULL, NULL, 'Stu.Brad@example.com');
image
INSERT INTO TableName (FirstName, DateOfBirth, Email)
VALUES
    (NULL, '2005-01-01', NULL),
    (NULL, '2005-01-02', NULL),
    ('Jacob', NULL, NULL),
    ('Stu', NULL, NULL),
    (NULL, NULL, 'john.smith@example.com'),
    (NULL, NULL, 'mariah.jones@example.com');

Copy as IN clause

image
image
image
IN
(
    'Cindy',
    'Mariah',
    'Jim'
)

Provide a clear, concise summary of the changes in this PR. What problem does it solve? Why is it needed? Link any related issues using issue closing keywords.

Code Changes Checklist

  • New or updated unit tests added
  • All existing tests pass (npm run test)
  • Code follows contributing guidelines
  • Telemetry/logging updated if relevant
  • No regressions or UX breakage

Reviewers: Please read our reviewer guidelines

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a “Copy As” submenu to the Notebook query results grid context menu (matching the regular query editor grid UX) and implements the associated client-side formatting/copying behavior, including an iframe→extension-host path for displaying errors.

Changes:

  • Adds a “Copy As” submenu to the notebook grid context menu with CSV/JSON/INSERT INTO/IN clause formatting.
  • Plumbs postMessage through the notebook renderer so the context menu can surface errors via the extension host.
  • Introduces unit tests covering the new formatter behaviors.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
localization/xliff/vscode-mssql.xlf Adds localized string entry for the IN-clause single-column error.
extensions/mssql/test/unit/notebooks/notebookContextMenu.test.ts New unit tests validating CSV/JSON/IN clause/INSERT formatting behavior.
extensions/mssql/src/webviews/pages/NotebookRenderer/notebookResultsOutput.tsx Passes postMessage down to the result grid component.
extensions/mssql/src/webviews/pages/NotebookRenderer/notebookResultGrid.tsx Extends grid props with postMessage and wires it into the context menu plugin.
extensions/mssql/src/webviews/pages/NotebookRenderer/notebookResultGrid.css Updates context menu styling and adds focused/hover/submenu indicator styling.
extensions/mssql/src/webviews/pages/NotebookRenderer/notebookRendererEntry.tsx Passes renderer postMessage into the notebook result grid.
extensions/mssql/src/webviews/pages/NotebookRenderer/notebookContextMenu.plugin.ts Implements submenu + new “Copy As” actions and client-side formatters; adds keyboard navigation and error posting.
extensions/mssql/src/webviews/common/locConstants.ts Adds localized constant for the IN-clause single-column error message.
extensions/mssql/src/sharedInterfaces/notebookQueryResult.ts Adds NotebookShowErrorMessage interface for renderer messaging.
extensions/mssql/src/notebooks/sqlNotebookController.ts Handles showError renderer messages by calling vscode.window.showErrorMessage.
extensions/mssql/l10n/bundle.l10n.json Adds localized string entry for the IN-clause single-column error.

Comment thread extensions/mssql/test/unit/notebooks/notebookContextMenu.test.ts Outdated
@github-actions

github-actions Bot commented Jun 26, 2026

Copy link
Copy Markdown

PR Changes

Category Target Branch PR Branch Difference
vscode-mssql VSIX 79314 KB 79291 KB ⚪ -23 KB ( 0% )
sql-database-projects VSIX 2950 KB 2945 KB ⚪ -5 KB ( 0% )
data-workspace VSIX 188 KB 188 KB ⚪ 0 KB ( 0% )
keymap VSIX 7 KB 7 KB ⚪ 0 KB ( 0% )

@codecov-commenter

Codecov Comments Bot (codecov-commenter) commented Jun 26, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 53.40000% with 233 lines in your changes missing coverage. Please review.
✅ Project coverage is 75.60%. Comparing base (b10efdc) to head (46cc53a).
⚠️ Report is 22 commits behind head on main.

Files with missing lines Patch % Lines
...ges/NotebookRenderer/notebookContextMenu.plugin.ts 53.41% 225 Missing ⚠️
...sions/mssql/src/notebooks/sqlNotebookController.ts 11.11% 8 Missing ⚠️

❗ There is a different number of reports uploaded between BASE (b10efdc) and HEAD (46cc53a). Click for more details.

HEAD has 3 uploads less than BASE
Flag BASE (b10efdc) HEAD (46cc53a)
mssql 2 1
sqlproj 2 1
data-workspace 2 1
Additional details and impacted files

Impacted file tree graph

@@             Coverage Diff             @@
##             main   #22395       +/-   ##
===========================================
- Coverage   88.25%   75.60%   -12.65%     
===========================================
  Files         415      408        -7     
  Lines      131028   131339      +311     
  Branches     8393     8439       +46     
===========================================
- Hits       115640    99303    -16337     
- Misses      15388    32036    +16648     
Flag Coverage Δ
data-workspace 78.37% <ø> (ø)
mssql 75.24% <53.40%> (-14.08%) ⬇️
sqlproj 78.87% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
.../mssql/src/sharedInterfaces/notebookQueryResult.ts 100.00% <100.00%> (ø)
...tensions/mssql/src/webviews/common/locConstants.ts 25.59% <100.00%> (-62.52%) ⬇️
...sions/mssql/src/notebooks/sqlNotebookController.ts 68.95% <11.11%> (-23.15%) ⬇️
...ges/NotebookRenderer/notebookContextMenu.plugin.ts 44.51% <53.41%> (ø)

... and 202 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@lewis-sanchez
Lewis Sanchez (lewis-sanchez) marked this pull request as draft July 7, 2026 20:06
@lewis-sanchez
Lewis Sanchez (lewis-sanchez) marked this pull request as ready for review July 27, 2026 17:46
Copilot AI review requested due to automatic review settings July 27, 2026 17:46

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated 2 comments.

Comments suppressed due to low confidence (5)

extensions/mssql/src/webviews/pages/NotebookRenderer/notebookContextMenu.plugin.ts:216

  • The context menu DOM is built from generic
    elements without ARIA roles, so screen readers won’t announce it as a menu. Add appropriate roles (and apply similarly to the submenu container) so assistive tech can interpret the structure.
        const menu = document.createElement("div");
        menu.className = "nb-context-menu";

extensions/mssql/src/webviews/pages/NotebookRenderer/notebookContextMenu.plugin.ts:390

  • Menu items are created as plain
    elements without a semantic role. This reduces screen reader usability and can make keyboard navigation expectations unclear. Mark items with role="menuitem" (and consider adding aria-haspopup/aria-expanded on the submenu item).
        const item = document.createElement("div");
        item.className = "nb-context-menu-item";

extensions/mssql/src/webviews/pages/NotebookRenderer/notebookContextMenu.plugin.ts:714

  • formatAsInClause can emit syntactically invalid SQL when a numeric-typed cell has an empty display value: the current logic treats it as a numeric literal and outputs a blank token in the IN list. Treat empty/whitespace numeric values as NULL (or quote them) to keep the output valid.
                const rawVal = cellVal?.displayValue ?? "";
                const val = cellVal?.isNull
                    ? "NULL"
                    : isNumeric && !/[eE]/.test(rawVal)
                      ? rawVal
                      : this.sqlStr(rawVal);

extensions/mssql/src/webviews/pages/NotebookRenderer/notebookContextMenu.plugin.ts:756

  • formatAsInsertInto can emit invalid SQL when a numeric-typed cell has an empty/whitespace display value: it returns the raw empty string as a literal (e.g., (, ...)). Handle empty numeric values explicitly (e.g., emit NULL) before deciding whether to quote.
                    const cellVal = item?.[col.field!];
                    if (cellVal?.isNull) return "NULL";
                    const val = cellVal?.displayValue ?? "";
                    return isNumeric && !/[eE]/.test(val) ? val : this.sqlStr(val);

extensions/mssql/test/unit/notebooks/notebookContextMenu.test.ts:30

  • This test file mutates global objects (global.navigator and global.Slick) at module load time and never restores them. That can leak state into other test files and create order-dependent failures. Prefer setting these globals in suite setup (before tests that construct NotebookContextMenu) and restoring them in teardown.
// Mock navigator.platform for isMac() in notebookContextMenu.plugin
// Use Object.defineProperty because navigator is read-only in Electron
Object.defineProperty(global, "navigator", {
    value: {
        platform: "Win32",
        clipboard: {
            writeText: async () => {},
        },
    },
    writable: true,
    configurable: true,
});

// Slick.EventHandler is a class field — mock the global before any test instantiates NotebookContextMenu.
(global as any).Slick = {
    EventHandler: class {
        subscribe() {}
        unsubscribeAll() {}
    },
};

Comment on lines +672 to +675
} else {
const displayVal = cellVal?.displayValue ?? "";
val = isJsonNumber ? displayVal : JSON.stringify(displayVal);
}
@lewis-sanchez
Lewis Sanchez (lewis-sanchez) marked this pull request as draft July 27, 2026 18:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: SQL Notebook result grid context menu is missing the "Copy As" submenu

3 participants