From 8e25c1c7e1e1784f57b15867481e1da6df15aaf1 Mon Sep 17 00:00:00 2001 From: Peter van Gulik Date: Mon, 17 Jul 2023 18:12:44 +0200 Subject: [PATCH] Add new activate and deploy LWC command for datapacks of type OmniScript --- .../omniscript/src/omniScriptActivator.ts | 13 ++- .../src/omniScriptDefinitionGenerator.ts | 4 +- .../src/deploymentSpecs/omniScript.ts | 2 +- packages/vscode-extension/commands.yaml | 39 +++++--- .../src/commands/datapacks/datapackCommand.ts | 4 +- .../datapacks/deployDatapackCommand.ts | 9 +- .../src/commands/datapacks/generateLwc.ts | 54 ---------- .../src/commands/datapacks/index.ts | 4 +- .../commands/datapacks/omniScriptActivate.ts | 82 +++++++++++++++ .../commands/datapacks/omniScriptDeployLwc.ts | 53 ++++++++++ .../datapacks/omniScriptGenerateLwc.ts | 99 +++++++++++++++++++ .../metadata/deployMetadataCommand.ts | 10 +- packages/vscode-extension/src/constants.ts | 4 +- .../vscode-extension/src/lib/commandBase.ts | 29 ------ .../src/lib/commandExecutor.ts | 37 ++++++- .../vscode-extension/src/lib/commandRouter.ts | 4 + .../src/lib/fs/vscodeFileSystemAdapter.ts | 8 +- .../src/lib/vlocodeService.ts | 25 ++++- 18 files changed, 353 insertions(+), 127 deletions(-) delete mode 100644 packages/vscode-extension/src/commands/datapacks/generateLwc.ts create mode 100644 packages/vscode-extension/src/commands/datapacks/omniScriptActivate.ts create mode 100644 packages/vscode-extension/src/commands/datapacks/omniScriptDeployLwc.ts create mode 100644 packages/vscode-extension/src/commands/datapacks/omniScriptGenerateLwc.ts diff --git a/packages/omniscript/src/omniScriptActivator.ts b/packages/omniscript/src/omniScriptActivator.ts index c138bd77..4ed4008f 100644 --- a/packages/omniscript/src/omniScriptActivator.ts +++ b/packages/omniscript/src/omniScriptActivator.ts @@ -79,7 +79,7 @@ export class OmniScriptActivator { // Deploy LWC when required if (options?.skipLwcDeployment !== true && script.isLwcEnabled) { const definition = await this.definitionProvider.getScriptDefinition(script.id); - await this.deployLwcComponent(definition, options); + await this.deployLwc(definition, options); } if (options?.reactivateDependentScripts && script.isReusable) { @@ -225,7 +225,7 @@ export class OmniScriptActivator { */ public async activateLwc(id: string, options?: OmniScriptActivationOptions) { const definition = await this.definitionProvider.getScriptDefinition(id); - await this.deployLwcComponent(definition, options); + await this.deployLwc(definition, options); } /** @@ -233,12 +233,17 @@ export class OmniScriptActivator { * @param id Id of the OmniScript * @returns Deployable Metadata package */ - public async getLwcComponentBundle(id: string) { + public async getMetadataPackage(id: string) { const definition = await this.definitionProvider.getScriptDefinition(id); return this.lwcCompiler.compileToPackage(definition); } - private async deployLwcComponent(definition: OmniScriptDefinition, options?: OmniScriptActivationOptions) { + /** + * Generate the LWC component bundlen for the specified OmniScript definition and deploy it to the org. + * @param definition Definition of the OmniScript to deploy + * @param options Extra options that control how the script is activated + */ + public async deployLwc(definition: OmniScriptDefinition, options?: OmniScriptActivationOptions) { const timer = new Timer(); const apiLabel = options?.toolingApi ? 'tooling' : 'metadata'; this.logger.info(`Deploying LWC ${definition.bpType}/${definition.bpSubType} (${apiLabel} api)...`); diff --git a/packages/omniscript/src/omniScriptDefinitionGenerator.ts b/packages/omniscript/src/omniScriptDefinitionGenerator.ts index 7c01d28f..3e9b0cb8 100644 --- a/packages/omniscript/src/omniScriptDefinitionGenerator.ts +++ b/packages/omniscript/src/omniScriptDefinitionGenerator.ts @@ -37,7 +37,9 @@ export class OmniScriptDefinitionGenerator implements OmniScriptDefinitionProvid } private async createRecordsFromDatapack(datapack: VlocityDatapack) { - const scriptRecord = this.createRecord(datapack.data); + // OmniScripts do not have version when exported; force them to version 1 so we don't get undefined version errors + const scriptData = { ...datapack.data, "%vlocity_namespace%__Version__c": datapack.version ?? 1 }; + const scriptRecord = this.createRecord(scriptData); const elementRecords = datapack.Element__c.map(ele => this.createRecord(ele)); return { scriptRecord, elementRecords }; } diff --git a/packages/vlocity-deploy/src/deploymentSpecs/omniScript.ts b/packages/vlocity-deploy/src/deploymentSpecs/omniScript.ts index ac71e939..9941dd9a 100644 --- a/packages/vlocity-deploy/src/deploymentSpecs/omniScript.ts +++ b/packages/vlocity-deploy/src/deploymentSpecs/omniScript.ts @@ -201,7 +201,7 @@ export class OmniScript implements DatapackDeploymentSpec { await forEachAsyncParallel(Iterable.filter(event.getDeployedRecords('OmniScript__c'), r => this.lwcEnabledDatapacks.has(r.datapackKey)), async record => { try { if (event.deployment.options.useMetadataApi) { - packages.push(await this.activator.getLwcComponentBundle(record.recordId)); + packages.push(await this.activator.getMetadataPackage(record.recordId)); } else { await this.activator.activateLwc(record.recordId, { toolingApi: true }); } diff --git a/packages/vscode-extension/commands.yaml b/packages/vscode-extension/commands.yaml index 341dc74e..4c52ae0c 100644 --- a/packages/vscode-extension/commands.yaml +++ b/packages/vscode-extension/commands.yaml @@ -61,19 +61,6 @@ vlocode.createOmniscriptLwc: group: v_vlocity menus: - menu: commandPalette -vlocode.generateLwc: - title: 'Vlocity: Generate LWC' - group: v_vlocity - menus: - - menu: commandPalette - - menu: explorer/context - when: &generateLwcWhen - - resourceScheme == folder && resourcePath =~ /.+\/OmniScript\/.+/ - - resourceScheme == folder && resourcePath =~ /.+\\OmniScript\\.+/ - - resourceScheme == file && resourcePath =~ /.+\/OmniScript\/.+\/.+_DataPack.json$/ - - resourceScheme == file && resourcePath =~ /.+\\OmniScript\\.+\\.+_DataPack.json$/ - - menu: editor/context - when: *generateLwcWhen # hide commands vlocode.buildDatapack: @@ -320,3 +307,29 @@ vlocode.developerLogs.setLogVisibility: menus: - menu: view/title group: navigation + +# OmniScript LWC commands +vlocode.omniScript.generateLwc: + title: 'OmniScript: Generate LWC' + group: v_vlocity_omniscript + menus: &omniScriptCommandMenus + - menu: explorer/context + when: + - resourceScheme == folder && resourcePath =~ /.+\\OmniScript\\[^\\]+$/ + - resourceScheme == folder && resourcePath =~ /.+\/OmniScript\/[^\/]+$/ + - resourceScheme == file && resourcePath =~ /.+\/OmniScript\/.+\/.+\.json$/ + - resourceScheme == file && resourcePath =~ /.+\\OmniScript\\.+\\.+\.json$/ + - menu: commandPalette + when: &whenOmniScript + - resourceScheme == file && resourcePath =~ /.+\/OmniScript\/.+\/.+\.json$/ + - resourceScheme == file && resourcePath =~ /.+\\OmniScript\\.+\\.+\.json$/ + - menu: editor/context + when: *whenOmniScript +vlocode.omniScript.deployLwc: + title: 'OmniScript: Deploy as LWC' + group: v_vlocity_omniscript + menus: *omniScriptCommandMenus +vlocode.omniScript.activate: + title: 'OmniScript: (Re-)Activate' + group: v_vlocity_omniscript + menus: *omniScriptCommandMenus diff --git a/packages/vscode-extension/src/commands/datapacks/datapackCommand.ts b/packages/vscode-extension/src/commands/datapacks/datapackCommand.ts index 76cbd870..64a8e7b3 100644 --- a/packages/vscode-extension/src/commands/datapacks/datapackCommand.ts +++ b/packages/vscode-extension/src/commands/datapacks/datapackCommand.ts @@ -62,7 +62,7 @@ export abstract class DatapackCommand extends CommandBase { */ protected async getSalesforceRecords(datapacks: VlocityDatapack[], options?: { showRecordSelection?: boolean }) { const matchingRecords = await mapAsync(await this.datapackService.getDatapackRecords(datapacks), async (matchedRecords, i) => - options?.showRecordSelection + options?.showRecordSelection && matchedRecords.length > 1 ? this.showRecordSelection(matchedRecords, datapacks[i].datapackType) : this.getBestRecord(matchedRecords, datapacks[i].datapackType) ); @@ -72,7 +72,7 @@ export abstract class DatapackCommand extends CommandBase { sobjectType: datapack.sobjectType, datapackType: datapack.datapackType, id: matchingRecords[i]?.Id, - record: matchingRecords[i] + values: matchingRecords[i] })); } diff --git a/packages/vscode-extension/src/commands/datapacks/deployDatapackCommand.ts b/packages/vscode-extension/src/commands/datapacks/deployDatapackCommand.ts index 7831eec7..b892de19 100644 --- a/packages/vscode-extension/src/commands/datapacks/deployDatapackCommand.ts +++ b/packages/vscode-extension/src/commands/datapacks/deployDatapackCommand.ts @@ -17,7 +17,7 @@ import { VlocityToolsDeployment } from '../../lib/vlocity/vlocityToolsDeploy'; const deployModeSuggestionKey: string = 'deploymodeSuggestion-0.17.0'; const suggestionInterval: number = 9 * 24 * 3600 * 1000; -@vscodeCommand(VlocodeCommand.deployDatapack, { focusLog: true }) +@vscodeCommand(VlocodeCommand.deployDatapack, { focusLog: true, showProductionWarning: true }) export class DeployDatapackCommand extends DatapackCommand { /** @@ -51,13 +51,6 @@ export class DeployDatapackCommand extends DatapackCommand { return; } - // Prevent prod deployment if not intended - if (await this.vlocode.salesforceService.isProductionOrg()) { - if (!await this.showProductionWarning(false)) { - return; - } - } - // Suggest vlocode? if (datapackHeaders.length > 1) { await this.suggestDeploymentModeChange(); diff --git a/packages/vscode-extension/src/commands/datapacks/generateLwc.ts b/packages/vscode-extension/src/commands/datapacks/generateLwc.ts deleted file mode 100644 index 40aa89c4..00000000 --- a/packages/vscode-extension/src/commands/datapacks/generateLwc.ts +++ /dev/null @@ -1,54 +0,0 @@ -import * as vscode from 'vscode'; -import { join } from 'path'; - -import { OmniScriptDefinitionGenerator, OmniScriptLwcCompiler } from '@vlocode/omniscript'; -import { VlocodeCommand } from '../../constants'; -import { vscodeCommand } from '../../lib/commandRouter'; -import { DatapackCommand } from './datapackCommand'; -import { container, FileSystem, FindOptions } from '@vlocode/core'; -import { spreadAsync } from '@vlocode/util'; - -@vscodeCommand(VlocodeCommand.generateLwc, { focusLog: true }) -export default class GenerateLwcCommand extends DatapackCommand { - - public execute(...args: any[]) : Promise { - return this.generateLwc(args[1] || [args[0] || this.currentOpenDocument]); - } - - protected async generateLwc(selectedFiles: vscode.Uri[]) : Promise { - const datapacks = await this.loadDatapacks(selectedFiles); - - for (const datapack of datapacks) { - const definition = await container.create(OmniScriptDefinitionGenerator).getScriptDefinitionFromDatapack(datapack); - const sfPackage = await container.create(OmniScriptLwcCompiler).compileToPackage(definition); - const outputFolder = await this.showOutputPathSelection(); - if (!outputFolder) { - throw new Error('No output folder selected'); - } - for (const comp of sfPackage.components()) { - for (const file of comp.files) { - await container.get(FileSystem).outputFile(join(outputFolder, file.packagePath), file.data!); - } - } - } - } - - protected async showOutputPathSelection() : Promise { - const cwd = vscode.workspace.workspaceFolders?.map(folder => folder.uri.fsPath); - const options: FindOptions = { cwd, findType: 'directory', exclude: ['**/node_modules/**', '.*'], limit: 10 }; - const results = (await spreadAsync(container.get(FileSystem).find('lwc', options))); - - let selectedFolder: string | undefined; - if (results.length === 0) { - selectedFolder = cwd?.shift() ?? '.'; - } else if (results.length === 1) { - selectedFolder = results[0]; - } else { - selectedFolder = await vscode.window.showQuickPick(results, { - placeHolder: 'Select the folder to save the generated LWC components to...' - }); - } - - return selectedFolder ? selectedFolder.split('/').slice(0,-1).join('/') : undefined; - } -} \ No newline at end of file diff --git a/packages/vscode-extension/src/commands/datapacks/index.ts b/packages/vscode-extension/src/commands/datapacks/index.ts index b91d07fc..eb6e6ab9 100644 --- a/packages/vscode-extension/src/commands/datapacks/index.ts +++ b/packages/vscode-extension/src/commands/datapacks/index.ts @@ -8,4 +8,6 @@ export * from './exportDatapackCommand'; export * from './openSalesforceCommand'; export * from './refreshDatapackCommand'; export * from './renameDatapackCommand'; -export * from './generateLwc'; +export * from './omniScriptGenerateLwc'; +export * from './omniScriptDeployLwc'; +export * from './omniScriptActivate'; diff --git a/packages/vscode-extension/src/commands/datapacks/omniScriptActivate.ts b/packages/vscode-extension/src/commands/datapacks/omniScriptActivate.ts new file mode 100644 index 00000000..de22404a --- /dev/null +++ b/packages/vscode-extension/src/commands/datapacks/omniScriptActivate.ts @@ -0,0 +1,82 @@ +import * as vscode from 'vscode'; + +import { OmniScriptActivationOptions, OmniScriptActivator, OmniScriptDefinitionGenerator, OmniScriptSpecification, VlocityDatapack } from '@vlocode/vlocity-deploy'; +import { VlocodeCommand } from '../../constants'; +import { vscodeCommand } from '../../lib/commandRouter'; +import { DatapackCommand } from './datapackCommand'; +import { container } from '@vlocode/core'; +import { ActivityProgress } from '../../lib/vlocodeActivity'; + +@vscodeCommand(VlocodeCommand.omniScriptActivate, { focusLog: true, showProductionWarning: true }) +export default class ActivateOmniScriptCommand extends DatapackCommand { + + public execute(...args: any[]) : Promise { + return this.executeWithSelection(args[1] || [args[0] || this.currentOpenDocument]); + } + + protected async executeWithSelection(selectedFiles: vscode.Uri[]) : Promise { + const datapacks = await this.loadDatapacks(selectedFiles); + const options = { + toolingApi: true, + remoteActivation: false, + reactivateDependentScripts: false + }; + + if (datapacks.length === 0) { + throw new Error('Selected file is not a Vlocity OmniScript DataPack'); + } + + const hasReusableScripts = datapacks.some(datapack => datapack.IsReusable__c); + if (hasReusableScripts && await this.promptDependencyReactivation()) { + options.reactivateDependentScripts = true; + } + + return this.vlocode.withActivity('OmniScript', (progress) => this.activateScripts(datapacks, options, progress)); + } + + protected async activateScripts( + datapacks: VlocityDatapack[], + options: OmniScriptActivationOptions, + progress: ActivityProgress + ) : Promise { + const activated = new Array(); + const failed = new Array(); + + for (const datapack of datapacks) { + const omniScriptSpec = { + type: datapack.Type__c, + subType: datapack.SubType__c, + language: datapack.Language__c + }; + + if (!omniScriptSpec.subType || !omniScriptSpec.type) { + throw new Error(`Datapack is not of type OmniScript: ${datapack.headerFile}`); + } + + progress.report({ message: `Activating ${omniScriptSpec.type}/${omniScriptSpec.subType}...` }); + + try { + await container.get(OmniScriptActivator).activate(omniScriptSpec, options); + activated.push(omniScriptSpec); + } catch (error) { + failed.push({...omniScriptSpec, error }); + this.logger.error(`Failed to activate ${omniScriptSpec.type}/${omniScriptSpec.subType}: ${error.message}`); + } + } + + void vscode.window.showInformationMessage(`Activated ${activated.length} OmniScript(s)`); + } + + private async promptDependencyReactivation() : Promise { + const outcome = await vscode.window.showQuickPick([ + { value: true, label: 'Yes', description: 'Reactivate scripts that use this script as dependency' }, + { value: false, label: 'No', description: 'Only reactivate this script and do not refresh any scripts embedding this script as dependency' } + ], { + placeHolder: 'Reactivate dependent scripts?' + }); + if (!outcome) { + throw new Error('User cancelled operation'); + } + return outcome.value; + } +} \ No newline at end of file diff --git a/packages/vscode-extension/src/commands/datapacks/omniScriptDeployLwc.ts b/packages/vscode-extension/src/commands/datapacks/omniScriptDeployLwc.ts new file mode 100644 index 00000000..5a45d1c3 --- /dev/null +++ b/packages/vscode-extension/src/commands/datapacks/omniScriptDeployLwc.ts @@ -0,0 +1,53 @@ +import * as vscode from 'vscode'; + +import { OmniScriptActivator, OmniScriptDefinitionGenerator, VlocityDatapack } from '@vlocode/vlocity-deploy'; +import { VlocodeCommand } from '../../constants'; +import { vscodeCommand } from '../../lib/commandRouter'; +import { DatapackCommand } from './datapackCommand'; +import { container } from '@vlocode/core'; +import { ActivityProgress } from '../../lib/vlocodeActivity'; + +@vscodeCommand(VlocodeCommand.omniScriptDeployLwc, { focusLog: true, showProductionWarning: true }) +export default class DeployLwcCommand extends DatapackCommand { + + public execute(...args: any[]) : Promise { + return this.executeWithSelection(args[1] || [args[0] || this.currentOpenDocument]); + } + + protected async executeWithSelection(selectedFiles: vscode.Uri[]) : Promise { + const datapacks = await this.loadDatapacks(selectedFiles) + + if (datapacks.length === 0) { + throw new Error('Selected file is not a Vlocity OmniScript DataPack'); + } + + const notLwcEnabled = datapacks.some(datapack => !datapack.IsLwcEnabled__c); + if (notLwcEnabled) { + const notLwcWarningResult = await vscode.window.showWarningMessage( + 'Not all selected OmniScripts are LWC enabled. Only LWC enabled OmniScripts can be deployed as LWC components. ' + + 'Deploying non-LWC enabled OmniScripts can result in errors during deployment.', + 'Continue', 'Cancel' + ); + if (notLwcWarningResult !== 'Continue') { + return; + } + } + + return this.vlocode.withActivity('OmniScript', (progress) => this.deployLwc(datapacks, progress)); + } + + protected async deployLwc(datapacks: VlocityDatapack[], progress: ActivityProgress) : Promise { + for (const datapack of datapacks) { + progress.report({ message: `Generating ${datapack.name} definitions...`, total: datapacks.length, increment: 1 }); + const definition = await container.get(OmniScriptDefinitionGenerator).getScriptDefinitionFromDatapack(datapack); + + progress.report({ message: `Deploying ${datapack.name} LWC...` }); + await container.get(OmniScriptActivator).deployLwc(definition, { + toolingApi: true, + remoteActivation: false, + reactivateDependentScripts: true + }); + } + void vscode.window.showInformationMessage(`Deployed LWC components for ${datapacks.length} OmniScript(s)`); + } +} \ No newline at end of file diff --git a/packages/vscode-extension/src/commands/datapacks/omniScriptGenerateLwc.ts b/packages/vscode-extension/src/commands/datapacks/omniScriptGenerateLwc.ts new file mode 100644 index 00000000..c5270986 --- /dev/null +++ b/packages/vscode-extension/src/commands/datapacks/omniScriptGenerateLwc.ts @@ -0,0 +1,99 @@ +import * as vscode from 'vscode'; +import { join, relative } from 'path'; + +import { OmniScriptDefinitionGenerator, OmniScriptLwcCompiler, VlocityDatapack } from '@vlocode/vlocity-deploy'; +import { VlocodeCommand } from '../../constants'; +import { vscodeCommand } from '../../lib/commandRouter'; +import { withProgress } from '../../lib/vlocodeService'; +import { DatapackCommand } from './datapackCommand'; +import { container, FileSystem, FindOptions } from '@vlocode/core'; +import { spreadAsync } from '@vlocode/util'; +import { SalesforcePackage } from '@vlocode/salesforce'; + +@vscodeCommand(VlocodeCommand.omniScriptGenerateLwc, { focusLog: true }) +export default class GenerateLwcCommand extends DatapackCommand { + + public execute(...args: any[]) : Promise { + return this.executeWithSelection(args[1] || [args[0] || this.currentOpenDocument]); + } + + private async executeWithSelection(selectedFiles: vscode.Uri[]) : Promise { + const datapacks = await this.loadDatapacks(selectedFiles); + if (datapacks.length === 0) { + throw new Error('Selected file is not a Vlocity OmniScript DataPack'); + } + + const notLwcEnabled = datapacks.some(datapack => !datapack.IsLwcEnabled__c); + if (notLwcEnabled) { + const notLwcWarningResult = await vscode.window.showWarningMessage( + 'Not all selected OmniScripts are LWC enabled. ' + + 'Generating an LWC form a non-LWC enabled OmniScript can cause the resulting component to be incomplete.', + 'Continue', 'Cancel' + ); + if (notLwcWarningResult !== 'Continue') { + return; + } + } + + const outputFolder = await this.promptOutputPathSelection(); + if (!outputFolder) { + throw new Error('No output folder selected'); + } + + return this.generateLwc(datapacks, outputFolder); + } + + @withProgress({ location: vscode.ProgressLocation.Notification, title: 'Generating LWC components...' }) + private async generateLwc(datapacks: VlocityDatapack[], outputFolder: string) : Promise { + const packageData = new SalesforcePackage(this.vlocode.apiVersion); + + for (const datapack of datapacks) { + const definition = await container.create(OmniScriptDefinitionGenerator).getScriptDefinitionFromDatapack(datapack); + const sfPackage = await container.create(OmniScriptLwcCompiler).compileToPackage(definition); + if (sfPackage.isEmpty) { + throw new Error('No LWC components generated'); + } + + for (const comp of sfPackage.components()) { + this.logger.info(`Saving ${datapack.Type__c}/${datapack.SubType__c} LWC to: ${outputFolder}/lwc/${comp.componentName}`); + for (const file of comp.files) { + const outputFilePath = join(outputFolder, file.packagePath); + this.logger.verbose(`Write LWC metadata ${relative(outputFolder, outputFilePath)} (${file.data!.length} bytes)`); + await container.get(FileSystem).outputFile(outputFilePath, file.data!); + } + } + + packageData.merge(sfPackage); + } + + const components = packageData.components(); + if (components.length === 1) { + const metaFile = components[0].files.find(file => file.packagePath.endsWith('.js-meta.xml'))!; + const componentPath = join(outputFolder, metaFile.packagePath.slice(0, -'-meta.xml'.length)); + void vscode.window.showTextDocument( + await vscode.workspace.openTextDocument (vscode.Uri.file(componentPath)) + ); + } else { + void vscode.window.showInformationMessage(`Generated LWC components for ${components.length} components from ${datapacks.length} DataPacks`); + } + } + + private async promptOutputPathSelection() : Promise { + const cwd = vscode.workspace.workspaceFolders?.map(folder => folder.uri.fsPath); + const options: FindOptions = { cwd, findType: 'directory', exclude: ['**/node_modules/**', '.*'], limit: 10 }; + const results = (await spreadAsync(container.get(FileSystem).find('lwc', options))); + + let selectedFolder: string | undefined; + if (results.length === 0) { + selectedFolder = cwd?.shift() ?? '.'; + } else if (results.length === 1) { + selectedFolder = results[0]; + } else { + selectedFolder = await vscode.window.showQuickPick(results, { + placeHolder: 'Select the folder to save the generated LWC components to...' + }); + } + + return selectedFolder ? selectedFolder.split('/').slice(0,-1).join('/') : undefined; + } +} \ No newline at end of file diff --git a/packages/vscode-extension/src/commands/metadata/deployMetadataCommand.ts b/packages/vscode-extension/src/commands/metadata/deployMetadataCommand.ts index 6645fa05..3f080f8f 100644 --- a/packages/vscode-extension/src/commands/metadata/deployMetadataCommand.ts +++ b/packages/vscode-extension/src/commands/metadata/deployMetadataCommand.ts @@ -10,10 +10,11 @@ import { ActivityProgress } from '../../lib/vlocodeActivity'; import { vscodeCommand } from '../../lib/commandRouter'; import MetadataCommand from './metadataCommand'; + /** * Command for handling addition/deploy of Metadata components in Salesforce */ -@vscodeCommand(VlocodeCommand.deployMetadata, { focusLog: true }) +@vscodeCommand(VlocodeCommand.deployMetadata, { focusLog: true, showProductionWarning: true }) export default class DeployMetadataCommand extends MetadataCommand { /** @@ -73,13 +74,6 @@ export default class DeployMetadataCommand extends MetadataCommand { } protected async deployMetadata(selectedFiles: vscode.Uri[]) { - // Prevent prod deployment if not intended - if (await this.salesforce.isProductionOrg()) { - if (!await this.showProductionWarning(false)) { - return; - } - } - // build package const packageBuilder = new SalesforcePackageBuilder(SalesforcePackageType.deploy, this.vlocode.getApiVersion()); const sfPackage = (await packageBuilder.addFiles(selectedFiles)).getPackage(); diff --git a/packages/vscode-extension/src/constants.ts b/packages/vscode-extension/src/constants.ts index 70d86dd7..3245c8b4 100644 --- a/packages/vscode-extension/src/constants.ts +++ b/packages/vscode-extension/src/constants.ts @@ -67,5 +67,7 @@ export enum VlocodeCommand { resumeDeploymentQueue = 'vlocode.resumeDeploymentQueue', addToProfile = 'vlocode.addToProfile', removeFromProfile = 'vlocode.removeFromProfile', - generateLwc = 'vlocode.generateLwc', + omniScriptGenerateLwc = 'vlocode.omniScript.generateLwc', + omniScriptDeployLwc = 'vlocode.omniScript.deployLwc', + omniScriptActivate = 'vlocode.omniScript.activate', } diff --git a/packages/vscode-extension/src/lib/commandBase.ts b/packages/vscode-extension/src/lib/commandBase.ts index e4ff5a70..b6c15dd7 100644 --- a/packages/vscode-extension/src/lib/commandBase.ts +++ b/packages/vscode-extension/src/lib/commandBase.ts @@ -25,35 +25,6 @@ export abstract class CommandBase implements Command { : undefined; } - /** - * Show a warning message about making changes to a production org allowing - * the user to cancel the operation in case it was unintended. - * - * By default this method will throw an exception if the user cancels the operation, use overload with the `throwException` parameter to change this behavior. - * - * @param throwException Throw an exception if the user cancels the operation - */ - protected async showProductionWarning(throwException?: true) : Promise; - protected async showProductionWarning(throwException: false) : Promise; - protected async showProductionWarning(throwException = true) : Promise { - const productionWarning = await vscode.window.showWarningMessage( - 'Make changes to a Production org?', - { - detail: 'You are about to make changes to a Production org. It is not recommended to direcly make changes to a Production instance doing so may result in data loss. Are you sure you want to continue?', - modal: true, - }, 'Yes', 'No' - ); - - if (productionWarning !== 'Yes') { - if (throwException) { - throw new Error('Operation cancelled by user due to production warning'); - } - return false; - } - - return true; - } - private getName() : string { return this.constructor?.name || 'Command'; } diff --git a/packages/vscode-extension/src/lib/commandExecutor.ts b/packages/vscode-extension/src/lib/commandExecutor.ts index c9507852..ec766065 100644 --- a/packages/vscode-extension/src/lib/commandExecutor.ts +++ b/packages/vscode-extension/src/lib/commandExecutor.ts @@ -1,21 +1,37 @@ import * as vscode from 'vscode'; -import { Logger, LogManager } from '@vlocode/core'; +import { Container, container, Logger, LogManager } from '@vlocode/core'; import { Command } from '../lib/command'; import { CommandOptions } from './commandRouter'; +import { SalesforceService } from '@vlocode/salesforce'; export class CommandExecutor implements Command { constructor( public readonly name: string, - public readonly command: Command, + public readonly command: Command, public readonly options?: CommandOptions, public readonly logger: Logger = LogManager.get(CommandExecutor) ) { } + public get salesforce() { + return container.get(SalesforceService) + } + public async execute(...args: any[]): Promise { this.logger.verbose(`Running command ${this.name}`); + // Prevent prod deployment if not intended + if (this.options?.showProductionWarning && await this.salesforce.isProductionOrg()) { + if (!await this.showProductionWarning()) { + this.logger.warn('Operation cancelled due to production warning'); + void vscode.window.showErrorMessage('Operation cancelled due to production warning'); + return + } else { + this.logger.info('Product org execution confirmed through user prompt'); + } + } + try { if (this.options?.executeParams) { args = [...this.options?.executeParams, ...args]; @@ -46,4 +62,21 @@ export class CommandExecutor implements Command { public initialize(): Promise | void { return this.command.initialize?.(); } + + /** + * Show a warning message about making changes to a production org allowing + * the user to cancel the operation in case it was unintended. + * + * By default this method will throw an exception if the user cancels the operation, use overload with the `throwException` parameter to change this behavior. + */ + protected async showProductionWarning() : Promise { + const productionWarning = await vscode.window.showWarningMessage( + 'Make changes to production org?', + { + detail: 'You are about to make changes to the currently selected production org. It is not recommended to direcly make changes to a production org from vscode, doing so may cause instability for end-users. Are you sure you want to continue?', + modal: true, + }, 'Yes', 'No' + ); + return productionWarning === 'Yes'; + } } diff --git a/packages/vscode-extension/src/lib/commandRouter.ts b/packages/vscode-extension/src/lib/commandRouter.ts index 244526e3..42e7fb81 100644 --- a/packages/vscode-extension/src/lib/commandRouter.ts +++ b/packages/vscode-extension/src/lib/commandRouter.ts @@ -12,6 +12,7 @@ export interface CommandOptions { params?: any[]; executeParams?: any[]; focusLog?: boolean; + showProductionWarning?: boolean; } /** @@ -26,6 +27,9 @@ const commandRegistry: { [id: string]: { command: CommandFn, options?: CommandOp */ export function vscodeCommand(id: string, options?: CommandOptions) { return (command: CommandFn) => { + if (commandRegistry[id]) { + throw new Error(`Command with id "${id}" is already registered; command ids should be unique`); + } commandRegistry[id] = { command, options }; }; } diff --git a/packages/vscode-extension/src/lib/fs/vscodeFileSystemAdapter.ts b/packages/vscode-extension/src/lib/fs/vscodeFileSystemAdapter.ts index 704eabfb..30d0f40e 100644 --- a/packages/vscode-extension/src/lib/fs/vscodeFileSystemAdapter.ts +++ b/packages/vscode-extension/src/lib/fs/vscodeFileSystemAdapter.ts @@ -21,7 +21,13 @@ export class VSCodeFileSystemAdapter extends FileSystem { } public async stat(path: string, options?: StatsOptions): Promise { - return new VSCodeFileStatsAdapter(path, await workspace.fs.stat(this.pathToUri(path))); + try { + return new VSCodeFileStatsAdapter(path, await workspace.fs.stat(this.pathToUri(path))); + } catch (err) { + if (options?.throws) { + throw err; + } + } } public async readDirectory(path: string): Promise { diff --git a/packages/vscode-extension/src/lib/vlocodeService.ts b/packages/vscode-extension/src/lib/vlocodeService.ts index b04c2d79..9553efb6 100644 --- a/packages/vscode-extension/src/lib/vlocodeService.ts +++ b/packages/vscode-extension/src/lib/vlocodeService.ts @@ -63,6 +63,10 @@ export default class VlocodeService implements vscode.Disposable, SalesforceConn return this.commandRouter; } + public get apiVersion() { + return this.config.salesforce.apiVersion; + } + // Ctor + Methods constructor( public readonly config: VlocodeConfiguration, @@ -312,10 +316,10 @@ export default class VlocodeService implements vscode.Disposable, SalesforceConn activityRecord.status = VlocodeActivityStatus.Failed; } if (options.propagateExceptions !== false) { + this.logger.debug(e); throw e; - } else { - this.logger.error(e); } + this.logger.error(e); } finally { if (cancelTokenSource?.token.isCancellationRequested) { activityRecord.status = VlocodeActivityStatus.Cancelled; @@ -587,3 +591,20 @@ export default class VlocodeService implements vscode.Disposable, SalesforceConn } } +/** + * Display a scode progress indicator while executing a command + * @param options Command options + * @returns decorator factory fn + */ +export function withProgress(options: { location: vscode.ProgressLocation, title: string }) { + return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) { + const originalMethod = descriptor.value; + descriptor.value = function(...args: any[]) { + return container.get(VlocodeService).withActivity({ + progressTitle: options.title, + ...options + }, () => originalMethod.apply(this, args)); + }; + return descriptor; + } +} \ No newline at end of file