diff --git a/package.json b/package.json index 1c88bf3ee..34838b5dd 100644 --- a/package.json +++ b/package.json @@ -284,6 +284,10 @@ "command": "mdb.refreshSchema", "title": "Refresh" }, + { + "command": "mdb.copySchemaFieldName", + "title": "Copy Field Name" + }, { "command": "mdb.startStreamLanguageServerLogs", "title": "LSP Inspector: Start Stream LSP Logs" @@ -434,6 +438,10 @@ { "command": "mdb.refreshSchema", "when": "view == mongoDB && viewItem == schemaTreeItem" + }, + { + "command": "mdb.copySchemaFieldName", + "when": "view == mongoDB && viewItem == fieldTreeItem" } ], "editor/title": [ @@ -535,6 +543,10 @@ { "command": "mdb.runPlayground", "when": "false" + }, + { + "command": "mdb.copySchemaFieldName", + "when": "false" } ] }, diff --git a/src/explorer/fieldTreeItem.ts b/src/explorer/fieldTreeItem.ts index f05ffe9b2..ec865b6ec 100644 --- a/src/explorer/fieldTreeItem.ts +++ b/src/explorer/fieldTreeItem.ts @@ -25,7 +25,7 @@ export enum FieldType { regex = 'Regular Expression', string = 'String', timestamp = 'Timestamp', - undefined = 'Undefined' + undefined = 'Undefined', } export type SchemaFieldType = { @@ -120,6 +120,8 @@ export const getIconFileNameForField = ( return null; }; +export const FIELD_TREE_ITEM_CONTEXT_VALUE = 'fieldTreeItem'; + export default class FieldTreeItem extends vscode.TreeItem implements vscode.TreeDataProvider, TreeItemParent { // This is a flag which notes that when this tree element is updated, @@ -133,7 +135,7 @@ export default class FieldTreeItem extends vscode.TreeItem field: SchemaFieldType; fieldName: string; - contextValue = 'fieldTreeItem'; + contextValue = FIELD_TREE_ITEM_CONTEXT_VALUE; isExpanded: boolean; @@ -240,6 +242,10 @@ export default class FieldTreeItem extends vscode.TreeItem return this._childrenCache; } + getFieldName(): string { + return this.fieldName; + } + get iconPath(): | string | vscode.Uri diff --git a/src/mdbExtensionController.ts b/src/mdbExtensionController.ts index e9fcbefb6..0571d296a 100644 --- a/src/mdbExtensionController.ts +++ b/src/mdbExtensionController.ts @@ -14,12 +14,13 @@ import TelemetryController from './telemetry/telemetryController'; import { StatusView } from './views'; import { createLogger } from './logging'; import { StorageController } from './storage'; -import DatabaseTreeItem from './explorer/databaseTreeItem'; import ConnectionTreeItem from './explorer/connectionTreeItem'; +import DatabaseTreeItem from './explorer/databaseTreeItem'; import SchemaTreeItem from './explorer/schemaTreeItem'; +import DocumentListTreeItem from './explorer/documentListTreeItem'; import DocumentTreeItem from './explorer/documentTreeItem'; import WebviewController from './views/webviewController'; -import DocumentListTreeItem from './explorer/documentListTreeItem'; +import FieldTreeItem from './explorer/fieldTreeItem'; const log = createLogger('commands'); @@ -200,19 +201,15 @@ export default class MDBExtensionController implements vscode.Disposable { ); this.registerCommand( 'mdb.copyConnectionString', - (element: ConnectionTreeItem) => { - // TODO: Password obfuscation. + async (element: ConnectionTreeItem): Promise => { const connectionString = this._connectionController.getConnectionStringFromConnectionId( element.connectionId ); - return new Promise((resolve, reject) => { - vscode.env.clipboard.writeText(connectionString).then(() => { - vscode.window.showInformationMessage('Copied to clipboard.'); + await vscode.env.clipboard.writeText(connectionString); + vscode.window.showInformationMessage('Copied to clipboard.'); - return resolve(true); - }, reject); - }); + return true; } ); this.registerCommand( @@ -232,7 +229,7 @@ export default class MDBExtensionController implements vscode.Disposable { vscode.window.showErrorMessage( 'Please wait for the connection to finish loading before adding a database.' ); - return Promise.resolve(false); + return false; } if ( @@ -242,72 +239,63 @@ export default class MDBExtensionController implements vscode.Disposable { vscode.window.showErrorMessage( 'Please connect to this connection before adding a database.' ); - return Promise.resolve(false); + return false; } if (this._connectionController.isDisconnecting()) { vscode.window.showErrorMessage( 'Unable to add database: currently disconnecting.' ); - return Promise.resolve(false); + return false; } if (this._connectionController.isConnecting()) { vscode.window.showErrorMessage( 'Unable to add database: currently connecting.' ); - return Promise.resolve(false); + return false; } - return new Promise((resolve, reject) => { - element - .onAddDatabaseClicked(this._context) - .then((successfullyAddedDatabase) => { - if (successfullyAddedDatabase) { - vscode.window.showInformationMessage( - 'Database and collection successfully created.' - ); - - // When we successfully added a database & collection, we need - // to update the explorer view. - this._explorerController.refresh(); - } - resolve(successfullyAddedDatabase); - }, reject); - }); + const successfullyAddedDatabase = await element.onAddDatabaseClicked( + this._context + ); + + if (successfullyAddedDatabase) { + vscode.window.showInformationMessage( + 'Database and collection successfully created.' + ); + + // When we successfully added a database & collection, we need + // to update the explorer view. + this._explorerController.refresh(); + } + return successfullyAddedDatabase; } ); this.registerCommand( 'mdb.copyDatabaseName', - (element: DatabaseTreeItem) => { - return new Promise((resolve, reject) => { - vscode.env.clipboard.writeText(element.databaseName).then(() => { - vscode.window.showInformationMessage('Copied to clipboard.'); - return resolve(true); - }, reject); - }); + async (element: DatabaseTreeItem) => { + await vscode.env.clipboard.writeText(element.databaseName); + vscode.window.showInformationMessage('Copied to clipboard.'); + return true; } ); this.registerCommand( 'mdb.dropDatabase', - (element: DatabaseTreeItem): Promise => { - return new Promise((resolve, reject) => { - element - .onDropDatabaseClicked() - .then((successfullyDroppedDatabase) => { - if (successfullyDroppedDatabase) { - vscode.window.showInformationMessage( - 'Database successfully dropped.' - ); - - // When we successfully drop a database, we need - // to update the explorer view. - this._explorerController.refresh(); - } - - resolve(successfullyDroppedDatabase); - }, reject); - }); + async (element: DatabaseTreeItem): Promise => { + const successfullyDroppedDatabase = await element.onDropDatabaseClicked(); + + if (successfullyDroppedDatabase) { + vscode.window.showInformationMessage( + 'Database successfully dropped.' + ); + + // When we successfully drop a database, we need + // to update the explorer view. + this._explorerController.refresh(); + } + + return successfullyDroppedDatabase; } ); this.registerCommand( @@ -324,58 +312,48 @@ export default class MDBExtensionController implements vscode.Disposable { vscode.window.showErrorMessage( 'Unable to add collection: currently disconnecting.' ); - return Promise.resolve(false); + return false; } - return new Promise((resolve, reject) => { - element - .onAddCollectionClicked(this._context) - .then((successfullyAddedCollection) => { - if (successfullyAddedCollection) { - vscode.window.showInformationMessage( - 'Collection successfully created.' - ); - - // When we successfully added a collection, we need - // to update the explorer view. - this._explorerController.refresh(); - } - resolve(true); - }, reject); - }); + const successfullyAddedCollection = await element + .onAddCollectionClicked(this._context); + if (successfullyAddedCollection) { + vscode.window.showInformationMessage( + 'Collection successfully created.' + ); + + // When we successfully added a collection, we need + // to update the explorer view. + this._explorerController.refresh(); + } + return true; } ); this.registerCommand( 'mdb.copyCollectionName', - (element: CollectionTreeItem): Promise => { - return new Promise((resolve, reject) => { - vscode.env.clipboard.writeText(element.collectionName).then(() => { - vscode.window.showInformationMessage('Copied to clipboard.'); - return resolve(true); - }, reject); - }); + async (element: CollectionTreeItem): Promise => { + await vscode.env.clipboard.writeText(element.collectionName); + vscode.window.showInformationMessage('Copied to clipboard.'); + + return true; } ); this.registerCommand( 'mdb.dropCollection', - (element: CollectionTreeItem): Promise => { - return new Promise((resolve, reject) => { - element - .onDropCollectionClicked() - .then((successfullyDroppedCollection) => { - if (successfullyDroppedCollection) { - vscode.window.showInformationMessage( - 'Collection successfully dropped.' - ); - - // When we successfully drop a collection, we need - // to update the explorer view. - this._explorerController.refresh(); - } - - resolve(successfullyDroppedCollection); - }, reject); - }); + async (element: CollectionTreeItem): Promise => { + const successfullyDroppedCollection = await element.onDropCollectionClicked(); + + if (successfullyDroppedCollection) { + vscode.window.showInformationMessage( + 'Collection successfully dropped.' + ); + + // When we successfully drop a collection, we need + // to update the explorer view. + this._explorerController.refresh(); + } + + return successfullyDroppedCollection; } ); this.registerCommand( @@ -417,6 +395,17 @@ export default class MDBExtensionController implements vscode.Disposable { return this._explorerController.refresh(); } ); + this.registerCommand( + 'mdb.copySchemaFieldName', + async (fieldTreeItem: FieldTreeItem): Promise => { + await vscode.env.clipboard.writeText( + fieldTreeItem.getFieldName() + ); + vscode.window.showInformationMessage('Copied to clipboard.'); + + return true; + } + ); } dispose(): void { diff --git a/src/test/suite/explorer/explorerController.test.ts b/src/test/suite/explorer/explorerController.test.ts index 2589ed001..179905e99 100644 --- a/src/test/suite/explorer/explorerController.test.ts +++ b/src/test/suite/explorer/explorerController.test.ts @@ -6,7 +6,7 @@ const sinon = require('sinon'); import { DefaultSavingLocations, - StorageScope, + StorageScope } from '../../../storage/storageController'; import { TEST_DATABASE_URI } from '../dbTestHelper'; import { mdbTestExtension } from '../stubbableMdbExtension'; @@ -84,8 +84,8 @@ suite('Explorer Controller Test Suite', function () { connectionModel: new Connection(), name: 'testConnectionName', driverUrl: 'url', - storageLocation: StorageScope.NONE, - }, + storageLocation: StorageScope.NONE + } }; testConnectionController.setConnnectingConnectionId(mockConnectionId); testConnectionController.setConnnecting(true); @@ -247,7 +247,7 @@ suite('Explorer Controller Test Suite', function () { driverUrl: '', name: 'aaa', id: 'aaa', - storageLocation: StorageScope.WORKSPACE, + storageLocation: StorageScope.WORKSPACE }; testConnectionController._connections.zzz = { @@ -256,7 +256,7 @@ suite('Explorer Controller Test Suite', function () { driverUrl: '', name: 'zzz', id: 'zzz', - storageLocation: StorageScope.WORKSPACE, + storageLocation: StorageScope.WORKSPACE }; const treeControllerChildren = await treeController.getChildren(); diff --git a/src/test/suite/explorer/fieldTreeItems.test.ts b/src/test/suite/explorer/fieldTreeItem.test.ts similarity index 94% rename from src/test/suite/explorer/fieldTreeItems.test.ts rename to src/test/suite/explorer/fieldTreeItem.test.ts index 3e78d9c2c..e67c0979c 100644 --- a/src/test/suite/explorer/fieldTreeItems.test.ts +++ b/src/test/suite/explorer/fieldTreeItem.test.ts @@ -1,7 +1,9 @@ import * as assert from 'assert'; import { afterEach } from 'mocha'; +const { contributes } = require('../../../../package.json'); import FieldTreeItem, { + FIELD_TREE_ITEM_CONTEXT_VALUE, fieldIsExpandable, getIconFileNameForField } from '../../../explorer/fieldTreeItem'; @@ -16,6 +18,21 @@ import { import { TestExtensionContext } from '../stubs'; suite('FieldTreeItem Test Suite', () => { + test('its context value should be in the package json', function() { + let registeredCommandInPackageJson = false; + + contributes.menus['view/item/context'].forEach((contextItem) => { + if (contextItem.when.includes(FIELD_TREE_ITEM_CONTEXT_VALUE)) { + registeredCommandInPackageJson = true; + } + }); + + assert( + registeredCommandInPackageJson, + 'Expected field tree item to be registered with a command in package json' + ); + }); + test('it should have a different icon depending on the field type', () => { ext.context = new TestExtensionContext(); diff --git a/src/test/suite/explorer/IndexTreeItem.test.ts b/src/test/suite/explorer/indexTreeItem.test.ts similarity index 100% rename from src/test/suite/explorer/IndexTreeItem.test.ts rename to src/test/suite/explorer/indexTreeItem.test.ts diff --git a/src/test/suite/extension.test.ts b/src/test/suite/extension.test.ts index f10037ca5..a93014ef4 100644 --- a/src/test/suite/extension.test.ts +++ b/src/test/suite/extension.test.ts @@ -57,6 +57,7 @@ suite('Extension Test Suite', () => { 'mdb.copyCollectionName', 'mdb.refreshCollection', 'mdb.refreshSchema', + 'mdb.copySchemaFieldName', // Editor commands. 'mdb.codeLens.showMoreDocumentsClicked' diff --git a/src/test/suite/mdbExtensionController.test.ts b/src/test/suite/mdbExtensionController.test.ts index c37d05c3b..41231057b 100644 --- a/src/test/suite/mdbExtensionController.test.ts +++ b/src/test/suite/mdbExtensionController.test.ts @@ -14,6 +14,7 @@ import { } from '../../explorer'; import { mdbTestExtension } from './stubbableMdbExtension'; import { StorageScope } from '../../storage/storageController'; +import FieldTreeItem from '../../explorer/fieldTreeItem'; const sinon = require('sinon'); const testDatabaseURI = 'mongodb://localhost:27018'; @@ -312,6 +313,37 @@ suite('MDBExtensionController Test Suite', () => { .then(done, done); }); + test('mdb.copySchemaFieldName command should try to copy the field name to the vscode env clipboard', async () => { + const mockTreeItem = new FieldTreeItem( + { + name: 'dolphins are sentient', + probability: 1, + type: 'String', + types: [] + }, + false, + {} + ); + + const mockCopyToClipboard = sinon.fake.resolves(); + sinon.replace(vscode.env.clipboard, 'writeText', mockCopyToClipboard); + + const commandResult = await vscode.commands.executeCommand( + 'mdb.copySchemaFieldName', + mockTreeItem + ); + + assert(commandResult); + assert( + mockCopyToClipboard.called, + 'Expected "writeText" to be called on "vscode.env.clipboard".' + ); + assert( + mockCopyToClipboard.firstArg === 'dolphins are sentient', + `Expected the clipboard to be sent the schema field name "dolphins are sentient", found ${mockCopyToClipboard.firstArg}.` + ); + }); + test('mdb.refreshDatabase command should reset the cache on the database tree item', (done) => { const mockTreeItem = new DatabaseTreeItem( 'pinkLemonade', @@ -406,7 +438,6 @@ suite('MDBExtensionController Test Suite', () => { const collectionChildren = await mockTreeItem.getChildren(); const docListTreeItem = collectionChildren[0]; - console.log('collectionChildren', collectionChildren); assert(docListTreeItem.description === '9K');