diff --git a/src/web/client/context/entityData.ts b/src/web/client/context/entityData.ts index 10c1be21d..1518a637f 100644 --- a/src/web/client/context/entityData.ts +++ b/src/web/client/context/entityData.ts @@ -7,7 +7,7 @@ import { IEntityInfo } from "../common/interfaces"; export interface IEntityData extends IEntityInfo { entityEtag: string; - entityColumn: Map; + entityColumn: Map; mappingEntityId?: string; } @@ -15,7 +15,7 @@ export class EntityData implements IEntityData { private _entityName!: string; private _entityId!: string; private _entityEtag!: string; - private _entityColumn!: Map; + private _entityColumn!: Map; private _mappingEntityId?: string; public get entityName(): string { @@ -27,7 +27,7 @@ export class EntityData implements IEntityData { public get entityEtag(): string { return this._entityEtag; } - public get entityColumn(): Map { + public get entityColumn(): Map { return this._entityColumn; } public get mappingEntityId(): string | undefined { @@ -43,7 +43,7 @@ export class EntityData implements IEntityData { entityId: string, entityName: string, entityEtag: string, - entityColumn: Map, + entityColumn: Map, mappingEntityId?: string ) { this._entityId = entityId; diff --git a/src/web/client/context/entityDataMap.ts b/src/web/client/context/entityDataMap.ts index 51ca5965c..9c53bb5e9 100644 --- a/src/web/client/context/entityDataMap.ts +++ b/src/web/client/context/entityDataMap.ts @@ -12,7 +12,7 @@ export class EntityDataMap { private updateEntityContent( entityId: string, columnName: string, - columnContent: string + columnContent: string | Uint8Array ) { const existingEntity = this.entityMap.get(entityId); @@ -36,7 +36,7 @@ export class EntityDataMap { attributeContent: string, mappingEntityId?: string ) { - let entityColumnMap = new Map(); + let entityColumnMap = new Map(); const existingEntity = this.entityMap.get(entityId); if (existingEntity) { @@ -84,7 +84,7 @@ export class EntityDataMap { public updateEntityColumnContent( entityId: string, columnName: IAttributePath, - columnAttributeContent: string + columnAttributeContent: string | Uint8Array ) { const existingEntity = this.entityMap.get(entityId); diff --git a/src/web/client/dal/fileSystemProvider.ts b/src/web/client/dal/fileSystemProvider.ts index 0e1d2d0f2..d979a97d3 100644 --- a/src/web/client/dal/fileSystemProvider.ts +++ b/src/web/client/dal/fileSystemProvider.ts @@ -30,7 +30,7 @@ import { updateFileDirtyChanges, updateFileEntityEtag, } from "../utilities/fileAndEntityUtil"; -import { isVersionControlEnabled } from "../utilities/commonUtil"; +import { getImageFileContent, isImageFileSupportedForEdit, isVersionControlEnabled, updateFileContentInFileDataMap } from "../utilities/commonUtil"; import { IFileInfo } from "../common/interfaces"; export class File implements vscode.FileStat { @@ -158,11 +158,13 @@ export class PortalsFS implements vscode.FileSystemProvider { async writeFile( uri: vscode.Uri, content: Uint8Array, - options: { create: boolean; overwrite: boolean } + options: { create: boolean; overwrite: boolean }, + isFirstTimeWrite = false ): Promise { const basename = path.posix.basename(uri.path); const parent = await this._lookupParentDirectory(uri); let entry = parent.entries.get(basename); + const isImageEdit = isImageFileSupportedForEdit(basename); if (entry instanceof Directory) { throw vscode.FileSystemError.FileIsADirectory(uri); @@ -180,10 +182,21 @@ export class PortalsFS implements vscode.FileSystemProvider { entry = new File(basename); parent.entries.set(basename, entry); this._fireSoon({ type: vscode.FileChangeType.Created, uri }); - } else if ( - WebExtensionContext.fileDataMap.getFileMap.get(uri.fsPath) + } + + if (!isFirstTimeWrite && + (WebExtensionContext.fileDataMap.getFileMap.get(uri.fsPath) ?.hasDirtyChanges + || isImageEdit) ) { + if (isImageEdit) { + WebExtensionContext.telemetry.sendInfoTelemetry( + telemetryEventNames.WEB_EXTENSION_SAVE_IMAGE_FILE_TRIGGERED + ); + + updateFileContentInFileDataMap(uri.fsPath, getImageFileContent(uri.fsPath, content), true); + } + // Save data to dataverse await vscode.window.withProgress( { diff --git a/src/web/client/dal/remoteFetchProvider.ts b/src/web/client/dal/remoteFetchProvider.ts index a3645df01..b7a94db78 100644 --- a/src/web/client/dal/remoteFetchProvider.ts +++ b/src/web/client/dal/remoteFetchProvider.ts @@ -629,7 +629,8 @@ async function createVirtualFile( await portalsFS.writeFile( vscode.Uri.parse(fileUri), fileContent, - { create: true, overwrite: true } + { create: true, overwrite: true }, + true ); // Maintain entity details in context diff --git a/src/web/client/extension.ts b/src/web/client/extension.ts index 42142cc19..4e99d05a8 100644 --- a/src/web/client/extension.ts +++ b/src/web/client/extension.ts @@ -18,13 +18,11 @@ import { showErrorDialog, } from "./common/errorHandler"; import { WebExtensionTelemetry } from "./telemetry/webExtensionTelemetry"; -import { convertContentToString, isCoPresenceEnabled } from "./utilities/commonUtil"; +import { isCoPresenceEnabled, updateFileContentInFileDataMap } from "./utilities/commonUtil"; import { NPSService } from "./services/NPSService"; import { vscodeExtAppInsightsResourceProvider } from "../../common/telemetry-generated/telemetryConfiguration"; import { NPSWebView } from "./webViews/NPSWebView"; import { - updateFileDirtyChanges, - updateEntityColumnContent, getFileEntityId, getFileEntityName, } from "./utilities/fileAndEntityUtil"; @@ -246,6 +244,8 @@ export function processWorkspaceStateChanges(context: vscode.ExtensionContext) { ); } +// This function will not be triggered for image file content update +// Image file content write to images needs to be handled in writeFile call directly export function processWillSaveDocument(context: vscode.ExtensionContext) { context.subscriptions.push( vscode.workspace.onWillSaveTextDocument(async (e) => { @@ -254,20 +254,7 @@ export function processWillSaveDocument(context: vscode.ExtensionContext) { if (vscode.window.activeTextEditor === undefined) { return; } else if (isActiveDocument(fileFsPath)) { - const fileData = - WebExtensionContext.fileDataMap.getFileMap.get(fileFsPath); - - // Update the latest content in context - if (fileData?.entityId && fileData.attributePath) { - let fileContent = e.document.getText(); - fileContent = convertContentToString(fileContent, fileData.encodeAsBase64 as boolean); - updateEntityColumnContent( - fileData?.entityId, - fileData.attributePath, - fileContent - ); - updateFileDirtyChanges(fileFsPath, true); - } + updateFileContentInFileDataMap(fileFsPath, e.document.getText()); } }) ); diff --git a/src/web/client/telemetry/constants.ts b/src/web/client/telemetry/constants.ts index b2a2ade42..34dcfe786 100644 --- a/src/web/client/telemetry/constants.ts +++ b/src/web/client/telemetry/constants.ts @@ -97,4 +97,6 @@ export enum telemetryEventNames { WEB_EXTENSION_POWER_PAGES_WEB_VIEW_REGISTER_FAILED = 'webExtensionPowerPagesWebViewRegisterFailed', WEB_EXTENSION_BACK_TO_STUDIO_TRIGGERED = 'webExtensionBackToStudioTriggered', WEB_EXTENSION_PREVIEW_SITE_TRIGGERED = 'webExtensionPreviewSiteTriggered', + WEB_EXTENSION_IMAGE_EDIT_SUPPORTED_FILE_EXTENSION = 'webExtensionImageEditSupportedFileExtension', + WEB_EXTENSION_SAVE_IMAGE_FILE_TRIGGERED = 'webExtensionSaveImageFileTriggered' } diff --git a/src/web/client/utilities/commonUtil.ts b/src/web/client/utilities/commonUtil.ts index aac3829c6..a6aff10f6 100644 --- a/src/web/client/utilities/commonUtil.ts +++ b/src/web/client/utilities/commonUtil.ts @@ -21,7 +21,8 @@ import { schemaEntityName } from "../schema/constants"; import { telemetryEventNames } from "../telemetry/constants"; import WebExtensionContext from "../WebExtensionContext"; import { SETTINGS_EXPERIMENTAL_STORE_NAME } from "../../../client/constants"; -import { doesFileExist } from "./fileAndEntityUtil"; +import { doesFileExist, getFileAttributePath, getFileEntityName, updateEntityColumnContent, updateFileDirtyChanges } from "./fileAndEntityUtil"; +import { isWebFileV2 } from "./schemaHelperUtil"; // decodes file content to UTF-8 export function convertContentToUint8Array(content: string, isBase64Encoded: boolean): Uint8Array { @@ -30,7 +31,7 @@ export function convertContentToUint8Array(content: string, isBase64Encoded: boo } // encodes file content to base64 or returns the content as is -export function convertContentToString(content: string, isBase64Encoded: boolean): string { +export function convertContentToString(content: string | Uint8Array, isBase64Encoded: boolean): string | Uint8Array { return isBase64Encoded ? Buffer.from(content).toString(BASE_64) : content; } @@ -223,3 +224,43 @@ export function getBackToStudioURL() { .replace("{.region}", region.toLowerCase() === STUDIO_PROD_REGION ? "" : `.${WebExtensionContext.urlParametersMap.get(queryParameters.REGION) as string}`) .replace("{webSiteId}", WebExtensionContext.urlParametersMap.get(queryParameters.WEBSITE_ID) as string); } +export function getSupportedImageFileExtensionsForEdit() { + return ['png', 'jpg', 'webp', 'bmp', 'tga', 'ico', 'jpeg', 'bmp', 'dib', 'jif', 'jpe', 'tpic']; // Luna paint supported image file extensions +} + +export function isImageFileSupportedForEdit(fileName: string): boolean { + const fileExtension = getFileExtension(fileName) as string; + const supportedImageFileExtensions = getSupportedImageFileExtensionsForEdit(); + const isSupported = fileExtension !== undefined ? + supportedImageFileExtensions.includes(fileExtension.toLowerCase()) : false; + + if (isSupported) { + WebExtensionContext.telemetry.sendInfoTelemetry(telemetryEventNames.WEB_EXTENSION_IMAGE_EDIT_SUPPORTED_FILE_EXTENSION, + { fileExtension: fileExtension }); + } + + return isSupported; +} + +export function updateFileContentInFileDataMap(fileFsPath: string, fileContent: string | Uint8Array, isFileContentBase64Encoded = false) { + const fileData = + WebExtensionContext.fileDataMap.getFileMap.get(fileFsPath); + + // Update the latest content in context + if (fileData?.entityId && fileData.attributePath) { + fileContent = convertContentToString(fileContent, isFileContentBase64Encoded ? false : fileData.encodeAsBase64 as boolean); + + updateEntityColumnContent( + fileData?.entityId, + fileData.attributePath, + fileContent + ); + updateFileDirtyChanges(fileFsPath, true); + } +} + +export function getImageFileContent(fileFsPath: string, fileContent: Uint8Array) { + const webFileV2 = isWebFileV2(getFileEntityName(fileFsPath), getFileAttributePath(fileFsPath)?.source); + + return webFileV2 ? fileContent : Buffer.from(fileContent).toString(BASE_64); +} diff --git a/src/web/client/utilities/fileAndEntityUtil.ts b/src/web/client/utilities/fileAndEntityUtil.ts index 1bf2ae9a6..27fd81050 100644 --- a/src/web/client/utilities/fileAndEntityUtil.ts +++ b/src/web/client/utilities/fileAndEntityUtil.ts @@ -28,6 +28,11 @@ export function getFileEntityName(fileFsPath: string) { ?.entityName as string ?? WebExtensionContext.getVscodeWorkspaceState(fileFsPath)?.entityName as string; } +export function getFileAttributePath(fileFsPath: string) { + return WebExtensionContext.fileDataMap.getFileMap.get(fileFsPath) + ?.attributePath as IAttributePath; +} + export function getFileEntityEtag(fileFsPath: string) { return WebExtensionContext.fileDataMap.getFileMap.get(fileFsPath) ?.entityEtag as string; @@ -91,7 +96,7 @@ export function updateEntityEtag(entityId: string, entityEtag: string) { export function updateEntityColumnContent( entityId: string, attributePath: IAttributePath, - fileContent: string + fileContent: string | Uint8Array ) { WebExtensionContext.entityDataMap.updateEntityColumnContent( entityId,