diff --git a/extensions/ql-vscode/package.json b/extensions/ql-vscode/package.json index fe32dcba03f..3711c8c8b7a 100644 --- a/extensions/ql-vscode/package.json +++ b/extensions/ql-vscode/package.json @@ -783,7 +783,7 @@ "view/item/context": [ { "command": "codeQLDatabasesExperimental.setSelectedItemContextMenu", - "when": "view == codeQLDatabasesExperimental && viewItem == canBeSelected" + "when": "view == codeQLDatabasesExperimental && viewItem =~ /canBeSelected/" }, { "command": "codeQLDatabases.setCurrentDatabase", @@ -812,7 +812,7 @@ }, { "command": "codeQLDatabasesExperimental.setSelectedItem", - "when": "view == codeQLDatabasesExperimental && viewItem == canBeSelected", + "when": "view == codeQLDatabasesExperimental && viewItem =~ /canBeSelected/", "group": "inline" }, { diff --git a/extensions/ql-vscode/src/databases/ui/db-tree-view-item-action.ts b/extensions/ql-vscode/src/databases/ui/db-tree-view-item-action.ts new file mode 100644 index 00000000000..e789154f87b --- /dev/null +++ b/extensions/ql-vscode/src/databases/ui/db-tree-view-item-action.ts @@ -0,0 +1,61 @@ +import { DbItem, DbItemKind, isSelectableDbItem } from "../db-item"; + +export type DbTreeViewItemAction = + | "canBeSelected" + | "canBeRemoved" + | "canBeRenamed" + | "canBeOpenedOnGitHub"; + +export function getDbItemActions(dbItem: DbItem): DbTreeViewItemAction[] { + const actions: DbTreeViewItemAction[] = []; + + if (canBeSelected(dbItem)) { + actions.push("canBeSelected"); + } + if (canBeRemoved(dbItem)) { + actions.push("canBeRemoved"); + } + if (canBeRenamed(dbItem)) { + actions.push("canBeRenamed"); + } + if (canBeOpenedOnGitHub(dbItem)) { + actions.push("canBeOpenedOnGitHub"); + } + + return actions; +} + +const dbItemKindsThatCanBeRemoved = [ + DbItemKind.LocalList, + DbItemKind.RemoteUserDefinedList, + DbItemKind.LocalDatabase, + DbItemKind.RemoteRepo, + DbItemKind.RemoteOwner, +]; + +const dbItemKindsThatCanBeRenamed = [ + DbItemKind.LocalList, + DbItemKind.RemoteUserDefinedList, + DbItemKind.LocalDatabase, +]; + +const dbItemKindsThatCanBeOpenedOnGitHub = [ + DbItemKind.RemoteOwner, + DbItemKind.RemoteRepo, +]; + +function canBeSelected(dbItem: DbItem): boolean { + return isSelectableDbItem(dbItem) && !dbItem.selected; +} + +function canBeRemoved(dbItem: DbItem): boolean { + return dbItemKindsThatCanBeRemoved.includes(dbItem.kind); +} + +function canBeRenamed(dbItem: DbItem): boolean { + return dbItemKindsThatCanBeRenamed.includes(dbItem.kind); +} + +function canBeOpenedOnGitHub(dbItem: DbItem): boolean { + return dbItemKindsThatCanBeOpenedOnGitHub.includes(dbItem.kind); +} diff --git a/extensions/ql-vscode/src/databases/ui/db-tree-view-item.ts b/extensions/ql-vscode/src/databases/ui/db-tree-view-item.ts index a0da088cd50..1f14d426d73 100644 --- a/extensions/ql-vscode/src/databases/ui/db-tree-view-item.ts +++ b/extensions/ql-vscode/src/databases/ui/db-tree-view-item.ts @@ -11,6 +11,7 @@ import { RootLocalDbItem, RootRemoteDbItem, } from "../db-item"; +import { getDbItemActions } from "./db-tree-view-item-action"; export const SELECTED_DB_ITEM_RESOURCE_URI = "codeql://databases?selected=true"; @@ -32,18 +33,21 @@ export class DbTreeViewItem extends vscode.TreeItem { ) { super(label, collapsibleState); - if (dbItem && isSelectableDbItem(dbItem)) { - if (dbItem.selected) { + if (dbItem) { + this.contextValue = getContextValue(dbItem); + if (isSelectableDbItem(dbItem) && dbItem.selected) { // Define the resource id to drive the UI to render this item as selected. this.resourceUri = vscode.Uri.parse(SELECTED_DB_ITEM_RESOURCE_URI); - } else { - // Define a context value to drive the UI to show an action to select the item. - this.contextValue = "canBeSelected"; } } } } +function getContextValue(dbItem: DbItem): string | undefined { + const actions = getDbItemActions(dbItem); + return actions.length > 0 ? actions.join(",") : undefined; +} + export function createDbTreeViewItemError( label: string, tooltip: string, diff --git a/extensions/ql-vscode/test/unit-tests/databases/ui/db-tree-view-item-action.test.ts b/extensions/ql-vscode/test/unit-tests/databases/ui/db-tree-view-item-action.test.ts new file mode 100644 index 00000000000..9b84b73e25a --- /dev/null +++ b/extensions/ql-vscode/test/unit-tests/databases/ui/db-tree-view-item-action.test.ts @@ -0,0 +1,109 @@ +import { getDbItemActions } from "../../../../src/databases/ui/db-tree-view-item-action"; +import { + createLocalDatabaseDbItem, + createLocalListDbItem, + createRemoteOwnerDbItem, + createRemoteRepoDbItem, + createRemoteSystemDefinedListDbItem, + createRemoteUserDefinedListDbItem, + createRootLocalDbItem, + createRootRemoteDbItem, +} from "../../../factories/db-item-factories"; + +describe("getDbItemActions", () => { + it("should return an empty array for root remote node", () => { + const dbItem = createRootRemoteDbItem(); + + const actions = getDbItemActions(dbItem); + + expect(actions).toEqual([]); + }); + + it("should return an empty array for root local node", () => { + const dbItem = createRootLocalDbItem(); + + const actions = getDbItemActions(dbItem); + + expect(actions).toEqual([]); + }); + + it("should set canBeSelected, canBeRemoved and canBeRenamed for local user defined db list", () => { + const dbItem = createLocalListDbItem(); + + const actions = getDbItemActions(dbItem); + + expect(actions).toEqual(["canBeSelected", "canBeRemoved", "canBeRenamed"]); + }); + + it("should set canBeSelected, canBeRemoved and canBeRenamed for local db", () => { + const dbItem = createLocalDatabaseDbItem(); + + const actions = getDbItemActions(dbItem); + + expect(actions).toEqual(["canBeSelected", "canBeRemoved", "canBeRenamed"]); + }); + + it("should set canBeSelected for remote system defined db list", () => { + const dbItem = createRemoteSystemDefinedListDbItem(); + + const actions = getDbItemActions(dbItem); + + expect(actions).toEqual(["canBeSelected"]); + }); + + it("should not set canBeSelected for remote system defined list that is already selected", () => { + const dbItem = createRemoteSystemDefinedListDbItem({ selected: true }); + + const actions = getDbItemActions(dbItem); + + expect(actions.length).toEqual(0); + }); + + it("should set canBeSelected, canBeRemoved and canBeRenamed for remote user defined db list", () => { + const dbItem = createRemoteUserDefinedListDbItem(); + + const actions = getDbItemActions(dbItem); + + expect(actions).toEqual(["canBeSelected", "canBeRemoved", "canBeRenamed"]); + }); + + it("should not set canBeSelected for remote user defined db list that is already selected", () => { + const dbItem = createRemoteUserDefinedListDbItem({ selected: true }); + + const actions = getDbItemActions(dbItem); + + expect(actions.includes("canBeSelected")).toBeFalsy(); + }); + + it("should set canBeSelected, canBeRemoved, canBeOpenedOnGitHub for remote owner", () => { + const dbItem = createRemoteOwnerDbItem(); + + const actions = getDbItemActions(dbItem); + + expect(actions).toEqual([ + "canBeSelected", + "canBeRemoved", + "canBeOpenedOnGitHub", + ]); + }); + + it("should set canBeSelected, canBeRemoved, canBeOpenedOnGitHub for remote db", () => { + const dbItem = createRemoteRepoDbItem(); + + const actions = getDbItemActions(dbItem); + + expect(actions).toEqual([ + "canBeSelected", + "canBeRemoved", + "canBeOpenedOnGitHub", + ]); + }); + + it("should not set canBeSelected for remote db that is already selected", () => { + const dbItem = createRemoteRepoDbItem({ selected: true }); + + const actions = getDbItemActions(dbItem); + + expect(actions.includes("canBeSelected")).toBeFalsy(); + }); +}); diff --git a/extensions/ql-vscode/test/vscode-tests/minimal-workspace/databases/db-panel.test.ts b/extensions/ql-vscode/test/vscode-tests/minimal-workspace/databases/db-panel.test.ts index 13ac502df96..9069cdc2642 100644 --- a/extensions/ql-vscode/test/vscode-tests/minimal-workspace/databases/db-panel.test.ts +++ b/extensions/ql-vscode/test/vscode-tests/minimal-workspace/databases/db-panel.test.ts @@ -772,6 +772,7 @@ describe("db panel", () => { expect(item.tooltip).toBe(`Top ${n} repositories of a language`); expect(item.iconPath).toEqual(new ThemeIcon("github")); expect(item.collapsibleState).toBe(TreeItemCollapsibleState.None); + checkDbItemActions(item, ["canBeSelected"]); } function checkUserDefinedListItem( @@ -783,6 +784,7 @@ describe("db panel", () => { expect(item.tooltip).toBeUndefined(); expect(item.iconPath).toBeUndefined(); expect(item.collapsibleState).toBe(TreeItemCollapsibleState.Collapsed); + checkDbItemActions(item, ["canBeSelected", "canBeRenamed", "canBeRemoved"]); expect(item.children).toBeTruthy(); expect(item.children.length).toBe(repos.length); @@ -796,6 +798,11 @@ describe("db panel", () => { expect(item.tooltip).toBeUndefined(); expect(item.iconPath).toEqual(new ThemeIcon("organization")); expect(item.collapsibleState).toBe(TreeItemCollapsibleState.None); + checkDbItemActions(item, [ + "canBeSelected", + "canBeRemoved", + "canBeOpenedOnGitHub", + ]); expect(item.children).toBeTruthy(); expect(item.children.length).toBe(0); } @@ -805,6 +812,11 @@ describe("db panel", () => { expect(item.tooltip).toBeUndefined(); expect(item.iconPath).toEqual(new ThemeIcon("database")); expect(item.collapsibleState).toBe(TreeItemCollapsibleState.None); + checkDbItemActions(item, [ + "canBeSelected", + "canBeRemoved", + "canBeOpenedOnGitHub", + ]); } function checkLocalListItem( @@ -816,6 +828,7 @@ describe("db panel", () => { expect(item.tooltip).toBeUndefined(); expect(item.iconPath).toBeUndefined(); expect(item.collapsibleState).toBe(TreeItemCollapsibleState.Collapsed); + checkDbItemActions(item, ["canBeSelected", "canBeRemoved", "canBeRenamed"]); expect(item.children).toBeTruthy(); expect(item.children.length).toBe(databases.length); @@ -832,6 +845,16 @@ describe("db panel", () => { expect(item.tooltip).toBe(`Language: ${database.language}`); expect(item.iconPath).toEqual(new ThemeIcon("database")); expect(item.collapsibleState).toBe(TreeItemCollapsibleState.None); + checkDbItemActions(item, ["canBeSelected", "canBeRemoved", "canBeRenamed"]); + } + + function checkDbItemActions(item: DbTreeViewItem, actions: string[]): void { + const itemActions = item.contextValue?.split(","); + expect(itemActions).toBeDefined(); + expect(itemActions!.length).toBe(actions.length); + for (const action of actions) { + expect(itemActions).toContain(action); + } } function checkErrorItem( @@ -852,14 +875,16 @@ describe("db panel", () => { function isTreeViewItemSelectable(treeViewItem: DbTreeViewItem) { return ( treeViewItem.resourceUri === undefined && - treeViewItem.contextValue === "canBeSelected" + treeViewItem.contextValue?.includes("canBeSelected") ); } function isTreeViewItemSelected(treeViewItem: DbTreeViewItem) { return ( treeViewItem.resourceUri?.toString(true) === - SELECTED_DB_ITEM_RESOURCE_URI && treeViewItem.contextValue === undefined + SELECTED_DB_ITEM_RESOURCE_URI && + (treeViewItem.contextValue === undefined || + !treeViewItem.contextValue.includes("canBeSelected")) ); }