diff --git a/.all-contributorsrc b/.all-contributorsrc
index 01f343b..99aa83a 100644
--- a/.all-contributorsrc
+++ b/.all-contributorsrc
@@ -133,6 +133,13 @@
"avatar_url": "https://avatars.githubusercontent.com/u/22429087?v=4",
"profile": "https://dev.to/kkazala",
"contributions": ["bug", "code", "maintenance", "test", "userTesting"]
+ },
+ {
+ "login": "Mosh-K",
+ "name": "Mosh-K",
+ "avatar_url": "https://avatars.githubusercontent.com/u/158998622?v=4",
+ "profile": "https://github.com/Mosh-K",
+ "contributions": ["code", "design", "userTesting"]
}
],
"contributorsPerLine": 7
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2c11c2d..3cdf061 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,10 +7,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
-## [v2.2.6](https://github.com/Power-Maverick/DataverseDevTools-VSCode/compare/v2.2.5...v2.2.6)
+## [v2.2.7](https://github.com/Power-Maverick/DataverseDevTools-VSCode/compare/v2.2.6...v2.2.7)
### Merged
+- Fix garbled non-ASCII text in Compare with Server [`#322`](https://github.com/Power-Maverick/DataverseDevTools-VSCode/pull/322)
+- Add plugin sync via pac plugin push [`#324`](https://github.com/Power-Maverick/DataverseDevTools-VSCode/pull/324)
+- Feature: tools realignment [`#297`](https://github.com/Power-Maverick/DataverseDevTools-VSCode/pull/297)
+
+### Commits
+
+- Add tool icons and implement command registration for tools [`40bb631`](https://github.com/Power-Maverick/DataverseDevTools-VSCode/commit/40bb631ba721fed3fa7209228c86c50d72d3c312)
+- Remove ERD generator function and its import from ToolsHelper [`429d4b1`](https://github.com/Power-Maverick/DataverseDevTools-VSCode/commit/429d4b1500ca682325a455a64edb47ac40a2fa57)
+- Docs: Add @Mosh-K as a contributor [`b2922cf`](https://github.com/Power-Maverick/DataverseDevTools-VSCode/commit/b2922cf057e5e8a2a4ab75de46e4727e33cef5e6)
+
+## [v2.2.6](https://github.com/Power-Maverick/DataverseDevTools-VSCode/compare/v2.2.5...v2.2.6) - 2025-10-10
+
+### Merged
+
+- Merge pull request #293 for release [`#294`](https://github.com/Power-Maverick/DataverseDevTools-VSCode/pull/294)
- docs: Update README for v2.2.5 release with new features and bug fixes [`#293`](https://github.com/Power-Maverick/DataverseDevTools-VSCode/pull/293)
- Fix: Context menu options not appearing after linking/initialization until VS Code restart [`#289`](https://github.com/Power-Maverick/DataverseDevTools-VSCode/pull/289)
- Fix: Update command registration and improve status bar text for conn⦠[`#291`](https://github.com/Power-Maverick/DataverseDevTools-VSCode/pull/291)
diff --git a/README.md b/README.md
index ba5978b..43106fc 100644
--- a/README.md
+++ b/README.md
@@ -20,17 +20,11 @@
-
-
-
-
-
-
-
-
+
+
@@ -66,6 +60,7 @@
- [Generate Typings](#generate-typings)
- [Intellisense for type generated](#intellisense-for-type-generated)
- [Upload Web Resources](#upload-web-resources)
+ - [Upload Plugins (pac plugin push)](#upload-plugins-pac-plugin-push)
- [Filter by solution](#filter-by-solution)
- [Entities](#entities)
- [Web Resources](#web-resources)
@@ -161,6 +156,24 @@ Once you build your project you can upload your script directly from the VS Code

+### Upload Plugins (pac plugin push)
+
+You can push Dataverse plugins (assembly / NuGet package) directly from VS Code using the Power Platform CLI.
+
+**How it works**
+
+1. In VS Code Explorer, right-click your plugin project `.csproj` file.
+2. Select **Dataverse DevTools: Link to Existing Dataverse Plugin** and pick the target plugin from your connected environment.
+ - This creates/updates a `dvdt.linker.xml` file in your workspace to remember the link (plugin id, type and environment).
+3. Right-click the same `.csproj` file and select **Dataverse DevTools: Push Plugin to Dataverse**.
+ - Dataverse DevTools runs `pac plugin push --pluginId --type --environment ` in the project folder.
+
+**Prerequisites**
+
+- Power Platform CLI (`pac`) must be installed.
+- `pac` must be authenticated to the target environment.
+ - If no auth profile is found, the extension will prompt you to run `pac auth create --environment ""`.
+
### Filter by solution
#### Entities
@@ -311,6 +324,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
 Lars Hildebrandt π» π€ π π§ β οΈ |
 Rob Montague π π |
 Kinga π π» π§ β οΈ π |
+  Mosh-K π» π¨ π |
diff --git a/package-lock.json b/package-lock.json
index 365b495..71b63e2 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "dataverse-devtools",
- "version": "2.2.5",
+ "version": "2.2.6",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "dataverse-devtools",
- "version": "2.2.5",
+ "version": "2.2.6",
"dependencies": {
"@azure/identity": "^3.0.0",
"@azure/identity-vscode": "^1.0.0",
diff --git a/package.json b/package.json
index 4d2c219..4d1e1e5 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "dataverse-devtools",
"displayName": "Dataverse DevTools",
- "version": "2.2.6",
+ "version": "2.2.7",
"description": "The all-in-one tool to develop code for Dataverse/Dynamics 365. Helps you connect to a Dataverse environment, generate TypeScript definitions for entities, create different type of Dataverse-specific projects and much more.",
"publisher": "danish-naglekar",
"author": {
@@ -50,6 +50,7 @@
"Power Platform",
"Dynamics 365",
"Xrm",
+ "DVDT",
"Dev Tools"
],
"qna": "https://github.com/Power-Maverick/DataverseDevTools-VSCode/discussions",
@@ -119,6 +120,16 @@
"command": "dvdt.explorer.webresources.compareWebResource",
"when": "resourceExtname in devtools.resourcesExtn && resourceFilename in devtools.linkedResources",
"group": "%dvdt.menus.group.wr%@3"
+ },
+ {
+ "command": "dvdt.explorer.plugins.linkPlugin",
+ "when": "resourceExtname == .csproj",
+ "group": "%dvdt.menus.group.plugin%@1"
+ },
+ {
+ "command": "dvdt.explorer.plugins.pushPlugin",
+ "when": "resourceExtname == .csproj",
+ "group": "%dvdt.menus.group.plugin%@2"
}
],
"view/title": [
@@ -522,6 +533,18 @@
"light": "resources/light/play.svg",
"dark": "resources/dark/play.svg"
}
+ },
+ {
+ "command": "dvdt.explorer.plugins.linkPlugin",
+ "title": "Link to Existing Dataverse Plugin",
+ "when": "false",
+ "category": "%dvdt.explorer.category%"
+ },
+ {
+ "command": "dvdt.explorer.plugins.pushPlugin",
+ "title": "Push Plugin to Dataverse",
+ "when": "false",
+ "category": "%dvdt.explorer.category%"
}
],
"configuration": {
diff --git a/package.nls.json b/package.nls.json
index 1a6f031..4303044 100644
--- a/package.nls.json
+++ b/package.nls.json
@@ -3,6 +3,7 @@
"dvdt.explorer.category": "Explorer",
"dvdt.menus.group.ts": "dvdevtools-ts",
"dvdt.menus.group.wr": "dvdevtools-wr",
+ "dvdt.menus.group.plugin": "dvdevtools-plugin",
"dvdt.config.title": "Dataverse DevTools",
"dvdt.config.enablePreview": "Enable a preview features for Dataverse DevTools. See [documentation for all available preview features](https://github.com/Power-Maverick/DataverseDevTools-VSCode#-early-access-preview).\n\n> _Note: these features are not fully tested; so if you encounter any bugs please report them on GitHub (your name will appear on the contributors list)._",
"dvdt.config.defaultTSTemplate": "Specifies how Dataverse DevTools should behave when you initiate a TypeScript project. This will skip the extra question during TS project initialization when you either select `Plain TypeScript` or `Webpack`"
diff --git a/resources/templates/dvdt.linker.xml b/resources/templates/dvdt.linker.xml
index 584213a..c9d5ed0 100644
--- a/resources/templates/dvdt.linker.xml
+++ b/resources/templates/dvdt.linker.xml
@@ -2,6 +2,8 @@
+
+
\ No newline at end of file
diff --git a/resources/toolIcons/cmt.png b/resources/toolIcons/dark/cmt.png
similarity index 100%
rename from resources/toolIcons/cmt.png
rename to resources/toolIcons/dark/cmt.png
diff --git a/resources/toolIcons/drb.png b/resources/toolIcons/dark/drb.png
similarity index 100%
rename from resources/toolIcons/drb.png
rename to resources/toolIcons/dark/drb.png
diff --git a/resources/toolIcons/dark/erd.svg b/resources/toolIcons/dark/erd.svg
new file mode 100644
index 0000000..93f20a8
--- /dev/null
+++ b/resources/toolIcons/dark/erd.svg
@@ -0,0 +1,23 @@
+
diff --git a/resources/toolIcons/dark/generic.svg b/resources/toolIcons/dark/generic.svg
new file mode 100644
index 0000000..ae8f055
--- /dev/null
+++ b/resources/toolIcons/dark/generic.svg
@@ -0,0 +1,8 @@
+
\ No newline at end of file
diff --git a/resources/toolIcons/pd.png b/resources/toolIcons/dark/pd.png
similarity index 100%
rename from resources/toolIcons/pd.png
rename to resources/toolIcons/dark/pd.png
diff --git a/resources/toolIcons/prt.png b/resources/toolIcons/dark/prt.png
similarity index 100%
rename from resources/toolIcons/prt.png
rename to resources/toolIcons/dark/prt.png
diff --git a/resources/toolIcons/light/cmt.png b/resources/toolIcons/light/cmt.png
new file mode 100644
index 0000000..a880dcf
Binary files /dev/null and b/resources/toolIcons/light/cmt.png differ
diff --git a/resources/toolIcons/light/drb.png b/resources/toolIcons/light/drb.png
new file mode 100644
index 0000000..969330a
Binary files /dev/null and b/resources/toolIcons/light/drb.png differ
diff --git a/resources/toolIcons/light/erd.svg b/resources/toolIcons/light/erd.svg
new file mode 100644
index 0000000..9c4044b
--- /dev/null
+++ b/resources/toolIcons/light/erd.svg
@@ -0,0 +1,23 @@
+
diff --git a/resources/toolIcons/light/generic.svg b/resources/toolIcons/light/generic.svg
new file mode 100644
index 0000000..02919c7
--- /dev/null
+++ b/resources/toolIcons/light/generic.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/resources/toolIcons/light/pd.png b/resources/toolIcons/light/pd.png
new file mode 100644
index 0000000..d6fc124
Binary files /dev/null and b/resources/toolIcons/light/pd.png differ
diff --git a/resources/toolIcons/light/prt.png b/resources/toolIcons/light/prt.png
new file mode 100644
index 0000000..d6fc124
Binary files /dev/null and b/resources/toolIcons/light/prt.png differ
diff --git a/src/commands/registerCommands.ts b/src/commands/registerCommands.ts
index b6a1db3..df10a9c 100644
--- a/src/commands/registerCommands.ts
+++ b/src/commands/registerCommands.ts
@@ -3,9 +3,10 @@ import * as vscode from "vscode";
import { CliCommandTreeItem } from "../cliCommands/cliCommandsDataProvider";
import { CLIHelper } from "../helpers/cliHelper";
import { DataverseHelper } from "../helpers/dataverseHelper";
-import { DRBHelper } from "../helpers/drbHelper";
import { ErrorHandler } from "../helpers/errorHandler";
+import { PluginHelper } from "../helpers/pluginHelper";
import { TemplateHelper } from "../helpers/templateHelper";
+import { ToolsHelper } from "../helpers/toolsHelper";
import { TypingsHelper } from "../helpers/typingsHelper";
import { WebResourceHelper } from "../helpers/webResourceHelper";
import { ToolsTreeItem } from "../tools/toolsDataProvider";
@@ -29,9 +30,10 @@ export async function registerCommands(vscontext: vscode.ExtensionContext, tr: T
const cliHelper = new CLIHelper(vscontext);
const templateHelper = new TemplateHelper(vscontext);
const wrHelper = new WebResourceHelper(vscontext, dvHelper);
+ const pluginHelper = new PluginHelper(vscontext);
const typingHelper = new TypingsHelper(vscontext, dvHelper);
- const drbHelper = new DRBHelper(vscontext);
const errorHandler = new ErrorHandler(tr);
+ const toolsHelper = new ToolsHelper(vscontext);
dvStatusBarItem = vscode.window.createStatusBarItem(connectionStatusBarUniqueId, vscode.StatusBarAlignment.Left);
vscontext.subscriptions.push(dvStatusBarItem);
@@ -203,46 +205,6 @@ export async function registerCommands(vscontext: vscode.ExtensionContext, tr: T
}
},
},
- {
- command: "dvdt.commands.openDRB",
- callback: async () => {
- try {
- drbHelper.openDRB(views);
- } catch (error) {
- errorHandler.log(error, "openDRB");
- }
- },
- },
- {
- command: "dvdt.commands.launchPRT",
- callback: async () => {
- try {
- cliHelper.launchPRT();
- } catch (error) {
- errorHandler.log(error, "launchPRT");
- }
- },
- },
- {
- command: "dvdt.commands.launchCMT",
- callback: async () => {
- try {
- cliHelper.launchCMT();
- } catch (error) {
- errorHandler.log(error, "launchCMT");
- }
- },
- },
- {
- command: "dvdt.commands.launchPD",
- callback: async () => {
- try {
- cliHelper.launchPD();
- } catch (error) {
- errorHandler.log(error, "launchPD");
- }
- },
- },
{
command: "dvdt.explorer.webresources.linkExistingWebResource",
callback: async (uri: vscode.Uri) => {
@@ -257,22 +219,7 @@ export async function registerCommands(vscontext: vscode.ExtensionContext, tr: T
command: "dvdt.explorer.tools.launchTool",
callback: async (toolItem: ToolsTreeItem) => {
try {
- switch (toolItem.toolShortName) {
- case "drb":
- drbHelper.openDRB(views);
- break;
- case "prt":
- cliHelper.launchPRT();
- break;
- case "cmt":
- cliHelper.launchCMT();
- break;
- case "pd":
- cliHelper.launchPD();
- break;
- default:
- break;
- }
+ toolsHelper.openTool(toolItem);
} catch (error) {
errorHandler.log(error, "launchTool");
}
@@ -308,6 +255,26 @@ export async function registerCommands(vscontext: vscode.ExtensionContext, tr: T
}
},
},
+ {
+ command: "dvdt.explorer.plugins.linkPlugin",
+ callback: async (uri: vscode.Uri) => {
+ try {
+ await pluginHelper.linkPlugin(uri.fsPath);
+ } catch (error) {
+ errorHandler.log(error, "linkPlugin");
+ }
+ },
+ },
+ {
+ command: "dvdt.explorer.plugins.pushPlugin",
+ callback: async (uri: vscode.Uri) => {
+ try {
+ await pluginHelper.pushPlugin(uri.fsPath);
+ } catch (error) {
+ errorHandler.log(error, "pushPlugin");
+ }
+ },
+ },
);
cmds.forEach((c) => {
vscontext.subscriptions.push(vscode.commands.registerCommand(c.command, c.callback));
@@ -315,6 +282,7 @@ export async function registerCommands(vscontext: vscode.ExtensionContext, tr: T
updateConnectionStatusBar(await dvHelper.reloadWorkspaceConnection());
vscode.commands.executeCommand("setContext", `${extensionPrefix}.linkedResources`, await wrHelper.getLinkedResourceStrings("@_localFileName"));
+ vscode.commands.executeCommand("setContext", `${extensionPrefix}.linkedPlugins`, await pluginHelper.getLinkedProjectPaths());
}
/**
diff --git a/src/commands/registerToolsCommands.ts b/src/commands/registerToolsCommands.ts
new file mode 100644
index 0000000..30b425a
--- /dev/null
+++ b/src/commands/registerToolsCommands.ts
@@ -0,0 +1,71 @@
+import TelemetryReporter from "@vscode/extension-telemetry";
+import * as vscode from "vscode";
+import { CLIHelper } from "../helpers/cliHelper";
+import { ErrorHandler } from "../helpers/errorHandler";
+import { ToolsHelper } from "../helpers/toolsHelper";
+import { ICommand } from "../utils/Interfaces";
+import { ViewBase } from "../views/ViewBase";
+
+export async function registerToolsCommands(vscontext: vscode.ExtensionContext, tr: TelemetryReporter): Promise {
+ const toolHelper = new ToolsHelper(vscontext);
+ const views = new ViewBase(vscontext);
+ const errorHandler = new ErrorHandler(tr);
+ const cliHelper = new CLIHelper(vscontext);
+
+ const cmds: Array = new Array(
+ {
+ command: "dvdt.commands.openDRB",
+ callback: async () => {
+ try {
+ toolHelper.openDRB(views);
+ } catch (error) {
+ errorHandler.log(error, "openDRB");
+ }
+ },
+ },
+ // {
+ // command: "dvdt.commands.openERDGenerator",
+ // callback: async () => {
+ // try {
+ // toolHelper.openERDGenerator();
+ // } catch (error) {
+ // errorHandler.log(error, "openERDGenerator");
+ // }
+ // },
+ // },
+ {
+ command: "dvdt.commands.launchPRT",
+ callback: async () => {
+ try {
+ cliHelper.launchPRT();
+ } catch (error) {
+ errorHandler.log(error, "launchPRT");
+ }
+ },
+ },
+ {
+ command: "dvdt.commands.launchCMT",
+ callback: async () => {
+ try {
+ cliHelper.launchCMT();
+ } catch (error) {
+ errorHandler.log(error, "launchCMT");
+ }
+ },
+ },
+ {
+ command: "dvdt.commands.launchPD",
+ callback: async () => {
+ try {
+ cliHelper.launchPD();
+ } catch (error) {
+ errorHandler.log(error, "launchPD");
+ }
+ },
+ },
+ );
+
+ cmds.forEach((c) => {
+ vscontext.subscriptions.push(vscode.commands.registerCommand(c.command, c.callback));
+ });
+}
diff --git a/src/extension.ts b/src/extension.ts
index 7ae5d89..85e7e34 100644
--- a/src/extension.ts
+++ b/src/extension.ts
@@ -3,6 +3,7 @@
import TelemetryReporter from "@vscode/extension-telemetry";
import * as vscode from "vscode";
import { registerCommands } from "./commands/registerCommands";
+import { registerToolsCommands } from "./commands/registerToolsCommands";
import { registerTreeDataProviders } from "./commands/registerTreeDataProviders";
import { DataverseHelper } from "./helpers/dataverseHelper";
import * as config from "./utils/Config";
@@ -32,6 +33,9 @@ export function activate(context: vscode.ExtensionContext) {
registerTreeDataProviders(context, reporter);
registerCommands(context, reporter);
+ registerToolsCommands(context, reporter);
+
+ console.log(`Extension ${extensionId} v${extensionVersion} is now active!`);
// Start periodic token expiration check
startTokenExpirationCheck(context);
@@ -59,20 +63,17 @@ export function deactivate() {
*/
function startTokenExpirationCheck(context: vscode.ExtensionContext) {
let lastNotifiedExpiration = false;
-
+
tokenExpirationTimer = setInterval(() => {
const dvHelper = new DataverseHelper(context);
const isExpired = dvHelper.isCurrentConnectionTokenExpired();
-
+
if (isExpired && !lastNotifiedExpiration) {
// Get current connection to create tree item for reconnect
const conn = dvHelper.getCurrentWorkspaceConnection();
-
+
// Show notification to user
- vscode.window.showWarningMessage(
- "Your Dataverse connection token has expired. Please reconnect to continue working.",
- "Reconnect"
- ).then(selection => {
+ vscode.window.showWarningMessage("Your Dataverse connection token has expired. Please reconnect to continue working.", "Reconnect").then((selection) => {
if (selection === "Reconnect" && conn) {
// Create a tree item with the connection name to pass to the command
const connItem = {
@@ -81,17 +82,17 @@ function startTokenExpirationCheck(context: vscode.ExtensionContext) {
collapsibleState: vscode.TreeItemCollapsibleState.Collapsed,
level: 2,
current: true,
- expired: true
+ expired: true,
};
vscode.commands.executeCommand("dvdt.explorer.connections.connectDataverse", connItem);
}
});
-
+
lastNotifiedExpiration = true;
-
+
// Refresh the connection tree to show expired icon
vscode.commands.executeCommand("dvdt.explorer.connections.refreshConnection");
-
+
// Update status bar to show expired state
if (conn) {
vscode.commands.executeCommand("dvdt.explorer.connections.updateStatusBar", conn);
diff --git a/src/helpers/dataverseHelper.ts b/src/helpers/dataverseHelper.ts
index ec96e09..1c4733a 100644
--- a/src/helpers/dataverseHelper.ts
+++ b/src/helpers/dataverseHelper.ts
@@ -10,6 +10,8 @@ import {
customDataverseClientId,
entityDefinitionsStoreKey,
environmentTypes,
+ pluginAssembliesStoreKey,
+ pluginPackagesStoreKey,
reservedWords,
solDefinitionsStoreKey,
wrDefinitionsStoreKey,
@@ -24,6 +26,8 @@ import {
IEntityMetadata,
IOptionSet,
IOptionSetMetadata,
+ IPluginAssemblies,
+ IPluginPackages,
ISolutionComponents,
ISolutions,
IWebResource,
@@ -145,8 +149,8 @@ export class DataverseHelper {
this.vsstate.saveInWorkspace(connectionCurrentStoreKey, conn);
progress.report({ increment: 30, message: "Getting entity metadata..." });
await this.getEntityDefinitions();
- progress.report({ increment: 70, message: "Getting web resources..." });
- await this.getWebResources();
+ progress.report({ increment: 70, message: "Getting web resources & plugins..." });
+ await Promise.all([this.getWebResources(), this.getPluginAssemblies(), this.getPluginPackages()]);
vscode.commands.executeCommand("dvdt.explorer.connections.refreshConnection");
return new Promise((resolve) => {
@@ -178,7 +182,7 @@ export class DataverseHelper {
const connFromWS: IConnection = this.vsstate.getFromWorkspace(connectionCurrentStoreKey);
if (connFromWS) {
await this.getEntityDefinitions();
- await this.getWebResources();
+ await Promise.all([this.getWebResources(), this.getPluginAssemblies(), this.getPluginPackages()]);
return connFromWS;
}
return undefined;
@@ -341,6 +345,24 @@ export class DataverseHelper {
vscode.commands.executeCommand("dvdt.explorer.webresources.loadWebResources");
}
+ /**
+ * Get plugin assemblies (unmanaged) from the current connection.
+ */
+ public async getPluginAssemblies(): Promise {
+ const respData = await this.request.requestData("pluginassemblies?$select=pluginassemblyid,name&$filter=ismanaged%20eq%20false%20and%20_packageid_value%20eq%20null");
+ this.vsstate.saveInWorkspace(pluginAssembliesStoreKey, respData);
+ return respData;
+ }
+
+ /**
+ * Get plugin packages (unmanaged) from the current connection.
+ */
+ public async getPluginPackages(): Promise {
+ const respData = await this.request.requestData("pluginpackages?$select=pluginpackageid,name&$filter=ismanaged%20eq%20false");
+ this.vsstate.saveInWorkspace(pluginPackagesStoreKey, respData);
+ return respData;
+ }
+
/**
* Gets the web resource content
* @param {string} wrId - The ID of the web resource to retrieve.
diff --git a/src/helpers/drbHelper.ts b/src/helpers/drbHelper.ts
deleted file mode 100644
index 86e2996..0000000
--- a/src/helpers/drbHelper.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-import * as vscode from "vscode";
-import { connectionCurrentStoreKey } from "../utils/Constants";
-import { ErrorMessages } from "../utils/ErrorMessages";
-import { IConnection } from "../utils/Interfaces";
-import { State } from "../utils/State";
-import { DataverseRestBuilderView } from "../views/DataverseRestBuilderView";
-import { ViewBase } from "../views/ViewBase";
-
-export class DRBHelper {
- private vsstate: State;
-
- /**
- * Initialization constructor for VS Code Context
- */
- constructor(private vscontext: vscode.ExtensionContext) {
- this.vsstate = new State(vscontext);
- }
-
- public async openDRB(view: ViewBase): Promise {
- const connFromWS: IConnection = this.vsstate.getFromWorkspace(connectionCurrentStoreKey);
- if (connFromWS && connFromWS.currentAccessToken) {
- const webview = await view.getWebView({ type: "openDRB", title: "Dataverse REST Builder" });
- new DataverseRestBuilderView(webview, this.vscontext, connFromWS.currentAccessToken);
- } else {
- vscode.window.showErrorMessage(ErrorMessages.drbConnectionError);
- }
- }
-}
diff --git a/src/helpers/pluginHelper.ts b/src/helpers/pluginHelper.ts
new file mode 100644
index 0000000..d0b5743
--- /dev/null
+++ b/src/helpers/pluginHelper.ts
@@ -0,0 +1,310 @@
+/* eslint-disable @typescript-eslint/naming-convention */
+import { spawn } from "child_process";
+import * as path from "path";
+import * as vscode from "vscode";
+import { connectionCurrentStoreKey, extensionPrefix, pluginAssembliesStoreKey, pluginPackagesStoreKey, PluginType } from "../utils/Constants";
+import { ErrorMessages } from "../utils/ErrorMessages";
+import { copyFolderOrFile, getRelativeFilePath, getWorkspaceFolder, readFileSync, writeFileSync } from "../utils/FileSystem";
+import { IConnection, ILinkerFile, ILinkerPlugin, IPluginAssemblies, IPluginPackages } from "../utils/Interfaces";
+import { jsonToXML, xmlToJSON } from "../utils/Parsers";
+import { Placeholders } from "../utils/Placeholders";
+import { State } from "../utils/State";
+
+interface IPluginQuickPickItem extends vscode.QuickPickItem {
+ id: string;
+ name: string;
+ type: PluginType;
+}
+
+interface ILookupResult {
+ entry: ILinkerPlugin;
+ index: number;
+}
+
+export class PluginHelper {
+ private vsstate: State;
+ private static outputChannel: vscode.OutputChannel | undefined;
+
+ constructor(private vscontext: vscode.ExtensionContext) {
+ this.vsstate = new State(vscontext);
+ }
+
+ //#region Public
+
+ public async linkPlugin(csprojFullPath: string): Promise {
+ const assemblies: IPluginAssemblies = this.vsstate.getFromWorkspace(pluginAssembliesStoreKey);
+ const packages: IPluginPackages = this.vsstate.getFromWorkspace(pluginPackagesStoreKey);
+ const conn: IConnection = this.vsstate.getFromWorkspace(connectionCurrentStoreKey);
+
+ if (!conn) {
+ vscode.window.showErrorMessage("Connect to a Dataverse environment before linking a plugin.");
+ return undefined;
+ }
+
+ const items: IPluginQuickPickItem[] = [];
+ if (assemblies && assemblies.value) {
+ assemblies.value.forEach((a) => items.push({ label: a.name, description: "Assembly", id: a.pluginassemblyid, name: a.name, type: PluginType.assembly }));
+ }
+ if (packages && packages.value) {
+ packages.value.forEach((p) => items.push({ label: p.name, description: "Package", id: p.pluginpackageid, name: p.name, type: PluginType.nuget }));
+ }
+
+ if (items.length === 0) {
+ vscode.window.showErrorMessage(ErrorMessages.pluginListUnavailable);
+ return undefined;
+ }
+
+ const qpOptions = Placeholders.getQuickPickOptions(Placeholders.pluginSelection);
+ const picked = await vscode.window.showQuickPick(items, qpOptions);
+ if (!picked) {
+ return undefined;
+ }
+
+ const localRelativePath = getRelativeFilePath(csprojFullPath, getWorkspaceFolder()?.fsPath!);
+ const existing = await this.lookupLinkedPlugin(csprojFullPath);
+
+ const newEntry: ILinkerPlugin = {
+ "@_Id": picked.id,
+ "@_dvName": picked.name,
+ "@_type": picked.type,
+ "@_localProjectPath": localRelativePath,
+ "@_environment": conn.environmentUrl.replace(/\/+$/, ""),
+ };
+
+ if (existing) {
+ const confirm = await vscode.window.showWarningMessage("This project is already linked. Re-link?", { detail: "Confirm your selection", modal: true }, "Yes", "No");
+ if (confirm !== "Yes") {
+ return undefined;
+ }
+ return await this.updateInLinkerFile(existing.index, newEntry);
+ }
+
+ return await this.addInLinkerFile(newEntry);
+ }
+
+ public async pushPlugin(csprojFullPath: string): Promise {
+ let linked = await this.lookupLinkedPlugin(csprojFullPath);
+ if (!linked) {
+ const linkOptions = ["Link to existing plugin & push"];
+ const qpOptions = Placeholders.getQuickPickOptions(Placeholders.pluginLinkSelection);
+ const choice = await vscode.window.showQuickPick(linkOptions, qpOptions);
+ if (!choice) {
+ return;
+ }
+ const newEntry = await this.linkPlugin(csprojFullPath);
+ if (!newEntry) {
+ return;
+ }
+ linked = { entry: newEntry, index: 0 };
+ }
+
+ const projectDir = path.dirname(csprojFullPath);
+ const projectName = path.basename(csprojFullPath);
+ const entry = linked.entry;
+
+ const authed = await this.ensurePacAuth(entry["@_environment"]);
+ if (!authed) {
+ return;
+ }
+
+ const args = ["plugin", "push", "--pluginId", entry["@_Id"], "--type", entry["@_type"], "--environment", entry["@_environment"]];
+
+ const channel = PluginHelper.getOutputChannel();
+ channel.clear();
+ channel.appendLine(`> pac ${args.join(" ")}`);
+ channel.appendLine(`> cwd: ${projectDir}`);
+ channel.appendLine("");
+
+ await vscode.window.withProgress(
+ {
+ location: vscode.ProgressLocation.Notification,
+ title: `Pushing ${projectName}`,
+ cancellable: false,
+ },
+ async (progress) => {
+ progress.report({ message: "Running pac plugin push..." });
+
+ const exitCode = await new Promise((resolve) => {
+ const child = spawn("pac", args, { cwd: projectDir, shell: true });
+
+ child.stdout.on("data", (data: Buffer) => channel.append(data.toString()));
+ child.stderr.on("data", (data: Buffer) => channel.append(data.toString()));
+ child.on("error", (err) => {
+ channel.appendLine(`\nFailed to start pac: ${err.message}`);
+ resolve(-1);
+ });
+ child.on("close", (code) => resolve(code ?? -1));
+ });
+
+ if (exitCode === 0) {
+ vscode.window.showInformationMessage(`${projectName} pushed.`);
+ } else {
+ channel.show(true);
+ vscode.window.showErrorMessage(ErrorMessages.pluginPushFailed);
+ }
+ },
+ );
+ }
+
+ public async getLinkedProjectPaths(): Promise {
+ const linkerFile = await this.getLinkerFile();
+ if (!linkerFile) {
+ return undefined;
+ }
+ const data = readFileSync(linkerFile.fsPath).toString();
+ const json = xmlToJSON(data);
+ if (!json.DVDT.Plugins || !json.DVDT.Plugins.Plugin) {
+ return [];
+ }
+ if (Array.isArray(json.DVDT.Plugins.Plugin)) {
+ return json.DVDT.Plugins.Plugin.map((p) => p["@_localProjectPath"]);
+ }
+ return [json.DVDT.Plugins.Plugin["@_localProjectPath"]];
+ }
+
+ //#endregion Public
+
+ //#region Private
+
+ private async ensurePacAuth(environment: string): Promise {
+ if (!(await this.isPacInstalled())) {
+ const choice = await vscode.window.showErrorMessage("Power Platform CLI (pac) was not found. Install it and try again.", "Open install docs");
+ if (choice === "Open install docs") {
+ vscode.env.openExternal(vscode.Uri.parse("https://aka.ms/PowerPlatformCLI"));
+ }
+ return false;
+ }
+
+ if (await this.hasPacProfileForEnv(environment)) {
+ return true;
+ }
+
+ const choice = await vscode.window.showWarningMessage(`pac is not authenticated to ${environment}. Authenticate first, then try Push again.`, "Authenticate");
+ if (choice === "Authenticate") {
+ const terminal = vscode.window.createTerminal("DVDT - pac auth");
+ terminal.show();
+ terminal.sendText(`pac auth create --environment "${environment}"`, false);
+ }
+ return false;
+ }
+
+ private async isPacInstalled(): Promise {
+ return new Promise((resolve) => {
+ const child = spawn("pac", [], { shell: true });
+ child.on("close", (code) => resolve(code === 0));
+ child.on("error", () => resolve(false));
+ });
+ }
+
+ private async hasPacProfileForEnv(environment: string): Promise {
+ const output = await new Promise((resolve) => {
+ let buf = "";
+ const child = spawn("pac", ["auth", "list"], { shell: true });
+ child.stdout.on("data", (d: Buffer) => (buf += d.toString()));
+ child.stderr.on("data", (d: Buffer) => (buf += d.toString()));
+ child.on("close", () => resolve(buf));
+ child.on("error", () => resolve(""));
+ });
+ const normalize = (u: string) => u.replace(/\/+$/, "").toLowerCase();
+ return normalize(output).includes(normalize(environment));
+ }
+
+ private async lookupLinkedPlugin(csprojFullPath: string): Promise {
+ const linkerFile = await this.getLinkerFile();
+ if (!linkerFile) {
+ return undefined;
+ }
+ const localRelativePath = getRelativeFilePath(csprojFullPath, getWorkspaceFolder()?.fsPath!);
+ const data = readFileSync(linkerFile.fsPath).toString();
+ const json = xmlToJSON(data);
+
+ if (!json.DVDT.Plugins || !json.DVDT.Plugins.Plugin) {
+ return undefined;
+ }
+
+ if (Array.isArray(json.DVDT.Plugins.Plugin)) {
+ const idx = json.DVDT.Plugins.Plugin.findIndex((p) => p["@_localProjectPath"] === localRelativePath);
+ return idx >= 0 ? { entry: json.DVDT.Plugins.Plugin[idx], index: idx } : undefined;
+ } else {
+ return json.DVDT.Plugins.Plugin["@_localProjectPath"] === localRelativePath ? { entry: json.DVDT.Plugins.Plugin, index: 0 } : undefined;
+ }
+ }
+
+ private async addInLinkerFile(entry: ILinkerPlugin): Promise {
+ const linkerFile = await this.createLinkerFile();
+ if (!linkerFile) {
+ return undefined;
+ }
+ const data = readFileSync(linkerFile.fsPath).toString();
+ const json = xmlToJSON(data);
+
+ if (!json.DVDT.Plugins) {
+ json.DVDT.Plugins = { Plugin: [] };
+ }
+ if (Array.isArray(json.DVDT.Plugins.Plugin)) {
+ json.DVDT.Plugins.Plugin.push(entry);
+ } else {
+ const existing = json.DVDT.Plugins.Plugin;
+ json.DVDT.Plugins.Plugin = [existing, entry];
+ }
+ writeFileSync(linkerFile.fsPath, jsonToXML(json));
+ await this.updateLinkedPluginsContext();
+ return entry;
+ }
+
+ private async updateInLinkerFile(oldIndex: number, newEntry: ILinkerPlugin): Promise {
+ const linkerFile = await this.getLinkerFile();
+ if (!linkerFile) {
+ return undefined;
+ }
+ const data = readFileSync(linkerFile.fsPath).toString();
+ const json = xmlToJSON(data);
+
+ if (!json.DVDT.Plugins) {
+ return undefined;
+ }
+ if (Array.isArray(json.DVDT.Plugins.Plugin)) {
+ json.DVDT.Plugins.Plugin[oldIndex] = newEntry;
+ } else {
+ json.DVDT.Plugins.Plugin = newEntry;
+ }
+ writeFileSync(linkerFile.fsPath, jsonToXML(json));
+ await this.updateLinkedPluginsContext();
+ return newEntry;
+ }
+
+ private async getLinkerFile(): Promise {
+ const files = await vscode.workspace.findFiles("**/dvdt.linker.xml", "**/node_modules/**");
+ return files.length > 0 ? files[0] : undefined;
+ }
+
+ private async createLinkerFile(): Promise {
+ if (!vscode.workspace.workspaceFolders) {
+ return undefined;
+ }
+ const existing = await this.getLinkerFile();
+ if (existing) {
+ return existing;
+ }
+ const workspaceFolder = vscode.workspace.workspaceFolders[0].uri;
+ const extPath = this.vscontext.extensionUri.fsPath;
+ const linkerTemplate = path.join(extPath, "resources", "templates", "dvdt.linker.xml");
+ const newLinkerFileUri = vscode.Uri.joinPath(workspaceFolder, "dvdt.linker.xml");
+ await copyFolderOrFile(linkerTemplate, newLinkerFileUri.fsPath);
+ return newLinkerFileUri;
+ }
+
+ private async updateLinkedPluginsContext(): Promise {
+ const paths = await this.getLinkedProjectPaths();
+ await vscode.commands.executeCommand("setContext", `${extensionPrefix}.linkedPlugins`, paths);
+ }
+
+ private static getOutputChannel(): vscode.OutputChannel {
+ if (!PluginHelper.outputChannel) {
+ PluginHelper.outputChannel = vscode.window.createOutputChannel("Dataverse DevTools - Plugin Push");
+ }
+ return PluginHelper.outputChannel;
+ }
+
+ //#endregion Private
+}
diff --git a/src/helpers/toolsHelper.ts b/src/helpers/toolsHelper.ts
new file mode 100644
index 0000000..2853281
--- /dev/null
+++ b/src/helpers/toolsHelper.ts
@@ -0,0 +1,64 @@
+import * as vscode from "vscode";
+import { ToolsTreeItem } from "../tools/toolsDataProvider";
+import { connectionCurrentStoreKey } from "../utils/Constants";
+import { ErrorMessages } from "../utils/ErrorMessages";
+import { IConnection } from "../utils/Interfaces";
+import { State } from "../utils/State";
+import { DataverseRestBuilderView } from "../views/DataverseRestBuilderView";
+import { ViewBase } from "../views/ViewBase";
+import { CLIHelper } from "./cliHelper";
+
+export class ToolsHelper {
+ private vsstate: State;
+
+ /**
+ * Initialization constructor for VS Code Context
+ */
+ constructor(private vscontext: vscode.ExtensionContext) {
+ this.vsstate = new State(vscontext);
+ }
+
+ public openTool(toolItem: ToolsTreeItem) {
+ const views = new ViewBase(this.vscontext);
+ const cliHelper = new CLIHelper(this.vscontext);
+
+ switch (toolItem.toolShortName) {
+ case "drb":
+ this.openDRB(views);
+ break;
+ // case "erd":
+ // this.openERDGenerator();
+ // break;
+ case "prt":
+ cliHelper.launchPRT();
+ break;
+ case "cmt":
+ cliHelper.launchCMT();
+ break;
+ case "pd":
+ cliHelper.launchPD();
+ break;
+ default:
+ break;
+ }
+ }
+
+ public async openDRB(view: ViewBase): Promise {
+ const connFromWS: IConnection = this.vsstate.getFromWorkspace(connectionCurrentStoreKey);
+ if (connFromWS && connFromWS.currentAccessToken) {
+ const webview = await view.getWebView({ type: "openDRB", title: "Dataverse REST Builder" });
+ new DataverseRestBuilderView(webview, this.vscontext, connFromWS.currentAccessToken);
+ } else {
+ vscode.window.showErrorMessage(ErrorMessages.drbConnectionError);
+ }
+ }
+
+ // public openERDGenerator(): void {
+ // const connFromWS: IConnection = this.vsstate.getFromWorkspace(connectionCurrentStoreKey);
+ // if (connFromWS && connFromWS.currentAccessToken) {
+ // showERDPanel(this.vscontext.extensionUri, connFromWS.environmentUrl, connFromWS.currentAccessToken);
+ // } else {
+ // vscode.window.showErrorMessage(ErrorMessages.commonToolsError);
+ // }
+ // }
+}
diff --git a/src/helpers/webResourceHelper.ts b/src/helpers/webResourceHelper.ts
index 8300fcf..ec1f64e 100644
--- a/src/helpers/webResourceHelper.ts
+++ b/src/helpers/webResourceHelper.ts
@@ -4,7 +4,7 @@ import * as path from "path";
import * as vscode from "vscode";
import { extensionPrefix, WebResourceType, wrDefinitionsStoreKey } from "../utils/Constants";
import { ErrorMessages } from "../utils/ErrorMessages";
-import { decodeFromBase64, encodeToBase64, extractGuid } from "../utils/ExtensionMethods";
+import { decodeFromBase64ToUTF8, encodeToBase64, extractGuid } from "../utils/ExtensionMethods";
import { copyFolderOrFile, createTempDirectory, getFileExtension, getFileName, getRelativeFilePath, getWorkspaceFolder, readFileAsBase64Sync, readFileSync, writeFileSync } from "../utils/FileSystem";
import { ILinkerFile, ILinkerRes, ISmartMatchRecord, IWebResource, IWebResources } from "../utils/Interfaces";
import { jsonToXML, xmlToJSON } from "../utils/Parsers";
@@ -63,7 +63,7 @@ export class WebResourceHelper {
if (resourceToCompare) {
const base64Content = await this.dvHelper.getWebResourceContent(resourceToCompare["@_Id"]);
if (base64Content) {
- const parsedContent = decodeFromBase64(base64Content);
+ const parsedContent = decodeFromBase64ToUTF8(base64Content);
const tempDirUri = await createTempDirectory();
const tempFilePath = vscode.Uri.joinPath(tempDirUri, `temp-${resourceToCompare["@_localFileName"]}`);
writeFileSync(tempFilePath.fsPath, parsedContent);
diff --git a/src/tools/tools.json b/src/tools/tools.json
index 49e3778..af36cb3 100644
--- a/src/tools/tools.json
+++ b/src/tools/tools.json
@@ -1,8 +1,28 @@
{
"tools": [
- { "toolName": "Dataverse REST Builder", "toolShortName": "drb", "toolAuthor": "Guido Preite" },
- { "toolName": "Plugin Registration", "toolShortName": "prt", "toolAuthor": "Microsoft" },
- { "toolName": "Configuration Migration", "toolShortName": "cmt", "toolAuthor": "Microsoft" },
- { "toolName": "Package Deployer", "toolShortName": "pd", "toolAuthor": "Microsoft" }
+ {
+ "name": "Dataverse REST Builder",
+ "shortName": "drb",
+ "author": "Guido Preite",
+ "icon": "drb.png"
+ },
+ {
+ "name": "Plugin Registration",
+ "shortName": "prt",
+ "author": "Microsoft",
+ "icon": "prt.png"
+ },
+ {
+ "name": "Configuration Migration",
+ "shortName": "cmt",
+ "author": "Microsoft",
+ "icon": "cmt.png"
+ },
+ {
+ "name": "Package Deployer",
+ "shortName": "pd",
+ "author": "Microsoft",
+ "icon": "pd.png"
+ }
]
-}
+}
\ No newline at end of file
diff --git a/src/tools/toolsDataProvider.ts b/src/tools/toolsDataProvider.ts
index 87ecda4..ab02f63 100644
--- a/src/tools/toolsDataProvider.ts
+++ b/src/tools/toolsDataProvider.ts
@@ -16,7 +16,7 @@ export class ToolsDataProvider implements vscode.TreeDataProvider
let toolsTree: ToolsTreeItem[] = [];
toolsArray.tools.map((tool) => {
- toolsTree.push(new ToolsTreeItem(tool.toolName, tool.toolShortName, tool.toolAuthor, vscode.TreeItemCollapsibleState.None));
+ toolsTree.push(new ToolsTreeItem(tool.name, tool.shortName, tool.author, vscode.TreeItemCollapsibleState.None, tool.icon));
});
return Promise.resolve(toolsTree);
@@ -24,28 +24,18 @@ export class ToolsDataProvider implements vscode.TreeDataProvider
}
export class ToolsTreeItem extends ToolsItemBase {
- constructor(public readonly toolName: string, public readonly toolShortName: string, public readonly authorName: string, public readonly collapsibleState: vscode.TreeItemCollapsibleState) {
+ constructor(
+ public readonly toolName: string,
+ public readonly toolShortName: string,
+ public readonly authorName: string,
+ public readonly collapsibleState: vscode.TreeItemCollapsibleState,
+ public readonly toolIcon?: string,
+ ) {
super(toolName, authorName, collapsibleState);
}
iconPath = {
- light: vscode.Uri.file(
- path.join(
- __filename,
- "..",
- "resources",
- "toolIcons",
- this.toolShortName === "drb" ? "drb.png" : this.toolShortName === "prt" ? "prt.png" : this.toolShortName === "cmt" ? "cmt.png" : this.toolShortName === "pd" ? "pd.png" : "generic.svg",
- ),
- ),
- dark: vscode.Uri.file(
- path.join(
- __filename,
- "..",
- "resources",
- "toolIcons",
- this.toolShortName === "drb" ? "drb.png" : this.toolShortName === "prt" ? "prt.png" : this.toolShortName === "cmt" ? "cmt.png" : this.toolShortName === "pd" ? "pd.png" : "generic.svg",
- ),
- ),
+ light: vscode.Uri.file(path.join(__filename, "..", "resources", "toolIcons", "light", this.toolIcon ?? "generic.svg")),
+ dark: vscode.Uri.file(path.join(__filename, "..", "resources", "toolIcons", "dark", this.toolIcon ?? "generic.svg")),
};
}
diff --git a/src/utils/Constants.ts b/src/utils/Constants.ts
index 28cd194..529d1aa 100644
--- a/src/utils/Constants.ts
+++ b/src/utils/Constants.ts
@@ -15,6 +15,8 @@ export const connectionStoreKey: string = `DataverseConnections`;
export const connectionCurrentStoreKey: string = `LiveDVConnection`;
export const entityDefinitionsStoreKey: string = `CurrentEntityDefinitions`;
export const wrDefinitionsStoreKey: string = `CurrentWRs`;
+export const pluginAssembliesStoreKey: string = `CurrentPluginAssemblies`;
+export const pluginPackagesStoreKey: string = `CurrentPluginPackages`;
export const solDefinitionsStoreKey: string = `CurrentSolutions`;
export const smartMatchStoreKey: string = `CurrentSmartMatch`;
export const environmentTypes: string[] = ["Dev", "QA", "UAT", "Prod"];
@@ -52,6 +54,11 @@ export enum WebResourceType {
others = -1,
}
+export enum PluginType {
+ assembly = "Assembly",
+ nuget = "Nuget",
+}
+
export enum ConfidenceScores {
_90 = 90,
_80 = 80,
diff --git a/src/utils/ErrorMessages.ts b/src/utils/ErrorMessages.ts
index c4a0aa6..2c3f83a 100644
--- a/src/utils/ErrorMessages.ts
+++ b/src/utils/ErrorMessages.ts
@@ -15,4 +15,7 @@ export class ErrorMessages {
public static wrUploadError: string = "Failed to upload web resource. Please check your connection and try again.";
public static drbConnectionError: string = "Connect to an environment before trying to load Dataverse REST Builder.";
public static invalidLoginType: string = "Invalid Login Type.";
+ public static commonToolsError: string = "An error occurred while trying to open the tool. Please try again.";
+ public static pluginPushFailed: string = "Failed to push plugin. See the 'Dataverse DevTools - Plugin Push' output channel for details.";
+ public static pluginListUnavailable: string = "No plugin assemblies or packages found in the connected environment.";
}
diff --git a/src/utils/ExtensionMethods.ts b/src/utils/ExtensionMethods.ts
index 4047bdf..9fbf1c2 100644
--- a/src/utils/ExtensionMethods.ts
+++ b/src/utils/ExtensionMethods.ts
@@ -79,6 +79,8 @@ export const extractGuid = (str: string): string | undefined => {
}
};
+export const decodeFromBase64ToUTF8 = (str: string): string => Buffer.from(str, "base64").toString("utf8");
+
export const decodeFromBase64 = (str: string): string => Buffer.from(str, "base64").toString("binary");
export const encodeToBase64 = (str: string): string => Buffer.from(str, "binary").toString("base64");
diff --git a/src/utils/Interfaces.ts b/src/utils/Interfaces.ts
index 7197155..8eb4e11 100644
--- a/src/utils/Interfaces.ts
+++ b/src/utils/Interfaces.ts
@@ -193,6 +193,7 @@ export interface ILinkerFile {
export interface ILinkerRoot {
WebResources: ILinkerResources;
+ Plugins?: ILinkerPlugins;
Settings: any[];
}
@@ -208,6 +209,36 @@ export interface ILinkerRes {
"@_Id": string;
}
+export interface ILinkerPlugins {
+ Plugin: Array | ILinkerPlugin;
+}
+
+export interface ILinkerPlugin {
+ "@_Id": string;
+ "@_dvName": string;
+ "@_type": string;
+ "@_localProjectPath": string;
+ "@_environment": string;
+}
+
+export interface IPluginAssemblies {
+ value: IPluginAssembly[];
+}
+
+export interface IPluginAssembly {
+ pluginassemblyid: string;
+ name: string;
+}
+
+export interface IPluginPackages {
+ value: IPluginPackage[];
+}
+
+export interface IPluginPackage {
+ pluginpackageid: string;
+ name: string;
+}
+
export interface IOptionSetMetadata {
LogicalName: string;
MetadataId: string;
@@ -294,9 +325,10 @@ export interface ITools {
}
export interface IToolDetails {
- toolName: string;
- toolShortName: string;
- toolAuthor: string;
+ name: string;
+ shortName: string;
+ author: string;
+ icon?: string;
}
export interface ICliCommandList {
commands: ICliCommand[];
diff --git a/src/utils/Placeholders.ts b/src/utils/Placeholders.ts
index 236ae8f..341858c 100644
--- a/src/utils/Placeholders.ts
+++ b/src/utils/Placeholders.ts
@@ -122,6 +122,12 @@ export class Placeholders {
case this.wrSearch:
qpOptions = { placeHolder: this.ipWRSearch.placeHolderText, title: this.ipWRSearch.prompt, ignoreFocusOut: ignoreFocus };
break;
+ case this.pluginSelection:
+ qpOptions = { placeHolder: this.ipPluginSelection.placeHolderText, title: this.ipPluginSelection.prompt, ignoreFocusOut: ignoreFocus };
+ break;
+ case this.pluginLinkSelection:
+ qpOptions = { placeHolder: this.ipPluginLinkSelection.placeHolderText, title: this.ipPluginLinkSelection.prompt, ignoreFocusOut: ignoreFocus };
+ break;
default:
qpOptions = { placeHolder: "", ignoreFocusOut: ignoreFocus };
break;
@@ -151,6 +157,8 @@ export class Placeholders {
public static tsTemplateType: string = "TS-TemplateType";
public static entitiesSearch: string = "EntitiesSearch";
public static wrSearch: string = "WRSearch";
+ public static pluginSelection: string = "PluginSelection";
+ public static pluginLinkSelection: string = "PluginLinkSelection";
public static required: string = "[Required]";
public static optional: string = "[Optional]";
@@ -239,4 +247,12 @@ export class Placeholders {
placeHolderText: `Search & Select Web Resources`,
prompt: `Pick multiple web resources`,
};
+ private static ipPluginSelection: IPlaceholder = {
+ placeHolderText: `Select the plugin assembly or package`,
+ prompt: `Pick the Dataverse plugin to link with this project`,
+ };
+ private static ipPluginLinkSelection: IPlaceholder = {
+ placeHolderText: `Select your option`,
+ prompt: `This project isn't linked. Link to an existing plugin?`,
+ };
}