diff --git a/.eslintrc.json b/.eslintrc.json index 7539659..8e1f2be 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -35,5 +35,11 @@ "@typescript-eslint" ], "rules": { + "@typescript-eslint/no-unused-vars": [ + "warn", + { + "argsIgnorePattern": "^_" + } + ] } } \ No newline at end of file diff --git a/package.json b/package.json index db95f29..b62d1a7 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,18 @@ "when": "true", "group": "navigation" } + ], + "view/item/context": [ + { + "command": "growthbook.copyFeatureKey", + "when": "view == featuresList", + "group": "inline" + }, + { + "command": "growthbook.editFeature", + "when": "view == featuresList", + "group": "inline" + } ] }, "viewsWelcome": [ @@ -51,6 +63,22 @@ { "command": "growthbook.createConfig", "title": "GrowthBook: Create a .growthbook.json configuration file" + }, + { + "command": "growthbook.copyFeatureKey", + "title": "Copy feature key", + "icon": { + "light": "resources/ic_copy_light.png", + "dark": "resources/ic_copy_dark.png" + } + }, + { + "command": "growthbook.editFeature", + "title": "Open feature in browser", + "icon": { + "light": "resources/ic_pencil_light.png", + "dark": "resources/ic_pencil_dark.png" + } } ] }, diff --git a/resources/ic_copy_dark.png b/resources/ic_copy_dark.png new file mode 100644 index 0000000..be47034 Binary files /dev/null and b/resources/ic_copy_dark.png differ diff --git a/resources/ic_copy_light.png b/resources/ic_copy_light.png new file mode 100644 index 0000000..ed40a70 Binary files /dev/null and b/resources/ic_copy_light.png differ diff --git a/resources/ic_pencil_dark.png b/resources/ic_pencil_dark.png new file mode 100644 index 0000000..c5bd36e Binary files /dev/null and b/resources/ic_pencil_dark.png differ diff --git a/resources/ic_pencil_light.png b/resources/ic_pencil_light.png new file mode 100644 index 0000000..7fcedbf Binary files /dev/null and b/resources/ic_pencil_light.png differ diff --git a/src/commands/copy-feature-key-command.ts b/src/commands/copy-feature-key-command.ts new file mode 100644 index 0000000..79a8938 --- /dev/null +++ b/src/commands/copy-feature-key-command.ts @@ -0,0 +1,40 @@ +import * as vscode from "vscode"; +import { Disposable } from "vscode"; +import { ICommand } from "./ICommand"; +import { FeatureListTreeItem } from "../features/FeatureListTreeDataProvider"; +import { + getGrowthBookConfig, + getWorkspaceRootPath, +} from "../utils/vscode-utils"; + +export class CopyFeatureKeyCommand implements ICommand { + register(): Disposable { + return vscode.commands.registerCommand( + "growthbook.copyFeatureKey", + (treeItem: FeatureListTreeItem) => { + const rootPath = getWorkspaceRootPath(); + if (!rootPath) { + vscode.window.showErrorMessage( + "Must be in a folder in order to create a GrowthBook configuration file" + ); + return; + } + + const config = getGrowthBookConfig(rootPath); + if (!config) { + vscode.window.showErrorMessage( + "The .growthbook.json file appears to be misconfigured" + ); + return; + } + + const feature = treeItem.getFeature(); + + vscode.env.clipboard.writeText(feature.id); + vscode.window.showInformationMessage( + `Feature key "${feature.id}" copied to clipboard` + ); + } + ); + } +} diff --git a/src/commands/create-config-command.ts b/src/commands/create-config-command.ts index 111cfb7..d425461 100644 --- a/src/commands/create-config-command.ts +++ b/src/commands/create-config-command.ts @@ -7,43 +7,47 @@ import { GrowthBookConfig, } from "../utils/vscode-utils"; import { ICommand } from "./ICommand"; +import { FeatureListTreeItem } from "../features/FeatureListTreeDataProvider"; export class CreateGrowthBookConfigCommand implements ICommand { register(): Disposable { - return vscode.commands.registerCommand("growthbook.createConfig", () => { - const rootPath = getWorkspaceRootPath(); - - if (!rootPath) { - vscode.window.showErrorMessage( - "Must be in a folder in order to create a GrowthBook configuration file" - ); - return; + return vscode.commands.registerCommand( + "growthbook.createConfig", + (_treeItem: FeatureListTreeItem) => { + const rootPath = getWorkspaceRootPath(); + + if (!rootPath) { + vscode.window.showErrorMessage( + "Must be in a folder in order to create a GrowthBook configuration file" + ); + return; + } + + const configPath = `${rootPath}/.growthbook.json`; + + const configFileExists = doesPathExist(configPath); + if (configFileExists) { + vscode.window.showWarningMessage( + "A .growthbook.json file already exists in the project root. Skipping." + ); + return; + } + + try { + const defaultConfig: GrowthBookConfig = { + featuresEndpoint: "", + appHost: "https://app.growthbook.io", + }; + + const buffer = Buffer.from(JSON.stringify(defaultConfig, null, 2)); + + const configUri = vscode.Uri.parse(configPath); + + vscode.workspace.fs.writeFile(configUri, buffer); + } catch (e) { + onExtensionError("Unable to write configuration file"); + } } - - const configPath = `${rootPath}/.growthbook.json`; - - const configFileExists = doesPathExist(configPath); - if (configFileExists) { - vscode.window.showWarningMessage( - "A .growthbook.json file already exists in the project root. Skipping." - ); - return; - } - - try { - const defaultConfig: GrowthBookConfig = { - featuresEndpoint: "", - appHost: "https://app.growthbook.io", - }; - - const buffer = Buffer.from(JSON.stringify(defaultConfig, null, 2)); - - const configUri = vscode.Uri.parse(configPath); - - vscode.workspace.fs.writeFile(configUri, buffer); - } catch (e) { - onExtensionError("Unable to write configuration file"); - } - }); + ); } } diff --git a/src/commands/edit-feature-command.ts b/src/commands/edit-feature-command.ts new file mode 100644 index 0000000..fe2d64b --- /dev/null +++ b/src/commands/edit-feature-command.ts @@ -0,0 +1,39 @@ +import * as vscode from "vscode"; +import { Disposable, Uri } from "vscode"; +import { ICommand } from "./ICommand"; +import { + getGrowthBookConfig, + getWorkspaceRootPath, +} from "../utils/vscode-utils"; +import { FeatureListTreeItem } from "../features/FeatureListTreeDataProvider"; + +export class EditFeatureCommand implements ICommand { + register(): Disposable { + return vscode.commands.registerCommand( + "growthbook.editFeature", + (treeItem: FeatureListTreeItem) => { + const rootPath = getWorkspaceRootPath(); + if (!rootPath) { + vscode.window.showErrorMessage( + "Must be in a folder in order to create a GrowthBook configuration file" + ); + return; + } + + const config = getGrowthBookConfig(rootPath); + if (!config) { + vscode.window.showErrorMessage( + "The .growthbook.json file appears to be misconfigured" + ); + return; + } + + const feature = treeItem.getFeature(); + const url = `${config.appHost}/features/${feature.id}`; + const uri = Uri.parse(url); + + vscode.env.openExternal(uri); + } + ); + } +} diff --git a/src/commands/refresh-growthbook-command.ts b/src/commands/refresh-growthbook-command.ts index 6d61d92..8fd7cff 100644 --- a/src/commands/refresh-growthbook-command.ts +++ b/src/commands/refresh-growthbook-command.ts @@ -1,13 +1,17 @@ import * as vscode from "vscode"; import { Disposable } from "vscode"; import { ICommand } from "./ICommand"; +import { FeatureListTreeItem } from "../features/FeatureListTreeDataProvider"; export class RefreshGrowthBookCommand implements ICommand { constructor(private performRefresh: () => Promise) {} register(): Disposable { - return vscode.commands.registerCommand("growthbook.refreshFeatures", () => { - this.performRefresh(); - }); + return vscode.commands.registerCommand( + "growthbook.refreshFeatures", + (_treeItem: FeatureListTreeItem) => { + this.performRefresh(); + } + ); } } diff --git a/src/features/FeatureListTreeDataProvider.ts b/src/features/FeatureListTreeDataProvider.ts index 1c1d6bf..9f22862 100644 --- a/src/features/FeatureListTreeDataProvider.ts +++ b/src/features/FeatureListTreeDataProvider.ts @@ -70,7 +70,11 @@ export class FeatureListTreeItem extends TreeItem { ) { super(label, collapsibleState); this.tooltip = this.feature.raw || ""; - this.description = `(default: ${this.feature.defaultValue})`; + this.description = typeof JSON.parse(feature.raw).defaultValue; + } + + getFeature() { + return this.feature; } iconPath = { diff --git a/src/services/ExtensionInitialization.ts b/src/services/ExtensionInitialization.ts index c5b2129..b909e24 100644 --- a/src/services/ExtensionInitialization.ts +++ b/src/services/ExtensionInitialization.ts @@ -1,6 +1,8 @@ import * as vscode from "vscode"; import { ApiClient } from "../api/api-client"; +import { CopyFeatureKeyCommand } from "../commands/copy-feature-key-command"; import { CreateGrowthBookConfigCommand } from "../commands/create-config-command"; +import { EditFeatureCommand } from "../commands/edit-feature-command"; import { RefreshGrowthBookCommand } from "../commands/refresh-growthbook-command"; import { FeatureListTreeDataProvider, @@ -107,7 +109,9 @@ export class ExtensionManagement implements IExtensionManagement { new RefreshGrowthBookCommand( this.refreshFeatures.bind(this) ).register(), - new CreateGrowthBookConfigCommand().register() + new CreateGrowthBookConfigCommand().register(), + new EditFeatureCommand().register(), + new CopyFeatureKeyCommand().register() ); // Initialize the tree view @@ -118,6 +122,7 @@ export class ExtensionManagement implements IExtensionManagement { return Promise.resolve(); } catch (e) { + console.error("Fetch error", e); return Promise.reject("GrowthBook: Cannot fetch features"); } }