diff --git a/client/src/components/LibraryNavigator/LibraryDataProvider.ts b/client/src/components/LibraryNavigator/LibraryDataProvider.ts index 0a2eb0c9b..dada5d219 100644 --- a/client/src/components/LibraryNavigator/LibraryDataProvider.ts +++ b/client/src/components/LibraryNavigator/LibraryDataProvider.ts @@ -50,6 +50,10 @@ class LibraryDataProvider return this._onDidChangeTreeData.event; } + get treeView(): TreeView { + return this._treeView; + } + constructor( private readonly model: LibraryModel, private readonly extensionUri: Uri, @@ -170,6 +174,11 @@ class LibraryDataProvider this._onDidChangeTreeData.fire(undefined); } + public async deleteTables(items: LibraryItem[]): Promise { + await this.model.deleteTables(items); + this._onDidChangeTreeData.fire(undefined); + } + public watch(): Disposable { // ignore, fires for all changes... return new Disposable(() => {}); diff --git a/client/src/components/LibraryNavigator/LibraryModel.ts b/client/src/components/LibraryNavigator/LibraryModel.ts index adb9410d9..039dae2f7 100644 --- a/client/src/components/LibraryNavigator/LibraryModel.ts +++ b/client/src/components/LibraryNavigator/LibraryModel.ts @@ -132,6 +132,24 @@ class LibraryModel { } } + public async deleteTables(items: LibraryItem[]) { + const failures: string[] = []; + + for (const item of items) { + try { + await this.libraryAdapter.deleteTable(item); + } catch { + failures.push(item.uid); + } + } + + if (failures.length > 0) { + throw new Error( + l10n.t(Messages.TableDeletionError, { tableName: failures.join(", ") }), + ); + } + } + public async getTableInfo(item: LibraryItem) { if (this.libraryAdapter.getTableInfo) { await this.libraryAdapter.setup(); diff --git a/client/src/components/LibraryNavigator/const.ts b/client/src/components/LibraryNavigator/const.ts index 6608f42ee..6bd3f102f 100644 --- a/client/src/components/LibraryNavigator/const.ts +++ b/client/src/components/LibraryNavigator/const.ts @@ -4,6 +4,9 @@ import { l10n } from "vscode"; export const Messages = { TableDeletionError: l10n.t("Unable to delete table {tableName}."), + TablesDeletionWarning: l10n.t( + "Are you sure you want to delete {count} table(s): {tableNames}?", + ), ViewTableCommandTitle: l10n.t("View SAS Table"), }; diff --git a/client/src/components/LibraryNavigator/index.ts b/client/src/components/LibraryNavigator/index.ts index 25baf8282..0dbe3fd38 100644 --- a/client/src/components/LibraryNavigator/index.ts +++ b/client/src/components/LibraryNavigator/index.ts @@ -7,6 +7,7 @@ import { Uri, commands, env, + l10n, window, workspace, } from "vscode"; @@ -24,6 +25,7 @@ import LibraryAdapterFactory from "./LibraryAdapterFactory"; import LibraryDataProvider from "./LibraryDataProvider"; import LibraryModel from "./LibraryModel"; import PaginatedResultSet from "./PaginatedResultSet"; +import { Messages } from "./const"; import { LibraryAdapter, LibraryItem, TableData } from "./types"; class LibraryNavigator implements SubscriptionProvider { @@ -66,8 +68,35 @@ class LibraryNavigator implements SubscriptionProvider { ), commands.registerCommand("SAS.refreshLibraries", () => this.refresh()), commands.registerCommand("SAS.deleteTable", async (item: LibraryItem) => { + const selectedItems = this.treeViewSelections(item); + + if (selectedItems.length === 0) { + return; + } + try { - await this.libraryDataProvider.deleteTable(item); + if (selectedItems.length === 1) { + await this.libraryDataProvider.deleteTable(selectedItems[0]); + } else { + const tableNames = selectedItems + .map((table) => `${table.library}.${table.name}`) + .join(", "); + + const result = await window.showWarningMessage( + l10n.t(Messages.TablesDeletionWarning, { + tableNames: tableNames, + count: selectedItems.length, + }), + { modal: true }, + "Delete", + ); + + if (result !== "Delete") { + return; + } + + await this.libraryDataProvider.deleteTables(selectedItems); + } } catch (error) { window.showErrorMessage(error.message); } @@ -128,6 +157,15 @@ class LibraryNavigator implements SubscriptionProvider { this.libraryDataProvider.useAdapter(this.libraryAdapterForConnectionType()); } + private treeViewSelections(item: LibraryItem): LibraryItem[] { + const items = + this.libraryDataProvider.treeView.selection.length > 1 || !item + ? this.libraryDataProvider.treeView.selection + : [item]; + + return items.filter(Boolean); + } + private async displayTableProperties( item: LibraryItem, showPropertiesTab: boolean = false, diff --git a/client/test/components/LibraryNavigator/LibraryDataProvider.test.ts b/client/test/components/LibraryNavigator/LibraryDataProvider.test.ts index 24601be3a..b57430141 100644 --- a/client/test/components/LibraryNavigator/LibraryDataProvider.test.ts +++ b/client/test/components/LibraryNavigator/LibraryDataProvider.test.ts @@ -270,4 +270,87 @@ describe("LibraryDataProvider", async function () { deleteTableStub.restore(); }); + + it("deleteTables - deletes multiple tables successfully", async () => { + const items: LibraryItem[] = [ + { + uid: "lib.table1", + id: "table1", + name: "table1", + type: "table", + readOnly: false, + library: "lib", + }, + { + uid: "lib.table2", + id: "table2", + name: "table2", + type: "table", + readOnly: false, + library: "lib", + }, + ]; + + const api = dataAccessApi(); + const deleteTableStub = sinon.stub(api, "deleteTable"); + + const provider = libraryDataProvider(api); + await provider.deleteTables(items); + + expect(deleteTableStub.callCount).to.equal(2); + deleteTableStub.restore(); + }); + + it("deleteTables - reports failures when some tables fail to delete", async () => { + const items: LibraryItem[] = [ + { + uid: "lib.table1", + id: "table1", + name: "table1", + type: "table", + readOnly: false, + library: "lib", + }, + { + uid: "lib.table2", + id: "table2", + name: "table2", + type: "table", + readOnly: false, + library: "lib", + }, + { + uid: "lib.table3", + id: "table3", + name: "table3", + type: "table", + readOnly: false, + library: "lib", + }, + ]; + + const api = dataAccessApi(); + const deleteTableStub = sinon.stub(api, "deleteTable"); + + // First table succeeds + deleteTableStub.onFirstCall().resolves(); + // Second table fails + deleteTableStub + .onSecondCall() + .throwsException(new Error("Failed to delete")); + // Third table succeeds + deleteTableStub.onThirdCall().resolves(); + + const provider = libraryDataProvider(api); + + try { + await provider.deleteTables(items); + expect.fail("Should have thrown an error"); + } catch (error) { + expect(error.message).to.contain("lib.table2"); + expect(deleteTableStub.callCount).to.equal(3); + } + + deleteTableStub.restore(); + }); });