diff --git a/l10n/bundle.l10n.json b/l10n/bundle.l10n.json index 5e4beea75..543eb689a 100644 --- a/l10n/bundle.l10n.json +++ b/l10n/bundle.l10n.json @@ -5,8 +5,9 @@ "Edit the site": "Edit the site", "Be careful making changes. Anyone can see the changes you make immediately. Choose Edit the site to make edits, or close the editor tab to cancel without editing.": "Be careful making changes. Anyone can see the changes you make immediately. Choose Edit the site to make edits, or close the editor tab to cancel without editing.", "You are editing a live, public site ": "You are editing a live, public site ", - "Get help writing code in HTML, CSS, and JS languages for Power Pages sites with Copilot.": "Get help writing code in HTML, CSS, and JS languages for Power Pages sites with Copilot.", - "Try Copilot for Power Pages": "Try Copilot for Power Pages", + "Preview site": "Preview site", + "Open in Power Pages": "Open in Power Pages", + "Opening preview site...": "Opening preview site...", "Microsoft wants your feeback": "Microsoft wants your feeback", "Check the URL and verify the parameters are correct": "Check the URL and verify the parameters are correct", "Unable to complete the request": "Unable to complete the request", diff --git a/loc/translations-export/vscode-powerplatform.xlf b/loc/translations-export/vscode-powerplatform.xlf index 358a0994f..a74eea106 100644 --- a/loc/translations-export/vscode-powerplatform.xlf +++ b/loc/translations-export/vscode-powerplatform.xlf @@ -121,9 +121,6 @@ The {3} represents Solution's Type (Managed or Unmanaged), but that test is loca File(s) already exist. No new files to add - - Get help writing code in HTML, CSS, and JS languages for Power Pages sites with Copilot. - Installing Power Pages generator(v{0})... {0} represents the version number @@ -182,6 +179,12 @@ The {3} represents Dataverse Environment's Organization ID (GUID) One or more attribute names have been changed or removed. Contact your admin. + + Open in Power Pages + + + Opening preview site... + PAC Telemetry disabled @@ -211,6 +214,9 @@ The {3} represents Dataverse Environment's Organization ID (GUID) Preparing pac CLI (v{0})... {0} represents the version number + + Preview site + Profile Kind: {0} The {0} represents the profile type (Admin vs Dataverse) @@ -247,9 +253,6 @@ The {3} represents Dataverse Environment's Organization ID (GUID) There’s a problem on the back end - - Try Copilot for Power Pages - Try again @@ -407,6 +410,9 @@ The second line should be '[TRANSLATION HERE](command:powerplatform-walkthrough. Overview + + POWER PAGES ACTIONS + Power Platform diff --git a/package.json b/package.json index 640f61c65..d7004a174 100644 --- a/package.json +++ b/package.json @@ -153,6 +153,16 @@ } ], "commands": [ + { + "command": "powerpages.powerPagesFileExplorer.powerPagesRuntimePreview", + "title": "Preview site", + "when": "never" + }, + { + "command": "powerpages.powerPagesFileExplorer.backToStudio", + "title": "Open in Power Pages", + "when": "never" + }, { "command": "extension.createChatView", "title": "Create Chat View" @@ -886,9 +896,12 @@ ], "explorer": [ { - "id": "powerpages.treeWebView", - "name": "Power Pages Actions", - "when": "isWeb && config.powerPlatform.experimental.enableCoPresenceFeature" + "id": "powerpages.powerPagesFileExplorer", + "name": "%microsoft-powerplatform-portals.navigation-loop.powerPagesFileExplorer.title%", + "when": "isWeb && virtualWorkspace", + "icon": "./src/web/client/assets/powerPages.svg", + "contextualTitle": "%microsoft-powerplatform-portals.navigation-loop.powerPagesFileExplorer.title%", + "visibility": "visible" } ] }, @@ -994,9 +1007,9 @@ "jwt-decode": "2.2.0", "mocha": "^9.2.2", "moment": "^2.29.4", + "nanoid": "^3.1.31", "node-fetch": "^2.6.7", "nyc": "^15.1.0", - "nanoid": "^3.1.31", "os-browserify": "^0.3.0", "path-browserify": "^1.0.1", "process": "^0.11.10", diff --git a/package.nls.json b/package.nls.json index 0ade7e5e4..99f9a882d 100644 --- a/package.nls.json +++ b/package.nls.json @@ -13,10 +13,10 @@ "pacCLI.authPanel.title": "Auth Profiles", "pacCLI.authPanel.welcome.whenInteractiveSupported": { - "message": "No auth profiles found on this computer.\n[Add Auth Profile](command:pacCLI.authPanel.newAuthProfile)", - "comment": [ - "This is a Markdown formatted string, and the formatting must persist across translations.", - "The second line should be '[TRANSLATION HERE](command:pacCLI.authPanel.newAuthProfile)', keeping brackets and the text in the parentheses unmodified" + "message": "No auth profiles found on this computer.\n[Add Auth Profile](command:pacCLI.authPanel.newAuthProfile)", + "comment": [ + "This is a Markdown formatted string, and the formatting must persist across translations.", + "The second line should be '[TRANSLATION HERE](command:pacCLI.authPanel.newAuthProfile)', keeping brackets and the text in the parentheses unmodified" ] }, "pacCLI.authPanel.welcome.whenInteractiveNotSupported": { @@ -25,8 +25,7 @@ "This is a Markdown formatted string, and the formatting must persist across translations.", "The second line should not translate the argument `--deviceCode`", "The third line should be '[TRANSLATION HERE](command:pacCLI.pacAuthHelp)', keeping brackets and the text in the parentheses unmodified" - - ] + ] }, "pacCLI.authPanel.clearAuthProfile.title": "Clear Auth Profiles", "pacCLI.authPanel.refresh.title": "Refresh", @@ -67,7 +66,7 @@ "comment": [ "This is a Markdown formatted string, and the formatting must persist across translations.", "The second line should be '[TRANSLATION HERE](command:powerplatform-walkthrough.overview-learn-more)', keeping brackets and the text in the parentheses unmodified" - ] + ] }, "microsoft-powerapps-portals.walkthrough.fileSystem.title": "File explorer", "microsoft-powerapps-portals.walkthrough.fileSystem.description": { @@ -76,7 +75,7 @@ "This is a Markdown formatted string, and the formatting must persist across translations.", "The seventh line should be '[TRANSLATION HERE](command:powerplatform-walkthrough.fileSystem-documentation).', keeping brackets and the text in the parentheses unmodified", "The eighth line should be '[TRANSLATION HERE](command:powerplatform-walkthrough.fileSystem-open-folder)', keeping brackets and the text in the parentheses unmodified" - ] + ] }, "microsoft-powerapps-portals.walkthrough.advancedCapabilities.title": "Advanced capabilities", "microsoft-powerapps-portals.walkthrough.advancedCapabilities.description": { @@ -85,7 +84,7 @@ "This is a Markdown formatted string, and the formatting must persist across translations.", "The fifth line should be '[TRANSLATION HERE](command:powerplatform-walkthrough.advancedCapabilities-learn-more) TRANSLATION', keeping brackets and the text in the parentheses unmodified", "The seventh line should be '[TRANSLATION HERE](command:powerplatform-walkthrough.advancedCapabilities-start-coding)', keeping brackets and the text in the parentheses unmodified" - ] + ] }, "microsoft-powerapps-portals.walkthrough.saveConflict.title": "Save conflict", "microsoft-powerapps-portals.walkthrough.saveConflict.description": { @@ -93,6 +92,7 @@ "comment": [ "This is a Markdown formatted string, and the formatting must persist across translations.", "The fifth line should be '[TRANSLATION HERE](command:powerplatform-walkthrough.saveConflict-learn-more).', keeping brackets and the text in the parentheses unmodified" - ] - } + ] + }, + "microsoft-powerplatform-portals.navigation-loop.powerPagesFileExplorer.title": "POWER PAGES ACTIONS" } diff --git a/src/web/client/WebExtensionContext.ts b/src/web/client/WebExtensionContext.ts index 6d975b018..dccfa2826 100644 --- a/src/web/client/WebExtensionContext.ts +++ b/src/web/client/WebExtensionContext.ts @@ -56,6 +56,7 @@ export interface IWebExtensionContext { defaultFileUri: vscode.Uri; // This will default to home page or current page in multifile scenario showMultifileInVSCode: boolean; extensionActivationTime: number; + extensionUri: vscode.Uri // Org specific details dataverseAccessToken: string; @@ -90,6 +91,7 @@ class WebExtensionContext implements IWebExtensionContext { private _defaultFileUri: vscode.Uri; private _showMultifileInVSCode: boolean; private _extensionActivationTime: number; + private _extensionUri: vscode.Uri; private _dataverseAccessToken: string; private _entityDataMap: EntityDataMap; private _isContextSet: boolean; @@ -149,6 +151,9 @@ class WebExtensionContext implements IWebExtensionContext { public get extensionActivationTime() { return this._extensionActivationTime } + public get extensionUri() { + return this._extensionUri + } public get dataverseAccessToken() { return this._dataverseAccessToken; } @@ -199,6 +204,7 @@ class WebExtensionContext implements IWebExtensionContext { this._defaultFileUri = vscode.Uri.parse(``); this._showMultifileInVSCode = false; this._extensionActivationTime = new Date().getTime(); + this._extensionUri = vscode.Uri.parse(""); this._isContextSet = false; this._currentSchemaVersion = ""; this._websiteLanguageCode = ""; @@ -212,7 +218,8 @@ class WebExtensionContext implements IWebExtensionContext { public setWebExtensionContext( entityName: string, entityId: string, - queryParamsMap: Map + queryParamsMap: Map, + extensionUri?: vscode.Uri ) { const schema = queryParamsMap.get(schemaKey.SCHEMA_VERSION) as string; // Initialize context from URL params @@ -227,6 +234,7 @@ class WebExtensionContext implements IWebExtensionContext { }/`, true ); + this._extensionUri = extensionUri as vscode.Uri; // Initialize multifile FF here const enableMultifile = queryParamsMap?.get(Constants.queryParameters.ENABLE_MULTIFILE); diff --git a/src/web/client/assets/backToStudio.svg b/src/web/client/assets/backToStudio.svg new file mode 100644 index 000000000..a7103f1b5 --- /dev/null +++ b/src/web/client/assets/backToStudio.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/web/client/assets/powerPages.svg b/src/web/client/assets/powerPages.svg new file mode 100644 index 000000000..a249689f3 --- /dev/null +++ b/src/web/client/assets/powerPages.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/web/client/common/constants.ts b/src/web/client/common/constants.ts index 960960ce3..f11a2358a 100644 --- a/src/web/client/common/constants.ts +++ b/src/web/client/common/constants.ts @@ -26,6 +26,8 @@ export const MAX_ENTITY_FETCH_COUNT = 100; export const MAX_CONCURRENT_REQUEST_COUNT = 50; export const MAX_CONCURRENT_REQUEST_QUEUE_COUNT = 1000; export const INTELLIGENCE_SCOPE_DEFAULT = "https://text.pai.dynamics.com/.default"; +export const BACK_TO_STUDIO_URL_TEMPLATE = "https://make{.region}.powerpages.microsoft.com/e/{environmentId}/sites/{webSiteId}/pages"; +export const STUDIO_PROD_REGION = "prod"; // Web extension constants export const BASE_64 = 'base64'; @@ -74,12 +76,14 @@ export enum queryParameters { ENV_ID = "envid", GEO = "geo", ENABLE_MULTIFILE = "enablemultifile", + WEBSITE_PREVIEW_URL = "websitepreviewurl" } export enum httpMethod { PATCH = "PATCH", GET = "GET", POST = "POST", + DELETE = "DELETE", } export enum SurveyConstants { diff --git a/src/web/client/extension.ts b/src/web/client/extension.ts index e419c4660..42142cc19 100644 --- a/src/web/client/extension.ts +++ b/src/web/client/extension.ts @@ -30,6 +30,7 @@ import { } from "./utilities/fileAndEntityUtil"; import { IEntityInfo } from "./common/interfaces"; import { telemetryEventNames } from "./telemetry/constants"; +import { PowerPagesNavigationProvider } from "./webViews/powerPagesNavigationProvider"; import * as copilot from "../../common/copilot/PowerPagesCopilot"; import { IOrgInfo } from "../../common/copilot/model"; import { copilotNotificationPanel, disposeNotificationPanel } from "../../common/copilot/welcome-notification/CopilotNotificationPanel"; @@ -102,7 +103,8 @@ export function activate(context: vscode.ExtensionContext): void { WebExtensionContext.setWebExtensionContext( entity, entityId, - queryParamsMap + queryParamsMap, + context.extensionUri ); WebExtensionContext.setVscodeWorkspaceState(context.workspaceState); WebExtensionContext.telemetry.sendExtensionInitPathParametersTelemetry( @@ -123,6 +125,8 @@ export function activate(context: vscode.ExtensionContext): void { processWalkthroughFirstRunExperience(context); + powerPagesNavigation(); + await vscode.window.withProgress( { location: vscode.ProgressLocation.Notification, @@ -187,6 +191,14 @@ export function activate(context: vscode.ExtensionContext): void { showWalkthrough(context, WebExtensionContext.telemetry); } +export function powerPagesNavigation() { + const powerPagesNavigationProvider = new PowerPagesNavigationProvider(); + vscode.window.registerTreeDataProvider('powerpages.powerPagesFileExplorer', powerPagesNavigationProvider); + vscode.commands.registerCommand('powerpages.powerPagesFileExplorer.powerPagesRuntimePreview', () => powerPagesNavigationProvider.previewPowerPageSite()); + vscode.commands.registerCommand('powerpages.powerPagesFileExplorer.backToStudio', () => powerPagesNavigationProvider.backToStudio()); + WebExtensionContext.telemetry.sendInfoTelemetry(telemetryEventNames.WEB_EXTENSION_POWER_PAGES_WEB_VIEW_REGISTERED); +} + export function processWalkthroughFirstRunExperience(context: vscode.ExtensionContext) { const isMultifileFirstRun = context.globalState.get( IS_MULTIFILE_FIRST_RUN_EXPERIENCE, diff --git a/src/web/client/telemetry/constants.ts b/src/web/client/telemetry/constants.ts index e7ca18d36..b2a2ade42 100644 --- a/src/web/client/telemetry/constants.ts +++ b/src/web/client/telemetry/constants.ts @@ -93,4 +93,8 @@ export enum telemetryEventNames { WEB_EXTENSION_WEB_COPILOT_REGISTRATION_FAILED = 'webExtensionCopilotRegisterFailed', WEB_EXTENSION_WEB_COPILOT_NOTIFICATION_SHOWN = 'webExtensionCopilotNotificationShown', WEB_EXTENSION_WEB_COPILOT_NOTIFICATION_EVENT_CLICKED = 'webExtensionCopilotNotificationEventClicked', + WEB_EXTENSION_POWER_PAGES_WEB_VIEW_REGISTERED = 'webExtensionPowerPagesWebViewRegistered', + WEB_EXTENSION_POWER_PAGES_WEB_VIEW_REGISTER_FAILED = 'webExtensionPowerPagesWebViewRegisterFailed', + WEB_EXTENSION_BACK_TO_STUDIO_TRIGGERED = 'webExtensionBackToStudioTriggered', + WEB_EXTENSION_PREVIEW_SITE_TRIGGERED = 'webExtensionPreviewSiteTriggered', } diff --git a/src/web/client/utilities/commonUtil.ts b/src/web/client/utilities/commonUtil.ts index eeb235be6..aac3829c6 100644 --- a/src/web/client/utilities/commonUtil.ts +++ b/src/web/client/utilities/commonUtil.ts @@ -5,13 +5,16 @@ import * as vscode from "vscode"; import { + BACK_TO_STUDIO_URL_TEMPLATE, BASE_64, CO_PRESENCE_FEATURE_SETTING_NAME, DATA, MULTI_FILE_FEATURE_SETTING_NAME, NO_CONTENT, + STUDIO_PROD_REGION, VERSION_CONTROL_FOR_WEB_EXTENSION_SETTING_NAME, - portalSchemaVersion + portalSchemaVersion, + queryParameters } from "../common/constants"; import { IAttributePath } from "../common/interfaces"; import { schemaEntityName } from "../schema/constants"; @@ -199,10 +202,24 @@ export function isPortalVersionV2(): boolean { return WebExtensionContext.currentSchemaVersion.toLowerCase() === portalSchemaVersion.V2; } -export function getWorkSpaceName(websiteId : string) : string { +export function getWorkSpaceName(websiteId: string): string { if (isPortalVersionV1()) { return `Site-v1-${websiteId}`; } else { return `Site-v2-${websiteId}`; } } + +// ENV_ID is the last part of the parameter value sent in the vscode URL from studio +export function getEnvironmentIdFromUrl() { + return (WebExtensionContext.urlParametersMap.get(queryParameters.ENV_ID) as string).split("/")?.pop() as string; +} + +export function getBackToStudioURL() { + const region = WebExtensionContext.urlParametersMap.get(queryParameters.REGION) as string; + + return BACK_TO_STUDIO_URL_TEMPLATE + .replace("{environmentId}", getEnvironmentIdFromUrl()) + .replace("{.region}", region.toLowerCase() === STUDIO_PROD_REGION ? "" : `.${WebExtensionContext.urlParametersMap.get(queryParameters.REGION) as string}`) + .replace("{webSiteId}", WebExtensionContext.urlParametersMap.get(queryParameters.WEBSITE_ID) as string); +} diff --git a/src/web/client/webViews/NPSWebView.ts b/src/web/client/webViews/NPSWebView.ts index 1cf3930cf..35fcdb404 100644 --- a/src/web/client/webViews/NPSWebView.ts +++ b/src/web/client/webViews/NPSWebView.ts @@ -8,6 +8,7 @@ import WebExtensionContext from "../WebExtensionContext"; import { queryParameters } from "../common/constants"; import { getDeviceType } from "../utilities/deviceType"; import { telemetryEventNames } from "../telemetry/constants"; +import { getEnvironmentIdFromUrl } from "../utilities/commonUtil"; export class NPSWebView { private readonly _webviewPanel: vscode.WebviewPanel; @@ -27,9 +28,7 @@ export class NPSWebView { const tid = WebExtensionContext.urlParametersMap?.get( queryParameters.TENANT_ID ); - const envId = WebExtensionContext.urlParametersMap - ?.get(queryParameters.ENV_ID) - ?.split("/")[4]; + const envId = getEnvironmentIdFromUrl(); const geo = WebExtensionContext.urlParametersMap?.get( queryParameters.GEO ); diff --git a/src/web/client/webViews/powerPagesNavigationProvider.ts b/src/web/client/webViews/powerPagesNavigationProvider.ts new file mode 100644 index 000000000..34972a1b6 --- /dev/null +++ b/src/web/client/webViews/powerPagesNavigationProvider.ts @@ -0,0 +1,152 @@ +/* + * 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 path from 'path'; +import WebExtensionContext from "../WebExtensionContext"; +import { httpMethod, queryParameters } from '../common/constants'; +import { getBackToStudioURL } from '../utilities/commonUtil'; +import { telemetryEventNames } from '../telemetry/constants'; + +export class PowerPagesNavigationProvider implements vscode.TreeDataProvider { + + private _onDidChangeTreeData: vscode.EventEmitter = new vscode.EventEmitter(); + readonly onDidChangeTreeData: vscode.Event = this._onDidChangeTreeData.event; + + refresh(): void { + this._onDidChangeTreeData.fire(); + } + + getTreeItem(element: PowerPagesNode): vscode.TreeItem { + return element; + } + + getChildren(element?: PowerPagesNode): Thenable { + if (element) { + return Promise.resolve(this.getNodes(path.join(element.label))); + } else { + return Promise.resolve(this.getNodes()); + } + } + + getNodes(label?: string): PowerPagesNode[] { + const nodes: PowerPagesNode[] = []; + const previewPowerPage = new PowerPagesNode(vscode.l10n.t("Preview site"), + { + command: 'powerpages.powerPagesFileExplorer.powerPagesRuntimePreview', + title: vscode.l10n.t("Preview site"), + arguments: [] + }, + 'powerPages.svg'); + const backToStudio = new PowerPagesNode(vscode.l10n.t("Open in Power Pages"), + { + command: 'powerpages.powerPagesFileExplorer.backToStudio', + title: vscode.l10n.t("Open in Power Pages"), + arguments: [] + }, + 'backToStudio.svg'); + + if (label && label === previewPowerPage.label) { + nodes.push(previewPowerPage); + } else if (label && label === backToStudio.label) { + nodes.push(backToStudio); + } else { + nodes.push(previewPowerPage); + nodes.push(backToStudio); + } + + return nodes; + } + + async previewPowerPageSite(): Promise { + let requestSentAtTime = new Date().getTime(); + const websitePreviewUrl = WebExtensionContext.urlParametersMap.get(queryParameters.WEBSITE_PREVIEW_URL) as string; + // Runtime clear cache call + const requestUrl = `${websitePreviewUrl.endsWith('/') ? websitePreviewUrl : websitePreviewUrl.concat('/')}_services/cache/config`; + + WebExtensionContext.telemetry.sendAPITelemetry( + requestUrl, + "Preview power pages site", + httpMethod.DELETE, + this.previewPowerPageSite.name + ); + requestSentAtTime = new Date().getTime(); + WebExtensionContext.dataverseAuthentication(); + + await vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + cancellable: true, + title: vscode.l10n.t("Opening preview site..."), + }, + async () => { + const response = await WebExtensionContext.concurrencyHandler.handleRequest(requestUrl, { + headers: { + authorization: "Bearer " + WebExtensionContext.dataverseAccessToken, + 'Accept': '*/*', + 'Content-Type': 'text/plain', + }, + method: 'DELETE', + }); + + if (response.ok) { + WebExtensionContext.telemetry.sendAPISuccessTelemetry( + requestUrl, + "Preview power pages site", + httpMethod.DELETE, + new Date().getTime() - requestSentAtTime, + this.previewPowerPageSite.name + ); + } else { + WebExtensionContext.telemetry.sendAPIFailureTelemetry( + requestUrl, + "Preview power pages site", + httpMethod.DELETE, + new Date().getTime() - requestSentAtTime, + this.previewPowerPageSite.name, + JSON.stringify(response), + '', + response?.status.toString() + ); + } + + + } + ); + + vscode.env.openExternal(vscode.Uri.parse(websitePreviewUrl)); + WebExtensionContext.telemetry.sendInfoTelemetry(telemetryEventNames.WEB_EXTENSION_PREVIEW_SITE_TRIGGERED); + } + + backToStudio(): void { + const backToStudioUrl = getBackToStudioURL(); + vscode.env.openExternal(vscode.Uri.parse(backToStudioUrl)); + + WebExtensionContext.telemetry.sendInfoTelemetry(telemetryEventNames.WEB_EXTENSION_BACK_TO_STUDIO_TRIGGERED, { + backToStudioUrl: backToStudioUrl + }); + } +} + +export class PowerPagesNode extends vscode.TreeItem { + constructor( + public readonly label: string, + public readonly command: vscode.Command, + public readonly svgFileName: string + ) { + super(label, vscode.TreeItemCollapsibleState.None); + + this.tooltip = this.label; + this.command = command; + this.iconPath = this.getIconPath(svgFileName); + } + + getIconPath(svgFileName: string) { + return { + light: vscode.Uri.joinPath(WebExtensionContext.extensionUri, '..', '..', 'src', 'web', 'client', 'assets', svgFileName), + dark: vscode.Uri.joinPath(WebExtensionContext.extensionUri, '..', '..', 'src', 'web', 'client', 'assets', svgFileName) + }; + } +} \ No newline at end of file