From de6ac4639c71b707de39387a55ff1601a03acb25 Mon Sep 17 00:00:00 2001 From: Rishabh Jain Date: Sat, 1 Mar 2025 04:01:22 +0530 Subject: [PATCH 01/15] Show mocked diff in a tree view --- package.json | 8 + src/client/extension.ts | 4 + src/common/ecs-features/ecsFeatureGates.ts | 10 + .../power-pages/metadata-diff/Constants.ts | 12 ++ .../power-pages/metadata-diff/MetadataDiff.ts | 55 ++++++ .../MetadataDiffTreeDataProvider.ts | 174 ++++++++++++++++++ .../tree-items/MetadataDiffFileItem.ts | 24 +++ .../tree-items/MetadataDiffFolderItem.ts | 14 ++ .../tree-items/MetadataDiffTreeItem.ts | 37 ++++ 9 files changed, 338 insertions(+) create mode 100644 src/common/power-pages/metadata-diff/Constants.ts create mode 100644 src/common/power-pages/metadata-diff/MetadataDiff.ts create mode 100644 src/common/power-pages/metadata-diff/MetadataDiffTreeDataProvider.ts create mode 100644 src/common/power-pages/metadata-diff/tree-items/MetadataDiffFileItem.ts create mode 100644 src/common/power-pages/metadata-diff/tree-items/MetadataDiffFolderItem.ts create mode 100644 src/common/power-pages/metadata-diff/tree-items/MetadataDiffTreeItem.ts diff --git a/package.json b/package.json index d6fcd7c6b..c423d1821 100644 --- a/package.json +++ b/package.json @@ -1141,6 +1141,14 @@ "icon": "./src/client/assets/powerPages.svg", "contextualTitle": "%microsoft.powerplatform.pages.actionsHub.title%", "visibility": "visible" + }, + { + "id": "microsoft.powerplatform.pages.metadataDiff", + "name": "%microsoft.powerplatform.pages.metadataDiff.title%", + "when": "microsoft.powerplatform.pages.metadataDiffEnabled", + "icon": "./src/client/assets/powerPages.svg", + "contextualTitle": "%microsoft.powerplatform.pages.metadataDiff.title%", + "visibility": "visible" } ] }, diff --git a/src/client/extension.ts b/src/client/extension.ts index 56f18103a..6f5c4dec1 100644 --- a/src/client/extension.ts +++ b/src/client/extension.ts @@ -47,6 +47,8 @@ import { ActionsHub } from "./power-pages/actions-hub/ActionsHub"; import { extractAuthInfo, extractOrgInfo } from "./power-pages/commonUtility"; import PacContext from "./pac/PacContext"; import ArtemisContext from "./ArtemisContext"; +import { MetadataDiff } from "../common/power-pages/metadata-diff/MetadataDiff"; +import { MetadataDiffTreeDataProvider } from "../common/power-pages/metadata-diff/MetadataDiffTreeDataProvider"; let client: LanguageClient; let _context: vscode.ExtensionContext; @@ -260,6 +262,8 @@ export async function activate( const workspaceFolderWatcher = vscode.workspace.onDidChangeWorkspaceFolders(handleWorkspaceFolderChange); _context.subscriptions.push(workspaceFolderWatcher); + MetadataDiff.initialize(context) + if (shouldEnableDebugger()) { activateDebugger(context); } diff --git a/src/common/ecs-features/ecsFeatureGates.ts b/src/common/ecs-features/ecsFeatureGates.ts index aa09ffea2..fd0a33924 100644 --- a/src/common/ecs-features/ecsFeatureGates.ts +++ b/src/common/ecs-features/ecsFeatureGates.ts @@ -68,3 +68,13 @@ export const { enableActionsHub: false, }, }); + +export const { + feature: EnableMetadataDiff +} = getFeatureConfigs({ + teamName: PowerPagesClientName, + description: 'Enable Metadata Diff comparison in VS Code', + fallback: { + enableMetadataDiff: false, + }, +}); diff --git a/src/common/power-pages/metadata-diff/Constants.ts b/src/common/power-pages/metadata-diff/Constants.ts new file mode 100644 index 000000000..caf9d1755 --- /dev/null +++ b/src/common/power-pages/metadata-diff/Constants.ts @@ -0,0 +1,12 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +export const Constants = { + EventNames: { + METADATA_DIFF_INITIALIZED: "metadataDiffInitialized", + METADATA_DIFF_INITIALIZATION_FAILED: "metadataDiffInitializationFailed", + METADATA_DIFF_CURRENT_ENV_FETCH_FAILED: "metadataDiffCurrentEnvFetchFailed", + } +}; diff --git a/src/common/power-pages/metadata-diff/MetadataDiff.ts b/src/common/power-pages/metadata-diff/MetadataDiff.ts new file mode 100644 index 000000000..559eb5286 --- /dev/null +++ b/src/common/power-pages/metadata-diff/MetadataDiff.ts @@ -0,0 +1,55 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +import * as vscode from "vscode"; +import { ECSFeaturesClient } from "../../ecs-features/ecsFeatureClient"; +import { EnableMetadataDiff } from "../../ecs-features/ecsFeatureGates"; +import { MetadataDiffTreeDataProvider } from "./MetadataDiffTreeDataProvider"; +import { oneDSLoggerWrapper } from "../../OneDSLoggerTelemetry/oneDSLoggerWrapper"; +import { Constants } from "./Constants"; + +export class MetadataDiff { + private static _isInitialized = false; + + static isEnabled(): boolean { + const enableMetadataDiff = ECSFeaturesClient.getConfig(EnableMetadataDiff).enableMetadataDiff + + if (enableMetadataDiff === undefined) { + return false; + } + + return enableMetadataDiff; + } + + static async initialize(context: vscode.ExtensionContext): Promise { + if (MetadataDiff._isInitialized) { + return; + } + + try { + const isMetadataDiffEnabled = MetadataDiff.isEnabled(); + + oneDSLoggerWrapper.getLogger().traceInfo("EnableMetadataDiff", { + isEnabled: isMetadataDiffEnabled.toString() + }); + + vscode.commands.executeCommand("setContext", "microsoft.powerplatform.pages.metadataDiffEnabled", isMetadataDiffEnabled); + + if (!isMetadataDiffEnabled) { + return; + } + const treeDataProvider = MetadataDiffTreeDataProvider.initialize(context); + context.subscriptions.push( + vscode.window.registerTreeDataProvider("microsoft.powerplatform.pages.metadataDiff", treeDataProvider) + ); + + MetadataDiff._isInitialized = true; + oneDSLoggerWrapper.getLogger().traceInfo(Constants.EventNames.METADATA_DIFF_INITIALIZED); + } catch (exception) { + const exceptionError = exception as Error; + oneDSLoggerWrapper.getLogger().traceError(Constants.EventNames.METADATA_DIFF_INITIALIZATION_FAILED, exceptionError.message, exceptionError); + } + } +} diff --git a/src/common/power-pages/metadata-diff/MetadataDiffTreeDataProvider.ts b/src/common/power-pages/metadata-diff/MetadataDiffTreeDataProvider.ts new file mode 100644 index 000000000..5a6d9b3ef --- /dev/null +++ b/src/common/power-pages/metadata-diff/MetadataDiffTreeDataProvider.ts @@ -0,0 +1,174 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +import * as vscode from "vscode"; +import * as fs from "fs"; +import * as path from "path"; +import { MetadataDiffTreeItem } from "./tree-items/MetadataDiffTreeItem"; +import { Constants } from "./Constants"; +import { oneDSLoggerWrapper } from "../../OneDSLoggerTelemetry/oneDSLoggerWrapper"; +import { MetadataDiffFileItem } from "./tree-items/MetadataDiffFileItem"; +import { MetadataDiffFolderItem } from "./tree-items/MetadataDiffFolderItem"; + +export class MetadataDiffTreeDataProvider implements vscode.TreeDataProvider { + private readonly _disposables: vscode.Disposable[] = []; + private readonly _context: vscode.ExtensionContext; + private _onDidChangeTreeData: vscode.EventEmitter = new vscode.EventEmitter(); + readonly onDidChangeTreeData: vscode.Event = this._onDidChangeTreeData.event; + + private constructor(context: vscode.ExtensionContext) { + this._context = context; + } + + private refresh(): void { + this._onDidChangeTreeData.fire(); + } + + public static initialize(context: vscode.ExtensionContext): MetadataDiffTreeDataProvider { + return new MetadataDiffTreeDataProvider(context); + } + + getTreeItem(element: MetadataDiffTreeItem): vscode.TreeItem | Thenable { + return element; + } + + private getAllFilesRecursively(dir: string, fileList: string[] = []): string[] { + const files = fs.readdirSync(dir); + + for (const file of files) { + const fullPath = path.join(dir, file); + if (fs.statSync(fullPath).isDirectory()) { + this.getAllFilesRecursively(fullPath, fileList); + } else { + fileList.push(fullPath); + } + } + + return fileList; + } + + private async getDiffFilesWithMockChanges(): Promise { + const workspaceFolders = vscode.workspace.workspaceFolders; + if (!workspaceFolders) { + return []; + } + + const workspacePath = workspaceFolders[0].uri.fsPath; + const storagePath = this._context.storageUri?.fsPath; + if (!storagePath) { + return []; + } + + const diffFiles: string[] = []; + const allFiles = this.getAllFilesRecursively(workspacePath); + + // Create copies in storagePath if they don't exist + for (const file of allFiles) { + const relativePath = path.relative(workspacePath, file); + const storageFile = path.join(storagePath, relativePath); + + // Ensure directory exists + fs.mkdirSync(path.dirname(storageFile), { recursive: true }); + + if (!fs.existsSync(storageFile)) { + fs.copyFileSync(file, storageFile); + } + } + + // Mock a random file change + const randomFile = allFiles[Math.floor(Math.random() * allFiles.length)]; + fs.writeFileSync(path.join(storagePath, path.relative(workspacePath, randomFile)), ""); + + // Compare files + for (const file of allFiles) { + const relativePath = path.relative(workspacePath, file); + const storageFile = path.join(storagePath, relativePath); + + if (fs.existsSync(storageFile)) { + const workspaceContent = fs.readFileSync(file, "utf8"); + const storageContent = fs.readFileSync(storageFile, "utf8"); + + if (workspaceContent !== storageContent) { + diffFiles.push(relativePath); + } + } else { + diffFiles.push(relativePath); + } + } + + return diffFiles; + } + + async getChildren(element?: MetadataDiffTreeItem): Promise { + if (element) { + return element.getChildren(); + } + + try { + const diffFiles = await this.getDiffFilesWithMockChanges(); + if (diffFiles.length === 0) { + return []; + } + + const filePathMap = new Map(); + + diffFiles.forEach(relativePath => { + const storedFileUri = vscode.Uri.joinPath(this._context.storageUri!, relativePath); + filePathMap.set(relativePath, storedFileUri.fsPath); + }); + + return this.buildTreeHierarchy(filePathMap); + } catch (error) { + oneDSLoggerWrapper.getLogger().traceError( + Constants.EventNames.METADATA_DIFF_CURRENT_ENV_FETCH_FAILED, + error as string, + error as Error, + { methodName: this.getChildren }, + {} + ); + return null; + } + } + + private buildTreeHierarchy(filePathMap: Map): MetadataDiffTreeItem[] { + const rootItems: Map = new Map(); + const workspaceRoot = vscode.workspace.workspaceFolders![0].uri.fsPath; + + filePathMap.forEach((storedFilePath, relativePath) => { + const parts = relativePath.split(path.sep); + let currentLevel = rootItems; + let parentItem: MetadataDiffTreeItem | undefined; + + for (let i = 0; i < parts.length; i++) { + const isFile = i === parts.length - 1; + const name = parts[i]; + + if (!currentLevel.has(name)) { + let newItem: MetadataDiffTreeItem; + + if (isFile) { + const absoluteWorkspaceFilePath = path.join(workspaceRoot, relativePath); + newItem = new MetadataDiffFileItem(name, absoluteWorkspaceFilePath, storedFilePath); + } else { + newItem = new MetadataDiffFolderItem(name); + } + + if (parentItem) { + (parentItem as MetadataDiffFolderItem).getChildrenMap().set(name, newItem); + } + + currentLevel.set(name, newItem); + } + + parentItem = currentLevel.get(name); + if (!isFile) { + currentLevel = (parentItem as MetadataDiffFolderItem).getChildrenMap(); + } + } + }); + + return Array.from(rootItems.values()); + } +} diff --git a/src/common/power-pages/metadata-diff/tree-items/MetadataDiffFileItem.ts b/src/common/power-pages/metadata-diff/tree-items/MetadataDiffFileItem.ts new file mode 100644 index 000000000..5f61e05e1 --- /dev/null +++ b/src/common/power-pages/metadata-diff/tree-items/MetadataDiffFileItem.ts @@ -0,0 +1,24 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +import * as vscode from "vscode"; +import { MetadataDiffTreeItem } from "./MetadataDiffTreeItem"; + +export class MetadataDiffFileItem extends MetadataDiffTreeItem { + constructor(label: string, workspaceFilePath: string, storedFilePath: string) { + super(label, vscode.TreeItemCollapsibleState.None, "file", workspaceFilePath); + + const workspaceUri = vscode.Uri.file(workspaceFilePath); + const storedUri = vscode.Uri.file(storedFilePath); + + this.resourceUri = workspaceUri; + this.command = { + command: "vscode.diff", + title: "Compare Changes", + arguments: [storedUri, workspaceUri, `Diff: ${label}`] + }; + this.iconPath = new vscode.ThemeIcon("file"); + } +} diff --git a/src/common/power-pages/metadata-diff/tree-items/MetadataDiffFolderItem.ts b/src/common/power-pages/metadata-diff/tree-items/MetadataDiffFolderItem.ts new file mode 100644 index 000000000..334da6e3c --- /dev/null +++ b/src/common/power-pages/metadata-diff/tree-items/MetadataDiffFolderItem.ts @@ -0,0 +1,14 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +import * as vscode from "vscode"; +import { MetadataDiffTreeItem } from "./MetadataDiffTreeItem"; + +export class MetadataDiffFolderItem extends MetadataDiffTreeItem { + constructor(label: string) { + super(label, vscode.TreeItemCollapsibleState.Collapsed, "folder"); + this.iconPath = new vscode.ThemeIcon("folder"); + } +} diff --git a/src/common/power-pages/metadata-diff/tree-items/MetadataDiffTreeItem.ts b/src/common/power-pages/metadata-diff/tree-items/MetadataDiffTreeItem.ts new file mode 100644 index 000000000..8aa2a9570 --- /dev/null +++ b/src/common/power-pages/metadata-diff/tree-items/MetadataDiffTreeItem.ts @@ -0,0 +1,37 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +import * as vscode from "vscode"; + +export abstract class MetadataDiffTreeItem extends vscode.TreeItem { + protected _children: Map = new Map(); + + constructor( + public readonly label: string, + public readonly collapsibleState: vscode.TreeItemCollapsibleState, + public readonly contextValue: string, + public readonly filePath?: string, // Workspace file path + public readonly storedFilePath?: string // Backup copy path + ) { + super(label, collapsibleState); + this.tooltip = this.label; + + if (filePath && storedFilePath) { + this.command = { + command: "metadataDiff.openDiff", + title: "Open Diff", + arguments: [filePath, storedFilePath] // Pass file paths to the command + }; + } + } + + public getChildren(): MetadataDiffTreeItem[] { + return Array.from(this._children.values()); + } + + public getChildrenMap(): Map { + return this._children; + } +} From 9ca62049069412768e701612a7db65761e2a26f9 Mon Sep 17 00:00:00 2001 From: Rishabh Jain Date: Sat, 1 Mar 2025 16:58:52 +0530 Subject: [PATCH 02/15] refactored data provider --- .../MetadataDiffTreeDataProvider.ts | 78 ++++++++++++------- 1 file changed, 51 insertions(+), 27 deletions(-) diff --git a/src/common/power-pages/metadata-diff/MetadataDiffTreeDataProvider.ts b/src/common/power-pages/metadata-diff/MetadataDiffTreeDataProvider.ts index 5a6d9b3ef..dfe989530 100644 --- a/src/common/power-pages/metadata-diff/MetadataDiffTreeDataProvider.ts +++ b/src/common/power-pages/metadata-diff/MetadataDiffTreeDataProvider.ts @@ -49,19 +49,31 @@ export class MetadataDiffTreeDataProvider implements vscode.TreeDataProvider { - const workspaceFolders = vscode.workspace.workspaceFolders; - if (!workspaceFolders) { - return []; - } + private async getDiffFiles(workspacePath: string, storagePath: string): Promise { + const diffFiles: string[] = []; + const allFiles = this.getAllFilesRecursively(workspacePath); - const workspacePath = workspaceFolders[0].uri.fsPath; - const storagePath = this._context.storageUri?.fsPath; - if (!storagePath) { - return []; + // Compare files + for (const file of allFiles) { + const relativePath = path.relative(workspacePath, file); + const storageFile = path.join(storagePath, relativePath); + + if (fs.existsSync(storageFile)) { + const workspaceContent = fs.readFileSync(file, "utf8"); + const storageContent = fs.readFileSync(storageFile, "utf8"); + + if (workspaceContent !== storageContent) { + diffFiles.push(relativePath); + } + } else { + diffFiles.push(relativePath); + } } + return diffFiles; + } + + private async mockChangesAndCopyFiles(workspacePath: string, storagePath: string): Promise { - const diffFiles: string[] = []; const allFiles = this.getAllFilesRecursively(workspacePath); // Create copies in storagePath if they don't exist @@ -81,24 +93,30 @@ export class MetadataDiffTreeDataProvider implements vscode.TreeDataProvider { + const workspaceFolders = vscode.workspace.workspaceFolders; + if (!workspaceFolders) { + return []; + } - if (workspaceContent !== storageContent) { - diffFiles.push(relativePath); - } - } else { - diffFiles.push(relativePath); - } + const workspacePath = workspaceFolders[0].uri.fsPath; + const storagePath = this._context.storageUri?.fsPath; + if (!storagePath) { + throw new Error("Storage path is not defined"); } - return diffFiles; + // Clean out any existing files in storagePath (so we have a "fresh" download) + if (fs.existsSync(storagePath)) { + fs.rmSync(storagePath, { recursive: true, force: true }); + } + fs.mkdirSync(storagePath, { recursive: true }); + + await this.mockChangesAndCopyFiles(workspacePath, storagePath); + + return this.getDiffFiles(workspacePath, storagePath); } async getChildren(element?: MetadataDiffTreeItem): Promise { @@ -115,8 +133,10 @@ export class MetadataDiffTreeDataProvider implements vscode.TreeDataProvider(); diffFiles.forEach(relativePath => { - const storedFileUri = vscode.Uri.joinPath(this._context.storageUri!, relativePath); - filePathMap.set(relativePath, storedFileUri.fsPath); + if (this._context.storageUri) { + const storedFileUri = vscode.Uri.joinPath(this._context.storageUri, relativePath); + filePathMap.set(relativePath, storedFileUri.fsPath); + } }); return this.buildTreeHierarchy(filePathMap); @@ -134,7 +154,11 @@ export class MetadataDiffTreeDataProvider implements vscode.TreeDataProvider): MetadataDiffTreeItem[] { const rootItems: Map = new Map(); - const workspaceRoot = vscode.workspace.workspaceFolders![0].uri.fsPath; + const workspaceFolders = vscode.workspace.workspaceFolders; + if (!workspaceFolders) { + throw new Error("No workspace folders found"); + } + const workspaceRoot = workspaceFolders[0].uri.fsPath; filePathMap.forEach((storedFilePath, relativePath) => { const parts = relativePath.split(path.sep); From ce9b048e76f7817ac79dfdf49eb7e6c2ce20ad79 Mon Sep 17 00:00:00 2001 From: Rishabh Jain Date: Tue, 4 Mar 2025 23:10:12 +0530 Subject: [PATCH 03/15] Download from pac --- package.json | 9 + package.nls.json | 10 +- src/client/extension.ts | 5 +- src/client/pac/PacWrapper.ts | 4 + .../metadata-diff/MetadataDiffDesktop.ts | 162 ++++++++++++++++++ .../power-pages/metadata-diff/Constants.ts | 5 + .../power-pages/metadata-diff/MetadataDiff.ts | 55 ------ .../MetadataDiffTreeDataProvider.ts | 88 ++++------ src/common/utilities/PacAuthUtil.ts | 31 ++-- 9 files changed, 243 insertions(+), 126 deletions(-) create mode 100644 src/client/power-pages/metadata-diff/MetadataDiffDesktop.ts delete mode 100644 src/common/power-pages/metadata-diff/MetadataDiff.ts diff --git a/package.json b/package.json index c423d1821..eef1a5aae 100644 --- a/package.json +++ b/package.json @@ -170,6 +170,11 @@ "view": "microsoft.powerplatform.pages.actionsHub", "contents": "%microsoft.powerplatform.pages.actionsHub.login%", "when": "!virtualWorkspace && pacCLI.authPanel.interactiveLoginSupported" + }, + { + "view": "microsoft.powerplatform.pages.metadataDiff", + "contents": "%microsoft.powerplatform.pages.metadataDiff.login%", + "when": "!virtualWorkspace && pacCLI.authPanel.interactiveLoginSupported" } ], "commands": [ @@ -439,6 +444,10 @@ { "command": "microsoft.powerplatform.pages.actionsHub.activeSite.uploadSite", "title": "%microsoft.powerplatform.pages.actionsHub.activeSite.uploadSite.title%" + }, + { + "command": "microsoft.powerplatform.pages.metadataDiff.triggerFlow", + "title": "%microsoft.powerplatform.pages.metadataDiff.login.title%" } ], "configuration": { diff --git a/package.nls.json b/package.nls.json index 89a2ac4b3..b2e24d7db 100644 --- a/package.nls.json +++ b/package.nls.json @@ -110,5 +110,13 @@ "microsoft.powerplatform.pages.actionsHub.currentActiveSite.revealInOS.mac.title": "Reveal in Finder", "microsoft.powerplatform.pages.actionsHub.currentActiveSite.revealInOS.linux.title": "Open Containing Folder", "microsoft.powerplatform.pages.actionsHub.inactiveSite.openSiteManagement.title": "Open site management", - "microsoft.powerplatform.pages.actionsHub.activeSite.uploadSite.title": "Upload Site" + "microsoft.powerplatform.pages.actionsHub.activeSite.uploadSite.title": "Upload Site", + "microsoft.powerplatform.pages.metadataDiff.title": "POWER PAGES METADATA COMPARATOR", + "microsoft.powerplatform.pages.metadataDiff.login":{ + "message": "Compare your Power Pages website against a Power Pages environment to view any differences. [Learn more](https://go.microsoft.com/fwlink/?linkid=2305702).\n[Get Started](command:microsoft.powerplatform.pages.metadataDiff.triggerFlow)", + "comment": [ + "This is a Markdown formatted string, and the formatting must persist across translations.", + "The second line should be '[TRANSLATION HERE](command:microsoft.powerplatform.pages.metadataDiff.triggerFlow).', keeping brackets and the text in the parentheses unmodified" + ] + } } diff --git a/src/client/extension.ts b/src/client/extension.ts index 6f5c4dec1..ff0ef7699 100644 --- a/src/client/extension.ts +++ b/src/client/extension.ts @@ -47,8 +47,7 @@ import { ActionsHub } from "./power-pages/actions-hub/ActionsHub"; import { extractAuthInfo, extractOrgInfo } from "./power-pages/commonUtility"; import PacContext from "./pac/PacContext"; import ArtemisContext from "./ArtemisContext"; -import { MetadataDiff } from "../common/power-pages/metadata-diff/MetadataDiff"; -import { MetadataDiffTreeDataProvider } from "../common/power-pages/metadata-diff/MetadataDiffTreeDataProvider"; +import { MetadataDiffDesktop } from "./power-pages/metadata-diff/MetadataDiffDesktop"; let client: LanguageClient; let _context: vscode.ExtensionContext; @@ -262,7 +261,7 @@ export async function activate( const workspaceFolderWatcher = vscode.workspace.onDidChangeWorkspaceFolders(handleWorkspaceFolderChange); _context.subscriptions.push(workspaceFolderWatcher); - MetadataDiff.initialize(context) + MetadataDiffDesktop.initialize(context, pacTerminal) if (shouldEnableDebugger()) { activateDebugger(context); diff --git a/src/client/pac/PacWrapper.ts b/src/client/pac/PacWrapper.ts index d8803e2ac..273e02bf3 100644 --- a/src/client/pac/PacWrapper.ts +++ b/src/client/pac/PacWrapper.ts @@ -180,6 +180,10 @@ export class PacWrapper { return this.executeCommandAndParseResults(new PacArguments("telemetry", "disable")); } + public async pagesDownload(path: string, websiteId: string): Promise { + return this.pacInterop.executeCommand(new PacArguments("pages", "download", "--path", path, "--webSiteId", websiteId)); + } + public exit(): void { this.pacInterop.exit(); } diff --git a/src/client/power-pages/metadata-diff/MetadataDiffDesktop.ts b/src/client/power-pages/metadata-diff/MetadataDiffDesktop.ts new file mode 100644 index 000000000..39533bac0 --- /dev/null +++ b/src/client/power-pages/metadata-diff/MetadataDiffDesktop.ts @@ -0,0 +1,162 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +import * as vscode from "vscode"; +import * as fs from "fs"; +import { ECSFeaturesClient } from "../../../common/ecs-features/ecsFeatureClient"; +import { EnableMetadataDiff } from "../../../common/ecs-features/ecsFeatureGates"; +import { oneDSLoggerWrapper } from "../../../common/OneDSLoggerTelemetry/oneDSLoggerWrapper"; +import { Constants, SUCCESS } from "../../../common/power-pages/metadata-diff/Constants"; +import { MetadataDiffTreeDataProvider } from "../../../common/power-pages/metadata-diff/MetadataDiffTreeDataProvider"; +import { PacTerminal } from "../../lib/PacTerminal"; +import { createAuthProfileExp } from "../../../common/utilities/PacAuthUtil"; +import path from "path"; +import { getWebsiteRecordId } from "../../../common/utilities/WorkspaceInfoFinderUtil"; + +export class MetadataDiffDesktop { + //private readonly _disposables: vscode.Disposable[] = []; + private static _isInitialized = false; + + static isEnabled(): boolean { + const enableMetadataDiff = ECSFeaturesClient.getConfig(EnableMetadataDiff).enableMetadataDiff + + if (enableMetadataDiff === undefined) { + return false; + } + + return enableMetadataDiff; + } + + static async initialize(context: vscode.ExtensionContext, pacTerminal: PacTerminal): Promise { + if (MetadataDiffDesktop._isInitialized) { + return; + } + + vscode.commands.registerCommand("microsoft.powerplatform.pages.metadataDiff.triggerFlow", async () => { + try { + const orgUrl = await vscode.window.showInputBox({ + prompt: "Enter the organization URL", + placeHolder: "https://your-org.crm.dynamics.com" + }); + + if (!orgUrl) { + vscode.window.showErrorMessage("Organization URL is required to trigger the metadata diff flow."); + return; + } + + const urlPattern = /^https:\/\/[a-zA-Z0-9.-]+\d*\.crm\.dynamics\.com\/?$/; + if (!urlPattern.test(orgUrl)) { + vscode.window.showErrorMessage("Invalid organization URL. Please enter a valid URL in the format: https://your-org.crm.dynamics.com", { modal: true }); + return; + } + + const pacWrapper = pacTerminal.getWrapper() + const pacActiveOrg = await pacWrapper.activeOrg(); + if(pacActiveOrg){ + if (pacActiveOrg.Status === SUCCESS) { + if(pacActiveOrg.Results.OrgUrl == orgUrl){ + vscode.window.showInformationMessage("Already connected to the specified environment."); + } + else{ + const pacOrgSelect = await pacWrapper.orgSelect(orgUrl); + if(pacOrgSelect && pacOrgSelect.Status === SUCCESS){ + vscode.window.showInformationMessage("Environment switched successfully."); + } + else{ + vscode.window.showErrorMessage("Failed to switch the environment."); + return; + } + } + } + else{ + await createAuthProfileExp(pacWrapper, orgUrl); + vscode.window.showInformationMessage("Auth profile created successfully."); + } + } + else { + vscode.window.showErrorMessage("Failed to fetch the current environment details."); + return; + } + + const workspaceFolders = vscode.workspace.workspaceFolders; + if (!workspaceFolders || workspaceFolders.length === 0) { + return []; + } + + const currentWorkspaceFolder = workspaceFolders[0].uri.fsPath; + const websiteId = getWebsiteRecordId(currentWorkspaceFolder); + path.join(websiteId, "metadataDiffStorage"); + const storagePath = context.storageUri?.fsPath; + if (!storagePath) { + throw new Error("Storage path is not defined"); + } + + // Clean out any existing files in storagePath (so we have a "fresh" download) + if (fs.existsSync(storagePath)) { + fs.rmSync(storagePath, { recursive: true, force: true }); + } + fs.mkdirSync(storagePath, { recursive: true }); + const progressOptions: vscode.ProgressOptions = { + location: vscode.ProgressLocation.Notification, + title: "Downloading website metadata", + cancellable: false + }; + let pacPagesDownload; + await vscode.window.withProgress(progressOptions, async (progress) => { + progress.report({ message: "This may take a few minutes..." }); + pacPagesDownload = await pacWrapper.pagesDownload(storagePath, websiteId); + vscode.window.showInformationMessage("Download completed."); + }); + if (pacPagesDownload) { + const treeDataProvider = MetadataDiffTreeDataProvider.initialize(context); + context.subscriptions.push( + vscode.window.registerTreeDataProvider("microsoft.powerplatform.pages.metadataDiff", treeDataProvider) + ); + } + else{ + vscode.window.showErrorMessage("Failed to download metadata."); + } + + } + catch (error) { + oneDSLoggerWrapper.getLogger().traceError(Constants.EventNames.METADATA_DIFF_REFRESH_FAILED, error as string, error as Error, { methodName: null }, {}); + } + }) + + try { + const isMetadataDiffEnabled = MetadataDiffDesktop.isEnabled(); + + oneDSLoggerWrapper.getLogger().traceInfo("EnableMetadataDiff", { + isEnabled: isMetadataDiffEnabled.toString() + }); + + vscode.commands.executeCommand("setContext", "microsoft.powerplatform.pages.metadataDiffEnabled", isMetadataDiffEnabled); + + if (!isMetadataDiffEnabled) { + return; + } + + const storagePath = context.storageUri?.fsPath; + if (!storagePath) { + throw new Error("Storage path is not defined"); + } + if (fs.existsSync(storagePath)) { + fs.rmSync(storagePath, { recursive: true, force: true }); + } + fs.mkdirSync(storagePath, { recursive: true }); + + const treeDataProvider = MetadataDiffTreeDataProvider.initialize(context); + context.subscriptions.push( + vscode.window.registerTreeDataProvider("microsoft.powerplatform.pages.metadataDiff", treeDataProvider) + ); + + MetadataDiffDesktop._isInitialized = true; + oneDSLoggerWrapper.getLogger().traceInfo(Constants.EventNames.METADATA_DIFF_INITIALIZED); + } catch (exception) { + const exceptionError = exception as Error; + oneDSLoggerWrapper.getLogger().traceError(Constants.EventNames.METADATA_DIFF_INITIALIZATION_FAILED, exceptionError.message, exceptionError); + } + } +} diff --git a/src/common/power-pages/metadata-diff/Constants.ts b/src/common/power-pages/metadata-diff/Constants.ts index caf9d1755..24b18d542 100644 --- a/src/common/power-pages/metadata-diff/Constants.ts +++ b/src/common/power-pages/metadata-diff/Constants.ts @@ -6,7 +6,12 @@ export const Constants = { EventNames: { METADATA_DIFF_INITIALIZED: "metadataDiffInitialized", + METADATA_DIFF_REFRESH_FAILED: "metadataDiffRefreshFailed", METADATA_DIFF_INITIALIZATION_FAILED: "metadataDiffInitializationFailed", METADATA_DIFF_CURRENT_ENV_FETCH_FAILED: "metadataDiffCurrentEnvFetchFailed", + ORGANIZATION_URL_MISSING: "Organization URL is missing in the results.", + EMPTY_RESULTS_ARRAY: "Results array is empty or not an array.", + PAC_AUTH_OUTPUT_FAILURE: "pacAuthCreateOutput is missing or unsuccessful.", } }; +export const SUCCESS = "Success"; diff --git a/src/common/power-pages/metadata-diff/MetadataDiff.ts b/src/common/power-pages/metadata-diff/MetadataDiff.ts deleted file mode 100644 index 559eb5286..000000000 --- a/src/common/power-pages/metadata-diff/MetadataDiff.ts +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - */ - -import * as vscode from "vscode"; -import { ECSFeaturesClient } from "../../ecs-features/ecsFeatureClient"; -import { EnableMetadataDiff } from "../../ecs-features/ecsFeatureGates"; -import { MetadataDiffTreeDataProvider } from "./MetadataDiffTreeDataProvider"; -import { oneDSLoggerWrapper } from "../../OneDSLoggerTelemetry/oneDSLoggerWrapper"; -import { Constants } from "./Constants"; - -export class MetadataDiff { - private static _isInitialized = false; - - static isEnabled(): boolean { - const enableMetadataDiff = ECSFeaturesClient.getConfig(EnableMetadataDiff).enableMetadataDiff - - if (enableMetadataDiff === undefined) { - return false; - } - - return enableMetadataDiff; - } - - static async initialize(context: vscode.ExtensionContext): Promise { - if (MetadataDiff._isInitialized) { - return; - } - - try { - const isMetadataDiffEnabled = MetadataDiff.isEnabled(); - - oneDSLoggerWrapper.getLogger().traceInfo("EnableMetadataDiff", { - isEnabled: isMetadataDiffEnabled.toString() - }); - - vscode.commands.executeCommand("setContext", "microsoft.powerplatform.pages.metadataDiffEnabled", isMetadataDiffEnabled); - - if (!isMetadataDiffEnabled) { - return; - } - const treeDataProvider = MetadataDiffTreeDataProvider.initialize(context); - context.subscriptions.push( - vscode.window.registerTreeDataProvider("microsoft.powerplatform.pages.metadataDiff", treeDataProvider) - ); - - MetadataDiff._isInitialized = true; - oneDSLoggerWrapper.getLogger().traceInfo(Constants.EventNames.METADATA_DIFF_INITIALIZED); - } catch (exception) { - const exceptionError = exception as Error; - oneDSLoggerWrapper.getLogger().traceError(Constants.EventNames.METADATA_DIFF_INITIALIZATION_FAILED, exceptionError.message, exceptionError); - } - } -} diff --git a/src/common/power-pages/metadata-diff/MetadataDiffTreeDataProvider.ts b/src/common/power-pages/metadata-diff/MetadataDiffTreeDataProvider.ts index dfe989530..572f6c1e4 100644 --- a/src/common/power-pages/metadata-diff/MetadataDiffTreeDataProvider.ts +++ b/src/common/power-pages/metadata-diff/MetadataDiffTreeDataProvider.ts @@ -50,11 +50,13 @@ export class MetadataDiffTreeDataProvider implements vscode.TreeDataProvider { - const diffFiles: string[] = []; - const allFiles = this.getAllFilesRecursively(workspacePath); + const diffFiles: Set = new Set(); - // Compare files - for (const file of allFiles) { + const workspaceFiles = this.getAllFilesRecursively(workspacePath); + const storageFiles = this.getAllFilesRecursively(storagePath); + + // Check workspace files against storage files + for (const file of workspaceFiles) { const relativePath = path.relative(workspacePath, file); const storageFile = path.join(storagePath, relativePath); @@ -63,60 +65,24 @@ export class MetadataDiffTreeDataProvider implements vscode.TreeDataProvider { - - const allFiles = this.getAllFilesRecursively(workspacePath); - - // Create copies in storagePath if they don't exist - for (const file of allFiles) { - const relativePath = path.relative(workspacePath, file); - const storageFile = path.join(storagePath, relativePath); - // Ensure directory exists - fs.mkdirSync(path.dirname(storageFile), { recursive: true }); + // Check storage files against workspace files (to detect deletions) + for (const file of storageFiles) { + const relativePath = path.relative(storagePath, file); + const workspaceFile = path.join(workspacePath, relativePath); - if (!fs.existsSync(storageFile)) { - fs.copyFileSync(file, storageFile); + if (!fs.existsSync(workspaceFile)) { + diffFiles.add(relativePath); } } - // Mock a random file change - const randomFile = allFiles[Math.floor(Math.random() * allFiles.length)]; - fs.writeFileSync(path.join(storagePath, path.relative(workspacePath, randomFile)), ""); - - return storagePath; - } - - private async getDiffFilesWithMockChanges(): Promise { - const workspaceFolders = vscode.workspace.workspaceFolders; - if (!workspaceFolders) { - return []; - } - - const workspacePath = workspaceFolders[0].uri.fsPath; - const storagePath = this._context.storageUri?.fsPath; - if (!storagePath) { - throw new Error("Storage path is not defined"); - } - - // Clean out any existing files in storagePath (so we have a "fresh" download) - if (fs.existsSync(storagePath)) { - fs.rmSync(storagePath, { recursive: true, force: true }); - } - fs.mkdirSync(storagePath, { recursive: true }); - - await this.mockChangesAndCopyFiles(workspacePath, storagePath); - - return this.getDiffFiles(workspacePath, storagePath); + return Array.from(diffFiles); } async getChildren(element?: MetadataDiffTreeItem): Promise { @@ -125,7 +91,25 @@ export class MetadataDiffTreeDataProvider implements vscode.TreeDataProvider fs.statSync(path.join(storagePath, file)).isDirectory()); + //check if storage path contains any files + if (folderNames.length === 0) { + return []; + } + const websitePath = path.join(storagePath, folderNames[0]); + + const diffFiles = await this.getDiffFiles(workspacePath, websitePath); if (diffFiles.length === 0) { return []; } @@ -133,10 +117,8 @@ export class MetadataDiffTreeDataProvider implements vscode.TreeDataProvider(); diffFiles.forEach(relativePath => { - if (this._context.storageUri) { - const storedFileUri = vscode.Uri.joinPath(this._context.storageUri, relativePath); + const storedFileUri = vscode.Uri.joinPath(vscode.Uri.file(websitePath), relativePath); filePathMap.set(relativePath, storedFileUri.fsPath); - } }); return this.buildTreeHierarchy(filePathMap); diff --git a/src/common/utilities/PacAuthUtil.ts b/src/common/utilities/PacAuthUtil.ts index 78305a5c4..1c5ad9878 100644 --- a/src/common/utilities/PacAuthUtil.ts +++ b/src/common/utilities/PacAuthUtil.ts @@ -10,22 +10,25 @@ import { AUTH_CREATE_FAILED, AUTH_CREATE_MESSAGE } from "../copilot/constants"; import { showInputBoxAndGetOrgUrl, showProgressWithNotification } from "./Utils"; - export async function createAuthProfileExp(pacWrapper: PacWrapper | undefined) { - const userOrgUrl = await showInputBoxAndGetOrgUrl(); - if (!userOrgUrl) { - return; - } + export async function createAuthProfileExp(pacWrapper: PacWrapper | undefined, userOrgUrl = "") { + if (!userOrgUrl) { + userOrgUrl = await showInputBoxAndGetOrgUrl() ?? ""; + } - if (!pacWrapper) { - vscode.window.showErrorMessage(AUTH_CREATE_FAILED); - return; - } + if (!userOrgUrl) { + return; + } - const pacAuthCreateOutput = await showProgressWithNotification(vscode.l10n.t(AUTH_CREATE_MESSAGE), async () => { return await pacWrapper?.authCreateNewAuthProfileForOrg(userOrgUrl) }); - if (pacAuthCreateOutput && pacAuthCreateOutput.Status !== SUCCESS) { - vscode.window.showErrorMessage(AUTH_CREATE_FAILED); - return; - } + if (!pacWrapper) { + vscode.window.showErrorMessage(AUTH_CREATE_FAILED); + return; + } + + const pacAuthCreateOutput = await showProgressWithNotification(vscode.l10n.t(AUTH_CREATE_MESSAGE), async () => { return await pacWrapper?.authCreateNewAuthProfileForOrg(userOrgUrl) }); + if (pacAuthCreateOutput && pacAuthCreateOutput.Status !== SUCCESS) { + vscode.window.showErrorMessage(AUTH_CREATE_FAILED); + return; + } return pacAuthCreateOutput; } From a7a129ff9f26f092c8b50ef2e1f8739fc0615c14 Mon Sep 17 00:00:00 2001 From: Rishabh Jain Date: Wed, 5 Mar 2025 14:37:10 +0530 Subject: [PATCH 04/15] enhance diff logic --- .../MetadataDiffTreeDataProvider.ts | 85 ++++++++++--------- .../tree-items/MetadataDiffFileItem.ts | 22 +++-- 2 files changed, 60 insertions(+), 47 deletions(-) diff --git a/src/common/power-pages/metadata-diff/MetadataDiffTreeDataProvider.ts b/src/common/power-pages/metadata-diff/MetadataDiffTreeDataProvider.ts index 572f6c1e4..0632c5147 100644 --- a/src/common/power-pages/metadata-diff/MetadataDiffTreeDataProvider.ts +++ b/src/common/power-pages/metadata-diff/MetadataDiffTreeDataProvider.ts @@ -49,40 +49,44 @@ export class MetadataDiffTreeDataProvider implements vscode.TreeDataProvider { - const diffFiles: Set = new Set(); + private async getDiffFiles(workspacePath: string, storagePath: string): Promise> { + const diffFiles = new Map(); const workspaceFiles = this.getAllFilesRecursively(workspacePath); const storageFiles = this.getAllFilesRecursively(storagePath); - // Check workspace files against storage files - for (const file of workspaceFiles) { - const relativePath = path.relative(workspacePath, file); + const workspaceFileSet = new Set(workspaceFiles.map(f => path.relative(workspacePath, f))); + const storageFileSet = new Set(storageFiles.map(f => path.relative(storagePath, f))); + + // Files only in workspace or modified files + for (const relativePath of workspaceFileSet) { + const workspaceFile = path.join(workspacePath, relativePath); const storageFile = path.join(storagePath, relativePath); - if (fs.existsSync(storageFile)) { - const workspaceContent = fs.readFileSync(file, "utf8"); + if (!storageFileSet.has(relativePath)) { + diffFiles.set(relativePath, { workspaceFile }); // File only in workspace + } else { + const workspaceContent = fs.readFileSync(workspaceFile, "utf8"); const storageContent = fs.readFileSync(storageFile, "utf8"); - if (workspaceContent !== storageContent) { - diffFiles.add(relativePath); + const normalizedWorkspaceContent = workspaceContent.replace(/\r\n/g, "\n"); + const normalizedStorageContent = storageContent.replace(/\r\n/g, "\n"); + if (normalizedWorkspaceContent !== normalizedStorageContent) { + diffFiles.set(relativePath, { workspaceFile, storageFile }); // Modified file + } } - } else { - diffFiles.add(relativePath); } } - // Check storage files against workspace files (to detect deletions) - for (const file of storageFiles) { - const relativePath = path.relative(storagePath, file); - const workspaceFile = path.join(workspacePath, relativePath); - - if (!fs.existsSync(workspaceFile)) { - diffFiles.add(relativePath); + // Files only in storage (deleted from workspace) + for (const relativePath of storageFileSet) { + if (!workspaceFileSet.has(relativePath)) { + const storageFile = path.join(storagePath, relativePath); + diffFiles.set(relativePath, { storageFile }); // File only in storage } } - return Array.from(diffFiles); + return diffFiles; } async getChildren(element?: MetadataDiffTreeItem): Promise { @@ -103,25 +107,17 @@ export class MetadataDiffTreeDataProvider implements vscode.TreeDataProvider fs.statSync(path.join(storagePath, file)).isDirectory()); - //check if storage path contains any files if (folderNames.length === 0) { return []; } const websitePath = path.join(storagePath, folderNames[0]); const diffFiles = await this.getDiffFiles(workspacePath, websitePath); - if (diffFiles.length === 0) { + if (diffFiles.size === 0) { return []; } - const filePathMap = new Map(); - - diffFiles.forEach(relativePath => { - const storedFileUri = vscode.Uri.joinPath(vscode.Uri.file(websitePath), relativePath); - filePathMap.set(relativePath, storedFileUri.fsPath); - }); - - return this.buildTreeHierarchy(filePathMap); + return this.buildTreeHierarchy(diffFiles); } catch (error) { oneDSLoggerWrapper.getLogger().traceError( Constants.EventNames.METADATA_DIFF_CURRENT_ENV_FETCH_FAILED, @@ -134,15 +130,10 @@ export class MetadataDiffTreeDataProvider implements vscode.TreeDataProvider): MetadataDiffTreeItem[] { + private buildTreeHierarchy(filePathMap: Map): MetadataDiffTreeItem[] { const rootItems: Map = new Map(); - const workspaceFolders = vscode.workspace.workspaceFolders; - if (!workspaceFolders) { - throw new Error("No workspace folders found"); - } - const workspaceRoot = workspaceFolders[0].uri.fsPath; - filePathMap.forEach((storedFilePath, relativePath) => { + filePathMap.forEach(({ workspaceFile, storageFile }, relativePath) => { const parts = relativePath.split(path.sep); let currentLevel = rootItems; let parentItem: MetadataDiffTreeItem | undefined; @@ -155,8 +146,8 @@ export class MetadataDiffTreeDataProvider implements vscode.TreeDataProvider { + return items.filter(item => { + if (item instanceof MetadataDiffFolderItem) { + const childrenArray = Array.from(item.getChildrenMap().values()); + const filteredChildren = filterEmptyFolders(childrenArray); + item.getChildrenMap().clear(); + filteredChildren.forEach(child => item.getChildrenMap().set(child.label, child)); + return filteredChildren.length > 0; + } else if (item instanceof MetadataDiffFileItem) { + return item.hasDiff; + } + return false; + }); + }; + + return filterEmptyFolders(Array.from(rootItems.values())); } } diff --git a/src/common/power-pages/metadata-diff/tree-items/MetadataDiffFileItem.ts b/src/common/power-pages/metadata-diff/tree-items/MetadataDiffFileItem.ts index 5f61e05e1..ad75b5b5a 100644 --- a/src/common/power-pages/metadata-diff/tree-items/MetadataDiffFileItem.ts +++ b/src/common/power-pages/metadata-diff/tree-items/MetadataDiffFileItem.ts @@ -7,18 +7,24 @@ import * as vscode from "vscode"; import { MetadataDiffTreeItem } from "./MetadataDiffTreeItem"; export class MetadataDiffFileItem extends MetadataDiffTreeItem { - constructor(label: string, workspaceFilePath: string, storedFilePath: string) { - super(label, vscode.TreeItemCollapsibleState.None, "file", workspaceFilePath); + public readonly workspaceFile?: string; + public readonly storageFile?: string; + public readonly hasDiff: boolean; - const workspaceUri = vscode.Uri.file(workspaceFilePath); - const storedUri = vscode.Uri.file(storedFilePath); + constructor(label: string, workspaceFile?: string, storageFile?: string, hasDiff = true) { + super(label, vscode.TreeItemCollapsibleState.None, "metadataDiffFileItem"); + this.workspaceFile = workspaceFile; + this.storageFile = storageFile; + this.hasDiff = hasDiff; + + const workspaceUri = workspaceFile ? vscode.Uri.file(workspaceFile) : vscode.Uri.parse(`untitled:${label} (Deleted)`); + const storedUri = storageFile ? vscode.Uri.file(storageFile) : vscode.Uri.parse(`untitled:${label} (Deleted)`); this.resourceUri = workspaceUri; this.command = { - command: "vscode.diff", - title: "Compare Changes", - arguments: [storedUri, workspaceUri, `Diff: ${label}`] + command: 'vscode.diff', + title: 'Show Diff', + arguments: [storedUri, workspaceUri, `${label} (Diff)`] }; - this.iconPath = new vscode.ThemeIcon("file"); } } From bfb1a25a139aa09bbdd2a57dcd51df7094432653 Mon Sep 17 00:00:00 2001 From: Rishabh Jain Date: Wed, 5 Mar 2025 14:40:15 +0530 Subject: [PATCH 05/15] refactor --- src/client/power-pages/metadata-diff/MetadataDiffDesktop.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/client/power-pages/metadata-diff/MetadataDiffDesktop.ts b/src/client/power-pages/metadata-diff/MetadataDiffDesktop.ts index 39533bac0..6a258c430 100644 --- a/src/client/power-pages/metadata-diff/MetadataDiffDesktop.ts +++ b/src/client/power-pages/metadata-diff/MetadataDiffDesktop.ts @@ -16,7 +16,6 @@ import path from "path"; import { getWebsiteRecordId } from "../../../common/utilities/WorkspaceInfoFinderUtil"; export class MetadataDiffDesktop { - //private readonly _disposables: vscode.Disposable[] = []; private static _isInitialized = false; static isEnabled(): boolean { From 21a681ac03388ffb176f79a6662c31a5cb37e03b Mon Sep 17 00:00:00 2001 From: Rishabh Jain Date: Wed, 23 Apr 2025 01:40:16 +0530 Subject: [PATCH 06/15] find model version --- src/client/pac/PacTypes.ts | 8 +++ src/client/pac/PacWrapper.ts | 10 +++- .../metadata-diff/MetadataDiffDesktop.ts | 56 +++++++++++++++++-- 3 files changed, 67 insertions(+), 7 deletions(-) diff --git a/src/client/pac/PacTypes.ts b/src/client/pac/PacTypes.ts index de37b0f63..e43b55874 100644 --- a/src/client/pac/PacTypes.ts +++ b/src/client/pac/PacTypes.ts @@ -62,6 +62,14 @@ export type OrgListOutput = { export type PacOrgListOutput = PacOutputWithResultList; +export type PagesList = { + FriendlyName: string, + WebsiteId: string, + ModelVersion: string +} + +export type PacPagesListOutput = PacOutputWithResultList; + export type ActiveOrgOutput = { OrgId: string, UniqueName: string, diff --git a/src/client/pac/PacWrapper.ts b/src/client/pac/PacWrapper.ts index 273e02bf3..be0647b1e 100644 --- a/src/client/pac/PacWrapper.ts +++ b/src/client/pac/PacWrapper.ts @@ -9,7 +9,7 @@ import * as readline from "readline"; import * as fs from "fs-extra"; import { ChildProcessWithoutNullStreams, spawn } from "child_process"; import { BlockingQueue } from "../../common/utilities/BlockingQueue"; -import { PacOutput, PacAdminListOutput, PacAuthListOutput, PacSolutionListOutput, PacOrgListOutput, PacOrgWhoOutput, PacAuthWhoOutput } from "./PacTypes"; +import { PacOutput, PacAdminListOutput, PacAuthListOutput, PacSolutionListOutput, PacOrgListOutput, PacOrgWhoOutput, PacAuthWhoOutput, PacPagesListOutput} from "./PacTypes"; import { v4 } from "uuid"; import { oneDSLoggerWrapper } from "../../common/OneDSLoggerTelemetry/oneDSLoggerWrapper"; @@ -180,8 +180,12 @@ export class PacWrapper { return this.executeCommandAndParseResults(new PacArguments("telemetry", "disable")); } - public async pagesDownload(path: string, websiteId: string): Promise { - return this.pacInterop.executeCommand(new PacArguments("pages", "download", "--path", path, "--webSiteId", websiteId)); + public async pagesDownload(path: string, websiteId: string, modelVersion: string): Promise { + return this.pacInterop.executeCommand(new PacArguments("pages", "download", "--path", path, "--webSiteId", websiteId, "--modelVersion", modelVersion)); + } + + public async pagesList(): Promise { + return this.executeCommandAndParseResults(new PacArguments("pages", "list", "--verbose")); } public exit(): void { diff --git a/src/client/power-pages/metadata-diff/MetadataDiffDesktop.ts b/src/client/power-pages/metadata-diff/MetadataDiffDesktop.ts index 6a258c430..3ee219de6 100644 --- a/src/client/power-pages/metadata-diff/MetadataDiffDesktop.ts +++ b/src/client/power-pages/metadata-diff/MetadataDiffDesktop.ts @@ -14,6 +14,7 @@ import { PacTerminal } from "../../lib/PacTerminal"; import { createAuthProfileExp } from "../../../common/utilities/PacAuthUtil"; import path from "path"; import { getWebsiteRecordId } from "../../../common/utilities/WorkspaceInfoFinderUtil"; +import { PagesList } from "../../pac/PacTypes"; export class MetadataDiffDesktop { private static _isInitialized = false; @@ -81,7 +82,8 @@ export class MetadataDiffDesktop { const workspaceFolders = vscode.workspace.workspaceFolders; if (!workspaceFolders || workspaceFolders.length === 0) { - return []; + vscode.window.showErrorMessage("No folders opened in the current workspace."); + return; } const currentWorkspaceFolder = workspaceFolders[0].uri.fsPath; @@ -104,9 +106,18 @@ export class MetadataDiffDesktop { }; let pacPagesDownload; await vscode.window.withProgress(progressOptions, async (progress) => { - progress.report({ message: "This may take a few minutes..." }); - pacPagesDownload = await pacWrapper.pagesDownload(storagePath, websiteId); - vscode.window.showInformationMessage("Download completed."); + progress.report({ message: "Looking for this website in the connected environment..." }); + const pacPagesList = await this.getPagesList(pacTerminal); + if (pacPagesList && pacPagesList.length > 0) { + const websiteRecord = pacPagesList.find((record) => record.id === websiteId); + if (!websiteRecord) { + vscode.window.showErrorMessage("Website not found in the connected environment."); + return; + } + progress.report({ message: `Downloading "${websiteRecord.name}" as ${websiteRecord.modelVersion === "v2" ? "enhanced" : "standard"} data model. Please wait...` }); + pacPagesDownload = await pacWrapper.pagesDownload(storagePath, websiteId, websiteRecord.modelVersion == "v1" ? "1" : "2"); + vscode.window.showInformationMessage("Download completed."); + } }); if (pacPagesDownload) { const treeDataProvider = MetadataDiffTreeDataProvider.initialize(context); @@ -158,4 +169,41 @@ export class MetadataDiffDesktop { oneDSLoggerWrapper.getLogger().traceError(Constants.EventNames.METADATA_DIFF_INITIALIZATION_FAILED, exceptionError.message, exceptionError); } } + + static async getPagesList(pacTerminal: PacTerminal): Promise<{ name: string, id: string, modelVersion: string }[]> { + const pacWrapper = pacTerminal.getWrapper(); + const pagesListOutput = await pacWrapper.pagesList(); + if (pagesListOutput && pagesListOutput.Status === SUCCESS && pagesListOutput.Information) { + // Parse the list of pages from the string output + const pagesList: PagesList[] = []; + if (Array.isArray(pagesListOutput.Information)) { + // If Information is already an array of strings + pagesListOutput.Information.forEach(line => { + // Skip empty lines or header lines + if (!line.trim() || !line.includes('[')) { + return; + } + + // Extract the relevant parts using regex + const match = line.match(/\[\d+\]\s+([a-f0-9-]+)\s+(.*?)\s+(v[12])\s*$/i); + if (match) { + pagesList.push({ + WebsiteId: match[1].trim(), + FriendlyName: match[2].trim(), + ModelVersion: match[3].trim() + }); + } + }); + } + return pagesList.map((site) => { + return { + name: site.FriendlyName, + id: site.WebsiteId, + modelVersion: site.ModelVersion + } + }); + } + + return []; + } } From fc4a1fd1e47f8dd116d29ac9633370871dc7d215 Mon Sep 17 00:00:00 2001 From: Rishabh Jain Date: Wed, 23 Apr 2025 04:02:20 +0530 Subject: [PATCH 07/15] Added generate/import/export for diff reports --- package.json | 30 ++ .../metadata-diff/MetadataDiffDesktop.ts | 305 +++++++++++++++++- .../power-pages/metadata-diff/Constants.ts | 1 + .../MetadataDiffTreeDataProvider.ts | 261 ++++++++++----- .../tree-items/MetadataDiffFileItem.ts | 33 +- .../tree-items/MetadataDiffFolderItem.ts | 4 +- .../tree-items/MetadataDiffTreeItem.ts | 15 +- 7 files changed, 548 insertions(+), 101 deletions(-) diff --git a/package.json b/package.json index eef1a5aae..6d6aca95b 100644 --- a/package.json +++ b/package.json @@ -448,6 +448,21 @@ { "command": "microsoft.powerplatform.pages.metadataDiff.triggerFlow", "title": "%microsoft.powerplatform.pages.metadataDiff.login.title%" + }, + { + "command": "microsoft.powerplatform.pages.metadataDiff.generateReport", + "title": "Generate Metadata Diff Report", + "icon": "$(output)" + }, + { + "command": "microsoft.powerplatform.pages.metadataDiff.exportReport", + "title": "Export Metadata Diff Report", + "icon": "$(save-as)" + }, + { + "command": "microsoft.powerplatform.pages.metadataDiff.importReport", + "title": "Import Metadata Diff Report", + "icon": "$(folder-opened)" } ], "configuration": { @@ -959,6 +974,21 @@ "command": "powerpages.copilot.clearConversation", "when": "view == powerpages.copilot", "group": "navigation" + }, + { + "command": "microsoft.powerplatform.pages.metadataDiff.generateReport", + "when": "view == microsoft.powerplatform.pages.metadataDiff", + "group": "navigation" + }, + { + "command": "microsoft.powerplatform.pages.metadataDiff.exportReport", + "when": "view == microsoft.powerplatform.pages.metadataDiff", + "group": "navigation" + }, + { + "command": "microsoft.powerplatform.pages.metadataDiff.importReport", + "when": "view == microsoft.powerplatform.pages.metadataDiff", + "group": "navigation" } ], "view/item/context": [ diff --git a/src/client/power-pages/metadata-diff/MetadataDiffDesktop.ts b/src/client/power-pages/metadata-diff/MetadataDiffDesktop.ts index 3ee219de6..4f6a1c9e8 100644 --- a/src/client/power-pages/metadata-diff/MetadataDiffDesktop.ts +++ b/src/client/power-pages/metadata-diff/MetadataDiffDesktop.ts @@ -16,6 +16,19 @@ import path from "path"; import { getWebsiteRecordId } from "../../../common/utilities/WorkspaceInfoFinderUtil"; import { PagesList } from "../../pac/PacTypes"; +interface DiffFile { + relativePath: string; + changes: string; + type: string; + workspaceContent?: string; + storageContent?: string; +} + +interface MetadataDiffReport { + generatedOn: string; + files: DiffFile[]; +} + export class MetadataDiffDesktop { private static _isInitialized = false; @@ -34,6 +47,28 @@ export class MetadataDiffDesktop { return; } + // Register command for handling file diffs + vscode.commands.registerCommand('metadataDiff.openDiff', async (workspaceFile: string, storedFile: string) => { + try { + const workspaceUri = vscode.Uri.file(workspaceFile); + const storedUri = vscode.Uri.file(storedFile); + const fileName = path.basename(workspaceFile); + + await vscode.commands.executeCommand('vscode.diff', + storedUri, + workspaceUri, + `${fileName} (Metadata Diff)` + ); + } catch (error) { + oneDSLoggerWrapper.getLogger().traceError( + Constants.EventNames.METADATA_DIFF_REPORT_FAILED, + error as string, + error as Error + ); + vscode.window.showErrorMessage("Failed to open diff view"); + } + }); + vscode.commands.registerCommand("microsoft.powerplatform.pages.metadataDiff.triggerFlow", async () => { try { const orgUrl = await vscode.window.showInputBox({ @@ -133,7 +168,126 @@ export class MetadataDiffDesktop { catch (error) { oneDSLoggerWrapper.getLogger().traceError(Constants.EventNames.METADATA_DIFF_REFRESH_FAILED, error as string, error as Error, { methodName: null }, {}); } - }) + }); + + vscode.commands.registerCommand("microsoft.powerplatform.pages.metadataDiff.generateReport", async () => { + try { + const workspaceFolders = vscode.workspace.workspaceFolders; + if (!workspaceFolders) { + vscode.window.showErrorMessage("No workspace folder open"); + return; + } + + const storagePath = context.storageUri?.fsPath; + if (!storagePath) { + vscode.window.showErrorMessage("Storage path not found"); + return; + } + + // Generate report content + const reportContent = await generateDiffReport(workspaceFolders[0].uri.fsPath, storagePath); + + // Create and show the report document + const doc = await vscode.workspace.openTextDocument({ + content: reportContent, + language: 'markdown' + }); + await vscode.window.showTextDocument(doc, { + preview: true, + viewColumn: vscode.ViewColumn.Beside + }); + } catch (error) { + oneDSLoggerWrapper.getLogger().traceError( + Constants.EventNames.METADATA_DIFF_REPORT_FAILED, + error as string, + error as Error + ); + vscode.window.showErrorMessage("Failed to generate metadata diff report"); + } + }); + + vscode.commands.registerCommand("microsoft.powerplatform.pages.metadataDiff.exportReport", async () => { + try { + const workspaceFolders = vscode.workspace.workspaceFolders; + if (!workspaceFolders) { + vscode.window.showErrorMessage("No workspace folder open"); + return; + } + + const storagePath = context.storageUri?.fsPath; + if (!storagePath) { + vscode.window.showErrorMessage("Storage path not found"); + return; + } + + // Get diff files + const diffFiles = await getAllDiffFiles(workspaceFolders[0].uri.fsPath, storagePath); + + // Create report object + const report: MetadataDiffReport = { + generatedOn: new Date().toISOString(), + files: diffFiles + }; + + // Save dialog + const saveUri = await vscode.window.showSaveDialog({ + defaultUri: vscode.Uri.file('metadata-diff-report.json'), + filters: { + 'JSON files': ['json'] + } + }); + + if (saveUri) { + fs.writeFileSync(saveUri.fsPath, JSON.stringify(report, null, 2)); + vscode.window.showInformationMessage("Report exported successfully"); + } + } catch (error) { + oneDSLoggerWrapper.getLogger().traceError( + Constants.EventNames.METADATA_DIFF_REPORT_FAILED, + error as string, + error as Error + ); + vscode.window.showErrorMessage("Failed to export metadata diff report"); + } + }); + + vscode.commands.registerCommand("microsoft.powerplatform.pages.metadataDiff.importReport", async () => { + try { + const fileUri = await vscode.window.showOpenDialog({ + canSelectFiles: true, + canSelectFolders: false, + canSelectMany: false, + filters: { + 'JSON files': ['json'] + } + }); + + if (fileUri && fileUri[0]) { + const reportContent = fs.readFileSync(fileUri[0].fsPath, 'utf8'); + const report = JSON.parse(reportContent) as MetadataDiffReport; + + // Clean up any existing tree data provider + const treeDataProvider = new MetadataDiffTreeDataProvider(context); + + // Update the tree with imported data + await treeDataProvider.setDiffFiles(report.files); + + // Register the new tree data provider + context.subscriptions.push( + vscode.window.registerTreeDataProvider("microsoft.powerplatform.pages.metadataDiff", treeDataProvider) + ); + + vscode.window.showInformationMessage("Report imported successfully"); + } + } catch (error) { + oneDSLoggerWrapper.getLogger().traceError( + Constants.EventNames.METADATA_DIFF_REPORT_FAILED, + error as string, + error as Error + ); + vscode.window.showErrorMessage("Failed to import metadata diff report"); + } + }); try { const isMetadataDiffEnabled = MetadataDiffDesktop.isEnabled(); @@ -207,3 +361,152 @@ export class MetadataDiffDesktop { return []; } } + +async function generateDiffReport(workspacePath: string, storagePath: string): Promise { + let report = '# Power Pages Metadata Diff Report\n\n'; + report += `Generated on: ${new Date().toLocaleString()}\n\n`; + + // Get diff files + const diffFiles = await getAllDiffFiles(workspacePath, storagePath); + + // Group by folders + const folderStructure = new Map(); + diffFiles.forEach(file => { + const dirPath = path.dirname(file.relativePath); + if (!folderStructure.has(dirPath)) { + folderStructure.set(dirPath, []); + } + folderStructure.get(dirPath)!.push(file); + }); + + // Sort folders and generate report + const sortedFolders = Array.from(folderStructure.keys()).sort(); + for (const folder of sortedFolders) { + const files = folderStructure.get(folder)!; + + // Add folder header (skip for root) + if (folder !== '.') { + report += `## ${folder}\n\n`; + } + + // Add files + for (const file of files.sort((a, b) => a.relativePath.localeCompare(b.relativePath))) { + report += `- ${path.basename(file.relativePath)}\n`; + report += ` - Changes: ${file.changes}\n`; + } + report += '\n'; + } + + return report; +} + +async function getAllDiffFiles(workspacePath: string, storagePath: string): Promise { + const diffFiles: DiffFile[] = []; + + // Get website directory from storage path + const folderNames = fs.readdirSync(storagePath).filter(file => + fs.statSync(path.join(storagePath, file)).isDirectory() + ); + if (folderNames.length === 0) { + return diffFiles; + } + const websitePath = path.join(storagePath, folderNames[0]); + + // Get all files + const workspaceFiles = await getAllFiles(workspacePath); + const storageFiles = await getAllFiles(websitePath); + + // Create normalized path maps + const workspaceMap = new Map(workspaceFiles.map(f => { + const normalized = path.relative(workspacePath, f).replace(/\\/g, '/'); + return [normalized, f]; + })); + const storageMap = new Map(storageFiles.map(f => { + const normalized = path.relative(websitePath, f).replace(/\\/g, '/'); + return [normalized, f]; + })); + + // Compare files + for (const [normalized, workspaceFile] of workspaceMap.entries()) { + const storageFile = storageMap.get(normalized); + if (!storageFile) { + diffFiles.push({ + relativePath: normalized, + changes: 'Only in workspace', + type: getFileType(normalized), + workspaceContent: fs.readFileSync(workspaceFile, 'utf8').replace(/\r\n/g, '\n') + }); + continue; + } + + // Compare content + const workspaceContent = fs.readFileSync(workspaceFile, 'utf8').replace(/\r\n/g, '\n'); + const storageContent = fs.readFileSync(storageFile, 'utf8').replace(/\r\n/g, '\n'); + if (workspaceContent !== storageContent) { + diffFiles.push({ + relativePath: normalized, + changes: 'Modified', + type: getFileType(normalized), + workspaceContent, + storageContent + }); + } + } + + // Check for files only in storage + for (const [normalized, storageFile] of storageMap.entries()) { + if (!workspaceMap.has(normalized)) { + diffFiles.push({ + relativePath: normalized, + changes: 'Only in remote', + type: getFileType(normalized), + storageContent: fs.readFileSync(storageFile, 'utf8').replace(/\r\n/g, '\n') + }); + } + } + + return diffFiles; +} + +async function getAllFiles(dirPath: string): Promise { + const files: string[] = []; + + function traverse(currentPath: string) { + const entries = fs.readdirSync(currentPath, { withFileTypes: true }); + for (const entry of entries) { + const fullPath = path.join(currentPath, entry.name); + if (entry.isDirectory()) { + traverse(fullPath); + } else { + files.push(fullPath); + } + } + } + + traverse(dirPath); + return files; +} + +function getFileType(filePath: string): string { + const ext = path.extname(filePath).toLowerCase(); + const basename = path.basename(filePath).toLowerCase(); + + if (basename === 'webrole.yml') return 'Roles'; + if (basename === 'websitelanguage.yml') return 'Languages'; + if (ext === '.yml' && filePath.includes('webpages')) return 'Pages'; + if (ext === '.yml' && filePath.includes('webtemplates')) return 'Templates'; + if (ext === '.yml' && filePath.includes('webfiles')) return 'Files'; + + return 'Other'; +} + +function groupDiffsByType(files: DiffFile[]): Record { + return files.reduce((groups: Record, file) => { + const type = file.type || 'Other'; + if (!groups[type]) { + groups[type] = []; + } + groups[type].push(file); + return groups; + }, {}); +} diff --git a/src/common/power-pages/metadata-diff/Constants.ts b/src/common/power-pages/metadata-diff/Constants.ts index 24b18d542..4c19af137 100644 --- a/src/common/power-pages/metadata-diff/Constants.ts +++ b/src/common/power-pages/metadata-diff/Constants.ts @@ -12,6 +12,7 @@ export const Constants = { ORGANIZATION_URL_MISSING: "Organization URL is missing in the results.", EMPTY_RESULTS_ARRAY: "Results array is empty or not an array.", PAC_AUTH_OUTPUT_FAILURE: "pacAuthCreateOutput is missing or unsuccessful.", + METADATA_DIFF_REPORT_FAILED: "metadataDiffReportFailed" } }; export const SUCCESS = "Success"; diff --git a/src/common/power-pages/metadata-diff/MetadataDiffTreeDataProvider.ts b/src/common/power-pages/metadata-diff/MetadataDiffTreeDataProvider.ts index 0632c5147..a429d9ec4 100644 --- a/src/common/power-pages/metadata-diff/MetadataDiffTreeDataProvider.ts +++ b/src/common/power-pages/metadata-diff/MetadataDiffTreeDataProvider.ts @@ -12,24 +12,33 @@ import { oneDSLoggerWrapper } from "../../OneDSLoggerTelemetry/oneDSLoggerWrappe import { MetadataDiffFileItem } from "./tree-items/MetadataDiffFileItem"; import { MetadataDiffFolderItem } from "./tree-items/MetadataDiffFolderItem"; +interface DiffFile { + relativePath: string; + changes: string; + type: string; + workspaceContent?: string; + storageContent?: string; +} + export class MetadataDiffTreeDataProvider implements vscode.TreeDataProvider { private readonly _disposables: vscode.Disposable[] = []; private readonly _context: vscode.ExtensionContext; private _onDidChangeTreeData: vscode.EventEmitter = new vscode.EventEmitter(); readonly onDidChangeTreeData: vscode.Event = this._onDidChangeTreeData.event; + private _diffItems: MetadataDiffTreeItem[] = []; - private constructor(context: vscode.ExtensionContext) { + constructor(context: vscode.ExtensionContext) { this._context = context; } - private refresh(): void { - this._onDidChangeTreeData.fire(); - } - public static initialize(context: vscode.ExtensionContext): MetadataDiffTreeDataProvider { return new MetadataDiffTreeDataProvider(context); } + private refresh(): void { + this._onDidChangeTreeData.fire(); + } + getTreeItem(element: MetadataDiffTreeItem): vscode.TreeItem | Thenable { return element; } @@ -52,48 +61,80 @@ export class MetadataDiffTreeDataProvider implements vscode.TreeDataProvider> { const diffFiles = new Map(); - const workspaceFiles = this.getAllFilesRecursively(workspacePath); - const storageFiles = this.getAllFilesRecursively(storagePath); + // Get website directory from storage path + const websitePath = await this.getWebsitePath(storagePath); + if (!websitePath) { + return diffFiles; + } - const workspaceFileSet = new Set(workspaceFiles.map(f => path.relative(workspacePath, f))); - const storageFileSet = new Set(storageFiles.map(f => path.relative(storagePath, f))); + const workspaceFiles = this.getAllFilesRecursively(workspacePath); + const storageFiles = this.getAllFilesRecursively(websitePath); + + // Create normalized path maps for comparison + const workspaceMap = new Map(workspaceFiles.map(f => { + const normalized = path.relative(workspacePath, f).replace(/\\/g, '/'); + return [normalized, f]; + })); + + const storageMap = new Map(storageFiles.map(f => { + const normalized = path.relative(websitePath, f).replace(/\\/g, '/'); + return [normalized, f]; + })); + + // Compare files + for (const [relativePath, workspaceFile] of workspaceMap.entries()) { + const storageFile = storageMap.get(relativePath); + + if (!storageFile) { + // File only exists in workspace + diffFiles.set(relativePath, { workspaceFile }); + continue; + } - // Files only in workspace or modified files - for (const relativePath of workspaceFileSet) { - const workspaceFile = path.join(workspacePath, relativePath); - const storageFile = path.join(storagePath, relativePath); + // Compare content only if both files exist + const workspaceContent = fs.readFileSync(workspaceFile, 'utf8').replace(/\r\n/g, '\n'); + const storageContent = fs.readFileSync(storageFile, 'utf8').replace(/\r\n/g, '\n'); - if (!storageFileSet.has(relativePath)) { - diffFiles.set(relativePath, { workspaceFile }); // File only in workspace - } else { - const workspaceContent = fs.readFileSync(workspaceFile, "utf8"); - const storageContent = fs.readFileSync(storageFile, "utf8"); - if (workspaceContent !== storageContent) { - const normalizedWorkspaceContent = workspaceContent.replace(/\r\n/g, "\n"); - const normalizedStorageContent = storageContent.replace(/\r\n/g, "\n"); - if (normalizedWorkspaceContent !== normalizedStorageContent) { - diffFiles.set(relativePath, { workspaceFile, storageFile }); // Modified file - } - } + if (workspaceContent !== storageContent) { + diffFiles.set(relativePath, { workspaceFile, storageFile }); } } - // Files only in storage (deleted from workspace) - for (const relativePath of storageFileSet) { - if (!workspaceFileSet.has(relativePath)) { - const storageFile = path.join(storagePath, relativePath); - diffFiles.set(relativePath, { storageFile }); // File only in storage + // Check for files only in storage + for (const [relativePath, storageFile] of storageMap.entries()) { + if (!workspaceMap.has(relativePath)) { + diffFiles.set(relativePath, { storageFile }); } } return diffFiles; } + private async getWebsitePath(storagePath: string): Promise { + try { + const folders = fs.readdirSync(storagePath).filter(f => + fs.statSync(path.join(storagePath, f)).isDirectory() + ); + + if (folders.length > 0) { + return path.join(storagePath, folders[0]); + } + } catch (error) { + console.error('Error finding website path:', error); + } + return undefined; + } + async getChildren(element?: MetadataDiffTreeItem): Promise { if (element) { return element.getChildren(); } + // If we have imported diff items, return those + if (this._diffItems && this._diffItems.length > 0) { + return this._diffItems; + } + try { const workspaceFolders = vscode.workspace.workspaceFolders; if (!workspaceFolders) { @@ -106,13 +147,7 @@ export class MetadataDiffTreeDataProvider implements vscode.TreeDataProvider fs.statSync(path.join(storagePath, file)).isDirectory()); - if (folderNames.length === 0) { - return []; - } - const websitePath = path.join(storagePath, folderNames[0]); - - const diffFiles = await this.getDiffFiles(workspacePath, websitePath); + const diffFiles = await this.getDiffFiles(workspacePath, storagePath); if (diffFiles.size === 0) { return []; } @@ -131,57 +166,125 @@ export class MetadataDiffTreeDataProvider implements vscode.TreeDataProvider): MetadataDiffTreeItem[] { - const rootItems: Map = new Map(); + const rootNode = new MetadataDiffFolderItem(''); - filePathMap.forEach(({ workspaceFile, storageFile }, relativePath) => { - const parts = relativePath.split(path.sep); - let currentLevel = rootItems; - let parentItem: MetadataDiffTreeItem | undefined; + for (const [relativePath, { workspaceFile, storageFile }] of filePathMap.entries()) { + const parts = relativePath.split('/'); + let currentNode = rootNode; - for (let i = 0; i < parts.length; i++) { - const isFile = i === parts.length - 1; - const name = parts[i]; + // Create folder hierarchy + for (let i = 0; i < parts.length - 1; i++) { + const folderName = parts[i]; + let folderNode = currentNode.getChildrenMap().get(folderName) as MetadataDiffFolderItem; - if (!currentLevel.has(name)) { - let newItem: MetadataDiffTreeItem; + if (!folderNode) { + folderNode = new MetadataDiffFolderItem(folderName); + currentNode.getChildrenMap().set(folderName, folderNode); + } - if (isFile) { - const hasDiff = workspaceFile === undefined || storageFile === undefined || fs.readFileSync(workspaceFile, "utf8") !== fs.readFileSync(storageFile, "utf8"); - newItem = new MetadataDiffFileItem(name, workspaceFile, storageFile, hasDiff); - } else { - newItem = new MetadataDiffFolderItem(name); - } + currentNode = folderNode; + } - if (parentItem) { - (parentItem as MetadataDiffFolderItem).getChildrenMap().set(name, newItem); - } + // Add file + const fileName = parts[parts.length - 1]; + const fileNode = new MetadataDiffFileItem( + fileName, + workspaceFile, + storageFile, + true + ); + fileNode.description = this.getChangeDescription(workspaceFile, storageFile); + currentNode.getChildrenMap().set(fileName, fileNode); + } - currentLevel.set(name, newItem); - } + // Convert the root's children map to array + return Array.from(rootNode.getChildrenMap().values()); + } + + private getFileType(filePath: string): string { + const ext = path.extname(filePath).toLowerCase(); + const basename = path.basename(filePath).toLowerCase(); + + if (basename === 'webrole.yml') return 'Roles'; + if (basename === 'websitelanguage.yml') return 'Languages'; + if (ext === '.yml' && filePath.includes('webpages')) return 'Pages'; + if (ext === '.yml' && filePath.includes('webtemplates')) return 'Templates'; + if (ext === '.yml' && filePath.includes('webfiles')) return 'Files'; + return 'Other'; + } + + private getChangeDescription(workspaceFile?: string, storageFile?: string): string { + if (!workspaceFile) return 'Only in remote'; + if (!storageFile) return 'Only in workspace'; + return 'Modified'; + } + + async setDiffFiles(files: DiffFile[]): Promise { + const rootNode = new MetadataDiffFolderItem(''); + const sortedFiles = files.sort((a, b) => a.relativePath.localeCompare(b.relativePath)); + + // Get temp directory for storing imported file contents + const tempDir = path.join(this._context.storageUri?.fsPath || '', 'imported_diff'); + if (!fs.existsSync(tempDir)) { + fs.mkdirSync(tempDir, { recursive: true }); + } - parentItem = currentLevel.get(name); - if (!isFile) { - currentLevel = (parentItem as MetadataDiffFolderItem).getChildrenMap(); + for (const file of sortedFiles) { + const parts = file.relativePath.split('/'); + let currentNode = rootNode; + + // Create folder hierarchy + for (let i = 0; i < parts.length - 1; i++) { + const folderName = parts[i]; + let folderNode = currentNode.getChildrenMap().get(folderName) as MetadataDiffFolderItem; + + if (!folderNode) { + // Create folder with expanded state in constructor + folderNode = new MetadataDiffFolderItem(folderName, vscode.TreeItemCollapsibleState.Expanded); + folderNode.iconPath = new vscode.ThemeIcon("folder"); + currentNode.getChildrenMap().set(folderName, folderNode); } + + currentNode = folderNode; + } + + // Rest of the file handling code... + const fileName = parts[parts.length - 1]; + const filePath = path.join(tempDir, file.relativePath); + const dirPath = path.dirname(filePath); + fs.mkdirSync(dirPath, { recursive: true }); + + let workspaceFilePath: string | undefined; + let storageFilePath: string | undefined; + + if (file.workspaceContent) { + workspaceFilePath = filePath + '.workspace'; + fs.writeFileSync(workspaceFilePath, file.workspaceContent); + } + + if (file.storageContent) { + storageFilePath = filePath + '.storage'; + fs.writeFileSync(storageFilePath, file.storageContent); } - }); - - // Filter out folders without changed files - const filterEmptyFolders = (items: MetadataDiffTreeItem[]): MetadataDiffTreeItem[] => { - return items.filter(item => { - if (item instanceof MetadataDiffFolderItem) { - const childrenArray = Array.from(item.getChildrenMap().values()); - const filteredChildren = filterEmptyFolders(childrenArray); - item.getChildrenMap().clear(); - filteredChildren.forEach(child => item.getChildrenMap().set(child.label, child)); - return filteredChildren.length > 0; - } else if (item instanceof MetadataDiffFileItem) { - return item.hasDiff; - } - return false; - }); - }; - return filterEmptyFolders(Array.from(rootItems.values())); + const fileNode = new MetadataDiffFileItem( + fileName, + workspaceFilePath, + storageFilePath, + true + ); + fileNode.description = file.changes; + fileNode.iconPath = new vscode.ThemeIcon("file"); + fileNode.command = { + command: 'metadataDiff.openDiff', + title: 'Show Diff', + arguments: [workspaceFilePath, storageFilePath] + }; + + currentNode.getChildrenMap().set(fileName, fileNode); + } + + this._diffItems = Array.from(rootNode.getChildrenMap().values()); + this._onDidChangeTreeData.fire(); } } diff --git a/src/common/power-pages/metadata-diff/tree-items/MetadataDiffFileItem.ts b/src/common/power-pages/metadata-diff/tree-items/MetadataDiffFileItem.ts index ad75b5b5a..f3fae460f 100644 --- a/src/common/power-pages/metadata-diff/tree-items/MetadataDiffFileItem.ts +++ b/src/common/power-pages/metadata-diff/tree-items/MetadataDiffFileItem.ts @@ -7,24 +7,29 @@ import * as vscode from "vscode"; import { MetadataDiffTreeItem } from "./MetadataDiffTreeItem"; export class MetadataDiffFileItem extends MetadataDiffTreeItem { - public readonly workspaceFile?: string; - public readonly storageFile?: string; - public readonly hasDiff: boolean; - constructor(label: string, workspaceFile?: string, storageFile?: string, hasDiff = true) { - super(label, vscode.TreeItemCollapsibleState.None, "metadataDiffFileItem"); + super( + label, + vscode.TreeItemCollapsibleState.None, + "metadataDiffFileItem", + workspaceFile, + storageFile + ); this.workspaceFile = workspaceFile; this.storageFile = storageFile; this.hasDiff = hasDiff; + this.iconPath = new vscode.ThemeIcon("file"); - const workspaceUri = workspaceFile ? vscode.Uri.file(workspaceFile) : vscode.Uri.parse(`untitled:${label} (Deleted)`); - const storedUri = storageFile ? vscode.Uri.file(storageFile) : vscode.Uri.parse(`untitled:${label} (Deleted)`); - - this.resourceUri = workspaceUri; - this.command = { - command: 'vscode.diff', - title: 'Show Diff', - arguments: [storedUri, workspaceUri, `${label} (Diff)`] - }; + if (hasDiff && (workspaceFile || storageFile)) { + this.command = { + command: 'metadataDiff.openDiff', + title: 'Show Diff', + arguments: [workspaceFile, storageFile] + }; + } } + + public readonly workspaceFile?: string; + public readonly storageFile?: string; + public readonly hasDiff: boolean; } diff --git a/src/common/power-pages/metadata-diff/tree-items/MetadataDiffFolderItem.ts b/src/common/power-pages/metadata-diff/tree-items/MetadataDiffFolderItem.ts index 334da6e3c..c2baeccbb 100644 --- a/src/common/power-pages/metadata-diff/tree-items/MetadataDiffFolderItem.ts +++ b/src/common/power-pages/metadata-diff/tree-items/MetadataDiffFolderItem.ts @@ -7,8 +7,8 @@ import * as vscode from "vscode"; import { MetadataDiffTreeItem } from "./MetadataDiffTreeItem"; export class MetadataDiffFolderItem extends MetadataDiffTreeItem { - constructor(label: string) { - super(label, vscode.TreeItemCollapsibleState.Collapsed, "folder"); + constructor(label: string, collapsibleState: vscode.TreeItemCollapsibleState = vscode.TreeItemCollapsibleState.Expanded) { + super(label, collapsibleState, "folder"); this.iconPath = new vscode.ThemeIcon("folder"); } } diff --git a/src/common/power-pages/metadata-diff/tree-items/MetadataDiffTreeItem.ts b/src/common/power-pages/metadata-diff/tree-items/MetadataDiffTreeItem.ts index 8aa2a9570..6ab0b9442 100644 --- a/src/common/power-pages/metadata-diff/tree-items/MetadataDiffTreeItem.ts +++ b/src/common/power-pages/metadata-diff/tree-items/MetadataDiffTreeItem.ts @@ -5,17 +5,22 @@ import * as vscode from "vscode"; -export abstract class MetadataDiffTreeItem extends vscode.TreeItem { - protected _children: Map = new Map(); +export class MetadataDiffTreeItem extends vscode.TreeItem { + private _childrenMap: Map; constructor( public readonly label: string, public readonly collapsibleState: vscode.TreeItemCollapsibleState, - public readonly contextValue: string, + public readonly contextValue?: string, public readonly filePath?: string, // Workspace file path public readonly storedFilePath?: string // Backup copy path ) { super(label, collapsibleState); + this._childrenMap = new Map(); + this.contextValue = contextValue; + this.iconPath = collapsibleState === vscode.TreeItemCollapsibleState.None ? + new vscode.ThemeIcon("file") : + new vscode.ThemeIcon("folder"); this.tooltip = this.label; if (filePath && storedFilePath) { @@ -28,10 +33,10 @@ export abstract class MetadataDiffTreeItem extends vscode.TreeItem { } public getChildren(): MetadataDiffTreeItem[] { - return Array.from(this._children.values()); + return Array.from(this._childrenMap.values()); } public getChildrenMap(): Map { - return this._children; + return this._childrenMap; } } From c7f6c6e2551f345c85dd12b9b13da296ab96edf2 Mon Sep 17 00:00:00 2001 From: Rishabh Jain Date: Wed, 23 Apr 2025 05:18:51 +0530 Subject: [PATCH 08/15] Refactor --- .../metadata-diff/MetadataDiffCommands.ts | 260 +++++++++++ .../metadata-diff/MetadataDiffDesktop.ts | 414 +----------------- .../metadata-diff/MetadataDiffUtils.ts | 169 +++++++ 3 files changed, 434 insertions(+), 409 deletions(-) create mode 100644 src/client/power-pages/metadata-diff/MetadataDiffCommands.ts create mode 100644 src/client/power-pages/metadata-diff/MetadataDiffUtils.ts diff --git a/src/client/power-pages/metadata-diff/MetadataDiffCommands.ts b/src/client/power-pages/metadata-diff/MetadataDiffCommands.ts new file mode 100644 index 000000000..e1031a6a9 --- /dev/null +++ b/src/client/power-pages/metadata-diff/MetadataDiffCommands.ts @@ -0,0 +1,260 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +import * as vscode from "vscode"; +import * as fs from "fs"; +import * as path from "path"; +import { PacTerminal } from "../../lib/PacTerminal"; +import { Constants, SUCCESS } from "../../../common/power-pages/metadata-diff/Constants"; +import { oneDSLoggerWrapper } from "../../../common/OneDSLoggerTelemetry/oneDSLoggerWrapper"; +import { MetadataDiffTreeDataProvider } from "../../../common/power-pages/metadata-diff/MetadataDiffTreeDataProvider"; +import { createAuthProfileExp } from "../../../common/utilities/PacAuthUtil"; +import { getWebsiteRecordId } from "../../../common/utilities/WorkspaceInfoFinderUtil"; +import { MetadataDiffDesktop } from "./MetadataDiffDesktop"; +import { generateDiffReport, getAllDiffFiles, MetadataDiffReport } from "./MetadataDiffUtils"; + +export async function registerMetadataDiffCommands(context: vscode.ExtensionContext, pacTerminal: PacTerminal): Promise { + // Register command for handling file diffs + vscode.commands.registerCommand('metadataDiff.openDiff', async (workspaceFile: string, storedFile: string) => { + try { + const workspaceUri = vscode.Uri.file(workspaceFile); + const storedUri = vscode.Uri.file(storedFile); + const fileName = path.basename(workspaceFile); + + await vscode.commands.executeCommand('vscode.diff', + storedUri, + workspaceUri, + `${fileName} (Metadata Diff)` + ); + } catch (error) { + oneDSLoggerWrapper.getLogger().traceError( + Constants.EventNames.METADATA_DIFF_REPORT_FAILED, + error as string, + error as Error + ); + vscode.window.showErrorMessage("Failed to open diff view"); + } + }); + + vscode.commands.registerCommand("microsoft.powerplatform.pages.metadataDiff.triggerFlow", async () => { + try { + const orgUrl = await vscode.window.showInputBox({ + prompt: "Enter the organization URL", + placeHolder: "https://your-org.crm.dynamics.com" + }); + + if (!orgUrl) { + vscode.window.showErrorMessage("Organization URL is required to trigger the metadata diff flow."); + return; + } + + const urlPattern = /^https:\/\/[a-zA-Z0-9.-]+\d*\.crm\.dynamics\.com\/?$/; + if (!urlPattern.test(orgUrl)) { + vscode.window.showErrorMessage("Invalid organization URL. Please enter a valid URL in the format: https://your-org.crm.dynamics.com", { modal: true }); + return; + } + + const pacWrapper = pacTerminal.getWrapper() + const pacActiveOrg = await pacWrapper.activeOrg(); + if(pacActiveOrg){ + if (pacActiveOrg.Status === SUCCESS) { + if(pacActiveOrg.Results.OrgUrl == orgUrl){ + vscode.window.showInformationMessage("Already connected to the specified environment."); + } + else{ + const pacOrgSelect = await pacWrapper.orgSelect(orgUrl); + if(pacOrgSelect && pacOrgSelect.Status === SUCCESS){ + vscode.window.showInformationMessage("Environment switched successfully."); + } + else{ + vscode.window.showErrorMessage("Failed to switch the environment."); + return; + } + } + } + else{ + await createAuthProfileExp(pacWrapper, orgUrl); + vscode.window.showInformationMessage("Auth profile created successfully."); + } + } + else { + vscode.window.showErrorMessage("Failed to fetch the current environment details."); + return; + } + + const workspaceFolders = vscode.workspace.workspaceFolders; + if (!workspaceFolders || workspaceFolders.length === 0) { + vscode.window.showErrorMessage("No folders opened in the current workspace."); + return; + } + + const currentWorkspaceFolder = workspaceFolders[0].uri.fsPath; + const websiteId = getWebsiteRecordId(currentWorkspaceFolder); + path.join(websiteId, "metadataDiffStorage"); + const storagePath = context.storageUri?.fsPath; + if (!storagePath) { + throw new Error("Storage path is not defined"); + } + + // Clean out any existing files in storagePath (so we have a "fresh" download) + if (fs.existsSync(storagePath)) { + fs.rmSync(storagePath, { recursive: true, force: true }); + } + fs.mkdirSync(storagePath, { recursive: true }); + const progressOptions: vscode.ProgressOptions = { + location: vscode.ProgressLocation.Notification, + title: "Downloading website metadata", + cancellable: false + }; + let pacPagesDownload; + await vscode.window.withProgress(progressOptions, async (progress) => { + progress.report({ message: "Looking for this website in the connected environment..." }); + const pacPagesList = await MetadataDiffDesktop.getPagesList(pacTerminal); + if (pacPagesList && pacPagesList.length > 0) { + const websiteRecord = pacPagesList.find((record) => record.id === websiteId); + if (!websiteRecord) { + vscode.window.showErrorMessage("Website not found in the connected environment."); + return; + } + progress.report({ message: `Downloading "${websiteRecord.name}" as ${websiteRecord.modelVersion === "v2" ? "enhanced" : "standard"} data model. Please wait...` }); + pacPagesDownload = await pacWrapper.pagesDownload(storagePath, websiteId, websiteRecord.modelVersion == "v1" ? "1" : "2"); + vscode.window.showInformationMessage("Download completed."); + } + }); + if (pacPagesDownload) { + const treeDataProvider = MetadataDiffTreeDataProvider.initialize(context); + context.subscriptions.push( + vscode.window.registerTreeDataProvider("microsoft.powerplatform.pages.metadataDiff", treeDataProvider) + ); + } + else{ + vscode.window.showErrorMessage("Failed to download metadata."); + } + + } + catch (error) { + oneDSLoggerWrapper.getLogger().traceError(Constants.EventNames.METADATA_DIFF_REFRESH_FAILED, error as string, error as Error, { methodName: null }, {}); + } + }); + + vscode.commands.registerCommand("microsoft.powerplatform.pages.metadataDiff.generateReport", async () => { + try { + const workspaceFolders = vscode.workspace.workspaceFolders; + if (!workspaceFolders) { + vscode.window.showErrorMessage("No workspace folder open"); + return; + } + + const storagePath = context.storageUri?.fsPath; + if (!storagePath) { + vscode.window.showErrorMessage("Storage path not found"); + return; + } + + // Generate report content + const reportContent = await generateDiffReport(workspaceFolders[0].uri.fsPath, storagePath); + + // Create and show the report document + const doc = await vscode.workspace.openTextDocument({ + content: reportContent, + language: 'markdown' + }); + await vscode.window.showTextDocument(doc, { + preview: true, + viewColumn: vscode.ViewColumn.Beside + }); + } catch (error) { + oneDSLoggerWrapper.getLogger().traceError( + Constants.EventNames.METADATA_DIFF_REPORT_FAILED, + error as string, + error as Error + ); + vscode.window.showErrorMessage("Failed to generate metadata diff report"); + } + }); + + vscode.commands.registerCommand("microsoft.powerplatform.pages.metadataDiff.exportReport", async () => { + try { + const workspaceFolders = vscode.workspace.workspaceFolders; + if (!workspaceFolders) { + vscode.window.showErrorMessage("No workspace folder open"); + return; + } + + const storagePath = context.storageUri?.fsPath; + if (!storagePath) { + vscode.window.showErrorMessage("Storage path not found"); + return; + } + + // Get diff files + const diffFiles = await getAllDiffFiles(workspaceFolders[0].uri.fsPath, storagePath); + + // Create report object + const report: MetadataDiffReport = { + generatedOn: new Date().toISOString(), + files: diffFiles + }; + + // Save dialog + const saveUri = await vscode.window.showSaveDialog({ + defaultUri: vscode.Uri.file('metadata-diff-report.json'), + filters: { + 'JSON files': ['json'] + } + }); + + if (saveUri) { + fs.writeFileSync(saveUri.fsPath, JSON.stringify(report, null, 2)); + vscode.window.showInformationMessage("Report exported successfully"); + } + } catch (error) { + oneDSLoggerWrapper.getLogger().traceError( + Constants.EventNames.METADATA_DIFF_REPORT_FAILED, + error as string, + error as Error + ); + vscode.window.showErrorMessage("Failed to export metadata diff report"); + } + }); + + vscode.commands.registerCommand("microsoft.powerplatform.pages.metadataDiff.importReport", async () => { + try { + const fileUri = await vscode.window.showOpenDialog({ + canSelectFiles: true, + canSelectFolders: false, + canSelectMany: false, + filters: { + 'JSON files': ['json'] + } + }); + + if (fileUri && fileUri[0]) { + const reportContent = fs.readFileSync(fileUri[0].fsPath, 'utf8'); + const report = JSON.parse(reportContent) as MetadataDiffReport; + + // Clean up any existing tree data provider + const treeDataProvider = new MetadataDiffTreeDataProvider(context); + + // Update the tree with imported data + await treeDataProvider.setDiffFiles(report.files); + + // Register the new tree data provider + context.subscriptions.push( + vscode.window.registerTreeDataProvider("microsoft.powerplatform.pages.metadataDiff", treeDataProvider) + ); + + vscode.window.showInformationMessage("Report imported successfully"); + } + } catch (error) { + oneDSLoggerWrapper.getLogger().traceError( + Constants.EventNames.METADATA_DIFF_REPORT_FAILED, + error as string, + error as Error + ); + vscode.window.showErrorMessage("Failed to import metadata diff report"); + } + }); +} diff --git a/src/client/power-pages/metadata-diff/MetadataDiffDesktop.ts b/src/client/power-pages/metadata-diff/MetadataDiffDesktop.ts index 4f6a1c9e8..70fff4d71 100644 --- a/src/client/power-pages/metadata-diff/MetadataDiffDesktop.ts +++ b/src/client/power-pages/metadata-diff/MetadataDiffDesktop.ts @@ -8,26 +8,11 @@ import * as fs from "fs"; import { ECSFeaturesClient } from "../../../common/ecs-features/ecsFeatureClient"; import { EnableMetadataDiff } from "../../../common/ecs-features/ecsFeatureGates"; import { oneDSLoggerWrapper } from "../../../common/OneDSLoggerTelemetry/oneDSLoggerWrapper"; -import { Constants, SUCCESS } from "../../../common/power-pages/metadata-diff/Constants"; +import { Constants } from "../../../common/power-pages/metadata-diff/Constants"; import { MetadataDiffTreeDataProvider } from "../../../common/power-pages/metadata-diff/MetadataDiffTreeDataProvider"; import { PacTerminal } from "../../lib/PacTerminal"; -import { createAuthProfileExp } from "../../../common/utilities/PacAuthUtil"; -import path from "path"; -import { getWebsiteRecordId } from "../../../common/utilities/WorkspaceInfoFinderUtil"; import { PagesList } from "../../pac/PacTypes"; - -interface DiffFile { - relativePath: string; - changes: string; - type: string; - workspaceContent?: string; - storageContent?: string; -} - -interface MetadataDiffReport { - generatedOn: string; - files: DiffFile[]; -} +import { registerMetadataDiffCommands } from "./MetadataDiffCommands"; export class MetadataDiffDesktop { private static _isInitialized = false; @@ -47,248 +32,6 @@ export class MetadataDiffDesktop { return; } - // Register command for handling file diffs - vscode.commands.registerCommand('metadataDiff.openDiff', async (workspaceFile: string, storedFile: string) => { - try { - const workspaceUri = vscode.Uri.file(workspaceFile); - const storedUri = vscode.Uri.file(storedFile); - const fileName = path.basename(workspaceFile); - - await vscode.commands.executeCommand('vscode.diff', - storedUri, - workspaceUri, - `${fileName} (Metadata Diff)` - ); - } catch (error) { - oneDSLoggerWrapper.getLogger().traceError( - Constants.EventNames.METADATA_DIFF_REPORT_FAILED, - error as string, - error as Error - ); - vscode.window.showErrorMessage("Failed to open diff view"); - } - }); - - vscode.commands.registerCommand("microsoft.powerplatform.pages.metadataDiff.triggerFlow", async () => { - try { - const orgUrl = await vscode.window.showInputBox({ - prompt: "Enter the organization URL", - placeHolder: "https://your-org.crm.dynamics.com" - }); - - if (!orgUrl) { - vscode.window.showErrorMessage("Organization URL is required to trigger the metadata diff flow."); - return; - } - - const urlPattern = /^https:\/\/[a-zA-Z0-9.-]+\d*\.crm\.dynamics\.com\/?$/; - if (!urlPattern.test(orgUrl)) { - vscode.window.showErrorMessage("Invalid organization URL. Please enter a valid URL in the format: https://your-org.crm.dynamics.com", { modal: true }); - return; - } - - const pacWrapper = pacTerminal.getWrapper() - const pacActiveOrg = await pacWrapper.activeOrg(); - if(pacActiveOrg){ - if (pacActiveOrg.Status === SUCCESS) { - if(pacActiveOrg.Results.OrgUrl == orgUrl){ - vscode.window.showInformationMessage("Already connected to the specified environment."); - } - else{ - const pacOrgSelect = await pacWrapper.orgSelect(orgUrl); - if(pacOrgSelect && pacOrgSelect.Status === SUCCESS){ - vscode.window.showInformationMessage("Environment switched successfully."); - } - else{ - vscode.window.showErrorMessage("Failed to switch the environment."); - return; - } - } - } - else{ - await createAuthProfileExp(pacWrapper, orgUrl); - vscode.window.showInformationMessage("Auth profile created successfully."); - } - } - else { - vscode.window.showErrorMessage("Failed to fetch the current environment details."); - return; - } - - const workspaceFolders = vscode.workspace.workspaceFolders; - if (!workspaceFolders || workspaceFolders.length === 0) { - vscode.window.showErrorMessage("No folders opened in the current workspace."); - return; - } - - const currentWorkspaceFolder = workspaceFolders[0].uri.fsPath; - const websiteId = getWebsiteRecordId(currentWorkspaceFolder); - path.join(websiteId, "metadataDiffStorage"); - const storagePath = context.storageUri?.fsPath; - if (!storagePath) { - throw new Error("Storage path is not defined"); - } - - // Clean out any existing files in storagePath (so we have a "fresh" download) - if (fs.existsSync(storagePath)) { - fs.rmSync(storagePath, { recursive: true, force: true }); - } - fs.mkdirSync(storagePath, { recursive: true }); - const progressOptions: vscode.ProgressOptions = { - location: vscode.ProgressLocation.Notification, - title: "Downloading website metadata", - cancellable: false - }; - let pacPagesDownload; - await vscode.window.withProgress(progressOptions, async (progress) => { - progress.report({ message: "Looking for this website in the connected environment..." }); - const pacPagesList = await this.getPagesList(pacTerminal); - if (pacPagesList && pacPagesList.length > 0) { - const websiteRecord = pacPagesList.find((record) => record.id === websiteId); - if (!websiteRecord) { - vscode.window.showErrorMessage("Website not found in the connected environment."); - return; - } - progress.report({ message: `Downloading "${websiteRecord.name}" as ${websiteRecord.modelVersion === "v2" ? "enhanced" : "standard"} data model. Please wait...` }); - pacPagesDownload = await pacWrapper.pagesDownload(storagePath, websiteId, websiteRecord.modelVersion == "v1" ? "1" : "2"); - vscode.window.showInformationMessage("Download completed."); - } - }); - if (pacPagesDownload) { - const treeDataProvider = MetadataDiffTreeDataProvider.initialize(context); - context.subscriptions.push( - vscode.window.registerTreeDataProvider("microsoft.powerplatform.pages.metadataDiff", treeDataProvider) - ); - } - else{ - vscode.window.showErrorMessage("Failed to download metadata."); - } - - } - catch (error) { - oneDSLoggerWrapper.getLogger().traceError(Constants.EventNames.METADATA_DIFF_REFRESH_FAILED, error as string, error as Error, { methodName: null }, {}); - } - }); - - vscode.commands.registerCommand("microsoft.powerplatform.pages.metadataDiff.generateReport", async () => { - try { - const workspaceFolders = vscode.workspace.workspaceFolders; - if (!workspaceFolders) { - vscode.window.showErrorMessage("No workspace folder open"); - return; - } - - const storagePath = context.storageUri?.fsPath; - if (!storagePath) { - vscode.window.showErrorMessage("Storage path not found"); - return; - } - - // Generate report content - const reportContent = await generateDiffReport(workspaceFolders[0].uri.fsPath, storagePath); - - // Create and show the report document - const doc = await vscode.workspace.openTextDocument({ - content: reportContent, - language: 'markdown' - }); - await vscode.window.showTextDocument(doc, { - preview: true, - viewColumn: vscode.ViewColumn.Beside - }); - } catch (error) { - oneDSLoggerWrapper.getLogger().traceError( - Constants.EventNames.METADATA_DIFF_REPORT_FAILED, - error as string, - error as Error - ); - vscode.window.showErrorMessage("Failed to generate metadata diff report"); - } - }); - - vscode.commands.registerCommand("microsoft.powerplatform.pages.metadataDiff.exportReport", async () => { - try { - const workspaceFolders = vscode.workspace.workspaceFolders; - if (!workspaceFolders) { - vscode.window.showErrorMessage("No workspace folder open"); - return; - } - - const storagePath = context.storageUri?.fsPath; - if (!storagePath) { - vscode.window.showErrorMessage("Storage path not found"); - return; - } - - // Get diff files - const diffFiles = await getAllDiffFiles(workspaceFolders[0].uri.fsPath, storagePath); - - // Create report object - const report: MetadataDiffReport = { - generatedOn: new Date().toISOString(), - files: diffFiles - }; - - // Save dialog - const saveUri = await vscode.window.showSaveDialog({ - defaultUri: vscode.Uri.file('metadata-diff-report.json'), - filters: { - 'JSON files': ['json'] - } - }); - - if (saveUri) { - fs.writeFileSync(saveUri.fsPath, JSON.stringify(report, null, 2)); - vscode.window.showInformationMessage("Report exported successfully"); - } - } catch (error) { - oneDSLoggerWrapper.getLogger().traceError( - Constants.EventNames.METADATA_DIFF_REPORT_FAILED, - error as string, - error as Error - ); - vscode.window.showErrorMessage("Failed to export metadata diff report"); - } - }); - - vscode.commands.registerCommand("microsoft.powerplatform.pages.metadataDiff.importReport", async () => { - try { - const fileUri = await vscode.window.showOpenDialog({ - canSelectFiles: true, - canSelectFolders: false, - canSelectMany: false, - filters: { - 'JSON files': ['json'] - } - }); - - if (fileUri && fileUri[0]) { - const reportContent = fs.readFileSync(fileUri[0].fsPath, 'utf8'); - const report = JSON.parse(reportContent) as MetadataDiffReport; - - // Clean up any existing tree data provider - const treeDataProvider = new MetadataDiffTreeDataProvider(context); - - // Update the tree with imported data - await treeDataProvider.setDiffFiles(report.files); - - // Register the new tree data provider - context.subscriptions.push( - vscode.window.registerTreeDataProvider("microsoft.powerplatform.pages.metadataDiff", treeDataProvider) - ); - - vscode.window.showInformationMessage("Report imported successfully"); - } - } catch (error) { - oneDSLoggerWrapper.getLogger().traceError( - Constants.EventNames.METADATA_DIFF_REPORT_FAILED, - error as string, - error as Error - ); - vscode.window.showErrorMessage("Failed to import metadata diff report"); - } - }); - try { const isMetadataDiffEnabled = MetadataDiffDesktop.isEnabled(); @@ -311,6 +54,8 @@ export class MetadataDiffDesktop { } fs.mkdirSync(storagePath, { recursive: true }); + await registerMetadataDiffCommands(context, pacTerminal); + const treeDataProvider = MetadataDiffTreeDataProvider.initialize(context); context.subscriptions.push( vscode.window.registerTreeDataProvider("microsoft.powerplatform.pages.metadataDiff", treeDataProvider) @@ -327,7 +72,7 @@ export class MetadataDiffDesktop { static async getPagesList(pacTerminal: PacTerminal): Promise<{ name: string, id: string, modelVersion: string }[]> { const pacWrapper = pacTerminal.getWrapper(); const pagesListOutput = await pacWrapper.pagesList(); - if (pagesListOutput && pagesListOutput.Status === SUCCESS && pagesListOutput.Information) { + if (pagesListOutput && pagesListOutput.Status === "Success" && pagesListOutput.Information) { // Parse the list of pages from the string output const pagesList: PagesList[] = []; if (Array.isArray(pagesListOutput.Information)) { @@ -361,152 +106,3 @@ export class MetadataDiffDesktop { return []; } } - -async function generateDiffReport(workspacePath: string, storagePath: string): Promise { - let report = '# Power Pages Metadata Diff Report\n\n'; - report += `Generated on: ${new Date().toLocaleString()}\n\n`; - - // Get diff files - const diffFiles = await getAllDiffFiles(workspacePath, storagePath); - - // Group by folders - const folderStructure = new Map(); - diffFiles.forEach(file => { - const dirPath = path.dirname(file.relativePath); - if (!folderStructure.has(dirPath)) { - folderStructure.set(dirPath, []); - } - folderStructure.get(dirPath)!.push(file); - }); - - // Sort folders and generate report - const sortedFolders = Array.from(folderStructure.keys()).sort(); - for (const folder of sortedFolders) { - const files = folderStructure.get(folder)!; - - // Add folder header (skip for root) - if (folder !== '.') { - report += `## ${folder}\n\n`; - } - - // Add files - for (const file of files.sort((a, b) => a.relativePath.localeCompare(b.relativePath))) { - report += `- ${path.basename(file.relativePath)}\n`; - report += ` - Changes: ${file.changes}\n`; - } - report += '\n'; - } - - return report; -} - -async function getAllDiffFiles(workspacePath: string, storagePath: string): Promise { - const diffFiles: DiffFile[] = []; - - // Get website directory from storage path - const folderNames = fs.readdirSync(storagePath).filter(file => - fs.statSync(path.join(storagePath, file)).isDirectory() - ); - if (folderNames.length === 0) { - return diffFiles; - } - const websitePath = path.join(storagePath, folderNames[0]); - - // Get all files - const workspaceFiles = await getAllFiles(workspacePath); - const storageFiles = await getAllFiles(websitePath); - - // Create normalized path maps - const workspaceMap = new Map(workspaceFiles.map(f => { - const normalized = path.relative(workspacePath, f).replace(/\\/g, '/'); - return [normalized, f]; - })); - const storageMap = new Map(storageFiles.map(f => { - const normalized = path.relative(websitePath, f).replace(/\\/g, '/'); - return [normalized, f]; - })); - - // Compare files - for (const [normalized, workspaceFile] of workspaceMap.entries()) { - const storageFile = storageMap.get(normalized); - if (!storageFile) { - diffFiles.push({ - relativePath: normalized, - changes: 'Only in workspace', - type: getFileType(normalized), - workspaceContent: fs.readFileSync(workspaceFile, 'utf8').replace(/\r\n/g, '\n') - }); - continue; - } - - // Compare content - const workspaceContent = fs.readFileSync(workspaceFile, 'utf8').replace(/\r\n/g, '\n'); - const storageContent = fs.readFileSync(storageFile, 'utf8').replace(/\r\n/g, '\n'); - if (workspaceContent !== storageContent) { - diffFiles.push({ - relativePath: normalized, - changes: 'Modified', - type: getFileType(normalized), - workspaceContent, - storageContent - }); - } - } - - // Check for files only in storage - for (const [normalized, storageFile] of storageMap.entries()) { - if (!workspaceMap.has(normalized)) { - diffFiles.push({ - relativePath: normalized, - changes: 'Only in remote', - type: getFileType(normalized), - storageContent: fs.readFileSync(storageFile, 'utf8').replace(/\r\n/g, '\n') - }); - } - } - - return diffFiles; -} - -async function getAllFiles(dirPath: string): Promise { - const files: string[] = []; - - function traverse(currentPath: string) { - const entries = fs.readdirSync(currentPath, { withFileTypes: true }); - for (const entry of entries) { - const fullPath = path.join(currentPath, entry.name); - if (entry.isDirectory()) { - traverse(fullPath); - } else { - files.push(fullPath); - } - } - } - - traverse(dirPath); - return files; -} - -function getFileType(filePath: string): string { - const ext = path.extname(filePath).toLowerCase(); - const basename = path.basename(filePath).toLowerCase(); - - if (basename === 'webrole.yml') return 'Roles'; - if (basename === 'websitelanguage.yml') return 'Languages'; - if (ext === '.yml' && filePath.includes('webpages')) return 'Pages'; - if (ext === '.yml' && filePath.includes('webtemplates')) return 'Templates'; - if (ext === '.yml' && filePath.includes('webfiles')) return 'Files'; - - return 'Other'; -} - -function groupDiffsByType(files: DiffFile[]): Record { - return files.reduce((groups: Record, file) => { - const type = file.type || 'Other'; - if (!groups[type]) { - groups[type] = []; - } - groups[type].push(file); - return groups; - }, {}); -} diff --git a/src/client/power-pages/metadata-diff/MetadataDiffUtils.ts b/src/client/power-pages/metadata-diff/MetadataDiffUtils.ts new file mode 100644 index 000000000..8e2ea4c42 --- /dev/null +++ b/src/client/power-pages/metadata-diff/MetadataDiffUtils.ts @@ -0,0 +1,169 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +import * as fs from "fs"; +import * as path from "path"; + +export interface DiffFile { + relativePath: string; + changes: string; + type: string; + workspaceContent?: string; + storageContent?: string; +} + +export interface MetadataDiffReport { + generatedOn: string; + files: DiffFile[]; +} + +export async function generateDiffReport(workspacePath: string, storagePath: string): Promise { + let report = '# Power Pages Metadata Diff Report\n\n'; + report += `Generated on: ${new Date().toLocaleString()}\n\n`; + + // Get diff files + const diffFiles = await getAllDiffFiles(workspacePath, storagePath); + + // Group by folders + const folderStructure = new Map(); + diffFiles.forEach(file => { + const dirPath = path.dirname(file.relativePath); + if (!folderStructure.has(dirPath)) { + folderStructure.set(dirPath, []); + } + folderStructure.get(dirPath)!.push(file); + }); + + // Sort folders and generate report + const sortedFolders = Array.from(folderStructure.keys()).sort(); + for (const folder of sortedFolders) { + const files = folderStructure.get(folder)!; + + // Add folder header (skip for root) + if (folder !== '.') { + report += `## ${folder}\n\n`; + } + + // Add files + for (const file of files.sort((a, b) => a.relativePath.localeCompare(b.relativePath))) { + report += `- ${path.basename(file.relativePath)}\n`; + report += ` - Changes: ${file.changes}\n`; + } + report += '\n'; + } + + return report; +} + +export async function getAllDiffFiles(workspacePath: string, storagePath: string): Promise { + const diffFiles: DiffFile[] = []; + + // Get website directory from storage path + const folderNames = fs.readdirSync(storagePath).filter(file => + fs.statSync(path.join(storagePath, file)).isDirectory() + ); + if (folderNames.length === 0) { + return diffFiles; + } + const websitePath = path.join(storagePath, folderNames[0]); + + // Get all files + const workspaceFiles = await getAllFiles(workspacePath); + const storageFiles = await getAllFiles(websitePath); + + // Create normalized path maps + const workspaceMap = new Map(workspaceFiles.map(f => { + const normalized = path.relative(workspacePath, f).replace(/\\/g, '/'); + return [normalized, f]; + })); + const storageMap = new Map(storageFiles.map(f => { + const normalized = path.relative(websitePath, f).replace(/\\/g, '/'); + return [normalized, f]; + })); + + // Compare files + for (const [normalized, workspaceFile] of workspaceMap.entries()) { + const storageFile = storageMap.get(normalized); + if (!storageFile) { + diffFiles.push({ + relativePath: normalized, + changes: 'Only in workspace', + type: getFileType(normalized), + workspaceContent: fs.readFileSync(workspaceFile, 'utf8').replace(/\r\n/g, '\n') + }); + continue; + } + + // Compare content + const workspaceContent = fs.readFileSync(workspaceFile, 'utf8').replace(/\r\n/g, '\n'); + const storageContent = fs.readFileSync(storageFile, 'utf8').replace(/\r\n/g, '\n'); + if (workspaceContent !== storageContent) { + diffFiles.push({ + relativePath: normalized, + changes: 'Modified', + type: getFileType(normalized), + workspaceContent, + storageContent + }); + } + } + + // Check for files only in storage + for (const [normalized, storageFile] of storageMap.entries()) { + if (!workspaceMap.has(normalized)) { + diffFiles.push({ + relativePath: normalized, + changes: 'Only in remote', + type: getFileType(normalized), + storageContent: fs.readFileSync(storageFile, 'utf8').replace(/\r\n/g, '\n') + }); + } + } + + return diffFiles; +} + +export async function getAllFiles(dirPath: string): Promise { + const files: string[] = []; + + function traverse(currentPath: string) { + const entries = fs.readdirSync(currentPath, { withFileTypes: true }); + for (const entry of entries) { + const fullPath = path.join(currentPath, entry.name); + if (entry.isDirectory()) { + traverse(fullPath); + } else { + files.push(fullPath); + } + } + } + + traverse(dirPath); + return files; +} + +export function getFileType(filePath: string): string { + const ext = path.extname(filePath).toLowerCase(); + const basename = path.basename(filePath).toLowerCase(); + + if (basename === 'webrole.yml') return 'Roles'; + if (basename === 'websitelanguage.yml') return 'Languages'; + if (ext === '.yml' && filePath.includes('webpages')) return 'Pages'; + if (ext === '.yml' && filePath.includes('webtemplates')) return 'Templates'; + if (ext === '.yml' && filePath.includes('webfiles')) return 'Files'; + + return 'Other'; +} + +export function groupDiffsByType(files: DiffFile[]): Record { + return files.reduce((groups: Record, file) => { + const type = file.type || 'Other'; + if (!groups[type]) { + groups[type] = []; + } + groups[type].push(file); + return groups; + }, {}); +} From 841dee815185e767e5c8393199f2e1ef831267d4 Mon Sep 17 00:00:00 2001 From: Rishabh Jain Date: Wed, 23 Apr 2025 12:53:11 +0530 Subject: [PATCH 09/15] removed file type logic --- .../metadata-diff/MetadataDiffUtils.ts | 19 +++---------------- .../MetadataDiffTreeDataProvider.ts | 12 ------------ 2 files changed, 3 insertions(+), 28 deletions(-) diff --git a/src/client/power-pages/metadata-diff/MetadataDiffUtils.ts b/src/client/power-pages/metadata-diff/MetadataDiffUtils.ts index 8e2ea4c42..791a624f0 100644 --- a/src/client/power-pages/metadata-diff/MetadataDiffUtils.ts +++ b/src/client/power-pages/metadata-diff/MetadataDiffUtils.ts @@ -90,7 +90,7 @@ export async function getAllDiffFiles(workspacePath: string, storagePath: string diffFiles.push({ relativePath: normalized, changes: 'Only in workspace', - type: getFileType(normalized), + type: path.dirname(normalized) || 'Other', workspaceContent: fs.readFileSync(workspaceFile, 'utf8').replace(/\r\n/g, '\n') }); continue; @@ -103,7 +103,7 @@ export async function getAllDiffFiles(workspacePath: string, storagePath: string diffFiles.push({ relativePath: normalized, changes: 'Modified', - type: getFileType(normalized), + type: path.dirname(normalized) || 'Other', workspaceContent, storageContent }); @@ -116,7 +116,7 @@ export async function getAllDiffFiles(workspacePath: string, storagePath: string diffFiles.push({ relativePath: normalized, changes: 'Only in remote', - type: getFileType(normalized), + type: path.dirname(normalized) || 'Other', storageContent: fs.readFileSync(storageFile, 'utf8').replace(/\r\n/g, '\n') }); } @@ -144,19 +144,6 @@ export async function getAllFiles(dirPath: string): Promise { return files; } -export function getFileType(filePath: string): string { - const ext = path.extname(filePath).toLowerCase(); - const basename = path.basename(filePath).toLowerCase(); - - if (basename === 'webrole.yml') return 'Roles'; - if (basename === 'websitelanguage.yml') return 'Languages'; - if (ext === '.yml' && filePath.includes('webpages')) return 'Pages'; - if (ext === '.yml' && filePath.includes('webtemplates')) return 'Templates'; - if (ext === '.yml' && filePath.includes('webfiles')) return 'Files'; - - return 'Other'; -} - export function groupDiffsByType(files: DiffFile[]): Record { return files.reduce((groups: Record, file) => { const type = file.type || 'Other'; diff --git a/src/common/power-pages/metadata-diff/MetadataDiffTreeDataProvider.ts b/src/common/power-pages/metadata-diff/MetadataDiffTreeDataProvider.ts index a429d9ec4..78f2a84d9 100644 --- a/src/common/power-pages/metadata-diff/MetadataDiffTreeDataProvider.ts +++ b/src/common/power-pages/metadata-diff/MetadataDiffTreeDataProvider.ts @@ -201,18 +201,6 @@ export class MetadataDiffTreeDataProvider implements vscode.TreeDataProvider Date: Wed, 23 Apr 2025 13:33:21 +0530 Subject: [PATCH 10/15] Added clear view functionality --- package.json | 31 ++++++++++++++++--- .../metadata-diff/MetadataDiffCommands.ts | 24 ++++++++++++++ .../metadata-diff/MetadataDiffDesktop.ts | 10 ++++++ .../MetadataDiffTreeDataProvider.ts | 17 ++++++++++ 4 files changed, 77 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 6d6aca95b..9de7b351f 100644 --- a/package.json +++ b/package.json @@ -447,21 +447,32 @@ }, { "command": "microsoft.powerplatform.pages.metadataDiff.triggerFlow", - "title": "%microsoft.powerplatform.pages.metadataDiff.login.title%" + "title": "Connect & Download", + "category": "Power Pages Metadata Diff", + "icon": "$(cloud-download)" }, { "command": "microsoft.powerplatform.pages.metadataDiff.generateReport", - "title": "Generate Metadata Diff Report", - "icon": "$(output)" + "title": "Generate Report", + "category": "Power Pages Metadata Diff", + "icon": "$(markdown)" + }, + { + "command": "microsoft.powerplatform.pages.metadataDiff.clearView", + "title": "Clear View", + "category": "Power Pages Metadata Diff", + "icon": "$(clear-all)" }, { "command": "microsoft.powerplatform.pages.metadataDiff.exportReport", - "title": "Export Metadata Diff Report", + "title": "Export Report", + "category": "Power Pages Metadata Diff", "icon": "$(save-as)" }, { "command": "microsoft.powerplatform.pages.metadataDiff.importReport", - "title": "Import Metadata Diff Report", + "title": "Import Report", + "category": "Power Pages Metadata Diff", "icon": "$(folder-opened)" } ], @@ -980,6 +991,16 @@ "when": "view == microsoft.powerplatform.pages.metadataDiff", "group": "navigation" }, + { + "command": "microsoft.powerplatform.pages.metadataDiff.triggerFlow", + "when": "view == microsoft.powerplatform.pages.metadataDiff", + "group": "navigation" + }, + { + "command": "microsoft.powerplatform.pages.metadataDiff.clearView", + "when": "view == microsoft.powerplatform.pages.metadataDiff", + "group": "navigation" + }, { "command": "microsoft.powerplatform.pages.metadataDiff.exportReport", "when": "view == microsoft.powerplatform.pages.metadataDiff", diff --git a/src/client/power-pages/metadata-diff/MetadataDiffCommands.ts b/src/client/power-pages/metadata-diff/MetadataDiffCommands.ts index e1031a6a9..628ceaaff 100644 --- a/src/client/power-pages/metadata-diff/MetadataDiffCommands.ts +++ b/src/client/power-pages/metadata-diff/MetadataDiffCommands.ts @@ -38,6 +38,30 @@ export async function registerMetadataDiffCommands(context: vscode.ExtensionCont } }); + vscode.commands.registerCommand("microsoft.powerplatform.pages.metadataDiff.clearView", async () => { + try { + MetadataDiffDesktop.resetTreeView(); + + // Set the context variable to false to show welcome message + await vscode.commands.executeCommand("setContext", "microsoft.powerplatform.pages.metadataDiff.hasData", false); + + // Reload tree data provider to show welcome message + const treeDataProvider = MetadataDiffTreeDataProvider.initialize(context); + context.subscriptions.push( + vscode.window.registerTreeDataProvider("microsoft.powerplatform.pages.metadataDiff", treeDataProvider) + ); + + vscode.window.showInformationMessage("Metadata diff view cleared successfully."); + } catch (error) { + oneDSLoggerWrapper.getLogger().traceError( + Constants.EventNames.METADATA_DIFF_REPORT_FAILED, + error as string, + error as Error + ); + vscode.window.showErrorMessage("Failed to clear metadata diff view"); + } + }); + vscode.commands.registerCommand("microsoft.powerplatform.pages.metadataDiff.triggerFlow", async () => { try { const orgUrl = await vscode.window.showInputBox({ diff --git a/src/client/power-pages/metadata-diff/MetadataDiffDesktop.ts b/src/client/power-pages/metadata-diff/MetadataDiffDesktop.ts index 70fff4d71..df2eb16d7 100644 --- a/src/client/power-pages/metadata-diff/MetadataDiffDesktop.ts +++ b/src/client/power-pages/metadata-diff/MetadataDiffDesktop.ts @@ -16,6 +16,7 @@ import { registerMetadataDiffCommands } from "./MetadataDiffCommands"; export class MetadataDiffDesktop { private static _isInitialized = false; + private static _treeDataProvider: MetadataDiffTreeDataProvider | undefined; static isEnabled(): boolean { const enableMetadataDiff = ECSFeaturesClient.getConfig(EnableMetadataDiff).enableMetadataDiff @@ -27,6 +28,14 @@ export class MetadataDiffDesktop { return enableMetadataDiff; } + static resetTreeView(): void { + if (this._treeDataProvider) { + this._treeDataProvider.clearItems(); + // Force reset tree view context to show welcome message + vscode.commands.executeCommand("setContext", "microsoft.powerplatform.pages.metadataDiff.hasData", false); + } + } + static async initialize(context: vscode.ExtensionContext, pacTerminal: PacTerminal): Promise { if (MetadataDiffDesktop._isInitialized) { return; @@ -57,6 +66,7 @@ export class MetadataDiffDesktop { await registerMetadataDiffCommands(context, pacTerminal); const treeDataProvider = MetadataDiffTreeDataProvider.initialize(context); + MetadataDiffDesktop._treeDataProvider = treeDataProvider; context.subscriptions.push( vscode.window.registerTreeDataProvider("microsoft.powerplatform.pages.metadataDiff", treeDataProvider) ); diff --git a/src/common/power-pages/metadata-diff/MetadataDiffTreeDataProvider.ts b/src/common/power-pages/metadata-diff/MetadataDiffTreeDataProvider.ts index 78f2a84d9..1c71b9385 100644 --- a/src/common/power-pages/metadata-diff/MetadataDiffTreeDataProvider.ts +++ b/src/common/power-pages/metadata-diff/MetadataDiffTreeDataProvider.ts @@ -39,6 +39,23 @@ export class MetadataDiffTreeDataProvider implements vscode.TreeDataProvider { return element; } From 55930d084f49d2dc302eba88d13895ddfa20d2ba Mon Sep 17 00:00:00 2001 From: Rishabh Jain Date: Wed, 23 Apr 2025 14:36:54 +0530 Subject: [PATCH 11/15] Added dropdown to org picker --- .../metadata-diff/MetadataDiffCommands.ts | 59 +++++++++++++++++-- 1 file changed, 54 insertions(+), 5 deletions(-) diff --git a/src/client/power-pages/metadata-diff/MetadataDiffCommands.ts b/src/client/power-pages/metadata-diff/MetadataDiffCommands.ts index 628ceaaff..e34a34bd2 100644 --- a/src/client/power-pages/metadata-diff/MetadataDiffCommands.ts +++ b/src/client/power-pages/metadata-diff/MetadataDiffCommands.ts @@ -64,10 +64,60 @@ export async function registerMetadataDiffCommands(context: vscode.ExtensionCont vscode.commands.registerCommand("microsoft.powerplatform.pages.metadataDiff.triggerFlow", async () => { try { - const orgUrl = await vscode.window.showInputBox({ - prompt: "Enter the organization URL", - placeHolder: "https://your-org.crm.dynamics.com" - }); + // Get the PAC wrapper to access org list + const pacWrapper = pacTerminal.getWrapper(); + + let orgUrl: string | undefined; + + // Get list of available organizations + const orgListResult = await pacWrapper.orgList(); + + if (orgListResult && orgListResult.Status === SUCCESS && orgListResult.Results.length > 0) { + // Create items for QuickPick + const items = orgListResult.Results.map(org => { + return { + label: org.FriendlyName, + description: org.EnvironmentUrl, + detail: `${org.OrganizationId} (${org.EnvironmentId})` + }; + }); + + // Add option to enter URL manually + items.push({ + label: "$(plus) Enter organization URL manually", + description: "", + detail: "Enter a custom organization URL" + }); + + // Show QuickPick to select environment + const selected = await vscode.window.showQuickPick(items, { + placeHolder: "Select an environment or enter URL manually", + ignoreFocusOut: true + }); + + if (selected) { + if (selected.description) { + // Use the selected org URL + orgUrl = selected.description; + } else { + // If manual entry option was selected + orgUrl = await vscode.window.showInputBox({ + prompt: "Enter the organization URL", + placeHolder: "https://your-org.crm.dynamics.com", + validateInput: (input) => { + const urlPattern = /^https:\/\/[a-zA-Z0-9.-]+\d*\.crm\.dynamics\.com\/?$/; + return urlPattern.test(input) ? null : "Please enter a valid URL in the format: https://your-org.crm.dynamics.com"; + } + }); + } + } + } else { + // Fallback to manual entry if no orgs are found + orgUrl = await vscode.window.showInputBox({ + prompt: "Enter the organization URL", + placeHolder: "https://your-org.crm.dynamics.com" + }); + } if (!orgUrl) { vscode.window.showErrorMessage("Organization URL is required to trigger the metadata diff flow."); @@ -80,7 +130,6 @@ export async function registerMetadataDiffCommands(context: vscode.ExtensionCont return; } - const pacWrapper = pacTerminal.getWrapper() const pacActiveOrg = await pacWrapper.activeOrg(); if(pacActiveOrg){ if (pacActiveOrg.Status === SUCCESS) { From 404d68b43df7800161b61b0161e446c1e29b9f0b Mon Sep 17 00:00:00 2001 From: Rishabh Jain Date: Wed, 23 Apr 2025 14:51:44 +0530 Subject: [PATCH 12/15] Fix report import file type parsing --- .../metadata-diff/MetadataDiffTreeDataProvider.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/common/power-pages/metadata-diff/MetadataDiffTreeDataProvider.ts b/src/common/power-pages/metadata-diff/MetadataDiffTreeDataProvider.ts index 1c71b9385..f00571da3 100644 --- a/src/common/power-pages/metadata-diff/MetadataDiffTreeDataProvider.ts +++ b/src/common/power-pages/metadata-diff/MetadataDiffTreeDataProvider.ts @@ -259,16 +259,21 @@ export class MetadataDiffTreeDataProvider implements vscode.TreeDataProvider Date: Wed, 23 Apr 2025 15:39:20 +0530 Subject: [PATCH 13/15] Improved md report ensuring there is no cx data --- .../metadata-diff/MetadataDiffCommands.ts | 10 ++- .../metadata-diff/MetadataDiffUtils.ts | 85 ++++++++++++++++++- 2 files changed, 91 insertions(+), 4 deletions(-) diff --git a/src/client/power-pages/metadata-diff/MetadataDiffCommands.ts b/src/client/power-pages/metadata-diff/MetadataDiffCommands.ts index e34a34bd2..c95f4ac05 100644 --- a/src/client/power-pages/metadata-diff/MetadataDiffCommands.ts +++ b/src/client/power-pages/metadata-diff/MetadataDiffCommands.ts @@ -234,10 +234,16 @@ export async function registerMetadataDiffCommands(context: vscode.ExtensionCont content: reportContent, language: 'markdown' }); + + // Force the document to be opened in preview mode with explicit encoding await vscode.window.showTextDocument(doc, { - preview: true, - viewColumn: vscode.ViewColumn.Beside + preview: false, // Set to false to ensure it opens properly + viewColumn: vscode.ViewColumn.Beside, + preserveFocus: true }); + + // Force markdown preview to open + await vscode.commands.executeCommand('markdown.showPreview', doc.uri); } catch (error) { oneDSLoggerWrapper.getLogger().traceError( Constants.EventNames.METADATA_DIFF_REPORT_FAILED, diff --git a/src/client/power-pages/metadata-diff/MetadataDiffUtils.ts b/src/client/power-pages/metadata-diff/MetadataDiffUtils.ts index 791a624f0..653e88e3a 100644 --- a/src/client/power-pages/metadata-diff/MetadataDiffUtils.ts +++ b/src/client/power-pages/metadata-diff/MetadataDiffUtils.ts @@ -38,7 +38,8 @@ export async function generateDiffReport(workspacePath: string, storagePath: str // Sort folders and generate report const sortedFolders = Array.from(folderStructure.keys()).sort(); - for (const folder of sortedFolders) { + for (let i = 0; i < sortedFolders.length; i++) { + const folder = sortedFolders[i]; const files = folderStructure.get(folder)!; // Add folder header (skip for root) @@ -50,13 +51,93 @@ export async function generateDiffReport(workspacePath: string, storagePath: str for (const file of files.sort((a, b) => a.relativePath.localeCompare(b.relativePath))) { report += `- ${path.basename(file.relativePath)}\n`; report += ` - Changes: ${file.changes}\n`; + + // Add detailed YAML property changes for YAML files + if (path.extname(file.relativePath).toLowerCase() === '.yml' || + path.extname(file.relativePath).toLowerCase() === '.yaml') { + try { + if (file.workspaceContent && file.storageContent && file.changes === 'Modified') { + const yaml = require('yaml'); + const workspaceYaml = yaml.parse(file.workspaceContent); + const storageYaml = yaml.parse(file.storageContent); + + const propertyChanges = findYamlPropertyChanges(workspaceYaml, storageYaml); + + if (propertyChanges.length > 0) { + report += ` - Property changes:\n`; + propertyChanges.forEach(change => { + report += ` - ${change}\n`; + }); + } + } + } catch (error) { + report += ` - Failed to parse YAML content: ${error}\n`; + } + } + } + + // Only add a blank line between folders, not after the last folder + if (i < sortedFolders.length - 1) { + report += '\n'; } - report += '\n'; } return report; } +/** + * Finds differences between two YAML objects and returns a list of changes + * @param workspaceObj The workspace YAML object + * @param storageObj The storage YAML object + * @returns An array of change descriptions + */ +function findYamlPropertyChanges(workspaceObj: any, storageObj: any, path = ''): string[] { + if (!workspaceObj || !storageObj) { + return []; + } + + const changes: string[] = []; + + // Check all properties in workspace object + const workspaceKeys = Object.keys(workspaceObj); + for (const key of workspaceKeys) { + const currentPath = path ? `${path}.${key}` : key; + + // Check if key exists in storage + if (!(key in storageObj)) { + changes.push(`Added property: \`${currentPath}\``); + continue; + } + + // Check if both values are objects (for nested properties) + if (typeof workspaceObj[key] === 'object' && workspaceObj[key] !== null && + typeof storageObj[key] === 'object' && storageObj[key] !== null) { + // Recursively check nested objects + const nestedChanges = findYamlPropertyChanges( + workspaceObj[key], + storageObj[key], + currentPath + ); + changes.push(...nestedChanges); + } + // Check primitive values (string, number, boolean, etc.) + else if (JSON.stringify(workspaceObj[key]) !== JSON.stringify(storageObj[key])) { + changes.push(`Modified property: \`${currentPath}\``); + } + } + + // Check for properties that exist in storage but not in workspace + const storageKeys = Object.keys(storageObj); + for (const key of storageKeys) { + const currentPath = path ? `${path}.${key}` : key; + if (!(key in workspaceObj)) { + changes.push(`Removed property: \`${currentPath}\``); + } + } + + return changes; +} + export async function getAllDiffFiles(workspacePath: string, storagePath: string): Promise { const diffFiles: DiffFile[] = []; From ab0f6c794e7737716ec548b9e19ef596de1555ec Mon Sep 17 00:00:00 2001 From: Rishabh Jain Date: Fri, 25 Apr 2025 13:31:56 +0530 Subject: [PATCH 14/15] refactor --- src/client/power-pages/metadata-diff/MetadataDiffUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/power-pages/metadata-diff/MetadataDiffUtils.ts b/src/client/power-pages/metadata-diff/MetadataDiffUtils.ts index 653e88e3a..e452c4b70 100644 --- a/src/client/power-pages/metadata-diff/MetadataDiffUtils.ts +++ b/src/client/power-pages/metadata-diff/MetadataDiffUtils.ts @@ -5,6 +5,7 @@ import * as fs from "fs"; import * as path from "path"; +import * as yaml from 'yaml'; export interface DiffFile { relativePath: string; @@ -57,7 +58,6 @@ export async function generateDiffReport(workspacePath: string, storagePath: str path.extname(file.relativePath).toLowerCase() === '.yaml') { try { if (file.workspaceContent && file.storageContent && file.changes === 'Modified') { - const yaml = require('yaml'); const workspaceYaml = yaml.parse(file.workspaceContent); const storageYaml = yaml.parse(file.storageContent); From de9ae55779073ef8815c563ab7294abc13cdcb62 Mon Sep 17 00:00:00 2001 From: Rishabh Jain Date: Fri, 25 Apr 2025 13:48:20 +0530 Subject: [PATCH 15/15] fixed md report preview issue --- .../metadata-diff/MetadataDiffCommands.ts | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/src/client/power-pages/metadata-diff/MetadataDiffCommands.ts b/src/client/power-pages/metadata-diff/MetadataDiffCommands.ts index c95f4ac05..d73d31895 100644 --- a/src/client/power-pages/metadata-diff/MetadataDiffCommands.ts +++ b/src/client/power-pages/metadata-diff/MetadataDiffCommands.ts @@ -229,21 +229,28 @@ export async function registerMetadataDiffCommands(context: vscode.ExtensionCont // Generate report content const reportContent = await generateDiffReport(workspaceFolders[0].uri.fsPath, storagePath); - // Create and show the report document + // Create the markdown document const doc = await vscode.workspace.openTextDocument({ content: reportContent, language: 'markdown' }); - // Force the document to be opened in preview mode with explicit encoding + // Show the document in column One await vscode.window.showTextDocument(doc, { - preview: false, // Set to false to ensure it opens properly - viewColumn: vscode.ViewColumn.Beside, - preserveFocus: true + preview: false, // Don't use preview mode to ensure stability + viewColumn: vscode.ViewColumn.One, + preserveFocus: false // Ensure focus is on the document }); - // Force markdown preview to open - await vscode.commands.executeCommand('markdown.showPreview', doc.uri); + // Increase delay to ensure document is fully loaded and stable + await new Promise(resolve => setTimeout(resolve, 800)); + + // Close any existing preview first to avoid conflicts + await vscode.commands.executeCommand('markdown.preview.refresh'); + + // Show preview in side-by-side mode + await vscode.commands.executeCommand('markdown.showPreviewToSide'); + } catch (error) { oneDSLoggerWrapper.getLogger().traceError( Constants.EventNames.METADATA_DIFF_REPORT_FAILED,