Skip to content

Commit 24a7348

Browse files
authored
Add support for adding local lists (#1907)
1 parent 31b80ef commit 24a7348

5 files changed

Lines changed: 136 additions & 25 deletions

File tree

extensions/ql-vscode/src/databases/config/db-config-store.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,28 @@ export class DbConfigStore extends DisposableObject {
129129
await this.writeConfig(config);
130130
}
131131

132+
public async addLocalList(listName: string): Promise<void> {
133+
if (!this.config) {
134+
throw Error("Cannot add local list if config is not loaded");
135+
}
136+
137+
if (listName === "") {
138+
throw Error("List name cannot be empty");
139+
}
140+
141+
if (this.doesLocalListExist(listName)) {
142+
throw Error(`A local list with the name '${listName}' already exists`);
143+
}
144+
145+
const config: DbConfig = cloneDbConfig(this.config);
146+
config.databases.local.lists.push({
147+
name: listName,
148+
databases: [],
149+
});
150+
151+
await this.writeConfig(config);
152+
}
153+
132154
public async addRemoteList(listName: string): Promise<void> {
133155
if (!this.config) {
134156
throw Error("Cannot add remote list if config is not loaded");

extensions/ql-vscode/src/databases/db-manager.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,8 +104,8 @@ export class DbManager {
104104
): Promise<void> {
105105
switch (listKind) {
106106
case DbListKind.Local:
107-
// Adding a local list is not supported yet.
108-
throw Error("Cannot add a local list");
107+
await this.dbConfigStore.addLocalList(listName);
108+
break;
109109
case DbListKind.Remote:
110110
await this.dbConfigStore.addRemoteList(listName);
111111
break;

extensions/ql-vscode/src/databases/ui/db-panel.ts

Lines changed: 47 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ export interface RemoteDatabaseQuickPickItem extends QuickPickItem {
2323
kind: string;
2424
}
2525

26+
export interface AddListQuickPickItem extends QuickPickItem {
27+
kind: DbListKind;
28+
}
29+
2630
export class DbPanel extends DisposableObject {
2731
private readonly dataProvider: DbTreeDataProvider;
2832
private readonly treeView: TreeView<DbTreeViewItem>;
@@ -179,6 +183,8 @@ export class DbPanel extends DisposableObject {
179183
}
180184

181185
private async addNewList(): Promise<void> {
186+
const listKind = await this.getAddNewListKind();
187+
182188
const listName = await window.showInputBox({
183189
prompt: "Enter a name for the new list",
184190
placeHolder: "example-list",
@@ -187,17 +193,6 @@ export class DbPanel extends DisposableObject {
187193
return;
188194
}
189195

190-
const highlightedItem = await this.getHighlightedDbItem();
191-
192-
// For now: we only support adding remote lists, so if no item is highlighted,
193-
// we default to the "RootRemote" kind.
194-
// In future: if the highlighted item is undefined, we'll show a quick pick where
195-
// a user can select whether to add a remote or local list.
196-
const highlightedItemKind = highlightedItem?.kind || DbItemKind.RootRemote;
197-
const listKind = remoteDbKinds.includes(highlightedItemKind)
198-
? DbListKind.Remote
199-
: DbListKind.Local;
200-
201196
if (this.dbManager.doesListExist(listKind, listName)) {
202197
void showAndLogErrorMessage(`The list '${listName}' already exists`);
203198
return;
@@ -206,6 +201,47 @@ export class DbPanel extends DisposableObject {
206201
await this.dbManager.addNewList(listKind, listName);
207202
}
208203

204+
private async getAddNewListKind(): Promise<DbListKind> {
205+
const highlightedItem = await this.getHighlightedDbItem();
206+
if (highlightedItem) {
207+
return remoteDbKinds.includes(highlightedItem.kind)
208+
? DbListKind.Remote
209+
: DbListKind.Local;
210+
} else {
211+
const quickPickItems = [
212+
{
213+
label: "$(cloud) Remote",
214+
detail: "Add a remote database from GitHub",
215+
alwaysShow: true,
216+
kind: DbListKind.Remote,
217+
},
218+
{
219+
label: "$(database) Local",
220+
detail: "Import a database from the cloud or a local file",
221+
alwaysShow: true,
222+
kind: DbListKind.Local,
223+
},
224+
];
225+
const selectedOption = await window.showQuickPick<AddListQuickPickItem>(
226+
quickPickItems,
227+
{
228+
title: "Add a new database",
229+
ignoreFocusOut: true,
230+
},
231+
);
232+
if (!selectedOption) {
233+
// We don't need to display a warning pop-up in this case, since the user just escaped out of the operation.
234+
// We set 'true' to make this a silent exception.
235+
throw new UserCancellationException(
236+
"No database list kind selected",
237+
true,
238+
);
239+
}
240+
241+
return selectedOption.kind;
242+
}
243+
}
244+
209245
private async setSelectedItem(treeViewItem: DbTreeViewItem): Promise<void> {
210246
if (treeViewItem.dbItem === undefined) {
211247
throw new Error(

extensions/ql-vscode/src/vscode-tests/cli-integration/databases/db-panel.test.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@ import { CodeQLExtensionInterface } from "../../../extension";
44
import { readJson } from "fs-extra";
55
import * as path from "path";
66
import { DbConfig } from "../../../databases/config/db-config";
7-
import { RemoteDatabaseQuickPickItem } from "../../../databases/ui/db-panel";
7+
import {
8+
AddListQuickPickItem,
9+
RemoteDatabaseQuickPickItem,
10+
} from "../../../databases/ui/db-panel";
11+
import { DbListKind } from "../../../databases/db-item";
812

913
jest.setTimeout(60_000);
1014

@@ -25,6 +29,9 @@ describe("Db panel UI commands", () => {
2529

2630
it("should add new remote db list", async () => {
2731
// Add db list
32+
jest.spyOn(window, "showQuickPick").mockResolvedValue({
33+
kind: DbListKind.Remote,
34+
} as AddListQuickPickItem);
2835
jest.spyOn(window, "showInputBox").mockResolvedValue("my-list-1");
2936
await commands.executeCommand("codeQLDatabasesExperimental.addNewList");
3037

@@ -35,6 +42,21 @@ describe("Db panel UI commands", () => {
3542
expect(dbConfig.databases.remote.repositoryLists[0].name).toBe("my-list-1");
3643
});
3744

45+
it("should add new local db list", async () => {
46+
// Add db list
47+
jest.spyOn(window, "showQuickPick").mockResolvedValue({
48+
kind: DbListKind.Local,
49+
} as AddListQuickPickItem);
50+
jest.spyOn(window, "showInputBox").mockResolvedValue("my-list-1");
51+
await commands.executeCommand("codeQLDatabasesExperimental.addNewList");
52+
53+
// Check db config
54+
const dbConfigFilePath = path.join(storagePath, "workspace-databases.json");
55+
const dbConfig: DbConfig = await readJson(dbConfigFilePath);
56+
expect(dbConfig.databases.local.lists).toHaveLength(1);
57+
expect(dbConfig.databases.local.lists[0].name).toBe("my-list-1");
58+
});
59+
3860
it("should add new remote repository", async () => {
3961
// Add db
4062
jest.spyOn(window, "showQuickPick").mockResolvedValue({

extensions/ql-vscode/src/vscode-tests/minimal-workspace/databases/db-panel.test.ts

Lines changed: 42 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -478,10 +478,7 @@ describe("db panel", () => {
478478

479479
await dbManager.addNewRemoteRepo("owner2/repo2");
480480

481-
// Read the workspace databases JSON file directly to check that the new repo has been added.
482-
// We can't use the dbConfigStore's `read` function here because it depends on the file watcher
483-
// picking up changes, and we don't control the timing of that.
484-
const dbConfigFileContents = await readJSON(dbConfigFilePath);
481+
const dbConfigFileContents = await readDbConfigDirectly();
485482
expect(dbConfigFileContents.databases.remote.repositories.length).toBe(2);
486483
expect(dbConfigFileContents.databases.remote.repositories[1]).toEqual(
487484
"owner2/repo2",
@@ -572,10 +569,7 @@ describe("db panel", () => {
572569

573570
await dbManager.addNewList(DbListKind.Remote, "my-list-2");
574571

575-
// Read the workspace databases JSON file directly to check that the new list has been added.
576-
// We can't use the dbConfigStore's `read` function here because it depends on the file watcher
577-
// picking up changes, and we don't control the timing of that.
578-
const dbConfigFileContents = await readJSON(dbConfigFilePath);
572+
const dbConfigFileContents = await readDbConfigDirectly();
579573
expect(dbConfigFileContents.databases.remote.repositoryLists.length).toBe(
580574
2,
581575
);
@@ -586,12 +580,42 @@ describe("db panel", () => {
586580
});
587581

588582
it("should throw error when adding a new list to a local node", async () => {
589-
const dbConfig: DbConfig = createDbConfig();
583+
const dbConfig: DbConfig = createDbConfig({
584+
localLists: [
585+
{
586+
name: "my-list-1",
587+
databases: [],
588+
},
589+
],
590+
});
590591
await saveDbConfig(dbConfig);
591592

592-
await expect(dbManager.addNewList(DbListKind.Local, "")).rejects.toThrow(
593-
new Error("Cannot add a local list"),
593+
const dbTreeItems = await dbTreeDataProvider.getChildren();
594+
595+
expect(dbTreeItems).toBeTruthy();
596+
const items = dbTreeItems!;
597+
598+
const localRootNode = items[1];
599+
const localUserDefinedLists = localRootNode.children.filter(
600+
(c) => c.dbItem?.kind === DbItemKind.LocalList,
601+
);
602+
const list1 = localRootNode.children.find(
603+
(c) =>
604+
c.dbItem?.kind === DbItemKind.LocalList &&
605+
c.dbItem?.listName === "my-list-1",
594606
);
607+
608+
expect(localUserDefinedLists.length).toBe(1);
609+
expect(localUserDefinedLists[0]).toBe(list1);
610+
611+
await dbManager.addNewList(DbListKind.Local, "my-list-2");
612+
613+
const dbConfigFileContents = await readDbConfigDirectly();
614+
expect(dbConfigFileContents.databases.local.lists.length).toBe(2);
615+
expect(dbConfigFileContents.databases.local.lists[1]).toEqual({
616+
name: "my-list-2",
617+
databases: [],
618+
});
595619
});
596620
});
597621

@@ -838,4 +862,11 @@ describe("db panel", () => {
838862
SELECTED_DB_ITEM_RESOURCE_URI && treeViewItem.contextValue === undefined
839863
);
840864
}
865+
866+
async function readDbConfigDirectly(): Promise<DbConfig> {
867+
// Read the workspace databases JSON file directly to check that the new list has been added.
868+
// We can't use the dbConfigStore's `read` function here because it depends on the file watcher
869+
// picking up changes, and we don't control the timing of that.
870+
return (await readJSON(dbConfigFilePath)) as DbConfig;
871+
}
841872
});

0 commit comments

Comments
 (0)