From 8b18d10fda568120b63f25dd2204ead65b71a7ef Mon Sep 17 00:00:00 2001 From: Dmitrii Shilov Date: Fri, 28 Nov 2025 14:03:33 +0100 Subject: [PATCH 1/3] feat: Implement document deletion and retrieval in Query Editor with updated handling --- src/cosmosdb/session/DocumentSession.ts | 17 ++++-- src/cosmosdb/session/QuerySession.ts | 51 ++++++++++++++++++ src/cosmosdb/session/QuerySessionResult.ts | 2 + src/cosmosdb/types/queryResult.ts | 2 + src/panels/QueryEditorTab.ts | 52 ++++++++++++------- src/utils/convertors.ts | 24 ++++----- src/utils/csvConverter.ts | 9 ++-- .../ResultPanel/DeleteItemButton.tsx | 16 ++---- .../ResultPanel/EditItemButton.tsx | 14 ++--- .../QueryEditor/ResultPanel/NewItemButton.tsx | 3 +- .../QueryEditor/ResultPanel/ResultTab.tsx | 1 + .../ResultPanel/ResultTabViewTable.tsx | 30 +++++++---- .../ResultPanel/ViewItemButton.tsx | 14 ++--- .../state/QueryEditorContextProvider.tsx | 19 ++++--- src/webviews/theme/slickgrid.scss | 5 ++ 15 files changed, 164 insertions(+), 95 deletions(-) diff --git a/src/cosmosdb/session/DocumentSession.ts b/src/cosmosdb/session/DocumentSession.ts index 1c44f57e8..cb59bc662 100644 --- a/src/cosmosdb/session/DocumentSession.ts +++ b/src/cosmosdb/session/DocumentSession.ts @@ -25,10 +25,10 @@ import { ext } from '../../extensionVariables'; import { type Channel } from '../../panels/Communication/Channel/Channel'; import { getErrorMessage } from '../../panels/Communication/Channel/CommonChannel'; import { getConfirmationAsInSettings } from '../../utils/dialogs/getConfirmation'; -import { arePartitionKeysEqual, extractPartitionKey } from '../../utils/document'; +import { arePartitionKeysEqual, extractPartitionKey, getDocumentId } from '../../utils/document'; import { getCosmosDBKeyCredential } from '../CosmosDBCredential'; import { type NoSqlQueryConnection } from '../NoSqlQueryConnection'; -import { type CosmosDBRecord, type CosmosDBRecordIdentifier } from '../types/queryResult'; +import { type CosmosDBRecord, type CosmosDBRecordIdentifier, type QueryResultRecord } from '../types/queryResult'; import { withClaimsChallengeHandling } from '../withClaimsChallengeHandling'; /** @@ -318,8 +318,8 @@ export class DocumentSession { }); } - public async delete(documentId: CosmosDBRecordIdentifier): Promise { - await callWithTelemetryAndErrorHandling('cosmosDB.nosql.document.session.delete', async (context) => { + public async delete(documentId: CosmosDBRecordIdentifier): Promise { + return callWithTelemetryAndErrorHandling('cosmosDB.nosql.document.session.delete', async (context) => { this.setTelemetryProperties(context); if (this.isDisposed) { @@ -340,7 +340,14 @@ export class DocumentSession { return; } - await this.deleteInternal(documentId, context); + return this.deleteInternal(documentId, context); + }); + } + + public async getDocumentId(document: QueryResultRecord): Promise { + return callWithTelemetryAndErrorHandling('cosmosDB.nosql.document.session.getDocumentId', async () => { + const partitionKey = await this.getPartitionKey(); + return getDocumentId(document, partitionKey); }); } diff --git a/src/cosmosdb/session/QuerySession.ts b/src/cosmosdb/session/QuerySession.ts index 8878cfd28..3976f7cef 100644 --- a/src/cosmosdb/session/QuerySession.ts +++ b/src/cosmosdb/session/QuerySession.ts @@ -21,6 +21,7 @@ import { type QueryMetadata, type QueryResultRecord, } from '../types/queryResult'; +import { DocumentSession } from './DocumentSession'; import { QuerySessionResult } from './QuerySessionResult'; export class QuerySession { @@ -236,6 +237,56 @@ export class QuerySession { }); } + public getDocumentId(row: number): Promise { + const result = this.sessionResult.getResult(this.currentIteration); + if (!result) { + throw new Error('No result found for current iteration'); + } + + const document = result.documents[row]; + if (!document) { + throw new Error(`No document found for row: ${row}`); + } + + const session = new DocumentSession(this.connection, this.channel); + return session.getDocumentId(document); + } + + public async deleteDocument(row: number): Promise { + const result = this.sessionResult.getResult(this.currentIteration); + if (!result) { + throw new Error('No result found for current iteration'); + } + + const document = result.documents[row]; + if (!document) { + throw new Error(`No document found for row: ${row}`); + } + + const session = new DocumentSession(this.connection, this.channel); + const documentId = await session.getDocumentId(document); + + if (!documentId) { + throw new Error('Document id not found'); + } + + const isDeleted = await session.delete(documentId); + if (isDeleted) { + result.deletedDocuments.push(row); + + await this.channel.postMessage({ + type: 'event', + name: 'queryResults', + params: [this.id, this.sessionResult.getSerializedResult(this.currentIteration), this.currentIteration], + }); + } + } + + public bulkDelete(_rows: number[]): Promise { + // TODO: implement bulk delete + throw new Error('Method not implemented.'); + } + public dispose(): void { this._isDisposed = true; this.abortController?.abort(); diff --git a/src/cosmosdb/session/QuerySessionResult.ts b/src/cosmosdb/session/QuerySessionResult.ts index e9e98b837..0be3d921b 100644 --- a/src/cosmosdb/session/QuerySessionResult.ts +++ b/src/cosmosdb/session/QuerySessionResult.ts @@ -64,6 +64,7 @@ export class QuerySessionResult { requestCharge: response.requestCharge, roundTrips: 1, // TODO: Is it required field? Query Pages Until Content Present hasMoreResults: response.hasMoreResults, + deletedDocuments: [], }); this.hasMoreResults = response.hasMoreResults; } @@ -85,6 +86,7 @@ export class QuerySessionResult { requestCharge: result.requestCharge, roundTrips: result.roundTrips, hasMoreResults: result.hasMoreResults, + deletedDocuments: result.deletedDocuments, query: this.query, }; diff --git a/src/cosmosdb/types/queryResult.ts b/src/cosmosdb/types/queryResult.ts index 2811ae3cb..48b020863 100644 --- a/src/cosmosdb/types/queryResult.ts +++ b/src/cosmosdb/types/queryResult.ts @@ -51,6 +51,7 @@ export type QueryResult = { requestCharge: number; roundTrips: number; hasMoreResults: boolean; + deletedDocuments: number[]; }; export type SerializedQueryMetrics = { @@ -81,6 +82,7 @@ export type SerializedQueryResult = { requestCharge: number; roundTrips: number; hasMoreResults: boolean; + deletedDocuments: number[]; query: string; // The query that was executed }; diff --git a/src/panels/QueryEditorTab.ts b/src/panels/QueryEditorTab.ts index 32753f8a8..668931a72 100644 --- a/src/panels/QueryEditorTab.ts +++ b/src/panels/QueryEditorTab.ts @@ -12,13 +12,8 @@ import { getThemedIconPath } from '../constants'; import { getCosmosDBKeyCredential } from '../cosmosdb/CosmosDBCredential'; import { getCosmosClient } from '../cosmosdb/getCosmosClient'; import { getNoSqlQueryConnection, type NoSqlQueryConnection } from '../cosmosdb/NoSqlQueryConnection'; -import { DocumentSession } from '../cosmosdb/session/DocumentSession'; import { QuerySession } from '../cosmosdb/session/QuerySession'; -import { - type CosmosDBRecordIdentifier, - type QueryMetadata, - type SerializedQueryResult, -} from '../cosmosdb/types/queryResult'; +import { type QueryMetadata, type SerializedQueryResult } from '../cosmosdb/types/queryResult'; import { withClaimsChallengeHandling } from '../cosmosdb/withClaimsChallengeHandling'; import { StorageNames, StorageService, type StorageItem } from '../services/storageService'; import { toStringUniversal } from '../utils/convertors'; @@ -163,11 +158,15 @@ export class QueryEditorTab extends BaseTab { case 'firstPage': return this.firstPage(payload.params[0] as string); case 'openDocument': - return this.openDocument(payload.params[0] as string, payload.params[1] as CosmosDBRecordIdentifier); + return this.openDocument( + payload.params[0] as string, + payload.params[1] as string, + payload.params[2] as number, + ); case 'deleteDocument': - return this.deleteDocument(payload.params[0] as CosmosDBRecordIdentifier); + return this.deleteDocument(payload.params[0] as string, payload.params[1] as number); case 'deleteDocuments': - return this.deleteDocuments(payload.params[0] as CosmosDBRecordIdentifier[]); + return this.deleteDocuments(payload.params[0] as string, payload.params[1] as number[]); case 'provideFeedback': return this.provideFeedback(); case 'saveCSV': @@ -565,14 +564,14 @@ export class QueryEditorTab extends BaseTab { void promptAfterActionEventually(ExperienceKind.NoSQL, UsageImpact.Medium, callbackId); } - private async openDocument(mode: string, documentId?: CosmosDBRecordIdentifier): Promise { + private async openDocument(executionId: string, mode: string, row?: number): Promise { const callbackId = 'cosmosDB.nosql.queryEditor.openDocument'; - await callWithTelemetryAndErrorHandling(callbackId, () => { + await callWithTelemetryAndErrorHandling(callbackId, async () => { if (!this.connection) { throw new Error(l10n.t('No connection')); } - if (!documentId && mode !== 'add') { + if (!row && mode !== 'add') { throw new Error(l10n.t('Impossible to open an item without an id')); } @@ -580,37 +579,52 @@ export class QueryEditorTab extends BaseTab { throw new Error(l10n.t('Invalid mode: {0}', mode)); } + const session = this.sessions.get(executionId); + if (!session) { + throw new Error(`No session found for executionId: ${executionId}`); + } + + const documentId = row ? await session.getDocumentId(row) : undefined; + DocumentTab.render(this.connection, mode, documentId, this.getNextViewColumn()); }); void promptAfterActionEventually(ExperienceKind.NoSQL, UsageImpact.Medium, callbackId); } - private async deleteDocument(documentId: CosmosDBRecordIdentifier): Promise { + private async deleteDocument(executionId: string, row: number): Promise { const callbackId = 'cosmosDB.nosql.queryEditor.deleteDocument'; await callWithTelemetryAndErrorHandling(callbackId, async () => { if (!this.connection) { throw new Error(l10n.t('No connection')); } - if (!documentId) { + const session = this.sessions.get(executionId); + if (!session) { + throw new Error(`No session found for executionId: ${executionId}`); + } + + if (!row) { throw new Error(l10n.t('Impossible to delete an item without an id')); } - const session = new DocumentSession(this.connection, this.channel); - await session.delete(documentId); + return session.deleteDocument(row); }); void promptAfterActionEventually(ExperienceKind.NoSQL, UsageImpact.Medium, callbackId); } - private async deleteDocuments(documentIds: CosmosDBRecordIdentifier[]): Promise { + private async deleteDocuments(executionId: string, rows: number[]): Promise { const callbackId = 'cosmosDB.nosql.queryEditor.deleteDocuments'; await callWithTelemetryAndErrorHandling(callbackId, async () => { if (!this.connection) { throw new Error(l10n.t('No connection')); } - const session = new DocumentSession(this.connection, this.channel); - await session.bulkDelete(documentIds); + const session = this.sessions.get(executionId); + if (!session) { + throw new Error(`No session found for executionId: ${executionId}`); + } + + return session.bulkDelete(rows); }); void promptAfterActionEventually(ExperienceKind.NoSQL, UsageImpact.Medium, callbackId); } diff --git a/src/utils/convertors.ts b/src/utils/convertors.ts index 7712f6d9e..b75a1b788 100644 --- a/src/utils/convertors.ts +++ b/src/utils/convertors.ts @@ -33,6 +33,7 @@ export type TableRecord = { export type TableData = { headers: string[]; dataset: TableRecord[]; + deletedRows: number[]; }; export type TreeData = { @@ -178,20 +179,13 @@ export const queryResultToJSON = (queryResult: SerializedQueryResult | null, sel return ''; } - if (selection) { - const selectedDocs = queryResult.documents - .map((doc, index) => { - if (!selection.includes(index)) { - return null; - } - return doc; - }) - .filter((doc) => doc !== null); - - return JSON.stringify(selectedDocs, null, 4); - } + const notDeletedRows = queryResult.documents + .map((_, index) => index) + .filter((index) => !queryResult.deletedDocuments.includes(index)); + const selectedRows = selection ? notDeletedRows.filter((index) => selection.includes(index)) : notDeletedRows; + const selectedDocs = queryResult.documents.filter((_, index) => selectedRows.includes(index)); - return JSON.stringify(queryResult.documents, null, 4); + return JSON.stringify(selectedDocs, null, 4); }; export const queryResultToTree = async ( @@ -524,7 +518,7 @@ export const queryResultToTable = async ( }, ): Promise => { if (!queryResult || !queryResult.documents) { - return { headers: [], dataset: [] }; + return { headers: [], dataset: [], deletedRows: [] }; } if (isSelectStar(queryResult.query ?? '')) { @@ -536,7 +530,7 @@ export const queryResultToTable = async ( const headers = getTableHeaders(queryResult.documents, partitionKey, options); const dataset = await getTableDataset(queryResult.documents, partitionKey, options); - return { headers, dataset }; + return { headers, dataset, deletedRows: queryResult.deletedDocuments }; }; export const queryMetricsToTable = async (queryResult: SerializedQueryResult | null): Promise => { diff --git a/src/utils/csvConverter.ts b/src/utils/csvConverter.ts index 8574b119b..4c2dcc10f 100644 --- a/src/utils/csvConverter.ts +++ b/src/utils/csvConverter.ts @@ -48,9 +48,12 @@ export const queryResultToCsv = async ( const sep = getCsvSeparator(); const headers = tableView.headers.map((hdr) => escapeCsvValue(hdr)).join(sep); - if (selection) { - tableView.dataset = tableView.dataset.filter((_, index) => selection.includes(index)); - } + const notDeletedRows = tableView.dataset + .map((_, index) => index) + .filter((index) => !tableView.deletedRows.includes(index)); + const selectedRows = selection ? notDeletedRows.filter((index) => selection.includes(index)) : notDeletedRows; + + tableView.dataset = tableView.dataset.filter((_, index) => selectedRows.includes(index)); const rows = tableView.dataset .map((row) => { diff --git a/src/webviews/cosmosdb/QueryEditor/ResultPanel/DeleteItemButton.tsx b/src/webviews/cosmosdb/QueryEditor/ResultPanel/DeleteItemButton.tsx index 149560b1d..a991f963c 100644 --- a/src/webviews/cosmosdb/QueryEditor/ResultPanel/DeleteItemButton.tsx +++ b/src/webviews/cosmosdb/QueryEditor/ResultPanel/DeleteItemButton.tsx @@ -6,8 +6,6 @@ import { DeleteRegular } from '@fluentui/react-icons'; import * as l10n from '@vscode/l10n'; import { useCallback, useMemo } from 'react'; -import { type CosmosDBRecordIdentifier } from '../../../../cosmosdb/types/queryResult'; -import { getDocumentId } from '../../../../utils/document'; import { HotkeyCommandService, useCommandHotkey } from '../../../common/hotkeys'; import { ToolbarOverflowButton } from '../../../common/ToolbarOverflow/ToolbarOverflowButton'; import { type ToolbarOverflowItemProps } from '../../../common/ToolbarOverflow/ToolbarOverflowItem'; @@ -18,25 +16,21 @@ export const DeleteItemButton = (props: ToolbarOverflowItemProps { - return state.selectedRows - .map((rowIndex): CosmosDBRecordIdentifier | undefined => { - const document = state.currentQueryResult?.documents[rowIndex]; - return document ? getDocumentId(document, state.partitionKey) : undefined; - }) - .filter((document) => document !== undefined); + return state.selectedRows.filter((rowIndex) => !state.currentQueryResult?.deletedDocuments.includes(rowIndex)); }, [state]); const deleteSelectedItem = useCallback(() => { const selectedDocuments = getSelectedDocuments(); if (selectedDocuments.length === 1) { - void dispatcher.deleteDocument(selectedDocuments[0]); + void dispatcher.deleteDocument(executionId, selectedDocuments[0]); } else { - void dispatcher.deleteDocuments(selectedDocuments); + void dispatcher.deleteDocuments(executionId, selectedDocuments); } - }, [dispatcher, getSelectedDocuments]); + }, [dispatcher, executionId, getSelectedDocuments]); const deleteItemHotkeyTooltip = useMemo( () => diff --git a/src/webviews/cosmosdb/QueryEditor/ResultPanel/EditItemButton.tsx b/src/webviews/cosmosdb/QueryEditor/ResultPanel/EditItemButton.tsx index f01699302..fe1ee7248 100644 --- a/src/webviews/cosmosdb/QueryEditor/ResultPanel/EditItemButton.tsx +++ b/src/webviews/cosmosdb/QueryEditor/ResultPanel/EditItemButton.tsx @@ -6,8 +6,6 @@ import { EditRegular } from '@fluentui/react-icons'; import * as l10n from '@vscode/l10n'; import { useCallback, useMemo } from 'react'; -import { type CosmosDBRecordIdentifier } from '../../../../cosmosdb/types/queryResult'; -import { getDocumentId } from '../../../../utils/document'; import { HotkeyCommandService, useCommandHotkey } from '../../../common/hotkeys'; import { ToolbarOverflowButton } from '../../../common/ToolbarOverflow/ToolbarOverflowButton'; import { type ToolbarOverflowItemProps } from '../../../common/ToolbarOverflow/ToolbarOverflowItem'; @@ -18,20 +16,16 @@ export const EditItemButton = (props: ToolbarOverflowItemProps { - return state.selectedRows - .map((rowIndex): CosmosDBRecordIdentifier | undefined => { - const document = state.currentQueryResult?.documents[rowIndex]; - return document ? getDocumentId(document, state.partitionKey) : undefined; - }) - .filter((document) => document !== undefined); + return state.selectedRows.filter((rowIndex) => !state.currentQueryResult?.deletedDocuments.includes(rowIndex)); }, [state]); const editSelectedItem = useCallback( - () => dispatcher.openDocuments('edit', getSelectedDocuments()), - [dispatcher, getSelectedDocuments], + () => dispatcher.openDocuments(executionId, 'edit', getSelectedDocuments()), + [dispatcher, executionId, getSelectedDocuments], ); const editItemHotkeyTooltip = useMemo( diff --git a/src/webviews/cosmosdb/QueryEditor/ResultPanel/NewItemButton.tsx b/src/webviews/cosmosdb/QueryEditor/ResultPanel/NewItemButton.tsx index f9e4d9ac9..39ba2e0da 100644 --- a/src/webviews/cosmosdb/QueryEditor/ResultPanel/NewItemButton.tsx +++ b/src/webviews/cosmosdb/QueryEditor/ResultPanel/NewItemButton.tsx @@ -15,7 +15,8 @@ import { useQueryEditorDispatcher, useQueryEditorState } from '../state/QueryEdi export const NewItemButton = (props: ToolbarOverflowItemProps) => { const state = useQueryEditorState(); const dispatcher = useQueryEditorDispatcher(); - const addNewItem = useCallback(() => dispatcher.openDocument('add'), [dispatcher]); + const executionId = state.currentExecutionId; + const addNewItem = useCallback(() => dispatcher.openDocument(executionId, 'add'), [dispatcher, executionId]); const isDisabled = state.isExecuting; diff --git a/src/webviews/cosmosdb/QueryEditor/ResultPanel/ResultTab.tsx b/src/webviews/cosmosdb/QueryEditor/ResultPanel/ResultTab.tsx index 08d50a980..2b4dd3c1d 100644 --- a/src/webviews/cosmosdb/QueryEditor/ResultPanel/ResultTab.tsx +++ b/src/webviews/cosmosdb/QueryEditor/ResultPanel/ResultTab.tsx @@ -184,6 +184,7 @@ export const ResultTab = ({ className }: ResultTabProps) => { )} {tableViewMode === 'Tree' && } diff --git a/src/webviews/cosmosdb/QueryEditor/ResultPanel/ResultTabViewTable.tsx b/src/webviews/cosmosdb/QueryEditor/ResultPanel/ResultTabViewTable.tsx index 8871cea87..d203cea9f 100644 --- a/src/webviews/cosmosdb/QueryEditor/ResultPanel/ResultTabViewTable.tsx +++ b/src/webviews/cosmosdb/QueryEditor/ResultPanel/ResultTabViewTable.tsx @@ -20,7 +20,7 @@ import { useColumnMenu } from './ColumnMenu'; type ResultTabViewTableProps = TableData & {}; -export const ResultTabViewTable = ({ headers, dataset }: ResultTabViewTableProps) => { +export const ResultTabViewTable = ({ headers, dataset, deletedRows }: ResultTabViewTableProps) => { const state = useQueryEditorState(); const dispatcher = useQueryEditorDispatcher(); const gridRef = useRef(null); @@ -50,16 +50,25 @@ export const ResultTabViewTable = ({ headers, dataset }: ResultTabViewTableProps }, ], }, - formatter: (_row, _cell, value) => { - if (value === undefined || value === null || value === '{}') { - const displayValue = value === undefined ? 'undefined' : value === null ? 'null' : '{}'; - return `${displayValue}`; + formatter: (row, _cell, value) => { + const text = + value === undefined || value === null || value === '{}' + ? `${value === undefined ? 'undefined' : value === null ? 'null' : '{}'}` + : String(value); + + if (deletedRows.includes(row)) { + return { + text, + addClasses: 'row-is-deleted', + toolTip: 'This document is deleted', + }; } - return String(value); + + return text; }, } as Column; }), - [handleHeaderButtonClick, headers], + [deletedRows, handleHeaderButtonClick, headers], ); React.useEffect(() => { @@ -96,12 +105,11 @@ export const ResultTabViewTable = ({ headers, dataset }: ResultTabViewTableProps // Open document in view mode const activeDocument = dataset[args.row]; - const documentId = activeDocument?.__documentId; - if (documentId) { - void dispatcher.openDocument('view', documentId); + if (activeDocument && !deletedRows.includes(args.row)) { + void dispatcher.openDocument(state.currentExecutionId, 'view', args.row); } }, - [dataset, dispatcher, state.isEditMode], + [dataset, deletedRows, dispatcher, state.currentExecutionId, state.isEditMode], ); // SlickGrid emits the event twice. First time for selecting 1 row, second time for selecting this row + all rows what were selected before. diff --git a/src/webviews/cosmosdb/QueryEditor/ResultPanel/ViewItemButton.tsx b/src/webviews/cosmosdb/QueryEditor/ResultPanel/ViewItemButton.tsx index 177eaac4a..104333742 100644 --- a/src/webviews/cosmosdb/QueryEditor/ResultPanel/ViewItemButton.tsx +++ b/src/webviews/cosmosdb/QueryEditor/ResultPanel/ViewItemButton.tsx @@ -6,8 +6,6 @@ import { EyeRegular } from '@fluentui/react-icons'; import * as l10n from '@vscode/l10n'; import { useCallback, useMemo } from 'react'; -import { type CosmosDBRecordIdentifier } from '../../../../cosmosdb/types/queryResult'; -import { getDocumentId } from '../../../../utils/document'; import { HotkeyCommandService, useCommandHotkey } from '../../../common/hotkeys'; import { ToolbarOverflowButton } from '../../../common/ToolbarOverflow/ToolbarOverflowButton'; import { type ToolbarOverflowItemProps } from '../../../common/ToolbarOverflow/ToolbarOverflowItem'; @@ -18,20 +16,16 @@ export const ViewItemButton = (props: ToolbarOverflowItemProps { - return state.selectedRows - .map((rowIndex): CosmosDBRecordIdentifier | undefined => { - const document = state.currentQueryResult?.documents[rowIndex]; - return document ? getDocumentId(document, state.partitionKey) : undefined; - }) - .filter((document) => document !== undefined); + return state.selectedRows.filter((rowIndex) => !state.currentQueryResult?.deletedDocuments.includes(rowIndex)); }, [state]); const viewSelectedItem = useCallback( - () => dispatcher.openDocuments('view', getSelectedDocuments()), - [dispatcher, getSelectedDocuments], + () => dispatcher.openDocuments(executionId, 'view', getSelectedDocuments()), + [dispatcher, executionId, getSelectedDocuments], ); const viewItemHotkeyTooltip = useMemo( diff --git a/src/webviews/cosmosdb/QueryEditor/state/QueryEditorContextProvider.tsx b/src/webviews/cosmosdb/QueryEditor/state/QueryEditorContextProvider.tsx index 5c47aa7f1..c2dfa0fe6 100644 --- a/src/webviews/cosmosdb/QueryEditor/state/QueryEditorContextProvider.tsx +++ b/src/webviews/cosmosdb/QueryEditor/state/QueryEditorContextProvider.tsx @@ -6,7 +6,6 @@ import { type PartitionKeyDefinition } from '@azure/cosmos'; import type * as React from 'react'; import { - type CosmosDBRecordIdentifier, DEFAULT_EXECUTION_TIMEOUT, DEFAULT_PAGE_SIZE, type QueryMetadata, @@ -94,19 +93,19 @@ export class QueryEditorContextProvider extends BaseContextProvider { this.dispatch({ type: 'setSelectedRows', selectedRows }); } - public async openDocument(mode: OpenDocumentMode, document?: CosmosDBRecordIdentifier): Promise { - await this.sendCommand('openDocument', mode, document); + public async openDocument(executionId: string, mode: OpenDocumentMode, row?: number): Promise { + await this.sendCommand('openDocument', executionId, mode, row); } - public async openDocuments(mode: OpenDocumentMode, documents: CosmosDBRecordIdentifier[]): Promise { - for (const document of documents) { - await this.openDocument(mode, document); + public async openDocuments(executionId: string, mode: OpenDocumentMode, rows: number[]): Promise { + for (const row of rows) { + await this.openDocument(executionId, mode, row); } } - public async deleteDocument(document: CosmosDBRecordIdentifier): Promise { - await this.sendCommand('deleteDocument', document); + public async deleteDocument(executionId: string, row: number): Promise { + await this.sendCommand('deleteDocument', executionId, row); } - public async deleteDocuments(documents: CosmosDBRecordIdentifier[]): Promise { - await this.sendCommand('deleteDocuments', documents); + public async deleteDocuments(executionId: string, rows: number[]): Promise { + await this.sendCommand('deleteDocuments', executionId, rows); } public async provideFeedback(): Promise { await this.sendCommand('provideFeedback'); diff --git a/src/webviews/theme/slickgrid.scss b/src/webviews/theme/slickgrid.scss index e6e95f395..d725cf509 100644 --- a/src/webviews/theme/slickgrid.scss +++ b/src/webviews/theme/slickgrid.scss @@ -111,3 +111,8 @@ $slick-header-resizable-hover: 1px solid var(--vscode-editor-selectionBackground $slick-pagination-icon-color: $slick-icon-color, $slick-header-menu-display: inline, ); + +.slick-cell.row-is-deleted { + text-decoration: line-through; + color: var(--vscode-editor-findMatchBackground); +} From 2f2f9a107e05a6f0d52214f268b33832f78550d7 Mon Sep 17 00:00:00 2001 From: Dmitrii Shilov Date: Fri, 28 Nov 2025 17:14:28 +0100 Subject: [PATCH 2/3] feat: Implement document deletion and retrieval in Query Editor with updated handling --- src/cosmosdb/session/DocumentSession.ts | 11 +- src/cosmosdb/session/QuerySession.ts | 161 ++++++++++++++++++------ 2 files changed, 131 insertions(+), 41 deletions(-) diff --git a/src/cosmosdb/session/DocumentSession.ts b/src/cosmosdb/session/DocumentSession.ts index cb59bc662..ab132d3f9 100644 --- a/src/cosmosdb/session/DocumentSession.ts +++ b/src/cosmosdb/session/DocumentSession.ts @@ -391,8 +391,8 @@ export class DocumentSession { return false; } - public async bulkDelete(documentIds: CosmosDBRecordIdentifier[]): Promise { - await callWithTelemetryAndErrorHandling('cosmosDB.nosql.document.session.bulkDelete', async (context) => { + public async bulkDelete(documentIds: CosmosDBRecordIdentifier[]): Promise { + return callWithTelemetryAndErrorHandling('cosmosDB.nosql.document.session.bulkDelete', async (context) => { this.setTelemetryProperties(context); if (this.isDisposed) { @@ -444,9 +444,8 @@ export class DocumentSession { const abortController = new AbortController(); const abortSignal = abortController.signal; - token.onCancellationRequested(async () => { + token.onCancellationRequested(() => { status.aborted = true; - await sendResponse(); abortController.abort(); }); @@ -494,7 +493,9 @@ export class DocumentSession { ); } - return sendResponse(); + await sendResponse(); + + return status; } catch (error) { await this.errorHandling(error, context); } diff --git a/src/cosmosdb/session/QuerySession.ts b/src/cosmosdb/session/QuerySession.ts index 3976f7cef..9f525e9c4 100644 --- a/src/cosmosdb/session/QuerySession.ts +++ b/src/cosmosdb/session/QuerySession.ts @@ -18,6 +18,7 @@ import { type NoSqlQueryConnection } from '../NoSqlQueryConnection'; import { DEFAULT_EXECUTION_TIMEOUT, DEFAULT_PAGE_SIZE, + type CosmosDBRecordIdentifier, type QueryMetadata, type QueryResultRecord, } from '../types/queryResult'; @@ -237,54 +238,142 @@ export class QuerySession { }); } - public getDocumentId(row: number): Promise { - const result = this.sessionResult.getResult(this.currentIteration); - if (!result) { - throw new Error('No result found for current iteration'); - } + public async getDocumentId(row: number): Promise { + return callWithTelemetryAndErrorHandling( + 'cosmosDB.nosql.queryEditor.session.getDocumentId', + async (context) => { + this.setTelemetryProperties(context); - const document = result.documents[row]; - if (!document) { - throw new Error(`No document found for row: ${row}`); - } + if (this.isDisposed) { + throw new Error(l10n.t('Session is disposed')); + } + + if (!this.iterator) { + throw new Error(l10n.t('Session is not running! Please run the session first')); + } + + const result = this.sessionResult.getResult(this.currentIteration); + if (!result) { + throw new Error('No result found for current iteration'); + } - const session = new DocumentSession(this.connection, this.channel); - return session.getDocumentId(document); + const document = result.documents[row]; + if (!document) { + throw new Error(`No document found for row: ${row}`); + } + + const session = new DocumentSession(this.connection, this.channel); + return session.getDocumentId(document); + }, + ); } public async deleteDocument(row: number): Promise { - const result = this.sessionResult.getResult(this.currentIteration); - if (!result) { - throw new Error('No result found for current iteration'); - } + return callWithTelemetryAndErrorHandling( + 'cosmosDB.nosql.queryEditor.session.deleteDocument', + async (context) => { + this.setTelemetryProperties(context); - const document = result.documents[row]; - if (!document) { - throw new Error(`No document found for row: ${row}`); - } + if (this.isDisposed) { + throw new Error(l10n.t('Session is disposed')); + } - const session = new DocumentSession(this.connection, this.channel); - const documentId = await session.getDocumentId(document); + if (!this.iterator) { + throw new Error(l10n.t('Session is not running! Please run the session first')); + } - if (!documentId) { - throw new Error('Document id not found'); - } + const result = this.sessionResult.getResult(this.currentIteration); + if (!result) { + throw new Error('No result found for current iteration'); + } - const isDeleted = await session.delete(documentId); - if (isDeleted) { - result.deletedDocuments.push(row); + const document = result.documents[row]; + if (!document) { + throw new Error(`No document found for row: ${row}`); + } - await this.channel.postMessage({ - type: 'event', - name: 'queryResults', - params: [this.id, this.sessionResult.getSerializedResult(this.currentIteration), this.currentIteration], - }); - } + const session = new DocumentSession(this.connection, this.channel); + const documentId = await session.getDocumentId(document); + + if (!documentId) { + throw new Error('Document id not found'); + } + + const isDeleted = await session.delete(documentId); + if (isDeleted) { + result.deletedDocuments.push(row); + + await this.channel.postMessage({ + type: 'event', + name: 'queryResults', + params: [ + this.id, + this.sessionResult.getSerializedResult(this.currentIteration), + this.currentIteration, + ], + }); + } + }, + ); } - public bulkDelete(_rows: number[]): Promise { - // TODO: implement bulk delete - throw new Error('Method not implemented.'); + public async bulkDelete(rows: number[]): Promise { + return callWithTelemetryAndErrorHandling('cosmosDB.nosql.queryEditor.session.bulkDelete', async (context) => { + this.setTelemetryProperties(context); + + if (this.isDisposed) { + throw new Error(l10n.t('Session is disposed')); + } + + if (!this.iterator) { + throw new Error(l10n.t('Session is not running! Please run the session first')); + } + + const result = this.sessionResult.getResult(this.currentIteration); + if (!result) { + throw new Error('No result found for current iteration'); + } + + const session = new DocumentSession(this.connection, this.channel); + const documents: Array<{ documentId: CosmosDBRecordIdentifier; row: number }> = []; + + for (const row of rows) { + const document = result.documents[row]; + if (!document) { + throw new Error(`No document found for row: ${row}`); + } + + const documentId = await session.getDocumentId(document); + if (!documentId) { + throw new Error('Document id not found'); + } + + documents.push({ documentId, row }); + } + + const documentsToDelete = documents.map((doc) => doc.documentId); + const status = await session.bulkDelete(documentsToDelete); + + if (status && status.deleted.length > 0) { + result.deletedDocuments = [ + ...result.deletedDocuments, + ...status.deleted.map((documentId) => { + const doc = documents.find((document) => document.documentId === documentId); + return doc ? doc.row : -1; + }), + ].filter((row) => row !== -1); + + await this.channel.postMessage({ + type: 'event', + name: 'queryResults', + params: [ + this.id, + this.sessionResult.getSerializedResult(this.currentIteration), + this.currentIteration, + ], + }); + } + }); } public dispose(): void { From c9a6ff1de9e27601ddc827c8714816f5bf8dbd94 Mon Sep 17 00:00:00 2001 From: Dmitrii Shilov Date: Mon, 1 Dec 2025 12:56:53 +0100 Subject: [PATCH 3/3] feat: Implement document deletion and retrieval in Query Editor with updated handling --- .../QueryEditor/ResultPanel/ResultTab.tsx | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/src/webviews/cosmosdb/QueryEditor/ResultPanel/ResultTab.tsx b/src/webviews/cosmosdb/QueryEditor/ResultPanel/ResultTab.tsx index 2b4dd3c1d..79ccb18ae 100644 --- a/src/webviews/cosmosdb/QueryEditor/ResultPanel/ResultTab.tsx +++ b/src/webviews/cosmosdb/QueryEditor/ResultPanel/ResultTab.tsx @@ -60,6 +60,7 @@ export const ResultTab = ({ className }: ResultTabProps) => { const [viewData, setViewData] = useState({}); const [isLoading, setIsLoading] = useState(false); const [resultCount, setResultCount] = useState(0); + const [deletedRows, setDeletedRows] = useState(0); const [hasPreviousData, setHasPreviousData] = useState(false); // Remove the second useEffect entirely and modify the first one @@ -80,8 +81,6 @@ export const ResultTab = ({ className }: ResultTabProps) => { setHasPreviousData(true); // Set loading state first setIsLoading(true); - // Update result count - setResultCount(currentQueryResult?.documents.length ?? -1); // Create an abort controller to cancel operations if needed const abortController = new AbortController(); @@ -91,10 +90,15 @@ export const ResultTab = ({ className }: ResultTabProps) => { const needsCalculation = (tableViewMode === 'Table' && !viewData.table) || (tableViewMode === 'Tree' && !viewData.tree) || - (tableViewMode === 'JSON' && !viewData.json); + (tableViewMode === 'JSON' && !viewData.json) || + deletedRows !== (viewData.table?.deletedRows.length ?? 0); // Only calculate if needed if (needsCalculation) { + // Update result count + setResultCount(currentQueryResult?.documents.length ?? -1); + setDeletedRows(currentQueryResult?.deletedDocuments.length ?? 0); + // Async calculation function with proper error handling const calculateData = async () => { try { @@ -147,7 +151,16 @@ export const ResultTab = ({ className }: ResultTabProps) => { return () => { abortController.abort(); }; - }, [tableViewMode, currentQueryResult, partitionKey, viewData.table, viewData.tree, viewData.json, isExecuting]); + }, [ + tableViewMode, + currentQueryResult, + partitionKey, + viewData.table, + viewData.tree, + viewData.json, + isExecuting, + deletedRows, + ]); if (!currentQueryResult || currentQueryResult.documents.length === 0) { return (