From d6af9442837e83479440c3e338ef8eeaa24102b6 Mon Sep 17 00:00:00 2001 From: tyaginidhi Date: Fri, 15 Sep 2023 02:10:54 +0530 Subject: [PATCH 1/9] Bringing Power pages copilot to Web extension (#696) * Web co-pilot loading. TODO - CSS and module load separation at runtime * Loading css on prod and artemis test url connect * UX working with test AIB call integration * Move back dataverse file to main folder * Dataverse calls for web extension - org url does not have slash at end - needs to be fixed * updated dataverse URL * remove invalid parameter "adx_targetentitylogicalname" * remove console log commands * Support EntityDefinition fetch for lists, forms and advanced forms in new data model and old data model testing done * Final load and test done for Desktop and web extension --- package.json | 9 +- src/client/extension.ts | 22 +-- src/common/ArtemisService.ts | 107 ++++++------- src/common/copilot/IntelligenceApiService.ts | 10 +- src/common/copilot/PowerPagesCopilot.ts | 146 ++++++++++-------- src/common/copilot/dataverseMetadata.ts | 43 ++++-- src/common/copilot/model.ts | 30 ++-- src/web/client/WebExtensionContext.ts | 7 +- .../client/common/authenticationProvider.ts | 10 +- src/web/client/context/fileData.ts | 14 +- src/web/client/context/fileDataMap.ts | 6 +- src/web/client/dal/remoteFetchProvider.ts | 13 +- src/web/client/extension.ts | 54 +++++++ src/web/client/schema/constants.ts | 1 + src/web/client/schema/portalSchema.ts | 22 ++- src/web/client/telemetry/constants.ts | 3 + src/web/client/utilities/fileAndEntityUtil.ts | 5 + src/web/client/utilities/schemaHelperUtil.ts | 4 + src/web/client/utilities/urlBuilderUtil.ts | 17 +- webpack.config.js | 3 - 20 files changed, 335 insertions(+), 191 deletions(-) diff --git a/package.json b/package.json index 430ea276..e93a8370 100644 --- a/package.json +++ b/package.json @@ -433,6 +433,11 @@ "type": "boolean", "markdownDescription": "Enable multiple file view in Visual Studio Code (Web extension only)", "default": true + }, + "powerPlatform.experimental.enableWebCopilot": { + "type": "boolean", + "markdownDescription": "Enable copilot in Visual Studio Code (Web extension)", + "default": true } } }, @@ -747,7 +752,7 @@ }, { "command": "powerpages.copilot.clearConversation", - "when": "!virtualWorkspace && view == powerpages.copilot", + "when": "view == powerpages.copilot", "group": "navigation" } ], @@ -845,7 +850,7 @@ "name": "Copilot In Power Pages", "initialSize": 6, "visibility": "visible", - "when": "!virtualWorkspace && powerpages.websiteYmlExists && config.powerPlatform.experimental.copilotEnabled" + "when": "(!virtualWorkspace && powerpages.websiteYmlExists && config.powerPlatform.experimental.copilotEnabled) || (isWeb && config.powerPlatform.experimental.enableWebCopilot)" } ] }, diff --git a/src/client/extension.ts b/src/client/extension.ts index 715af4f1..b0644efe 100644 --- a/src/client/extension.ts +++ b/src/client/extension.ts @@ -161,18 +161,18 @@ export async function activate( vscode.workspace.workspaceFolders?.map( (fl) => ({ ...fl, uri: fl.uri.fsPath } as WorkspaceFolder) ) || []; - + // TODO: Handle for VSCode.dev also - if (workspaceContainsPortalConfigFolder(workspaceFolders)) { + if (workspaceContainsPortalConfigFolder(workspaceFolders)) { let telemetryData = ''; let listOfActivePortals = []; try { listOfActivePortals = getPortalsOrgURLs(workspaceFolders, _telemetry); telemetryData = JSON.stringify(listOfActivePortals); - _telemetry.sendTelemetryEvent("VscodeDesktopUsage", {listOfActivePortals: telemetryData, countOfActivePortals: listOfActivePortals.length.toString()}); - }catch(exception){ - _telemetry.sendTelemetryException(exception as Error, {eventName: 'VscodeDesktopUsage'}); - } + _telemetry.sendTelemetryEvent("VscodeDesktopUsage", { listOfActivePortals: telemetryData, countOfActivePortals: listOfActivePortals.length.toString() }); + } catch (exception) { + _telemetry.sendTelemetryException(exception as Error, { eventName: 'VscodeDesktopUsage' }); + } _telemetry.sendTelemetryEvent("PowerPagesWebsiteYmlExists"); // Capture's PowerPages Users vscode.commands.executeCommand('setContext', 'powerpages.websiteYmlExists', true); initializeGenerator(_context, cliContext, _telemetry); // Showing the create command only if website.yml exists @@ -351,19 +351,19 @@ function handleWorkspaceFolderChange() { } } -function showNotificationForCopilot(telemetry: TelemetryReporter, telemetryData:string, countOfActivePortals: string) { - if(vscode.workspace.getConfiguration('powerPlatform').get('experimental.copilotEnabled') === false) { +function showNotificationForCopilot(telemetry: TelemetryReporter, telemetryData: string, countOfActivePortals: string) { + if (vscode.workspace.getConfiguration('powerPlatform').get('experimental.copilotEnabled') === false) { return; } const message = vscode.l10n.t('Get help writing code in HTML, CSS, and JS languages for Power Pages sites with Copilot.'); const actionTitle = vscode.l10n.t('Try Copilot for Power Pages'); - telemetry.sendTelemetryEvent(CopilotNotificationShown, {listOfOrgs: telemetryData, countOfActivePortals}); + telemetry.sendTelemetryEvent(CopilotNotificationShown, { listOfOrgs: telemetryData, countOfActivePortals }); vscode.window.showInformationMessage(message, actionTitle).then((selection) => { if (selection === actionTitle) { - telemetry.sendTelemetryEvent(CopilotTryNotificationClickedEvent, {listOfOrgs: telemetryData, countOfActivePortals}); - + telemetry.sendTelemetryEvent(CopilotTryNotificationClickedEvent, { listOfOrgs: telemetryData, countOfActivePortals }); + vscode.commands.executeCommand('powerpages.copilot.focus') } }); diff --git a/src/common/ArtemisService.ts b/src/common/ArtemisService.ts index a6d17371..2ef8ea7f 100644 --- a/src/common/ArtemisService.ts +++ b/src/common/ArtemisService.ts @@ -3,63 +3,64 @@ * Licensed under the MIT License. See License.txt in the project root for license information. */ -import fetch, {RequestInit} from "node-fetch"; +import fetch, { RequestInit } from "node-fetch"; import { COPILOT_UNAVAILABLE, US_GEO } from "./copilot/constants"; import { ITelemetry } from "../client/telemetry/ITelemetry"; import { sendTelemetryEvent } from "./copilot/telemetry/copilotTelemetry"; import { CopilotArtemisFailureEvent, CopilotArtemisSuccessEvent } from "./copilot/telemetry/telemetryConstants"; -export async function getIntelligenceEndpoint (orgId: string, telemetry:ITelemetry, sessionID:string) { - const { tstUrl, preprodUrl, prodUrl } = convertGuidToUrls(orgId); - const endpoints = [tstUrl, preprodUrl, prodUrl]; +declare const IS_DESKTOP: string | undefined; +export async function getIntelligenceEndpoint(orgId: string, telemetry: ITelemetry, sessionID: string) { + const { tstUrl, preprodUrl, prodUrl } = convertGuidToUrls(orgId); + const endpoints = [tstUrl, preprodUrl, prodUrl]; - const artemisResponse = await fetchIslandInfo(endpoints, telemetry, sessionID); + const artemisResponse = await fetchIslandInfo(endpoints, telemetry, sessionID); - if (!artemisResponse) { - return null; - } + if (!artemisResponse) { + return null; + } - const { clusterNumber, geoName, environment } = artemisResponse[0]; - sendTelemetryEvent(telemetry, {eventName: CopilotArtemisSuccessEvent, copilotSessionId: sessionID, geoName: String(geoName), orgId: orgId}) + const { geoName, environment, clusterNumber } = artemisResponse[0]; + sendTelemetryEvent(telemetry, { eventName: CopilotArtemisSuccessEvent, copilotSessionId: sessionID, geoName: String(geoName), orgId: orgId }); - if(geoName !== US_GEO) { - return COPILOT_UNAVAILABLE; - } + if (geoName !== US_GEO) { + return COPILOT_UNAVAILABLE; + } - const intelligenceEndpoint = `https://aibuildertextapiservice.${geoName}-il${clusterNumber}.gateway.${environment}.island.powerapps.com/v1.0/${orgId}/appintelligence/chat` + const intelligenceEndpoint = `https://aibuildertextapiservice.${geoName}-${IS_DESKTOP ? 'il' + clusterNumber : 'il001'}.gateway.${environment}.island.powerapps.com/v1.0/${orgId}/appintelligence/chat` - return intelligenceEndpoint; + return intelligenceEndpoint; } async function fetchIslandInfo(endpoints: string[], telemetry: ITelemetry, sessionID: string) { - const requestInit: RequestInit = { - method: 'GET', - redirect: 'follow' - }; - - try { - const promises = endpoints.map(async endpoint => { - try { - const response = await fetch(endpoint, requestInit); - if (!response.ok) { - throw new Error('Request failed'); - } - return response.json(); - } catch (error) { - return null; + const requestInit: RequestInit = { + method: 'GET', + redirect: 'follow' + }; + + try { + const promises = endpoints.map(async endpoint => { + try { + const response = await fetch(endpoint, requestInit); + if (!response.ok) { + throw new Error('Request failed'); } - }); - - const responses = await Promise.all(promises); - const successfulResponses = responses.filter(response => response !== null); - return successfulResponses; - } catch (error) { - sendTelemetryEvent(telemetry, {eventName: CopilotArtemisFailureEvent, copilotSessionId: sessionID, error: error as Error}) - return null; - } + return response.json(); + } catch (error) { + return null; + } + }); + + const responses = await Promise.all(promises); + const successfulResponses = responses.filter(response => response !== null); + return successfulResponses; + } catch (error) { + sendTelemetryEvent(telemetry, { eventName: CopilotArtemisFailureEvent, copilotSessionId: sessionID, error: error as Error }) + return null; } +} /** @@ -71,18 +72,18 @@ async function fetchIslandInfo(endpoints: string[], telemetry: ITelemetry, sessi * Prod: https:// c7809087d9b84a00a78aa4b901caa2.3f.organization.api.powerplatform.com/artemis */ export function convertGuidToUrls(orgId: string) { - const updatedOrgId = orgId.replace(/-/g, ""); - const domain = updatedOrgId.slice(0, -1); - const domainProd = updatedOrgId.slice(0, -2); - const nonProdSegment = updatedOrgId.slice(-1); - const prodSegment = updatedOrgId.slice(-2); - const tstUrl = `https://${domain}.${nonProdSegment}.organization.api.test.powerplatform.com/gateway/cluster?api-version=1`; - const preprodUrl = `https://${domain}.${nonProdSegment}.organization.api.preprod.powerplatform.com/gateway/cluster?api-version=1`; - const prodUrl = `https://${domainProd}.${prodSegment}.organization.api.powerplatform.com/gateway/cluster?api-version=1`; - - return { - tstUrl, - preprodUrl, - prodUrl - }; + const updatedOrgId = orgId.replace(/-/g, ""); + const domain = updatedOrgId.slice(0, -1); + const domainProd = updatedOrgId.slice(0, -2); + const nonProdSegment = updatedOrgId.slice(-1); + const prodSegment = updatedOrgId.slice(-2); + const tstUrl = `https://${domain}.${nonProdSegment}.organization.api.test.powerplatform.com/gateway/cluster?api-version=1`; + const preprodUrl = `https://${domain}.${nonProdSegment}.organization.api.preprod.powerplatform.com/gateway/cluster?api-version=1`; + const prodUrl = `https://${domainProd}.${prodSegment}.organization.api.powerplatform.com/gateway/cluster?api-version=1`; + + return { + tstUrl, + preprodUrl, + prodUrl + }; } diff --git a/src/common/copilot/IntelligenceApiService.ts b/src/common/copilot/IntelligenceApiService.ts index 572cdec5..9c09ee2f 100644 --- a/src/common/copilot/IntelligenceApiService.ts +++ b/src/common/copilot/IntelligenceApiService.ts @@ -17,14 +17,10 @@ const clientVersion = getExtensionVersion(); export async function sendApiRequest(userPrompt: string, activeFileParams: IActiveFileParams, orgID: string, apiToken: string, sessionID: string, entityName: string, entityColumns: string[], telemetry: ITelemetry, aibEndpoint: string | null) { - if (!aibEndpoint) { return NetworkError; } - - - const requestBody = { "question": userPrompt, "top": 1, @@ -40,8 +36,8 @@ export async function sendApiRequest(userPrompt: string, activeFileParams: IActi "activeFileContent": '', "targetEntity": entityName, "targetColumns": entityColumns, - "clientType" : clientType, - "clientVersion" : clientVersion, + "clientType": clientType, + "clientVersion": clientVersion, } } }; @@ -107,7 +103,7 @@ export async function sendApiRequest(userPrompt: string, activeFileParams: IActi } else if (errorCode === RELEVANCY_CHECK_FAILED || errorCode === INAPPROPRIATE_CONTENT || errorCode === INPUT_CONTENT_FILTERED) { return MalaciousScenerioResponse; - } else if(errorCode === PROMPT_LIMIT_EXCEEDED || errorCode === INVALID_INFERENCE_INPUT) { + } else if (errorCode === PROMPT_LIMIT_EXCEEDED || errorCode === INVALID_INFERENCE_INPUT) { return PromptLimitExceededResponse; } else if (errorMessage) { diff --git a/src/common/copilot/PowerPagesCopilot.ts b/src/common/copilot/PowerPagesCopilot.ts index ae1379dc..8cd9a72a 100644 --- a/src/common/copilot/PowerPagesCopilot.ts +++ b/src/common/copilot/PowerPagesCopilot.ts @@ -11,37 +11,45 @@ import { v4 as uuidv4 } from 'uuid' import { PacWrapper } from "../../client/pac/PacWrapper"; import { ITelemetry } from "../../client/telemetry/ITelemetry"; import { AUTH_CREATE_FAILED, AUTH_CREATE_MESSAGE, AuthProfileNotFound, COPILOT_UNAVAILABLE, CopilotDisclaimer, CopilotStylePathSegments, DataverseEntityNameMap, EntityFieldMap, FieldTypeMap, PAC_SUCCESS, WebViewMessage, sendIconSvg } from "./constants"; -import { IActiveFileParams, IActiveFileData} from './model'; +import { IActiveFileParams, IActiveFileData, IOrgInfo } from './model'; import { escapeDollarSign, getLastThreePartsOfFileName, getNonce, getUserName, showConnectedOrgMessage, showInputBoxAndGetOrgUrl, showProgressWithNotification } from "../Utils"; import { CESUserFeedback } from "./user-feedback/CESSurvey"; import { GetAuthProfileWatchPattern } from "../../client/lib/AuthPanelView"; -import { PacActiveOrgListOutput } from "../../client/pac/PacTypes"; +import { ActiveOrgOutput } from "../../client/pac/PacTypes"; import { CopilotWalkthroughEvent, CopilotCopyCodeToClipboardEvent, CopilotInsertCodeToEditorEvent, CopilotLoadedEvent, CopilotOrgChangedEvent, CopilotUserFeedbackThumbsDownEvent, CopilotUserFeedbackThumbsUpEvent, CopilotUserPromptedEvent, CopilotCodeLineCountEvent, CopilotClearChatEvent } from "./telemetry/telemetryConstants"; import { sendTelemetryEvent } from "./telemetry/copilotTelemetry"; -import { getEntityColumns, getEntityName } from "./dataverseMetadata"; import { INTELLIGENCE_SCOPE_DEFAULT, PROVIDER_ID } from "../../web/client/common/constants"; import { getIntelligenceEndpoint } from "../ArtemisService"; +import TelemetryReporter from "@vscode/extension-telemetry"; +import { getEntityColumns, getEntityName } from "./dataverseMetadata"; + +let intelligenceApiToken: string; +let userID: string; // Populated from PAC or intelligence API +let userName: string; // Populated from intelligence API +let sessionID: string; // Generated per session -let apiToken: string; -let userName: string; let orgID: string; let environmentName: string; -let userID: string; let activeOrgUrl: string; -let sessionID: string; +declare const IS_DESKTOP: string | undefined; //TODO: Check if it can be converted to singleton export class PowerPagesCopilot implements vscode.WebviewViewProvider { public static readonly viewType = "powerpages.copilot"; private _view?: vscode.WebviewView; - private readonly _pacWrapper: PacWrapper; + private readonly _pacWrapper?: PacWrapper; private _extensionContext: vscode.ExtensionContext; private readonly _disposables: vscode.Disposable[] = []; private loginButtonRendered = false; private telemetry: ITelemetry; private aibEndpoint: string | null = null; - constructor(private readonly _extensionUri: vscode.Uri, _context: vscode.ExtensionContext, telemetry: ITelemetry, pacWrapper: PacWrapper) { + constructor( + private readonly _extensionUri: vscode.Uri, + _context: vscode.ExtensionContext, + telemetry: ITelemetry | TelemetryReporter, + pacWrapper?: PacWrapper, + orgInfo?: IOrgInfo) { this.telemetry = telemetry; this._extensionContext = _context; sessionID = uuidv4(); @@ -57,11 +65,17 @@ export class PowerPagesCopilot implements vscode.WebviewViewProvider { } ) ); - this.setupFileWatcher(); - } + if (this._pacWrapper) { + this.setupFileWatcher(); + } - private isDesktop: boolean = vscode.env.uiKind === vscode.UIKind.Desktop; + if (orgInfo) { + orgID = orgInfo.orgId; + environmentName = orgInfo.environmentName; + activeOrgUrl = orgInfo.activeOrgUrl; + } + } public dispose(): void { this._disposables.forEach(d => d.dispose()); @@ -82,26 +96,29 @@ export class PowerPagesCopilot implements vscode.WebviewViewProvider { private async handleOrgChange() { orgID = ''; - const pacOutput = await this._pacWrapper.activeOrg(); + const pacOutput = await this._pacWrapper?.activeOrg(); + + if (pacOutput && pacOutput.Status === PAC_SUCCESS) { + this.handleOrgChangeSuccess(pacOutput.Results); + } else if (this._view?.visible) { - if (pacOutput.Status === PAC_SUCCESS) { - this.handleOrgChangeSuccess(pacOutput); + if (pacOutput && pacOutput.Status === PAC_SUCCESS) { + this.handleOrgChangeSuccess(pacOutput.Results); } else if (this._view?.visible) { const userOrgUrl = await showInputBoxAndGetOrgUrl(); if (!userOrgUrl) { return; } - const pacAuthCreateOutput = await showProgressWithNotification(vscode.l10n.t(AUTH_CREATE_MESSAGE), async() => { return await this._pacWrapper.authCreateNewAuthProfileForOrg(userOrgUrl)}); - if (pacAuthCreateOutput.Status !== PAC_SUCCESS) { + const pacAuthCreateOutput = await showProgressWithNotification(vscode.l10n.t(AUTH_CREATE_MESSAGE), async () => { return await this._pacWrapper?.authCreateNewAuthProfileForOrg(userOrgUrl) }); + if (pacAuthCreateOutput && pacAuthCreateOutput.Status !== PAC_SUCCESS) { vscode.window.showErrorMessage(AUTH_CREATE_FAILED); // TODO: Provide Experience to create auth profile return; } - + } } } - public async resolveWebviewView( webviewView: vscode.WebviewView, // eslint-disable-next-line @typescript-eslint/no-unused-vars @@ -119,10 +136,12 @@ export class PowerPagesCopilot implements vscode.WebviewViewProvider { localResourceRoots: [this._extensionUri], }; - const pacOutput = await this._pacWrapper.activeOrg(); + const pacOutput = await this._pacWrapper?.activeOrg(); - if (pacOutput.Status === PAC_SUCCESS) { - this.handleOrgChangeSuccess(pacOutput); + if (pacOutput && pacOutput.Status === PAC_SUCCESS) { + this.handleOrgChangeSuccess(pacOutput.Results); + } else if (!IS_DESKTOP && orgID && activeOrgUrl) { + this.handleOrgChangeSuccess({ OrgId: orgID, UserId: userID, OrgUrl: activeOrgUrl } as ActiveOrgOutput); } webviewView.webview.html = this._getHtmlForWebview(webviewView.webview); @@ -131,14 +150,13 @@ export class PowerPagesCopilot implements vscode.WebviewViewProvider { switch (data.type) { case "webViewLoaded": { sendTelemetryEvent(this.telemetry, { eventName: CopilotLoadedEvent, copilotSessionId: sessionID, orgId: orgID }); - this.sendMessageToWebview({ type: 'env'}); //TODO Use IS_DESKTOP + this.sendMessageToWebview({ type: 'env' }); //TODO Use IS_DESKTOP await this.checkAuthentication(); - if(orgID && userName) { - this.sendMessageToWebview({type: 'isLoggedIn', value: true}); + if (orgID && userName) { + this.sendMessageToWebview({ type: 'isLoggedIn', value: true }); this.sendMessageToWebview({ type: 'userName', value: userName }); - }else - { - this.sendMessageToWebview({type: 'isLoggedIn', value: false}); + } else { + this.sendMessageToWebview({ type: 'isLoggedIn', value: false }); this.loginButtonRendered = true; } this.sendMessageToWebview({ type: "welcomeScreen" }); @@ -151,9 +169,9 @@ export class PowerPagesCopilot implements vscode.WebviewViewProvider { case "newUserPrompt": { sendTelemetryEvent(this.telemetry, { eventName: CopilotUserPromptedEvent, copilotSessionId: sessionID, aibEndpoint: this.aibEndpoint ?? '', orgId: orgID }); //TODO: Add active Editor info orgID - ? (() => { + ? (async () => { const { activeFileParams } = this.getActiveEditorContent(); - this.authenticateAndSendAPIRequest(data.value, activeFileParams, orgID, this.telemetry); + await this.authenticateAndSendAPIRequest(data.value, activeFileParams, orgID, this.telemetry); })() : (() => { this.sendMessageToWebview({ type: 'apiResponse', value: AuthProfileNotFound }); @@ -220,12 +238,12 @@ export class PowerPagesCopilot implements vscode.WebviewViewProvider { private async handleLogin() { - const pacOutput = await this._pacWrapper.activeOrg(); - if (pacOutput.Status === PAC_SUCCESS) { - this.handleOrgChangeSuccess.call(this, pacOutput); + const pacOutput = await this._pacWrapper?.activeOrg(); + if (pacOutput && pacOutput.Status === PAC_SUCCESS) { + this.handleOrgChangeSuccess.call(this, pacOutput.Results); - intelligenceAPIAuthentication(this.telemetry, sessionID).then(({ accessToken, user }) => { - this.intelligenceAPIAuthenticationHandler.call(this, accessToken, user); + intelligenceAPIAuthentication(this.telemetry, sessionID).then(({ accessToken, user, userId }) => { + this.intelligenceAPIAuthenticationHandler.call(this, accessToken, user, userId); }); } else if (this._view?.visible) { @@ -241,10 +259,10 @@ export class PowerPagesCopilot implements vscode.WebviewViewProvider { return; } - const pacAuthCreateOutput = await showProgressWithNotification(AUTH_CREATE_MESSAGE, async() => { return await this._pacWrapper.authCreateNewAuthProfileForOrg(userOrgUrl)}); - pacAuthCreateOutput.Status === PAC_SUCCESS - ? intelligenceAPIAuthentication(this.telemetry, sessionID).then(({ accessToken, user }) => - this.intelligenceAPIAuthenticationHandler.call(this, accessToken, user) + const pacAuthCreateOutput = await showProgressWithNotification(AUTH_CREATE_MESSAGE, async () => { return await this._pacWrapper?.authCreateNewAuthProfileForOrg(userOrgUrl) }); + pacAuthCreateOutput && pacAuthCreateOutput.Status === PAC_SUCCESS + ? intelligenceAPIAuthentication(this.telemetry, sessionID).then(({ accessToken, user, userId }) => + this.intelligenceAPIAuthenticationHandler.call(this, accessToken, user, userId) ) : vscode.window.showErrorMessage(AUTH_CREATE_FAILED); @@ -255,38 +273,41 @@ export class PowerPagesCopilot implements vscode.WebviewViewProvider { private async checkAuthentication() { const session = await vscode.authentication.getSession(PROVIDER_ID, [`${INTELLIGENCE_SCOPE_DEFAULT}`], { silent: true }); if (session) { - apiToken = session.accessToken; - userName = getUserName(session.account.label); + intelligenceApiToken = session.accessToken; + userName = getUserName(session.account.label); + userID = session?.account.id.split("/").pop() ?? + session?.account.id; } else { - apiToken = ""; - userName = ""; + intelligenceApiToken = ""; + userName = ""; } -} + } private openWalkthrough() { const walkthroughUri = vscode.Uri.joinPath(this._extensionUri, 'src', 'common', 'copilot', 'assets', 'walkthrough', 'Copilot-In-PowerPages.md'); vscode.commands.executeCommand("markdown.showPreview", walkthroughUri); } - private authenticateAndSendAPIRequest(data: string, activeFileParams: IActiveFileParams, orgID: string, telemetry: ITelemetry) { + private async authenticateAndSendAPIRequest(data: string, activeFileParams: IActiveFileParams, orgID: string, telemetry: ITelemetry) { return intelligenceAPIAuthentication(telemetry, sessionID) - .then(async ({ accessToken, user }) => { - apiToken = accessToken; + .then(async ({ accessToken, user, userId }) => { + intelligenceApiToken = accessToken; userName = getUserName(user); + userID = userId; + this.sendMessageToWebview({ type: 'userName', value: userName }); let entityName = ""; let entityColumns: string[] = []; if (activeFileParams.dataverseEntity == "adx_entityform" || activeFileParams.dataverseEntity == 'adx_entitylist') { - entityName = getEntityName(telemetry, sessionID, activeFileParams.dataverseEntity); + entityName = await getEntityName(telemetry, sessionID, activeFileParams.dataverseEntity); const dataverseToken = await dataverseAuthentication(activeOrgUrl, true); entityColumns = await getEntityColumns(entityName, activeOrgUrl, dataverseToken, telemetry, sessionID); } - - return sendApiRequest(data, activeFileParams, orgID, apiToken, sessionID, entityName, entityColumns, telemetry, this.aibEndpoint); + return sendApiRequest(data, activeFileParams, orgID, intelligenceApiToken, sessionID, entityName, entityColumns, telemetry, this.aibEndpoint); }) .then(apiResponse => { this.sendMessageToWebview({ type: 'apiResponse', value: apiResponse }); @@ -295,9 +316,8 @@ export class PowerPagesCopilot implements vscode.WebviewViewProvider { } - private async handleOrgChangeSuccess(pacOutput: PacActiveOrgListOutput) { - const activeOrg = pacOutput.Results; - if(orgID === activeOrg.OrgId) { + private async handleOrgChangeSuccess(activeOrg: ActiveOrgOutput) { + if (IS_DESKTOP && orgID === activeOrg.OrgId) { return; } @@ -310,29 +330,29 @@ export class PowerPagesCopilot implements vscode.WebviewViewProvider { sendTelemetryEvent(this.telemetry, { eventName: CopilotOrgChangedEvent, copilotSessionId: sessionID, orgId: orgID }); this.aibEndpoint = await getIntelligenceEndpoint(orgID, this.telemetry, sessionID); - if(this.aibEndpoint === COPILOT_UNAVAILABLE) { - this.sendMessageToWebview({ type: 'Unavailable'}); + if (this.aibEndpoint === COPILOT_UNAVAILABLE) { + this.sendMessageToWebview({ type: 'Unavailable' }); } else { - this.sendMessageToWebview({ type: 'Available'}); + this.sendMessageToWebview({ type: 'Available' }); } - if (this._view?.visible) { + if (IS_DESKTOP && this._view?.visible) { showConnectedOrgMessage(environmentName, activeOrgUrl); } } - private async intelligenceAPIAuthenticationHandler(accessToken: string, user: string) { + private async intelligenceAPIAuthenticationHandler(accessToken: string, user: string, userId: string) { if (accessToken && user) { - apiToken = accessToken; + intelligenceApiToken = accessToken; userName = getUserName(user); - this.sendMessageToWebview({ type: 'isLoggedIn', value: true}) + userID = userId; + + this.sendMessageToWebview({ type: 'isLoggedIn', value: true }) this.sendMessageToWebview({ type: 'userName', value: userName }); this.sendMessageToWebview({ type: "welcomeScreen" }); } } - - private getActiveEditorContent(): IActiveFileData { const activeEditor = vscode.window.activeTextEditor; const activeFileData: IActiveFileData = { @@ -366,8 +386,6 @@ export class PowerPagesCopilot implements vscode.WebviewViewProvider { } private _getHtmlForWebview(webview: vscode.Webview) { - - const copilotScriptPath = vscode.Uri.joinPath(this._extensionUri, 'src', 'common', 'copilot', 'assets', 'scripts', 'copilot.js'); const copilotScriptUri = webview.asWebviewUri(copilotScriptPath); diff --git a/src/common/copilot/dataverseMetadata.ts b/src/common/copilot/dataverseMetadata.ts index 34d87d06..fb23b0e1 100644 --- a/src/common/copilot/dataverseMetadata.ts +++ b/src/common/copilot/dataverseMetadata.ts @@ -6,19 +6,21 @@ import fetch, { RequestInit } from "node-fetch"; import path from "path"; import * as vscode from "vscode"; -import fs from "fs"; import yaml from 'yaml'; import { ITelemetry } from "../../client/telemetry/ITelemetry"; import { sendTelemetryEvent } from "./telemetry/copilotTelemetry"; import { CopilotDataverseMetadataFailureEvent, CopilotDataverseMetadataSuccessEvent, CopilotGetEntityFailureEvent, CopilotYamlParsingFailureEvent } from "./telemetry/telemetryConstants"; +import { getFileLogicalEntityName } from "../../web/client/utilities/fileAndEntityUtil"; interface Attribute { LogicalName: string; } -export async function getEntityColumns(entityName: string, orgUrl: string, apiToken: string, telemetry:ITelemetry, sessionID:string): Promise { +declare const IS_DESKTOP: string | undefined; + +export async function getEntityColumns(entityName: string, orgUrl: string, apiToken: string, telemetry: ITelemetry, sessionID: string): Promise { try { - const dataverseURL = `${orgUrl}api/data/v9.2/EntityDefinitions(LogicalName='${entityName}')?$expand=Attributes($select=LogicalName)`; + const dataverseURL = `${orgUrl.endsWith('/') ? orgUrl : orgUrl.concat('/')}api/data/v9.2/EntityDefinitions(LogicalName='${entityName}')?$expand=Attributes($select=LogicalName)`; const requestInit: RequestInit = { method: "GET", headers: { @@ -33,11 +35,11 @@ export async function getEntityColumns(entityName: string, orgUrl: string, apiTo const responseTime = endTime - startTime || 0; const attributes = getAttributesFromResponse(jsonResponse); - sendTelemetryEvent(telemetry, {eventName: CopilotDataverseMetadataSuccessEvent, copilotSessionId: sessionID, durationInMills: responseTime, orgUrl: orgUrl}) + sendTelemetryEvent(telemetry, { eventName: CopilotDataverseMetadataSuccessEvent, copilotSessionId: sessionID, durationInMills: responseTime, orgUrl: orgUrl }) return attributes.map((attribute: Attribute) => attribute.LogicalName); } catch (error) { - sendTelemetryEvent(telemetry, {eventName: CopilotDataverseMetadataFailureEvent, copilotSessionId: sessionID, error: error as Error, orgUrl: orgUrl}) + sendTelemetryEvent(telemetry, { eventName: CopilotDataverseMetadataFailureEvent, copilotSessionId: sessionID, error: error as Error, orgUrl: orgUrl }) return []; } } @@ -63,7 +65,7 @@ function getAttributesFromResponse(jsonResponse: any): Attribute[] { } -export function getEntityName(telemetry: ITelemetry, sessionID:string, dataverseEntity: string): string { +export async function getEntityName(telemetry: ITelemetry, sessionID: string, dataverseEntity: string): Promise { let entityName = ''; try { @@ -76,35 +78,42 @@ export function getEntityName(telemetry: ITelemetry, sessionID:string, dataverse const activeFileName = path.basename(absoluteFilePath); //"Copilot-Student-Loan-Registration-56a4.basicform.custom_javascript.js" const fileNameFirstPart = activeFileName.split('.')[0]; //"Copilot-Student-Loan-Registration-56a4" - const matchingFiles = getMatchingFiles(activeFileFolderPath, fileNameFirstPart); // ["Copilot-Student-Loan-Registration-56a4.basicform.yml"] + const matchingFiles = await getMatchingFiles(activeFileFolderPath, fileNameFirstPart); // ["Copilot-Student-Loan-Registration-56a4.basicform.yml"] - if (matchingFiles[0]) { + if (IS_DESKTOP && matchingFiles[0]) { + const diskRead = await import('fs'); const yamlFilePath = path.join(activeFileFolderPath, matchingFiles[0]); - const yamlContent = fs.readFileSync(yamlFilePath, 'utf8'); + const yamlContent = diskRead.readFileSync(yamlFilePath, 'utf8'); const parsedData = parseYamlContent(yamlContent, telemetry, sessionID, dataverseEntity); entityName = parsedData['adx_entityname'] || parsedData['adx_targetentitylogicalname']; + } else if (!IS_DESKTOP) { + entityName = getFileLogicalEntityName(document.uri.fsPath); } } } catch (error) { - sendTelemetryEvent(telemetry, { eventName: CopilotGetEntityFailureEvent, copilotSessionId:sessionID, dataverseEntity: dataverseEntity, error: error as Error}); + sendTelemetryEvent(telemetry, { eventName: CopilotGetEntityFailureEvent, copilotSessionId: sessionID, dataverseEntity: dataverseEntity, error: error as Error }); entityName = ''; } - return entityName; } -function getMatchingFiles(folderPath: string, fileNameFirstPart: string): string[] { - const files = fs.readdirSync(folderPath); - const pattern = new RegExp(`^${fileNameFirstPart}\\.(basicform|list|advancedformstep)\\.yml$`); - return files.filter((fileName) => pattern.test(fileName)); +async function getMatchingFiles(folderPath: string, fileNameFirstPart: string): Promise { + if (IS_DESKTOP) { + const diskRead = await import('fs'); + const files = diskRead.readdirSync(folderPath); + const pattern = new RegExp(`^${fileNameFirstPart}\\.(basicform|list|advancedformstep)\\.yml$`); + return files.filter((fileName) => pattern.test(fileName)); + } + + return []; } // eslint-disable-next-line @typescript-eslint/no-explicit-any -function parseYamlContent(content: string, telemetry: ITelemetry, sessionID:string, dataverseEntity: string): any { +function parseYamlContent(content: string, telemetry: ITelemetry, sessionID: string, dataverseEntity: string): any { try { return yaml.parse(content); } catch (error) { - sendTelemetryEvent(telemetry, { eventName: CopilotYamlParsingFailureEvent, copilotSessionId:sessionID, dataverseEntity: dataverseEntity, error: error as Error }); + sendTelemetryEvent(telemetry, { eventName: CopilotYamlParsingFailureEvent, copilotSessionId: sessionID, dataverseEntity: dataverseEntity, error: error as Error }); return {}; } } diff --git a/src/common/copilot/model.ts b/src/common/copilot/model.ts index 62fbde62..e118f084 100644 --- a/src/common/copilot/model.ts +++ b/src/common/copilot/model.ts @@ -4,18 +4,24 @@ */ export interface IFeedbackData { - IsDismissed: boolean; - ProductContext: { key: string, value: string }[]; - Feedbacks: { key: string, value: string }[]; - } - + IsDismissed: boolean; + ProductContext: { key: string, value: string }[]; + Feedbacks: { key: string, value: string }[]; +} + export interface IActiveFileParams { - dataverseEntity: string; - entityField: string; - fieldType: string; - } + dataverseEntity: string; + entityField: string; + fieldType: string; +} export interface IActiveFileData { - activeFileParams: IActiveFileParams; - activeFileContent: string -} \ No newline at end of file + activeFileParams: IActiveFileParams; + activeFileContent: string +} + +export interface IOrgInfo { + orgId: string; + environmentName: string; + activeOrgUrl: string; +} diff --git a/src/web/client/WebExtensionContext.ts b/src/web/client/WebExtensionContext.ts index ce4377fd..6d975b01 100644 --- a/src/web/client/WebExtensionContext.ts +++ b/src/web/client/WebExtensionContext.ts @@ -335,7 +335,8 @@ class WebExtensionContext implements IWebExtensionContext { attributePath: IAttributePath, encodeAsBase64: boolean, mimeType?: string, - isContentLoaded?: boolean + isContentLoaded?: boolean, + logicalEntityName?: string ) { this.fileDataMap.setEntity( fileUri, @@ -347,8 +348,8 @@ class WebExtensionContext implements IWebExtensionContext { attributePath, encodeAsBase64, mimeType, - isContentLoaded - ); + isContentLoaded, + logicalEntityName); } public async updateEntityDetailsInContext( diff --git a/src/web/client/common/authenticationProvider.ts b/src/web/client/common/authenticationProvider.ts index 7ce938f9..0bfbc43a 100644 --- a/src/web/client/common/authenticationProvider.ts +++ b/src/web/client/common/authenticationProvider.ts @@ -34,9 +34,10 @@ export function getCommonHeaders( } //Get access token for Intelligence API service -export async function intelligenceAPIAuthentication(telemetry: ITelemetry, sessionID: string, firstTimeAuth = false): Promise<{ accessToken: string, user: string }> { +export async function intelligenceAPIAuthentication(telemetry: ITelemetry, sessionID: string, firstTimeAuth = false): Promise<{ accessToken: string, user: string, userId: string }> { let accessToken = ''; let user = ''; + let userId = ''; try { let session = await vscode.authentication.getSession(PROVIDER_ID, [`${INTELLIGENCE_SCOPE_DEFAULT}`], { silent: true }); if (!session) { @@ -45,11 +46,14 @@ export async function intelligenceAPIAuthentication(telemetry: ITelemetry, sessi } accessToken = session?.accessToken ?? ''; user = session.account.label; + userId = session?.account.id.split("/").pop() ?? + session?.account.id ?? + ""; if (!accessToken) { throw new Error(ERRORS.NO_ACCESS_TOKEN); } - if(firstTimeAuth) { + if (firstTimeAuth) { sendTelemetryEvent(telemetry, { eventName: CopilotLoginSuccessEvent, copilotSessionId: sessionID }); } } catch (error) { @@ -58,7 +62,7 @@ export async function intelligenceAPIAuthentication(telemetry: ITelemetry, sessi vscode.l10n.t("There was a permissions problem with the server")); sendTelemetryEvent(telemetry, { eventName: CopilotLoginFailureEvent, copilotSessionId: sessionID, error: authError }); } - return { accessToken, user }; + return { accessToken, user, userId }; } export async function dataverseAuthentication( diff --git a/src/web/client/context/fileData.ts b/src/web/client/context/fileData.ts index 26907c0b..2097ae12 100644 --- a/src/web/client/context/fileData.ts +++ b/src/web/client/context/fileData.ts @@ -14,6 +14,7 @@ export interface IFileData extends IFileInfo { encodeAsBase64: boolean | undefined; mimeType: string | undefined; isContentLoaded?: boolean; + logicalEntityName?: string; } export class FileData implements IFileData { @@ -28,6 +29,7 @@ export class FileData implements IFileData { private _encodeAsBase64: boolean | undefined; private _mimeType: string | undefined; private _isContentLoaded: boolean | undefined; + private _logicalEntityName: string | undefined; // Getters public get entityName(): string { @@ -64,6 +66,10 @@ export class FileData implements IFileData { return this._isContentLoaded; } + public get logicalEntityName(): string | undefined { + return this._logicalEntityName; + } + // Setters public set setHasDirtyChanges(value: boolean) { this._hasDirtyChanges = value; @@ -75,6 +81,10 @@ export class FileData implements IFileData { this._hasDiffViewTriggered = value; } + public set setLogicalEntityName(value: string | undefined) { + this._logicalEntityName = value; + } + constructor( entityId: string, entityName: string, @@ -84,7 +94,8 @@ export class FileData implements IFileData { attributePath: IAttributePath, encodeAsBase64?: boolean, mimeType?: string, - isContentLoaded?: boolean + isContentLoaded?: boolean, + logicalEntityName?: string ) { this._entityId = entityId; this._entityName = entityName; @@ -97,5 +108,6 @@ export class FileData implements IFileData { this._hasDirtyChanges = false; this._hasDiffViewTriggered = false; this._isContentLoaded = isContentLoaded; + this._logicalEntityName = logicalEntityName; } } diff --git a/src/web/client/context/fileDataMap.ts b/src/web/client/context/fileDataMap.ts index 945bde06..47979077 100644 --- a/src/web/client/context/fileDataMap.ts +++ b/src/web/client/context/fileDataMap.ts @@ -24,7 +24,8 @@ export class FileDataMap { attributePath: IAttributePath, isBase64Encoded: boolean, mimeType?: string, - isContentLoaded?: boolean + isContentLoaded?: boolean, + logicalEntityName?: string ) { const fileData = new FileData( entityId, @@ -35,7 +36,8 @@ export class FileDataMap { attributePath, isBase64Encoded, mimeType, - isContentLoaded + isContentLoaded, + logicalEntityName ); this.fileMap.set(vscode.Uri.parse(fileUri).fsPath, fileData); } diff --git a/src/web/client/dal/remoteFetchProvider.ts b/src/web/client/dal/remoteFetchProvider.ts index a675bc05..a3645df0 100644 --- a/src/web/client/dal/remoteFetchProvider.ts +++ b/src/web/client/dal/remoteFetchProvider.ts @@ -14,7 +14,7 @@ import { isWebfileContentLoadNeeded, setFileContent, } from "../utilities/commonUtil"; -import { getCustomRequestURL, getMappingEntityContent, getMappingEntityId, getMimeType, getRequestURL } from "../utilities/urlBuilderUtil"; +import { getCustomRequestURL, getLogicalEntityName, getMappingEntityContent, getMappingEntityId, getMimeType, getRequestURL } from "../utilities/urlBuilderUtil"; import { getCommonHeaders } from "../common/authenticationProvider"; import * as Constants from "../common/constants"; import { ERRORS, showErrorDialog } from "../common/errorHandler"; @@ -23,6 +23,7 @@ import { encodeAsBase64, getAttributePath, getEntity, + getLogicalEntityParameter, isBase64Encoded, } from "../utilities/schemaHelperUtil"; import WebExtensionContext from "../WebExtensionContext"; @@ -419,6 +420,7 @@ async function createFile( let mappingEntityId = null // By default content is preloaded for all the files except for non-text webfiles for V2 const isPreloadedContent = mappingEntityFetchQuery ? isWebfileContentLoadNeeded(fileNameWithExtension, fileUri) : true; + const logicalEntityName = getLogicalEntityParameter(entityName); // update func for webfiles for V2 const attributePath: IAttributePath = getAttributePath( @@ -455,7 +457,8 @@ async function createFile( result[Constants.ODATA_ETAG], mimeType ?? result[Constants.MIMETYPE], isPreloadedContent, - mappingEntityId + mappingEntityId, + getLogicalEntityName(result, logicalEntityName) ); } @@ -604,7 +607,8 @@ async function createVirtualFile( odataEtag: string, mimeType?: string, isPreloadedContent?: boolean, - mappingEntityId?: string + mappingEntityId?: string, + logicalEntityName?: string ) { // Maintain file information in context await WebExtensionContext.updateFileDetailsInContext( @@ -617,7 +621,8 @@ async function createVirtualFile( attributePath, encodeAsBase64, mimeType, - isPreloadedContent + isPreloadedContent, + logicalEntityName ); // Call file system provider write call for buffering file data in VFS diff --git a/src/web/client/extension.ts b/src/web/client/extension.ts index 2cb6d4af..1424b82a 100644 --- a/src/web/client/extension.ts +++ b/src/web/client/extension.ts @@ -30,6 +30,8 @@ import { } from "./utilities/fileAndEntityUtil"; import { IEntityInfo } from "./common/interfaces"; import { telemetryEventNames } from "./telemetry/constants"; +import * as copilot from "../../common/copilot/PowerPagesCopilot"; +import { IOrgInfo } from "../../common/copilot/model"; export function activate(context: vscode.ExtensionContext): void { // setup telemetry @@ -127,6 +129,7 @@ export function activate(context: vscode.ExtensionContext): void { }, async () => { await portalsFS.readDirectory(WebExtensionContext.rootDirectory, true); + registerCopilot(context); } ); @@ -377,6 +380,57 @@ export function showWalkthrough( ); } +export function registerCopilot(context: vscode.ExtensionContext) { + try { + const orgInfo = { + orgId: WebExtensionContext.urlParametersMap.get( + queryParameters.ORG_ID + ) as string, + environmentName: "", + activeOrgUrl: WebExtensionContext.urlParametersMap.get(queryParameters.ORG_URL) as string + } as IOrgInfo; + + const copilotPanel = new copilot.PowerPagesCopilot(context.extensionUri, + context, + WebExtensionContext.telemetry.getTelemetryReporter(), + undefined, + orgInfo); + + context.subscriptions.push(vscode.window.registerWebviewViewProvider(copilot.PowerPagesCopilot.viewType, copilotPanel, { + webviewOptions: { + retainContextWhenHidden: true, + } + })); + + showNotificationForCopilot(orgInfo.orgId); + } catch (error) { + WebExtensionContext.telemetry.sendErrorTelemetry( + telemetryEventNames.WEB_EXTENSION_WEB_COPILOT_REGISTRATION_FAILED, + registerCopilot.name, + (error as Error)?.message, + error as Error); + } +} + +function showNotificationForCopilot(orgId: string) { + if (vscode.workspace.getConfiguration('powerPlatform').get('experimental.enableWebCopilot') === false) { + return; + } + const message = vscode.l10n.t('Get help writing code in HTML, CSS, and JS languages for Power Pages sites with Copilot.'); + const actionTitle = vscode.l10n.t('Try Copilot for Power Pages'); + + WebExtensionContext.telemetry.sendInfoTelemetry(telemetryEventNames.WEB_EXTENSION_WEB_COPILOT_NOTIFICATION_SHOWN, + { orgId: orgId }); + + vscode.window.showInformationMessage(message, actionTitle).then((selection) => { + if (selection === actionTitle) { + WebExtensionContext.telemetry.sendInfoTelemetry(telemetryEventNames.WEB_EXTENSION_WEB_COPILOT_NOTIFICATION_EVENT_CLICKED, + { orgId: orgId }); + vscode.commands.executeCommand('powerpages.copilot.focus') + } + }); +} + export async function deactivate(): Promise { const telemetry = WebExtensionContext.telemetry; if (telemetry) { diff --git a/src/web/client/schema/constants.ts b/src/web/client/schema/constants.ts index 20615805..8bcdfb58 100644 --- a/src/web/client/schema/constants.ts +++ b/src/web/client/schema/constants.ts @@ -22,6 +22,7 @@ export enum schemaEntityKey { ATTRIBUTES_EXTENSION = "_attributesExtension", VSCODE_ENTITY_NAME = "_vscodeentityname", DATAVERSE_ENTITY_NAME = "_dataverseenityname", + DATAVERSE_LOGICAL_ENTITY_NAME = "_dataverselogicalentityname", FETCH_QUERY_PARAMETERS = "_fetchQueryParameters", MULTI_FILE_FETCH_QUERY_PARAMETERS = "_multiFileFetchQueryParameters", MAPPING_ENTITY_ID = "_mappingEntityId", diff --git a/src/web/client/schema/portalSchema.ts b/src/web/client/schema/portalSchema.ts index 55e44a5a..0eadce30 100644 --- a/src/web/client/schema/portalSchema.ts +++ b/src/web/client/schema/portalSchema.ts @@ -155,6 +155,7 @@ export const portal_schema_V1 = { relationships: "", _vscodeentityname: "lists", _dataverseenityname: "adx_entitylists", + _dataverselogicalentityname: "adx_entityname", _displayname: "Lists", _etc: "10060", _primaryidfield: "adx_entitylistid", @@ -163,8 +164,8 @@ export const portal_schema_V1 = { _foldername: "lists", _propextension: "", _exporttype: "SingleFolder", - _fetchQueryParameters: "?$filter=adx_entitylistid eq {entityId} &$select=adx_name,adx_registerstartupscript", - _multiFileFetchQueryParameters: "?$filter=_adx_websiteid_value eq {websiteId} &$select=adx_name,adx_registerstartupscript,adx_entitylistid&$count=true", + _fetchQueryParameters: "?$filter=adx_entitylistid eq {entityId} &$select=adx_name,adx_registerstartupscript,adx_entityname", + _multiFileFetchQueryParameters: "?$filter=_adx_websiteid_value eq {websiteId} &$select=adx_name,adx_registerstartupscript,adx_entitylistid,adx_entityname&$count=true", _attributes: "adx_registerstartupscript", _attributesExtension: new Map([["adx_registerstartupscript", "list.customjs.js"]]), }, @@ -172,6 +173,7 @@ export const portal_schema_V1 = { relationships: "", _vscodeentityname: "basicforms", _dataverseenityname: "adx_entityforms", + _dataverselogicalentityname: "adx_entityname", _displayname: "Basic Forms", _etc: "10060", _primaryidfield: "adx_entityformid", @@ -180,8 +182,8 @@ export const portal_schema_V1 = { _foldername: "basic-forms", _propextension: "", _exporttype: "SingleFolder", - _fetchQueryParameters: "?$filter=adx_entityformid eq {entityId} &$select=adx_name,adx_registerstartupscript", - _multiFileFetchQueryParameters: "?$filter=_adx_websiteid_value eq {websiteId}&$select=adx_name,adx_registerstartupscript,adx_entityformid&$count=true", + _fetchQueryParameters: "?$filter=adx_entityformid eq {entityId} &$select=adx_name,adx_registerstartupscript,adx_entityname", + _multiFileFetchQueryParameters: "?$filter=_adx_websiteid_value eq {websiteId}&$select=adx_name,adx_registerstartupscript,adx_entityformid,adx_entityname&$count=true", _attributes: "adx_registerstartupscript", _attributesExtension: new Map([["adx_registerstartupscript", "basicform.customjs.js"]]), }, @@ -197,8 +199,8 @@ export const portal_schema_V1 = { _foldername: "advanced-forms", _propextension: "", _exporttype: "SubFolders", - _fetchQueryParameters: "?$filter=adx_webformid eq {entityId} &$select=adx_name&$expand=adx_webformstep_webform($select=adx_name,adx_registerstartupscript,adx_webformstepid)", - _multiFileFetchQueryParameters: "?$filter=_adx_websiteid_value eq {websiteId} &$select=adx_name,adx_webformid&$expand=adx_webformstep_webform($select=adx_name,adx_registerstartupscript,adx_webformstepid)&$count=true", + _fetchQueryParameters: "?$filter=adx_webformid eq {entityId} &$select=adx_name&$expand=adx_webformstep_webform($select=adx_name,adx_registerstartupscript,adx_webformstepid,adx_targetentitylogicalname)", + _multiFileFetchQueryParameters: "?$filter=_adx_websiteid_value eq {websiteId} &$select=adx_name,adx_webformid&$expand=adx_webformstep_webform($select=adx_name,adx_registerstartupscript,adx_webformstepid,adx_targetentitylogicalname)&$count=true", _attributes: "adx_webformstep_webform", _attributesExtension: new Map([]), }, @@ -206,6 +208,7 @@ export const portal_schema_V1 = { relationships: "", _vscodeentityname: "advancedformsteps", _dataverseenityname: "adx_webformsteps", + _dataverselogicalentityname: "adx_targetentitylogicalname", _displayname: "Advanced Form Steps", _etc: "10060", _primaryidfield: "adx_webformstepid", @@ -214,8 +217,8 @@ export const portal_schema_V1 = { _foldername: "advanced-form-steps", _propextension: "", _exporttype: "SubFolders", - _fetchQueryParameters: "?$filter=_adx_webform_value eq {entityId} &$select=adx_name,adx_registerstartupscript,adx_webformstepid", - _multiFileFetchQueryParameters: "?$filter=_adx_websiteid_value eq {websiteId} &$select=adx_name,adx_registerstartupscript,adx_webformstepid&$count=true", + _fetchQueryParameters: "?$filter=adx_webformstepid eq {entityId} &$select=adx_name,adx_registerstartupscript,adx_webformstepid,adx_targetentitylogicalname", + _multiFileFetchQueryParameters: "?$filter=_adx_websiteid_value eq {websiteId} &$select=adx_name,adx_registerstartupscript,adx_webformstepid,adx_targetentitylogicalname&$count=true", _attributes: "adx_registerstartupscript", _attributesExtension: new Map([["adx_registerstartupscript", "advancedformstep.customjs.js"]]), }, @@ -366,6 +369,7 @@ export const portal_schema_V2 = { relationships: "", _vscodeentityname: "lists", _dataverseenityname: "powerpagecomponents", + _dataverselogicalentityname: "content.entityname", _displayname: "Lists", _etc: "10271", _primaryidfield: "powerpagecomponentid", @@ -384,6 +388,7 @@ export const portal_schema_V2 = { relationships: "", _vscodeentityname: "basicforms", _dataverseenityname: "powerpagecomponents", + _dataverselogicalentityname: "content.entityname", _displayname: "Basic Forms", _etc: "10271", _primaryidfield: "powerpagecomponentid", @@ -420,6 +425,7 @@ export const portal_schema_V2 = { relationships: "", _vscodeentityname: "advancedformsteps", _dataverseenityname: "powerpagecomponents", + _dataverselogicalentityname: "content.targetentitylogicalname", _displayname: "Advanced Form Steps", _etc: "10060", _primaryidfield: "powerpagecomponentid", diff --git a/src/web/client/telemetry/constants.ts b/src/web/client/telemetry/constants.ts index 82086b7e..f93304cf 100644 --- a/src/web/client/telemetry/constants.ts +++ b/src/web/client/telemetry/constants.ts @@ -88,4 +88,7 @@ export enum telemetryEventNames { WEB_EXTENSION_DATAVERSE_API_CALL_FILE_FETCH_COUNT = 'webExtensionDataverseFileFetchCount', WEB_EXTENSION_WEBFILE_EXTENSION = 'webExtensionWebFileExtension', WEB_EXTENSION_FILE_NO_CONTENT = 'webExtensionFileNoContent', + WEB_EXTENSION_WEB_COPILOT_REGISTRATION_FAILED = 'webExtensionCopilotRegisterFailed', + WEB_EXTENSION_WEB_COPILOT_NOTIFICATION_SHOWN = 'webExtensionCopilotNotificationShown', + WEB_EXTENSION_WEB_COPILOT_NOTIFICATION_EVENT_CLICKED = 'webExtensionCopilotNotificationEventClicked', } \ No newline at end of file diff --git a/src/web/client/utilities/fileAndEntityUtil.ts b/src/web/client/utilities/fileAndEntityUtil.ts index 0b1ff441..1bf2ae9a 100644 --- a/src/web/client/utilities/fileAndEntityUtil.ts +++ b/src/web/client/utilities/fileAndEntityUtil.ts @@ -33,6 +33,11 @@ export function getFileEntityEtag(fileFsPath: string) { ?.entityEtag as string; } +export function getFileLogicalEntityName(fileFsPath: string) { + return WebExtensionContext.fileDataMap.getFileMap.get(fileFsPath) + ?.logicalEntityName as string; +} + export function updateFileEntityEtag(fileFsPath: string, entityEtag: string) { WebExtensionContext.fileDataMap.updateEtagValue(fileFsPath, entityEtag); } diff --git a/src/web/client/utilities/schemaHelperUtil.ts b/src/web/client/utilities/schemaHelperUtil.ts index 518c8b18..8f3218aa 100644 --- a/src/web/client/utilities/schemaHelperUtil.ts +++ b/src/web/client/utilities/schemaHelperUtil.ts @@ -21,6 +21,10 @@ export function getEntityFetchQuery(entity: string, useRegularFetchQuery = false ); } +export function getLogicalEntityParameter(entity: string) { + return getEntity(entity)?.get(schemaEntityKey.DATAVERSE_LOGICAL_ENTITY_NAME); +} + export function getPortalSchema(schema: string) { if ( schema.toLowerCase() === diff --git a/src/web/client/utilities/urlBuilderUtil.ts b/src/web/client/utilities/urlBuilderUtil.ts index 298740b3..acbcd80c 100644 --- a/src/web/client/utilities/urlBuilderUtil.ts +++ b/src/web/client/utilities/urlBuilderUtil.ts @@ -16,7 +16,7 @@ import { schemaEntityName, schemaKey, } from "../schema/constants"; -import { getEntity, getEntityFetchQuery } from "./schemaHelperUtil"; +import { getAttributePath, getEntity, getEntityFetchQuery } from "./schemaHelperUtil"; export const getParameterizedRequestUrlTemplate = ( useSingleEntityUrl: boolean @@ -198,6 +198,21 @@ export function getMimeType(result: any) { return result[MIMETYPE]; } +// TODO - Make Json for different response type and update any here +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function getLogicalEntityName(result: any, logicalEntityName?: string) { + let logicalEntity; + + if (logicalEntityName) { + const attributePath = getAttributePath(logicalEntityName); + logicalEntity = attributePath.relativePath.length > 0 ? + JSON.parse(result[attributePath.source])[attributePath.relativePath] : + result[attributePath.source]; + } + + return logicalEntity; +} + export function pathHasEntityFolderName(uri: string): boolean { for (const entry of WebExtensionContext.entitiesFolderNameMap.entries()) { if (uri.includes(entry[1])) { diff --git a/webpack.config.js b/webpack.config.js index 87b4b247..504fb647 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -102,9 +102,6 @@ const webConfig = { }] }, plugins: [ - new webpack.ProvidePlugin({ - process: 'process/browser', // provide a shim for the global `process` variable - }), new webpack.ProvidePlugin({ Buffer: [ 'buffer', 'Buffer' ], }), From f30227f1aa161c02013a4b7b56b2d0759520881d Mon Sep 17 00:00:00 2001 From: Ritik Ramuka <56073559+ritikramuka@users.noreply.github.com> Date: Fri, 15 Sep 2023 19:30:24 +0530 Subject: [PATCH 2/9] userDataMap intreface for co-presence audience (#702) * userDataMap schema added * updated some errors --- src/web/client/context/userDataMap.ts | 81 +++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 src/web/client/context/userDataMap.ts diff --git a/src/web/client/context/userDataMap.ts b/src/web/client/context/userDataMap.ts new file mode 100644 index 00000000..d95dac1a --- /dev/null +++ b/src/web/client/context/userDataMap.ts @@ -0,0 +1,81 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +export interface IUserData { + containerId: string; + fileName: string; + filePath: string; + userName: string; + userId: string; +} + +export class UserData implements IUserData { + _containerId: string; + _fileName: string; + _filePath: string; + _userName: string; + _userId: string; + + // Getters + public get containerId(): string { + return this._containerId; + } + public get fileName(): string { + return this._fileName; + } + public get filePath(): string { + return this._filePath; + } + public get userName(): string { + return this._userName; + } + public get userId(): string { + return this._userId; + } + + constructor( + containerId: string, + fileName: string, + filePath: string, + userName: string, + userId: string + ) { + this._fileName = fileName; + this._containerId = containerId; + this._filePath = filePath; + this._userName = userName; + this._userId = userId; + } +} + +export class UserDataMap { + private usersMap: Map = new Map(); + + public get getUserMap(): Map { + return this.usersMap; + } + + public setUserData( + containerId: string, + fileName: string, + filePath: string, + userName: string, + userId: string + ) { + const userData = new UserData( + containerId, + fileName, + filePath, + userName, + userId + ); + + this.usersMap.set(userId, userData); + } + + public removeUser(userId: string) { + this.usersMap.delete(userId); + } +} From 298bbac96874f64a99f6da935b2e04ed2ffb3c4c Mon Sep 17 00:00:00 2001 From: Ritik Ramuka <56073559+ritikramuka@users.noreply.github.com> Date: Sat, 16 Sep 2023 03:27:10 +0530 Subject: [PATCH 3/9] co presence feature flag (#701) * feature flag co-presence --- package.json | 5 +++++ src/web/client/common/constants.ts | 5 ++++- src/web/client/extension.ts | 10 +++++++++- src/web/client/telemetry/constants.ts | 4 +++- src/web/client/utilities/commonUtil.ts | 23 ++++++++++++++++++++++- 5 files changed, 43 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index e93a8370..573e4c56 100644 --- a/package.json +++ b/package.json @@ -438,6 +438,11 @@ "type": "boolean", "markdownDescription": "Enable copilot in Visual Studio Code (Web extension)", "default": true + }, + "powerPlatform.experimental.enableCoPresenceFeature": { + "type": "boolean", + "markdownDescription": "Enable co-presence in Visual Studio Code (Web extension only)", + "default": true } } }, diff --git a/src/web/client/common/constants.ts b/src/web/client/common/constants.ts index ce8e5f5f..960960ce 100644 --- a/src/web/client/common/constants.ts +++ b/src/web/client/common/constants.ts @@ -48,6 +48,9 @@ export const VERSION_CONTROL_FOR_WEB_EXTENSION_SETTING_NAME = // Multi-file feature constants export const MULTI_FILE_FEATURE_SETTING_NAME = "enableMultiFileFeature"; +// Co-presence feature constants +export const CO_PRESENCE_FEATURE_SETTING_NAME = "enableCoPresenceFeature"; + export enum initializationEntityName { WEBSITE = "websites", WEBSITELANGUAGE = "websitelanguages", @@ -89,4 +92,4 @@ export enum SurveyConstants { export enum portalSchemaVersion { V1 = "portalschemav1", V2 = "portalschemav2", -} \ No newline at end of file +} diff --git a/src/web/client/extension.ts b/src/web/client/extension.ts index 1424b82a..e20ea23d 100644 --- a/src/web/client/extension.ts +++ b/src/web/client/extension.ts @@ -18,7 +18,7 @@ import { showErrorDialog, } from "./common/errorHandler"; import { WebExtensionTelemetry } from "./telemetry/webExtensionTelemetry"; -import { convertContentToString } from "./utilities/commonUtil"; +import { convertContentToString, isCoPresenceEnabled } from "./utilities/commonUtil"; import { NPSService } from "./services/NPSService"; import { vscodeExtAppInsightsResourceProvider } from "../../common/telemetry-generated/telemetryConfiguration"; import { NPSWebView } from "./webViews/NPSWebView"; @@ -180,6 +180,8 @@ export function activate(context: vscode.ExtensionContext): void { processWillSaveDocument(context); + processWillStartCollaboartion(); + showWalkthrough(context, WebExtensionContext.telemetry); } @@ -257,6 +259,12 @@ export function processWillSaveDocument(context: vscode.ExtensionContext) { ); } +export function processWillStartCollaboartion() { + if (isCoPresenceEnabled()) { + // TODO: Add copresence logic + } +} + export async function showSiteVisibilityDialog() { if ( WebExtensionContext.urlParametersMap.get( diff --git a/src/web/client/telemetry/constants.ts b/src/web/client/telemetry/constants.ts index f93304cf..e7ca18d3 100644 --- a/src/web/client/telemetry/constants.ts +++ b/src/web/client/telemetry/constants.ts @@ -73,6 +73,8 @@ export enum telemetryEventNames { WEB_EXTENSION_MULTI_FILE_FEATURE_FLAG_ENABLED = "WebExtensionMultiFileFeatureFlagEnabled", WEB_EXTENSION_MULTI_FILE_FEATURE_FLAG_DISABLED = "WebExtensionMultiFileFeatureFlagDisabled", WEB_EXTENSION_MULTI_FILE_MANDATORY_PARAMETERS_MISSING = "WebExtensionMultiFileMandatoryParametersMissing", + WEB_EXTENSION_CO_PRESENCE_FEATURE_FLAG_DISABLED = "WebExtensionCoPresenceFeatureFlagDisabled", + WEB_EXTENSION_CO_PRESENCE_FEATURE_FLAG_ENABLED = "WebExtensionCoPresenceFeatureFlagEnabled", WEB_EXTENSION_FILES_LOAD_SUCCESS = "WebExtensionFilesLoadSuccess", WEB_EXTENSION_PREPARE_WORKSPACE_SUCCESS = "webExtensionPrepareWorkspaceSuccess", WEB_EXTENSION_MULTI_FILE_FEATURE_AVAILABILITY = "WebExtensionMultiFileFeatureAvailability", @@ -91,4 +93,4 @@ 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', -} \ No newline at end of file +} diff --git a/src/web/client/utilities/commonUtil.ts b/src/web/client/utilities/commonUtil.ts index 170137b8..f16e8790 100644 --- a/src/web/client/utilities/commonUtil.ts +++ b/src/web/client/utilities/commonUtil.ts @@ -6,6 +6,7 @@ import * as vscode from "vscode"; import { BASE_64, + CO_PRESENCE_FEATURE_SETTING_NAME, DATA, MULTI_FILE_FEATURE_SETTING_NAME, NO_CONTENT, @@ -130,6 +131,25 @@ export function isMultifileEnabled() { return isMultifileEnabled as boolean; } +export function isCoPresenceEnabled() { + const isCoPresenceEnabled = vscode.workspace + .getConfiguration(SETTINGS_EXPERIMENTAL_STORE_NAME) + .get(CO_PRESENCE_FEATURE_SETTING_NAME); + + if (!isCoPresenceEnabled) { + WebExtensionContext.telemetry.sendInfoTelemetry( + telemetryEventNames.WEB_EXTENSION_CO_PRESENCE_FEATURE_FLAG_DISABLED + ); + } + else { + WebExtensionContext.telemetry.sendInfoTelemetry( + telemetryEventNames.WEB_EXTENSION_CO_PRESENCE_FEATURE_FLAG_ENABLED + ); + } + + return isCoPresenceEnabled as boolean; +} + /** * Utility function. Check if it's Null Or Undefined * @param object object to be validated @@ -171,10 +191,11 @@ export function isWebfileContentLoadNeeded(fileName: string, fsPath: string): bo doesFileExist(fsPath) : false; } + export function isPortalVersionV1(): boolean { return WebExtensionContext.currentSchemaVersion.toLowerCase() === portalSchemaVersion.V1; } export function isPortalVersionV2(): boolean { return WebExtensionContext.currentSchemaVersion.toLowerCase() === portalSchemaVersion.V2; -} \ No newline at end of file +} From 6c3fc468306d6dd03b0295e575fcebc24c151cd2 Mon Sep 17 00:00:00 2001 From: tyaginidhi Date: Mon, 18 Sep 2023 09:12:42 +0530 Subject: [PATCH 4/9] preview extension "Learn more about Copilot" image update around correct casing (#698) --- .../copilot/assets/walkthrough/Copilot-In-PowerPages.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/common/copilot/assets/walkthrough/Copilot-In-PowerPages.md b/src/common/copilot/assets/walkthrough/Copilot-In-PowerPages.md index 24a393f7..b15e8e95 100644 --- a/src/common/copilot/assets/walkthrough/Copilot-In-PowerPages.md +++ b/src/common/copilot/assets/walkthrough/Copilot-In-PowerPages.md @@ -2,7 +2,7 @@ Copilot in Visual Studio Code helps you code using natural language chat interaction. In Power Pages, you work with site code that includes HTML, JS, or CSS code to make site customizations that are not currently supported in Power Pages low-code design studio. This Copilot chat experience assists Power Pages developers like you to write code by simply describing your expected code behavior using natural language. You can then refine the generated code and use it when customizing your site. -![Copilot Screen](./images/copilotImage.svg) +![Copilot Screen](./images/copilotimage.svg) ## Prerequisites @@ -12,6 +12,7 @@ Review the [terms](https://go.microsoft.com/fwlink/?linkid=2189520) and [Respons - Open site root folder in Visual Studio Code. ![Demo Site Screen](./images/websiteselection.svg) + - Login to Power Pages Copilot with your Dataverse Environment credentials. ## How to use Copilot to generate code @@ -19,7 +20,7 @@ Review the [terms](https://go.microsoft.com/fwlink/?linkid=2189520) and [Respons Copilot in Visual Studio Code is tuned to generate code for Power Pages sites, so its functionalities are limited to Power Pages site supported languages like HTML, JS, and CSS. The generated code from Copilot makes use of supported frameworks like bootstrap and jQuery. 1. In the Copilot chat, describe the code behavior you want using natural language. For example, code for form validation or Ajax calls using the Power Pages Web API. -1. Continue to repharse your questions in the Copilot chat and iterate them till you’ve got what you need. +1. Continue to repharse your questions in the Copilot chat and iterate them till you’ve got what you need. 1. Once you are happy with the generated code, you can easily copy and paste the code snippet or insert the code to Power Pages site and modify the code further. 1. Use the **up/down** arrow key to navigate between recently entered prompts. From a40f2e8c089479954a56799ac12c48dd518c3816 Mon Sep 17 00:00:00 2001 From: tyaginidhi Date: Mon, 18 Sep 2023 22:29:20 +0530 Subject: [PATCH 5/9] Intelligence API prod endpoint update (#699) --- src/common/ArtemisService.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/common/ArtemisService.ts b/src/common/ArtemisService.ts index 2ef8ea7f..92a39978 100644 --- a/src/common/ArtemisService.ts +++ b/src/common/ArtemisService.ts @@ -9,7 +9,6 @@ import { ITelemetry } from "../client/telemetry/ITelemetry"; import { sendTelemetryEvent } from "./copilot/telemetry/copilotTelemetry"; import { CopilotArtemisFailureEvent, CopilotArtemisSuccessEvent } from "./copilot/telemetry/telemetryConstants"; -declare const IS_DESKTOP: string | undefined; export async function getIntelligenceEndpoint(orgId: string, telemetry: ITelemetry, sessionID: string) { const { tstUrl, preprodUrl, prodUrl } = convertGuidToUrls(orgId); const endpoints = [tstUrl, preprodUrl, prodUrl]; @@ -27,7 +26,7 @@ export async function getIntelligenceEndpoint(orgId: string, telemetry: ITelemet return COPILOT_UNAVAILABLE; } - const intelligenceEndpoint = `https://aibuildertextapiservice.${geoName}-${IS_DESKTOP ? 'il' + clusterNumber : 'il001'}.gateway.${environment}.island.powerapps.com/v1.0/${orgId}/appintelligence/chat` + const intelligenceEndpoint = `https://aibuildertextapiservice.${geoName}-${'il' + clusterNumber}.gateway.${environment}.island.powerapps.com/v1.0/${orgId}/appintelligence/chat` return intelligenceEndpoint; From 72e40e09a3ffb7ca65fedcc71ad6dc132cbd341e Mon Sep 17 00:00:00 2001 From: tyaginidhi Date: Mon, 18 Sep 2023 23:27:16 +0530 Subject: [PATCH 6/9] Show "Preview" text in copilot webview (#711) * Show "Preview" text in copilot webview * Update the preview tag with brackets for different emphasis --- src/common/copilot/PowerPagesCopilot.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/common/copilot/PowerPagesCopilot.ts b/src/common/copilot/PowerPagesCopilot.ts index 8cd9a72a..de64c47a 100644 --- a/src/common/copilot/PowerPagesCopilot.ts +++ b/src/common/copilot/PowerPagesCopilot.ts @@ -128,7 +128,8 @@ export class PowerPagesCopilot implements vscode.WebviewViewProvider { ) { this._view = webviewView; - webviewView.description = "PREVIEW" + webviewView.title = "Copilot In Power Pages" + (IS_DESKTOP ? "" : " [PREVIEW]"); + webviewView.description = "PREVIEW"; webviewView.webview.options = { // Allow scripts in the webview enableScripts: true, From 99bc1aca6f48008158d8fa1f2ee3ff9be242f844 Mon Sep 17 00:00:00 2001 From: amitjoshi438 <54068463+amitjoshi438@users.noreply.github.com> Date: Mon, 18 Sep 2023 23:32:38 +0530 Subject: [PATCH 7/9] [Powerpages] Copilot notification panel (#712) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * copilot notification panel * updated image * made panel responsive * Added telemetry * updated global state constant * Web extension copilot notification update * removed commented code * unload webview correctly for web extension and rename variable * updated css to align with vscode * updated telemetry and suggested prompt * removed commented code --------- Co-authored-by: Amit Joshi Co-authored-by: Nidhi Tyagi 🌟🐇🌴❄️ Co-authored-by: tyaginidhi --- public/images/codicon-arrow-rightarrow.svg | 3 + src/client/extension.ts | 19 ++- src/common/Utils.ts | 5 + src/common/copilot/PowerPagesCopilot.ts | 9 +- src/common/copilot/assets/scripts/copilot.js | 10 +- src/common/copilot/constants.ts | 1 + .../copilot/telemetry/telemetryConstants.ts | 4 +- .../CopilotNotificationPanel.ts | 130 ++++++++++++++++++ .../copilot/welcome-notification/arrow.svg | 3 + .../copilotNotification.css | 93 +++++++++++++ .../copilotNotification.js | 35 +++++ .../welcome-notification/notification.svg | 109 +++++++++++++++ src/web/client/extension.ts | 29 ++-- 13 files changed, 412 insertions(+), 38 deletions(-) create mode 100644 public/images/codicon-arrow-rightarrow.svg create mode 100644 src/common/copilot/welcome-notification/CopilotNotificationPanel.ts create mode 100644 src/common/copilot/welcome-notification/arrow.svg create mode 100644 src/common/copilot/welcome-notification/copilotNotification.css create mode 100644 src/common/copilot/welcome-notification/copilotNotification.js create mode 100644 src/common/copilot/welcome-notification/notification.svg diff --git a/public/images/codicon-arrow-rightarrow.svg b/public/images/codicon-arrow-rightarrow.svg new file mode 100644 index 00000000..27221c17 --- /dev/null +++ b/public/images/codicon-arrow-rightarrow.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/client/extension.ts b/src/client/extension.ts index b0644efe..2a8f8d6a 100644 --- a/src/client/extension.ts +++ b/src/client/extension.ts @@ -32,7 +32,9 @@ import { readUserSettings } from "./telemetry/localfileusersettings"; import { initializeGenerator } from "./power-pages/create/CreateCommandWrapper"; import { disposeDiagnostics } from "./power-pages/validationDiagnostics"; import { bootstrapDiff } from "./power-pages/bootstrapdiff/BootstrapDiff"; -import { CopilotNotificationShown, CopilotTryNotificationClickedEvent } from "../common/copilot/telemetry/telemetryConstants"; +import { CopilotNotificationShown } from "../common/copilot/telemetry/telemetryConstants"; +import { copilotNotificationPanel, disposeNotificationPanel } from "../common/copilot/welcome-notification/CopilotNotificationPanel"; +import { COPILOT_NOTIFICATION_DISABLED } from "../common/copilot/constants"; let client: LanguageClient; let _context: vscode.ExtensionContext; @@ -208,6 +210,7 @@ export async function deactivate(): Promise { disposeDiagnostics(); deactivateDebugger(); + disposeNotificationPanel(); } function didOpenTextDocument(document: vscode.TextDocument): void { @@ -355,18 +358,14 @@ function showNotificationForCopilot(telemetry: TelemetryReporter, telemetryData: if (vscode.workspace.getConfiguration('powerPlatform').get('experimental.copilotEnabled') === false) { return; } - const message = vscode.l10n.t('Get help writing code in HTML, CSS, and JS languages for Power Pages sites with Copilot.'); - const actionTitle = vscode.l10n.t('Try Copilot for Power Pages'); - telemetry.sendTelemetryEvent(CopilotNotificationShown, { listOfOrgs: telemetryData, countOfActivePortals }); + const isCopilotNotificationDisabled = _context.globalState.get(COPILOT_NOTIFICATION_DISABLED, false); - vscode.window.showInformationMessage(message, actionTitle).then((selection) => { - if (selection === actionTitle) { - telemetry.sendTelemetryEvent(CopilotTryNotificationClickedEvent, { listOfOrgs: telemetryData, countOfActivePortals }); + if (!isCopilotNotificationDisabled) { + telemetry.sendTelemetryEvent(CopilotNotificationShown, { listOfOrgs: telemetryData, countOfActivePortals }); + copilotNotificationPanel(_context, telemetry, telemetryData, countOfActivePortals); + } - vscode.commands.executeCommand('powerpages.copilot.focus') - } - }); } // allow for DI without direct reference to vscode's d.ts file: that definintions file is being generated at VS Code runtime diff --git a/src/common/Utils.ts b/src/common/Utils.ts index 554e8dbd..7ce9c19f 100644 --- a/src/common/Utils.ts +++ b/src/common/Utils.ts @@ -93,3 +93,8 @@ export function getExtensionVersion(): string { export function getExtensionType(): string { return vscode.env.uiKind === vscode.UIKind.Desktop ? "Desktop" : "Web"; } + +export function openWalkthrough(extensionUri: vscode.Uri) { + const walkthroughUri = vscode.Uri.joinPath(extensionUri, 'src', 'common', 'copilot', 'assets', 'walkthrough', 'Copilot-In-PowerPages.md'); + vscode.commands.executeCommand("markdown.showPreview", walkthroughUri); +} diff --git a/src/common/copilot/PowerPagesCopilot.ts b/src/common/copilot/PowerPagesCopilot.ts index de64c47a..4b4a6c5a 100644 --- a/src/common/copilot/PowerPagesCopilot.ts +++ b/src/common/copilot/PowerPagesCopilot.ts @@ -12,7 +12,7 @@ import { PacWrapper } from "../../client/pac/PacWrapper"; import { ITelemetry } from "../../client/telemetry/ITelemetry"; import { AUTH_CREATE_FAILED, AUTH_CREATE_MESSAGE, AuthProfileNotFound, COPILOT_UNAVAILABLE, CopilotDisclaimer, CopilotStylePathSegments, DataverseEntityNameMap, EntityFieldMap, FieldTypeMap, PAC_SUCCESS, WebViewMessage, sendIconSvg } from "./constants"; import { IActiveFileParams, IActiveFileData, IOrgInfo } from './model'; -import { escapeDollarSign, getLastThreePartsOfFileName, getNonce, getUserName, showConnectedOrgMessage, showInputBoxAndGetOrgUrl, showProgressWithNotification } from "../Utils"; +import { escapeDollarSign, getLastThreePartsOfFileName, getNonce, getUserName, openWalkthrough, showConnectedOrgMessage, showInputBoxAndGetOrgUrl, showProgressWithNotification } from "../Utils"; import { CESUserFeedback } from "./user-feedback/CESSurvey"; import { GetAuthProfileWatchPattern } from "../../client/lib/AuthPanelView"; import { ActiveOrgOutput } from "../../client/pac/PacTypes"; @@ -219,7 +219,7 @@ export class PowerPagesCopilot implements vscode.WebviewViewProvider { } case "walkthrough": { sendTelemetryEvent(this.telemetry, { eventName: CopilotWalkthroughEvent, copilotSessionId: sessionID, orgId: orgID }); - this.openWalkthrough(); + openWalkthrough(this._extensionUri); break; } case "codeLineCount": { @@ -284,11 +284,6 @@ export class PowerPagesCopilot implements vscode.WebviewViewProvider { } } - private openWalkthrough() { - const walkthroughUri = vscode.Uri.joinPath(this._extensionUri, 'src', 'common', 'copilot', 'assets', 'walkthrough', 'Copilot-In-PowerPages.md'); - vscode.commands.executeCommand("markdown.showPreview", walkthroughUri); - } - private async authenticateAndSendAPIRequest(data: string, activeFileParams: IActiveFileParams, orgID: string, telemetry: ITelemetry) { return intelligenceAPIAuthentication(telemetry, sessionID) .then(async ({ accessToken, user, userId }) => { diff --git a/src/common/copilot/assets/scripts/copilot.js b/src/common/copilot/assets/scripts/copilot.js index 08cf471c..4b88864d 100644 --- a/src/common/copilot/assets/scripts/copilot.js +++ b/src/common/copilot/assets/scripts/copilot.js @@ -260,10 +260,10 @@ const suggestedPrompt = document.createElement("div"); suggestedPrompt.classList.add("suggested-prompts"); - const formPrompt = 'How do I write custom validation in table form?'; - const webApiPrompt = 'How do I create a web API?'; - const listPrompt = 'How can I customize a table list with custom Js?'; - + const formPrompt = 'Write JavaScript code for form field validation to check phone field value is in the valid format.'; + const webApiPrompt = 'Write web API code to query active contact records.'; + const listPrompt = 'Write JavaScript code to highlight the row where email field is empty in table list.'; + suggestedPrompt.innerHTML = `

Here are a few suggestions to get you started

${starIconSvg} @@ -434,7 +434,7 @@ welcomeScreen = setWelcomeScreen(); break; - } + } case "isLoggedIn": { isLoggedIn = message.value; break; diff --git a/src/common/copilot/constants.ts b/src/common/copilot/constants.ts index 1373e9c8..22417b0c 100644 --- a/src/common/copilot/constants.ts +++ b/src/common/copilot/constants.ts @@ -25,6 +25,7 @@ export const INAPPROPRIATE_CONTENT = 'InappropriateContentDetected'; export const INPUT_CONTENT_FILTERED = 'InputContentFiltered'; export const PROMPT_LIMIT_EXCEEDED = 'PromptLimitExceeded'; export const INVALID_INFERENCE_INPUT = 'InvalidInferenceInput'; +export const COPILOT_NOTIFICATION_DISABLED = 'isCopilotNotificationDisabled' export type WebViewMessage = { type: string; diff --git a/src/common/copilot/telemetry/telemetryConstants.ts b/src/common/copilot/telemetry/telemetryConstants.ts index 5823182c..9ea77794 100644 --- a/src/common/copilot/telemetry/telemetryConstants.ts +++ b/src/common/copilot/telemetry/telemetryConstants.ts @@ -29,4 +29,6 @@ export const CopilotWalkthroughEvent = 'CopilotWalkthroughEvent'; export const CopilotResponseOkFailureEvent = 'CopilotResponseOkFailureEvent'; export const CopilotResponseFailureEventWithMessage = 'CopilotResponseFailureEventWithMessage'; export const CopilotCodeLineCountEvent = 'CopilotCodeLineCountEvent'; -export const CopilotNotificationShown = 'CopilotNotificationShown'; \ No newline at end of file +export const CopilotNotificationShown = 'CopilotNotificationShown'; +export const CopilotNotificationDoNotShowChecked = 'CopilotNotificationDoNotShowChecked' +export const CopilotNotificationDoNotShowUnchecked = 'CopilotNotificationDoNotShowUnchecked' diff --git a/src/common/copilot/welcome-notification/CopilotNotificationPanel.ts b/src/common/copilot/welcome-notification/CopilotNotificationPanel.ts new file mode 100644 index 00000000..ff065c6e --- /dev/null +++ b/src/common/copilot/welcome-notification/CopilotNotificationPanel.ts @@ -0,0 +1,130 @@ +/* + * 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 { getNonce, openWalkthrough } from "../../Utils"; +import TelemetryReporter from "@vscode/extension-telemetry"; +import { CopilotNotificationDoNotShowChecked, CopilotTryNotificationClickedEvent, CopilotWalkthroughEvent, CopilotNotificationDoNotShowUnchecked } from "../telemetry/telemetryConstants"; +import { COPILOT_NOTIFICATION_DISABLED } from "../constants"; + +let NotificationPanel: vscode.WebviewPanel | undefined; + +export async function copilotNotificationPanel(context: vscode.ExtensionContext, telemetry: TelemetryReporter, telemetryData: string, countOfActivePortals?: string) { + + if (NotificationPanel) { + NotificationPanel.dispose(); + } + + NotificationPanel = createNotificationPanel(); + + console.log(telemetry, telemetryData, countOfActivePortals) + + const { notificationCssUri, notificationJsUri, copilotImageUri, arrowImageUri } = getWebviewURIs(context, NotificationPanel); + + const nonce = getNonce(); + const webview = NotificationPanel.webview + NotificationPanel.webview.html = getWebviewContent(notificationCssUri, notificationJsUri, copilotImageUri, arrowImageUri, nonce, webview); + + NotificationPanel.webview.onDidReceiveMessage( + async message => { + switch (message.command) { + case 'checked': + telemetry.sendTelemetryEvent(CopilotNotificationDoNotShowChecked, { listOfOrgs: telemetryData, countOfActivePortals: countOfActivePortals as string }); + context.globalState.update(COPILOT_NOTIFICATION_DISABLED, true); + break; + case 'unchecked': + telemetry.sendTelemetryEvent(CopilotNotificationDoNotShowUnchecked, { listOfOrgs: telemetryData, countOfActivePortals: countOfActivePortals as string }); + context.globalState.update(COPILOT_NOTIFICATION_DISABLED, false); + break; + case 'tryCopilot': + telemetry.sendTelemetryEvent(CopilotTryNotificationClickedEvent, { listOfOrgs: telemetryData, countOfActivePortals: countOfActivePortals as string }); + vscode.commands.executeCommand('powerpages.copilot.focus') + NotificationPanel?.dispose(); + break; + case 'learnMore': + telemetry.sendTelemetryEvent(CopilotWalkthroughEvent, { listOfOrgs: telemetryData, countOfActivePortals: countOfActivePortals as string }); + openWalkthrough(context.extensionUri); + } + }, + undefined, + context.subscriptions + ); +} + +function createNotificationPanel(): vscode.WebviewPanel { + const NotificationPanel = vscode.window.createWebviewPanel( + "CopilotNotification", + "Copilot in Power Pages", + { + viewColumn: vscode.ViewColumn.Beside, + preserveFocus: true, + }, + { + enableScripts: true, + } + ); + + return NotificationPanel; +} + +function getWebviewURIs(context: vscode.ExtensionContext, NotificationPanel: vscode.WebviewPanel): { notificationCssUri: vscode.Uri, notificationJsUri: vscode.Uri, copilotImageUri: vscode.Uri, arrowImageUri: vscode.Uri } { + + const srcPath = vscode.Uri.joinPath(context.extensionUri, 'src', 'common', 'copilot', "welcome-notification"); + + const notificationCssPath = vscode.Uri.joinPath(srcPath, "copilotNotification.css"); + const notificationCssUri = NotificationPanel.webview.asWebviewUri(notificationCssPath); + + const notificationJsPath = vscode.Uri.joinPath(srcPath, "copilotNotification.js"); + const notificationJsUri = NotificationPanel.webview.asWebviewUri(notificationJsPath); + + const copilotImagePath = vscode.Uri.joinPath(srcPath, "notification.svg"); + const copilotImageUri = NotificationPanel.webview.asWebviewUri(copilotImagePath); + + const arrowImagePath = vscode.Uri.joinPath(srcPath, "arrow.svg"); + const arrowImageUri = NotificationPanel.webview.asWebviewUri(arrowImagePath); + + return { notificationCssUri, notificationJsUri, copilotImageUri, arrowImageUri }; +} + + +function getWebviewContent(notificationCssUri: vscode.Uri, notificationJsUri: vscode.Uri, copilotImageUri: vscode.Uri, arrowImageUri: vscode.Uri, nonce: string, webview: vscode.Webview) { + + return ` + + + + + + + + Feedback + + + +
+ + +
+ + + + `; +} + +export function disposeNotificationPanel() { + if (NotificationPanel) { + NotificationPanel.dispose(); + NotificationPanel = undefined; + } +} + diff --git a/src/common/copilot/welcome-notification/arrow.svg b/src/common/copilot/welcome-notification/arrow.svg new file mode 100644 index 00000000..27221c17 --- /dev/null +++ b/src/common/copilot/welcome-notification/arrow.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/common/copilot/welcome-notification/copilotNotification.css b/src/common/copilot/welcome-notification/copilotNotification.css new file mode 100644 index 00000000..87ca13f4 --- /dev/null +++ b/src/common/copilot/welcome-notification/copilotNotification.css @@ -0,0 +1,93 @@ +body { + font-size: 13px; + font-weight: var(--vscode-font-weight); + font-family: var(--vscode-font-family); + margin: 0; + padding: 0; + background-color: var(--vscode-editor-background); + display: flex; + flex-direction: column; + align-items: center; +} + +.container { + display: flex; + /* flex-direction: column; */ + align-items: center; + justify-content: space-between; + margin-left: 32px; + margin-right: 32px; + margin-top: 159px; +} + +.container-text { + flex: 1; + margin-right: 20px; +} + +/* Media query for smaller screens */ +@media (max-width: 650px) { + .container { + flex-direction: column; /* Stack elements on top of each other */ + align-items: center; + } + + .container-text { + margin-right: 0; /* Remove margin for smaller screens */ + } + + } + +#welcome-text { + margin-top: 12px; + margin-bottom: 32px; + word-wrap: break-word; +} + +img { + max-width: 100%; + height: auto; +} + +.text { + margin-top: 20px; +} + +button { + background-color: var(--vscode-button-background);; + color: var(--vscode-button-foreground); + padding: 8px 11px; + border: none; + border-radius: 3px; + cursor: pointer; + margin-bottom: 16px; +} + +.walkthrough-content { + padding: 6px 0px 6px 0px; + margin-bottom: 68px; + text-decoration: none; + display: block; + align-items: center; + +} + +.checkbox-container { + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 30px; + margin-top: 180px; +} + +#arrow-icon { + margin-left: 6x; +} + +input[type="checkbox"] { + margin-right: 12px; + background-color: var(--vscode-checkbox-background); + border-radius: 3px; + border: var(--vscode-checkbox-border); + cursor: pointer; +} diff --git a/src/common/copilot/welcome-notification/copilotNotification.js b/src/common/copilot/welcome-notification/copilotNotification.js new file mode 100644 index 00000000..935f8407 --- /dev/null +++ b/src/common/copilot/welcome-notification/copilotNotification.js @@ -0,0 +1,35 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +/* eslint-disable no-undef */ + +(function script() { + const vscode = acquireVsCodeApi(); + + const checkbox = document.getElementById('checkbox'); + const tryCopilotButton = document.getElementById('try-button'); + const learnMoreLink = document.getElementById('walkthroughLink'); + + checkbox.addEventListener('change', (event) => { + // Handle the checkbox change event + if (event.target) { + const checked = event.target.checked; + if(checked) { + vscode.postMessage({command:'checked'}) + } else { + vscode.postMessage({command:'unchecked'}) + } + } + }); + + tryCopilotButton.addEventListener('click', () => { + vscode.postMessage({command:'tryCopilot'}) + }); + + learnMoreLink.addEventListener('click', () => { + vscode.postMessage({command:'learnMore'}) + }); + +})(); diff --git a/src/common/copilot/welcome-notification/notification.svg b/src/common/copilot/welcome-notification/notification.svg new file mode 100644 index 00000000..ba4b7bb0 --- /dev/null +++ b/src/common/copilot/welcome-notification/notification.svg @@ -0,0 +1,109 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/web/client/extension.ts b/src/web/client/extension.ts index e20ea23d..e419c466 100644 --- a/src/web/client/extension.ts +++ b/src/web/client/extension.ts @@ -32,6 +32,8 @@ import { IEntityInfo } from "./common/interfaces"; import { telemetryEventNames } from "./telemetry/constants"; import * as copilot from "../../common/copilot/PowerPagesCopilot"; import { IOrgInfo } from "../../common/copilot/model"; +import { copilotNotificationPanel, disposeNotificationPanel } from "../../common/copilot/welcome-notification/CopilotNotificationPanel"; +import { COPILOT_NOTIFICATION_DISABLED } from "../../common/copilot/constants"; export function activate(context: vscode.ExtensionContext): void { // setup telemetry @@ -410,7 +412,7 @@ export function registerCopilot(context: vscode.ExtensionContext) { } })); - showNotificationForCopilot(orgInfo.orgId); + showNotificationForCopilot(context, orgInfo.orgId); } catch (error) { WebExtensionContext.telemetry.sendErrorTelemetry( telemetryEventNames.WEB_EXTENSION_WEB_COPILOT_REGISTRATION_FAILED, @@ -420,23 +422,19 @@ export function registerCopilot(context: vscode.ExtensionContext) { } } -function showNotificationForCopilot(orgId: string) { +function showNotificationForCopilot(context: vscode.ExtensionContext, orgId: string) { if (vscode.workspace.getConfiguration('powerPlatform').get('experimental.enableWebCopilot') === false) { return; } - const message = vscode.l10n.t('Get help writing code in HTML, CSS, and JS languages for Power Pages sites with Copilot.'); - const actionTitle = vscode.l10n.t('Try Copilot for Power Pages'); - - WebExtensionContext.telemetry.sendInfoTelemetry(telemetryEventNames.WEB_EXTENSION_WEB_COPILOT_NOTIFICATION_SHOWN, - { orgId: orgId }); - - vscode.window.showInformationMessage(message, actionTitle).then((selection) => { - if (selection === actionTitle) { - WebExtensionContext.telemetry.sendInfoTelemetry(telemetryEventNames.WEB_EXTENSION_WEB_COPILOT_NOTIFICATION_EVENT_CLICKED, - { orgId: orgId }); - vscode.commands.executeCommand('powerpages.copilot.focus') - } - }); + + const isCopilotNotificationDisabled = context.globalState.get(COPILOT_NOTIFICATION_DISABLED, false); + if (!isCopilotNotificationDisabled) { + WebExtensionContext.telemetry.sendInfoTelemetry(telemetryEventNames.WEB_EXTENSION_WEB_COPILOT_NOTIFICATION_SHOWN, + { orgId: orgId }); + + const telemetryData = JSON.stringify({ orgId: orgId }); + copilotNotificationPanel(context, WebExtensionContext.telemetry.getTelemetryReporter(), telemetryData); + } } export async function deactivate(): Promise { @@ -444,6 +442,7 @@ export async function deactivate(): Promise { if (telemetry) { telemetry.sendInfoTelemetry("End"); } + disposeNotificationPanel(); } function isActiveDocument(fileFsPath: string): boolean { From 902ded76262a2730e47933e54300cfd4bbe2b958 Mon Sep 17 00:00:00 2001 From: Ritik Ramuka <56073559+ritikramuka@users.noreply.github.com> Date: Tue, 19 Sep 2023 00:31:30 +0530 Subject: [PATCH 8/9] Tree Web View added (#706) * tree web view added * co-presence feature flag made false by default * version corrected --- package.json | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 573e4c56..62fb7f7c 100644 --- a/package.json +++ b/package.json @@ -442,7 +442,7 @@ "powerPlatform.experimental.enableCoPresenceFeature": { "type": "boolean", "markdownDescription": "Enable co-presence in Visual Studio Code (Web extension only)", - "default": true + "default": false } } }, @@ -857,6 +857,13 @@ "visibility": "visible", "when": "(!virtualWorkspace && powerpages.websiteYmlExists && config.powerPlatform.experimental.copilotEnabled) || (isWeb && config.powerPlatform.experimental.enableWebCopilot)" } + ], + "explorer": [ + { + "id": "powerpages.treeWebView", + "name": "Power Pages Actions", + "when": "isWeb && config.powerPlatform.experimental.enableCoPresenceFeature" + } ] }, "walkthroughs": [ From e2504dde48f026e68593e49ca633c894cfb4cc69 Mon Sep 17 00:00:00 2001 From: Ritik Ramuka <56073559+ritikramuka@users.noreply.github.com> Date: Tue, 19 Sep 2023 00:38:41 +0530 Subject: [PATCH 9/9] Shared Workspace Id as per studio defined naming convention (#710) --- src/web/client/utilities/commonUtil.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/web/client/utilities/commonUtil.ts b/src/web/client/utilities/commonUtil.ts index f16e8790..eeb235be 100644 --- a/src/web/client/utilities/commonUtil.ts +++ b/src/web/client/utilities/commonUtil.ts @@ -191,7 +191,6 @@ export function isWebfileContentLoadNeeded(fileName: string, fsPath: string): bo doesFileExist(fsPath) : false; } - export function isPortalVersionV1(): boolean { return WebExtensionContext.currentSchemaVersion.toLowerCase() === portalSchemaVersion.V1; } @@ -199,3 +198,11 @@ export function isPortalVersionV1(): boolean { export function isPortalVersionV2(): boolean { return WebExtensionContext.currentSchemaVersion.toLowerCase() === portalSchemaVersion.V2; } + +export function getWorkSpaceName(websiteId : string) : string { + if (isPortalVersionV1()) { + return `Site-v1-${websiteId}`; + } else { + return `Site-v2-${websiteId}`; + } +}