Skip to content
10 changes: 10 additions & 0 deletions l10n/bundle.l10n.json
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,9 @@
"User cancelled environment switch": "User cancelled environment switch",
"User cancelled folder selection": "User cancelled folder selection",
"Download failed: {0}": "Download failed: {0}",
"Metadata diff import URI is missing the required 'filePath' query parameter.": "Metadata diff import URI is missing the required 'filePath' query parameter.",
"Metadata diff import URI must reference an absolute file path without '..' segments.": "Metadata diff import URI must reference an absolute file path without '..' segments.",
"Metadata diff import failed: {0}": "Metadata diff import failed: {0}",
"Site download cancelled. Authentication is required to proceed.": "Site download cancelled. Authentication is required to proceed.",
"Site download cancelled. Correct environment connection is required.": "Site download cancelled. Correct environment connection is required.",
"Site download cancelled. No folder selected.": "Site download cancelled. No folder selected.",
Expand Down Expand Up @@ -435,6 +438,7 @@
"Export Site Comparison": "Export Site Comparison",
"Importing comparison...": "Importing comparison...",
"Import Site Comparison": "Import Site Comparison",
"Metadata diff file must be a .json file.": "Metadata diff file must be a .json file.",
"Invalid file format. The file does not contain valid metadata diff data.": "Invalid file format. The file does not contain valid metadata diff data.",
"Unsupported version. This file was created with a newer version of the extension.": "Unsupported version. This file was created with a newer version of the extension.",
"This file was exported with a newer version of the extension. Please update your extension to import this file.": "This file was exported with a newer version of the extension. Please update your extension to import this file.",
Expand Down Expand Up @@ -613,6 +617,12 @@
"Error message when import fails. {0} is the error message."
]
},
"Metadata diff file not found: {0}/Error message when the supplied import file is missing. {0} is the file path.": {
"message": "Metadata diff file not found: {0}",
"comment": [
"Error message when the supplied import file is missing. {0} is the file path."
]
},
"Refreshing comparison for {0} ([details](command:microsoft.powerplatform.pages.actionsHub.showOutputChannel \"Show download output\")).../This is a markdown formatting which must persist across translations.": {
"message": "Refreshing comparison for {0} ([details](command:microsoft.powerplatform.pages.actionsHub.showOutputChannel \"Show download output\"))...",
"comment": [
Expand Down
16 changes: 16 additions & 0 deletions loc/translations-export/vscode-powerplatform.xlf
Original file line number Diff line number Diff line change
Expand Up @@ -825,6 +825,22 @@ Return to this chat and @powerpages can help you write and edit your website cod
<trans-unit id="++CODE++8a194d5287e3da019883144787cd5f282753f5ce892d933be0833b3818c25503">
<source xml:lang="en">Maximum 30 characters allowed</source>
</trans-unit>
<trans-unit id="++CODE++3ab101664903513d135cf6e3a5e2f260910f63b67cdf40df9266777777989cd9">
<source xml:lang="en">Metadata diff file must be a .json file.</source>
</trans-unit>
<trans-unit id="++CODE++c9e90f17b8b088e896b5de437e61ed7b0df4ccd3fdcbf82c82bfda388f3859bb">
<source xml:lang="en">Metadata diff file not found: {0}</source>
<note>Error message when the supplied import file is missing. {0} is the file path.</note>
</trans-unit>
<trans-unit id="++CODE++83e1d90c7af88ff613efd0f32526d98bb9f37f4401422ea5b7ac512a2a86857a">
<source xml:lang="en">Metadata diff import URI is missing the required 'filePath' query parameter.</source>
</trans-unit>
<trans-unit id="++CODE++63de590b02582ef032b1d20773a96c3341b92bcd46ccb62914b85b219914ae72">
<source xml:lang="en">Metadata diff import URI must reference an absolute file path without '..' segments.</source>
</trans-unit>
<trans-unit id="++CODE++b70ce9905812faa5fb6d0b257f49dab5ea4b7671ff776b0ba480550dd7b5669e">
<source xml:lang="en">Metadata diff import failed: {0}</source>
</trans-unit>
<trans-unit id="++CODE++b1c8258fb401975727a82d9ce473af0a600f076cc7b73dbd16524e815cf4153d">
<source xml:lang="en">Microsoft wants your feedback</source>
</trans-unit>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -386,7 +386,9 @@ export class ActionsHubTreeDataProvider implements vscode.TreeDataProvider<Actio
vscode.commands.registerCommand(Constants.Commands.METADATA_DIFF_DISCARD_SITE, discardSiteChanges),
vscode.commands.registerCommand(Constants.Commands.METADATA_DIFF_GENERATE_HTML_REPORT, generateHtmlReport),
vscode.commands.registerCommand(Constants.Commands.METADATA_DIFF_EXPORT, exportMetadataDiff),
vscode.commands.registerCommand(Constants.Commands.METADATA_DIFF_IMPORT, importMetadataDiff),
// Invoked from tree-item menus, which pass the tree node as the first
// argument; drop it so the command always uses the file-picker flow.
vscode.commands.registerCommand(Constants.Commands.METADATA_DIFF_IMPORT, () => importMetadataDiff()),
vscode.commands.registerCommand(Constants.Commands.METADATA_DIFF_RESYNC, resyncMetadataDiff(this._pacTerminal, this._context)),
MetadataDiffDecorationProvider.getInstance().register(),
ReadOnlyContentProvider.getInstance().register()
Expand Down
11 changes: 11 additions & 0 deletions src/client/power-pages/actions-hub/Constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,7 @@ export const Constants = {
METADATA_DIFF_EXPORT_TITLE: vscode.l10n.t("Export Site Comparison"),
METADATA_DIFF_IMPORT_PROGRESS: vscode.l10n.t("Importing comparison..."),
METADATA_DIFF_IMPORT_TITLE: vscode.l10n.t("Import Site Comparison"),
METADATA_DIFF_IMPORT_NOT_JSON: vscode.l10n.t("Metadata diff file must be a .json file."),
METADATA_DIFF_EXPORT_INVALID_FILE: vscode.l10n.t("Invalid file format. The file does not contain valid metadata diff data."),
METADATA_DIFF_EXPORT_UNSUPPORTED_VERSION: vscode.l10n.t("Unsupported version. This file was created with a newer version of the extension."),
METADATA_DIFF_EXPORT_NEWER_EXTENSION_VERSION: vscode.l10n.t("This file was exported with a newer version of the extension. Please update your extension to import this file."),
Expand Down Expand Up @@ -520,6 +521,15 @@ export const Constants = {
args: [errorMessage],
comment: ["Error message when import fails. {0} is the error message."]
}),
/**
* Returns the error message when a pre-supplied import file does not exist
*/
METADATA_DIFF_IMPORT_FILE_NOT_FOUND: (filePath: string) =>
vscode.l10n.t({
message: "Metadata diff file not found: {0}",
args: [filePath],
comment: ["Error message when the supplied import file is missing. {0} is the file path."]
}),
/**
* Returns the progress message when resyncing a site comparison
*/
Expand Down Expand Up @@ -708,6 +718,7 @@ export const Constants = {
ACTIONS_HUB_METADATA_DIFF_IMPORT_CALLED: "ActionsHubMetadataDiffImportCalled",
ACTIONS_HUB_METADATA_DIFF_IMPORT_SUCCESS: "ActionsHubMetadataDiffImportSuccess",
ACTIONS_HUB_METADATA_DIFF_IMPORT_FAILED: "ActionsHubMetadataDiffImportFailed",
ACTIONS_HUB_METADATA_DIFF_IMPORT_AUTO_OPEN_FAILED: "ActionsHubMetadataDiffImportAutoOpenFailed",
ACTIONS_HUB_METADATA_DIFF_RESYNC_CALLED: "ActionsHubMetadataDiffResyncCalled",
ACTIONS_HUB_METADATA_DIFF_RESYNC_COMPLETED: "ActionsHubMetadataDiffResyncCompleted",
ACTIONS_HUB_METADATA_DIFF_RESYNC_FAILED: "ActionsHubMetadataDiffResyncFailed",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ import * as fs from "fs";
import path from "path";
import { Constants } from "../../Constants";
import { traceError, traceInfo } from "../../TelemetryHelper";
import { IMetadataDiffExport, METADATA_DIFF_EXPORT_VERSION } from "../../models/IMetadataDiffExport";
import { IExportableFileComparisonResult, IMetadataDiffExport, METADATA_DIFF_EXPORT_VERSION } from "../../models/IMetadataDiffExport";
import { FileComparisonStatus, IFileComparisonResult } from "../../models/IFileComparisonResult";
import MetadataDiffContext from "../../MetadataDiffContext";
import { MetadataDiffFileTreeItem } from "../../tree-items/metadata-diff/MetadataDiffFileTreeItem";
import { getExtensionVersion } from "../../../../../common/utilities/Utils";

/**
Expand Down Expand Up @@ -112,26 +113,59 @@ function validateImportData(data: unknown): string | undefined {
/**
* Imports a metadata diff from a JSON file
*/
export async function importMetadataDiff(): Promise<void> {
export async function importMetadataDiff(
presuppliedFileUri?: vscode.Uri,
options?: { openFirstFile?: boolean }
): Promise<void> {
traceInfo(Constants.EventNames.ACTIONS_HUB_METADATA_DIFF_IMPORT_CALLED, {
methodName: importMetadataDiff.name
methodName: importMetadataDiff.name,
source: presuppliedFileUri ? "uri_handler" : "command_palette"
});

try {
// Show open dialog first (before progress)
const openUris = await vscode.window.showOpenDialog({
canSelectMany: false,
filters: {
[Constants.Strings.METADATA_DIFF_EXPORT_FILTER_NAME]: ["json"]
},
title: Constants.Strings.METADATA_DIFF_IMPORT_TITLE
});
let fileUri: vscode.Uri;

if (!openUris || openUris.length === 0) {
return; // User cancelled
}
if (presuppliedFileUri) {
// URI-handler path: validate the supplied file before proceeding.
if (!fs.existsSync(presuppliedFileUri.fsPath)) {
vscode.window.showErrorMessage(
Constants.StringFunctions.METADATA_DIFF_IMPORT_FILE_NOT_FOUND(presuppliedFileUri.fsPath)
);
traceError(
Constants.EventNames.ACTIONS_HUB_METADATA_DIFF_IMPORT_FAILED,
new Error("Pre-supplied file path does not exist"),
{ methodName: importMetadataDiff.name, reason: "file_not_found" }
);
return;
}
if (!presuppliedFileUri.fsPath.toLowerCase().endsWith(".json")) {
vscode.window.showErrorMessage(
Constants.Strings.METADATA_DIFF_IMPORT_NOT_JSON
);
traceError(
Constants.EventNames.ACTIONS_HUB_METADATA_DIFF_IMPORT_FAILED,
new Error("Pre-supplied file is not .json"),
{ methodName: importMetadataDiff.name, reason: "wrong_extension" }
);
return;
}
fileUri = presuppliedFileUri;
} else {
// Command path: existing file-picker flow (unchanged).
const openUris = await vscode.window.showOpenDialog({
canSelectMany: false,
filters: {
[Constants.Strings.METADATA_DIFF_EXPORT_FILTER_NAME]: ["json"]
},
title: Constants.Strings.METADATA_DIFF_IMPORT_TITLE
});

if (!openUris || openUris.length === 0) {
return; // User cancelled
}

const fileUri = openUris[0];
fileUri = openUris[0];
}

// Read and parse the file first to validate before showing progress
let importData: IMetadataDiffExport;
Expand Down Expand Up @@ -189,6 +223,10 @@ export async function importMetadataDiff(): Promise<void> {
return;
}

// Hoisted so the success path can locate the first viewable file after the
// progress task completes (used for the optional auto-open below).
const comparisonResults: IFileComparisonResult[] = [];

// Now show progress while doing the actual file writing work
await vscode.window.withProgress(
{
Expand All @@ -215,8 +253,6 @@ export async function importMetadataDiff(): Promise<void> {
fs.mkdirSync(importedDiffsPath, { recursive: true });

// Write the file contents to the storage
const comparisonResults: IFileComparisonResult[] = [];

for (const file of importData.files) {
const localPath = path.join(importedDiffsPath, "local", file.relativePath);
const remotePath = path.join(importedDiffsPath, "remote", file.relativePath);
Expand Down Expand Up @@ -291,6 +327,63 @@ export async function importMetadataDiff(): Promise<void> {
vscode.window.showInformationMessage(
Constants.StringFunctions.METADATA_DIFF_IMPORT_SUCCESS(displayName)
);

// Optional auto-open (opt-in, e.g. from the URI deep-link flow): surface the
// first viewable file diff so the user lands directly on a comparison.
if (options?.openFirstFile) {
// A file can be opened when openMetadataDiffFile has content to show for its
// status: modified -> diff (needs both sides), added -> local, deleted -> remote.
const isOpenable = (f: IExportableFileComparisonResult): boolean => {
switch (f.status) {
case FileComparisonStatus.MODIFIED:
return f.localContent != null && f.remoteContent != null;
case FileComparisonStatus.ADDED:
return f.localContent != null;
case FileComparisonStatus.DELETED:
return f.remoteContent != null;
default:
return false;
}
};
// Prefer a modified file (shows a real diff); otherwise open the first
// openable file of any status so at least one file surfaces.
const files = importData.files ?? [];
const viewableFile =
files.find(f => f.status === FileComparisonStatus.MODIFIED && isOpenable(f)) ??
files.find(isOpenable);
const comparisonResult = viewableFile
? comparisonResults.find(r => r.relativePath === viewableFile.relativePath)
: undefined;

if (comparisonResult) {
// [DIAG] Temporary diagnostics to find why auto-open silently no-ops.
let diag = `viewable=${viewableFile?.relativePath}\nstatus=${comparisonResult.status}`
+ `\nlocalExists=${fs.existsSync(comparisonResult.localPath)}`
+ `\nremoteExists=${fs.existsSync(comparisonResult.remotePath)}`
+ `\nlocal=${comparisonResult.localPath}\nremote=${comparisonResult.remotePath}`;
try {
const fileTreeItem = new MetadataDiffFileTreeItem(comparisonResult, displayName, true);
await vscode.commands.executeCommand(
Constants.Commands.METADATA_DIFF_OPEN_FILE,
fileTreeItem
);
diag += `\nexecuteCommand=OK`;
} catch (error) {
// Best-effort: import already succeeded; auto-open failure is non-fatal.
diag += `\nERROR=${error instanceof Error ? (error.stack || error.message) : String(error)}`;
traceError(
Constants.EventNames.ACTIONS_HUB_METADATA_DIFF_IMPORT_AUTO_OPEN_FAILED,
error as Error,
{ methodName: importMetadataDiff.name }
);
}
vscode.window.showInformationMessage(`[auto-open diag]\n${diag}`, { modal: true });
} else {
// [DIAG] No file matched the openable criteria.
const summary = (importData.files ?? []).map(f => `${f.status}:${f.relativePath} L=${f.localContent != null} R=${f.remoteContent != null}`).join("\n");
vscode.window.showInformationMessage(`[auto-open diag] no openable file selected.\n${summary}`, { modal: true });
}
}
} catch (error) {
traceError(
Constants.EventNames.ACTIONS_HUB_METADATA_DIFF_IMPORT_FAILED,
Expand Down
Loading
Loading