diff --git a/README.md b/README.md index da8d93c0f..6278b9681 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,7 @@ If you use Terraform to manage your infrastructure, MongoDB for VS Code helps yo - `mdb.show`: Show or hide the MongoDB view. - `mdb.defaultLimit`: The number of documents to fetch when viewing documents from a collection. - `mdb.confirmRunAll`: Show a confirmation message before running commands in a playground. +- `mdb.confirmDeleteDocument`: Show a confirmation message before deleting a document in the tree view. - `mdb.excludeFromPlaygroundsSearch`: Exclude files and folders while searching for playground in the the current workspace. - `mdb.connectionSaving.hideOptionToChooseWhereToSaveNewConnections`: When a connection is added, a prompt is shown that let's the user decide where the new connection should be saved. When this setting is checked, the prompt is not shown and the default connection saving location setting is used. - `mdb.connectionSaving.defaultConnectionSavingLocation`: When the setting that hides the option to choose where to save new connections is checked, this setting sets if and where new connections are saved. diff --git a/package.json b/package.json index 48f60d2d1..2eb5b92fd 100644 --- a/package.json +++ b/package.json @@ -412,6 +412,10 @@ { "command": "mdb.copyDocumentContentsFromTreeView", "title": "Copy Document" + }, + { + "command": "mdb.deleteDocumentFromTreeView", + "title": "Delete Document..." } ], "menus": { @@ -608,6 +612,11 @@ "command": "mdb.copyDocumentContentsFromTreeView", "when": "view == mongoDBConnectionExplorer && viewItem == documentTreeItem", "group": "2@1" + }, + { + "command": "mdb.deleteDocumentFromTreeView", + "when": "view == mongoDBConnectionExplorer && viewItem == documentTreeItem", + "group": "3@1" } ], "editor/title": [ @@ -781,6 +790,10 @@ { "command": "mdb.copyDocumentContentsFromTreeView", "when": "false" + }, + { + "command": "mdb.deleteDocumentFromTreeView", + "when": "false" } ] }, @@ -904,6 +917,11 @@ "default": true, "description": "Show a confirmation message before running commands in a playground." }, + "mdb.confirmDeleteDocument": { + "type": "boolean", + "default": true, + "description": "Show a confirmation message before deleting a document from the tree view." + }, "mdb.sendTelemetry": { "type": "boolean", "default": true, diff --git a/src/commands/index.ts b/src/commands/index.ts index 6bf21723e..792b2c845 100644 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -61,6 +61,7 @@ enum EXTENSION_COMMANDS { MDB_INSERT_OBJECTID_TO_EDITOR = 'mdb.insertObjectIdToEditor', MDB_GENERATE_OBJECTID_TO_CLIPBOARD = 'mdb.generateObjectIdToClipboard', MDB_COPY_DOCUMENT_CONTENTS_FROM_TREE_VIEW = 'mdb.copyDocumentContentsFromTreeView', + MDB_DELETE_DOCUMENT_FROM_TREE_VIEW = 'mdb.deleteDocumentFromTreeView', } export default EXTENSION_COMMANDS; diff --git a/src/connectionController.ts b/src/connectionController.ts index b711ba5e5..bf2e3378b 100644 --- a/src/connectionController.ts +++ b/src/connectionController.ts @@ -819,7 +819,7 @@ export default class ConnectionController { return connectionString; } - getActiveDataService(): DataService | null { + getActiveDataService() { return this._activeDataService; } diff --git a/src/explorer/documentListTreeItem.ts b/src/explorer/documentListTreeItem.ts index 79fb825f0..518bdfa1d 100644 --- a/src/explorer/documentListTreeItem.ts +++ b/src/explorer/documentListTreeItem.ts @@ -175,7 +175,8 @@ export default class DocumentListTreeItem (pastTreeItem as DocumentTreeItem).document, this.namespace, index, - this._dataService + this._dataService, + () => this.resetCache() ) ); }); @@ -223,7 +224,8 @@ export default class DocumentListTreeItem document, this.namespace, index, - this._dataService + this._dataService, + () => this.resetCache() ) ); }); diff --git a/src/explorer/documentTreeItem.ts b/src/explorer/documentTreeItem.ts index 0739afcfc..6bf89448a 100644 --- a/src/explorer/documentTreeItem.ts +++ b/src/explorer/documentTreeItem.ts @@ -17,12 +17,14 @@ export default class DocumentTreeItem dataService: DataService; document: Document; documentId: EJSON.SerializableTypes; + resetDocumentListCache: () => Promise; constructor( document: Document, namespace: string, documentIndexInTree: number, - dataService: DataService + dataService: DataService, + resetDocumentListCache: () => Promise ) { // A document can not have a `_id` when it is in a view. In this instance // we just show the document's index in the tree. @@ -41,6 +43,7 @@ export default class DocumentTreeItem this.document = document; this.documentId = document._id; this.namespace = namespace; + this.resetDocumentListCache = resetDocumentListCache; this.tooltip = documentLabel; } @@ -71,4 +74,45 @@ export default class DocumentTreeItem throw new Error(formatError(error).message); } } + + async onDeleteDocumentClicked(): Promise { + const shouldConfirmDeleteDocument = vscode.workspace + .getConfiguration('mdb') + .get('confirmDeleteDocument'); + + if (shouldConfirmDeleteDocument === true) { + const confirmationResult = await vscode.window.showInformationMessage( + `Are you sure you wish to drop this document "${this.tooltip}"? This confirmation can be disabled in the extension settings.`, + { + modal: true, + }, + 'Yes' + ); + + if (confirmationResult !== 'Yes') { + return false; + } + } + + try { + const deleteOne = promisify( + this.dataService.deleteOne.bind(this.dataService) + ); + const deleteResult = await deleteOne( + this.namespace, + { _id: this.documentId }, + {} + ); + + if (deleteResult.deletedCount !== 1) { + throw new Error('document not found'); + } + + await this.resetDocumentListCache(); + + return true; + } catch (error) { + throw new Error(formatError(error).message); + } + } } diff --git a/src/mdbExtensionController.ts b/src/mdbExtensionController.ts index a656fa5e3..5d414d36b 100644 --- a/src/mdbExtensionController.ts +++ b/src/mdbExtensionController.ts @@ -561,6 +561,25 @@ export default class MDBExtensionController implements vscode.Disposable { return true; } ); + this.registerCommand( + EXTENSION_COMMANDS.MDB_DELETE_DOCUMENT_FROM_TREE_VIEW, + async (documentTreeItem: DocumentTreeItem): Promise => { + const successfullyDropped = + await documentTreeItem.onDeleteDocumentClicked(); + + if (successfullyDropped) { + void vscode.window.showInformationMessage( + 'Document successfully deleted.' + ); + + // When we successfully drop a document, we need + // to update the explorer view. + this._explorerController.refresh(); + } + + return successfullyDropped; + } + ); this.registerCommand( EXTENSION_COMMANDS.MDB_INSERT_OBJECTID_TO_EDITOR, async (): Promise => { diff --git a/src/test/suite/explorer/documentTreeItem.test.ts b/src/test/suite/explorer/documentTreeItem.test.ts index 78f49c8c0..561adefca 100644 --- a/src/test/suite/explorer/documentTreeItem.test.ts +++ b/src/test/suite/explorer/documentTreeItem.test.ts @@ -16,7 +16,8 @@ suite('DocumentTreeItem Test Suite', () => { mockDocument, 'namespace', 1, - {} as any + {} as any, + () => Promise.resolve() ); const documentTreeItemLabel = testCollectionTreeItem.label; @@ -40,7 +41,8 @@ suite('DocumentTreeItem Test Suite', () => { mockDocument, 'namespace', 1, - mockDataService + mockDataService, + () => Promise.resolve() ); const documentTreeItemLabel = testCollectionTreeItem.label; @@ -61,7 +63,8 @@ suite('DocumentTreeItem Test Suite', () => { mockDocument, 'namespace', 1, - mockDataService + mockDataService, + () => Promise.resolve() ); const documentTreeItemLabel = testCollectionTreeItem.label; diff --git a/src/test/suite/extension.test.ts b/src/test/suite/extension.test.ts index 62c70b643..b6042e480 100644 --- a/src/test/suite/extension.test.ts +++ b/src/test/suite/extension.test.ts @@ -55,6 +55,7 @@ suite('Extension Test Suite', () => { 'mdb.openMongoDBDocumentFromTree', 'mdb.openMongoDBDocumentFromCodeLens', 'mdb.copyDocumentContentsFromTreeView', + 'mdb.deleteDocumentFromTreeView', // Editor commands. 'mdb.codeLens.showMoreDocumentsClicked', diff --git a/src/test/suite/mdbExtensionController.test.ts b/src/test/suite/mdbExtensionController.test.ts index c8af6be03..053c11d8b 100644 --- a/src/test/suite/mdbExtensionController.test.ts +++ b/src/test/suite/mdbExtensionController.test.ts @@ -29,15 +29,14 @@ suite('MDBExtensionController Test Suite', function () { this.timeout(10000); const sandbox: any = sinon.createSandbox(); - const fakeShowInformationMessage: any = sinon.fake(); + let fakeShowInformationMessage: sinon.SinonStub; beforeEach(() => { // Here we stub the showInformationMessage process because it is too much // for the render process and leads to crashes while testing. - sinon.replace( + fakeShowInformationMessage = sinon.stub( vscode.window, - 'showInformationMessage', - fakeShowInformationMessage + 'showInformationMessage' ); }); @@ -1151,7 +1150,8 @@ suite('MDBExtensionController Test Suite', function () { mockDocument, 'waffle.house', 0, - {} as any as DataService + {} as any as DataService, + () => Promise.resolve() ); await vscode.commands.executeCommand( @@ -1180,9 +1180,9 @@ suite('MDBExtensionController Test Suite', function () { const expectedMessage = "The document was saved successfully to 'waffle.house'"; - assert( - fakeShowInformationMessage.firstArg === expectedMessage, - `Expected an error message "${expectedMessage}" to be shown when attempting to add a database to a not connected connection found "${fakeShowInformationMessage.firstArg}"` + assert.strictEqual( + fakeShowInformationMessage.firstCall.firstArg, + expectedMessage ); }); @@ -1198,7 +1198,8 @@ suite('MDBExtensionController Test Suite', function () { mockDocument, 'waffle.house', 0, - {} as any as DataService + {} as any as DataService, + () => Promise.resolve() ); const mockFetchDocument: any = sinon.fake.resolves(null); @@ -1539,7 +1540,8 @@ suite('MDBExtensionController Test Suite', function () { mockDocument, 'waffle.house', 0, - mockDataService + mockDataService, + () => Promise.resolve() ); const mockCopyToClipboard: any = sinon.fake(); @@ -1565,6 +1567,95 @@ suite('MDBExtensionController Test Suite', function () { assert.strictEqual(namespaceUsed, 'waffle.house'); }); + test('mdb.deleteDocumentFromTreeView should not delete a document when the confirmation is cancelled', async () => { + const mockDocument = { + _id: 'pancakes', + time: { + $time: '12345', + }, + }; + + let calledDelete = false; + + const mockDataService: DataService = { + deleteOne: ( + namespace: string, + _id: any, + options: object, + callback: (error: Error | undefined, documents: object[]) => void + ) => { + calledDelete = true; + callback(undefined, [mockDocument]); + }, + } as any; + + const documentTreeItem = new DocumentTreeItem( + mockDocument, + 'waffle.house', + 0, + mockDataService, + () => Promise.resolve() + ); + + const result = await vscode.commands.executeCommand( + 'mdb.deleteDocumentFromTreeView', + documentTreeItem + ); + + assert.strictEqual(result, false); + assert.strictEqual(calledDelete, false); + }); + + test('mdb.deleteDocumentFromTreeView deletes a document after confirmation', async () => { + fakeShowInformationMessage.resolves('Yes'); + + const mockDocument = { + _id: 'pancakes', + time: { + $time: '12345', + }, + }; + + let namespaceUsed = ''; + let _idUsed; + + const mockDataService: DataService = { + deleteOne: ( + namespace: string, + query: any, + options: object, + callback: ( + error: Error | undefined, + result: { deletedCount: number } + ) => void + ) => { + _idUsed = query; + namespaceUsed = namespace; + callback(undefined, { + deletedCount: 1, + }); + }, + } as any; + + const documentTreeItem = new DocumentTreeItem( + mockDocument, + 'waffle.house', + 0, + mockDataService, + () => Promise.resolve() + ); + + const result = await vscode.commands.executeCommand( + 'mdb.deleteDocumentFromTreeView', + documentTreeItem + ); + assert.deepStrictEqual(_idUsed, { + _id: 'pancakes', + }); + assert.strictEqual(namespaceUsed, 'waffle.house'); + assert.strictEqual(result, true); + }); + suite( 'when a user hasnt been shown the initial overview page yet and they have no connections saved', () => {