diff --git a/resources/instructions/createSObjectLwcQuickActions.html b/resources/instructions/createSObjectLwcQuickActions.html index 973f646..9756cdd 100644 --- a/resources/instructions/createSObjectLwcQuickActions.html +++ b/resources/instructions/createSObjectLwcQuickActions.html @@ -56,54 +56,81 @@

Create sObject LWC Quick Actions

- - - diff --git a/resources/templates/quickAction.xml b/resources/templates/quickAction.xml new file mode 100644 index 0000000..7114658 --- /dev/null +++ b/resources/templates/quickAction.xml @@ -0,0 +1,9 @@ + + + ScreenAction + + ///TEMPLATE_LWC_NAME/// + false + LightningWebComponent + ///TEMPLATE_QUICK_ACTION_ICON/// + \ No newline at end of file diff --git a/src/commands/wizard/lwcGenerationCommand.ts b/src/commands/wizard/lwcGenerationCommand.ts index 2591aad..819fe2d 100644 --- a/src/commands/wizard/lwcGenerationCommand.ts +++ b/src/commands/wizard/lwcGenerationCommand.ts @@ -1,3 +1,10 @@ +/* + * Copyright (c) 2023, salesforce.com, inc. + * All rights reserved. + * SPDX-License-Identifier: MIT + * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT + */ + import { Uri, l10n } from 'vscode'; import { access } from 'fs/promises'; import { InstructionsWebviewProvider } from '../../webviews/instructions'; @@ -6,6 +13,7 @@ import { WorkspaceUtils } from '../../utils/workspaceUtils'; import { CommonUtils } from '@salesforce/lwc-dev-mobile-core'; import { OrgUtils } from '../../utils/orgUtils'; import * as fs from 'fs'; +import { CodeBuilder } from '../../utils/codeBuilder'; import * as path from 'path'; export type QuickActionStatus = { @@ -27,10 +35,52 @@ export type GetSObjectsStatus = { }; export class LwcGenerationCommand { - extensionUri: Uri; + static async createSObjectLwcQuickActions(extensionUri: Uri) { + return new Promise((resolve) => { + new InstructionsWebviewProvider( + extensionUri + ).showInstructionWebview( + l10n.t('Offline Starter Kit: Create sObject LWC Quick Actions'), + 'resources/instructions/createSObjectLwcQuickActions.html', + [ + { + type: 'continueButton', + action: (panel) => { + panel.dispose(); + return resolve(); + } + }, + { + type: 'generateLwcQuickActions', + action: async (_panel, _data, callback) => { + const quickActionStatus = + await LwcGenerationCommand.checkForExistingQuickActions(); - constructor(extensionUri: Uri) { - this.extensionUri = extensionUri; + const newLwcQuickActionStatus = + await LwcGenerationCommand.generateMissingLwcsAndQuickActions( + extensionUri, + quickActionStatus + ); + + // send back updates so UI can be refreshed + if (callback) { + callback(newLwcQuickActionStatus); + } + } + }, + { + type: 'getQuickActionStatus', + action: async (_panel, _data, callback) => { + if (callback) { + const quickActionStatus = + await LwcGenerationCommand.checkForExistingQuickActions(); + callback(quickActionStatus); + } + } + } + ] + ); + }); } static async getSObjectsFromLandingPage(): Promise { @@ -62,44 +112,6 @@ export class LwcGenerationCommand { }); } - async createSObjectLwcQuickActions() { - return new Promise((resolve) => { - new InstructionsWebviewProvider( - this.extensionUri - ).showInstructionWebview( - l10n.t('Offline Starter Kit: Create sObject LWC Quick Actions'), - 'resources/instructions/createSObjectLwcQuickActions.html', - [ - { - type: 'generateLwcQuickActionsButton', - action: (panel) => { - panel.dispose(); - return resolve(); - } - }, - { - type: 'getQuickActionStatus', - action: async (_panel, _data, callback) => { - if (callback) { - const quickActionStatus = - await LwcGenerationCommand.checkForExistingQuickActions(); - - for (const key in quickActionStatus.sobjects) { - const layoutFields = - await OrgUtils.getCompactLayoutFieldsForSObject( - key - ); - } - - callback(quickActionStatus); - } - } - } - ] - ); - }); - } - static async checkForExistingQuickActions(): Promise { return new Promise(async (resolve) => { const results: SObjectQuickActionStatus = { sobjects: {} }; @@ -139,6 +151,59 @@ export class LwcGenerationCommand { }); } + static async generateMissingLwcsAndQuickActions( + extensionUri: Uri, + quickActionStatus: SObjectQuickActionStatus + ): Promise { + return new Promise(async (resolve) => { + for (const sobject in quickActionStatus.sobjects) { + try { + const quickActions = quickActionStatus.sobjects[sobject]; + + if ( + !quickActions.create || + !quickActions.edit || + !quickActions.view + ) { + // at least 1 needs to be created + const compactLayoutFields = + await OrgUtils.getCompactLayoutFieldsForSObject( + sobject + ); + + const codeBuilder = new CodeBuilder( + extensionUri, + sobject, + compactLayoutFields + ); + + if (!quickActions.view) { + await codeBuilder.generateView(); + } + + if (!quickActions.edit) { + await codeBuilder.generateEdit(); + } + + if (!quickActions.create) { + await codeBuilder.generateCreate(); + } + } + } catch (err) { + console.error( + `Could not generate quick actions for sobject ${sobject}, so skipping`, + err + ); + } + } + + // Just double check now that things have been created. + const newStatus = + await LwcGenerationCommand.checkForExistingQuickActions(); + resolve(newStatus); + }); + } + private static checkForExistingQuickAction( sobject: string, qaName: string diff --git a/src/commands/wizard/onboardingWizard.ts b/src/commands/wizard/onboardingWizard.ts index f27f258..681214e 100644 --- a/src/commands/wizard/onboardingWizard.ts +++ b/src/commands/wizard/onboardingWizard.ts @@ -12,6 +12,7 @@ import { DeployToOrgCommand } from './deployToOrgCommand'; import { ConfigureProjectCommand } from './configureProjectCommand'; import { AuthorizeCommand } from './authorizeCommand'; import { InstructionsWebviewProvider } from '../../webviews/instructions'; +import { LwcGenerationCommand } from './lwcGenerationCommand'; const wizardCommand = 'salesforcedx-vscode-offline-app.onboardingWizard'; const onboardingWizardStateKey = @@ -28,6 +29,7 @@ async function runPostProjectConfigurationSteps( await AuthorizeCommand.authorizeToOrg(); await BriefcaseCommand.setupBriefcase(extensionUri); await TemplateChooserCommand.chooseTemplate(extensionUri); + await LwcGenerationCommand.createSObjectLwcQuickActions(extensionUri); await AuthorizeCommand.authorizeToOrg(); await DeployToOrgCommand.deployToOrg(); diff --git a/src/test/suite/utils/codeBuilder.test.ts b/src/test/suite/utils/codeBuilder.test.ts new file mode 100644 index 0000000..b084ba5 --- /dev/null +++ b/src/test/suite/utils/codeBuilder.test.ts @@ -0,0 +1,400 @@ +/* + * Copyright (c) 2023, salesforce.com, inc. + * All rights reserved. + * SPDX-License-Identifier: MIT + * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT + */ + +import * as assert from 'assert'; +import * as sinon from 'sinon'; +import { CodeBuilder } from '../../../utils/codeBuilder'; +import { Uri } from 'vscode'; +import { afterEach, beforeEach } from 'mocha'; +import * as fs from 'fs'; +import * as path from 'path'; +import { CompactLayoutField } from '../../../utils/orgUtils'; + +suite('CodeBuilder Test Suite', () => { + var extensionUri = Uri.parse('file:///tmp/'); + + const SAMPLE_CSS_DATA = 'css content'; + const SAMPLE_HTML_DATA = 'html content'; + const SAMPLE_JS_DATA = 'js content'; + const SAMPLE_XML_DATA = 'xml content'; + const SAMPLE_QA_DATA = 'qa content'; + + beforeEach(function () {}); + + afterEach(function () { + sinon.restore(); + }); + + test('All values substituted before writing', async () => { + // we will test the qa file with content that includes all template fields. Since all template fields + // follow the format of "///TEMPLATE_XYZ///" we will just ensure that no "///" value exists in the file + // which proves that all placeholders were replaced. + const allTemplateFields = [ + CodeBuilder.TEMPLATE_CREATE_LWC_LABEL, + CodeBuilder.TEMPLATE_EDIT_LWC_LABEL, + CodeBuilder.TEMPLATE_FIELDS, + CodeBuilder.TEMPLATE_IMPORTS, + CodeBuilder.TEMPLATE_LIGHTNING_INPUT_CREATE_FIELDS_HTML, + CodeBuilder.TEMPLATE_LIGHTNING_INPUT_EDIT_FIELDS_HTML, + CodeBuilder.TEMPLATE_OBJECT_API_NAME, + CodeBuilder.TEMPLATE_VARIABLES, + CodeBuilder.TEMPLATE_VARIABLE_ASSIGNMENTS, + CodeBuilder.TEMPLATE_VIEW_LWC_LABEL + ]; + + // but only these are substituted in a qa + const allQaTemplateFields = [ + CodeBuilder.TEMPLATE_LWC_NAME, + CodeBuilder.TEMPLATE_QUICK_ACTION_ICON, + CodeBuilder.TEMPLATE_QUICK_ACTION_LABEL + ]; + + var allTemplateFieldsContent = ''; + allTemplateFields.forEach((field) => { + allTemplateFieldsContent += `///${field}///\n`; + }); + + var allQaTemplateFieldsContent = allTemplateFieldsContent; + allQaTemplateFields.forEach((field) => { + allQaTemplateFieldsContent += `///${field}///\n`; + }); + + var result = stubFileSystem([ + allTemplateFieldsContent, + allTemplateFieldsContent, + allTemplateFieldsContent, + allTemplateFieldsContent, + allQaTemplateFieldsContent + ]); + const recordedFiles = result[0]; + const compactLayoutFields = buildTestCompactLayoutFields(); + const codeBuilder = new CodeBuilder( + extensionUri, + 'Account', + compactLayoutFields + ); + + await codeBuilder.generateView(); + assert.equal(recordedFiles.length, 5); + + // Test that every file written out had all template values replaced + recordedFiles.forEach((file: any) => { + assert.ok( + !file.data.includes('///'), + `All values should have been replaced in file ${file.filePath}!` + ); + }); + }); + + test('Generate view lwc and quick action', async () => { + var result = stubFileSystem([ + SAMPLE_CSS_DATA, + SAMPLE_HTML_DATA, + SAMPLE_JS_DATA, + SAMPLE_XML_DATA, + SAMPLE_QA_DATA + ]); + const recordedFiles = result[0]; + const mkdirStub = result[1]; + + const compactLayoutFields = buildTestCompactLayoutFields(); + const codeBuilder = new CodeBuilder( + extensionUri, + 'Account', + compactLayoutFields + ); + + await codeBuilder.generateView(); + assert.equal(recordedFiles.length, 5); + assert.equal(mkdirStub.callCount, 5); // for every file in test case + + // CSS file + var dirPath = 'force-app/main/default/lwc/viewAccountRecord'; + assert.equal( + recordedFiles[0].filePath, + path.normalize(`${dirPath}/viewAccountRecord.css`) + ); + assert.equal(recordedFiles[0].data, SAMPLE_CSS_DATA); + + // HTML file + assert.equal( + recordedFiles[1].filePath, + path.normalize(`${dirPath}/viewAccountRecord.html`) + ); + assert.equal(recordedFiles[1].data, SAMPLE_HTML_DATA); + + // JS file + assert.equal( + recordedFiles[2].filePath, + path.normalize(`${dirPath}/viewAccountRecord.js`) + ); + assert.equal(recordedFiles[2].data, SAMPLE_JS_DATA); + + // XML file + assert.equal( + recordedFiles[3].filePath, + path.normalize(`${dirPath}/viewAccountRecord.js-meta.xml`) + ); + assert.equal(recordedFiles[3].data, SAMPLE_XML_DATA); + + // QA file + assert.equal( + recordedFiles[4].filePath, + path.normalize( + 'force-app/main/default/quickActions/Account.view.quickAction-meta.xml' + ) + ); + assert.equal(recordedFiles[4].data, SAMPLE_QA_DATA); + }); + + test('Generate edit lwc and quick action', async () => { + var result = stubFileSystem([ + SAMPLE_CSS_DATA, + SAMPLE_HTML_DATA, + SAMPLE_JS_DATA, + SAMPLE_XML_DATA, + SAMPLE_QA_DATA + ]); + const recordedFiles = result[0]; + const mkdirStub = result[1]; + + const compactLayoutFields = buildTestCompactLayoutFields(); + const codeBuilder = new CodeBuilder( + extensionUri, + 'Account', + compactLayoutFields + ); + + await codeBuilder.generateEdit(); + assert.equal(recordedFiles.length, 5); + assert.equal(mkdirStub.callCount, 5); // for every file in test case + + // CSS file + var dirPath = 'force-app/main/default/lwc/editAccountRecord'; + assert.equal( + recordedFiles[0].filePath, + path.normalize(`${dirPath}/editAccountRecord.css`) + ); + assert.equal(recordedFiles[0].data, SAMPLE_CSS_DATA); + + // HTML file + assert.equal( + recordedFiles[1].filePath, + path.normalize(`${dirPath}/editAccountRecord.html`) + ); + assert.equal(recordedFiles[1].data, SAMPLE_HTML_DATA); + + // JS file + assert.equal( + recordedFiles[2].filePath, + path.normalize(`${dirPath}/editAccountRecord.js`) + ); + assert.equal(recordedFiles[2].data, SAMPLE_JS_DATA); + + // XML file + assert.equal( + recordedFiles[3].filePath, + path.normalize(`${dirPath}/editAccountRecord.js-meta.xml`) + ); + assert.equal(recordedFiles[3].data, SAMPLE_XML_DATA); + + // QA file + assert.equal( + recordedFiles[4].filePath, + path.normalize( + 'force-app/main/default/quickActions/Account.edit.quickAction-meta.xml' + ) + ); + assert.equal(recordedFiles[4].data, SAMPLE_QA_DATA); + }); + + test('Generate create lwc and quick action', async () => { + var result = stubFileSystem([ + SAMPLE_CSS_DATA, + SAMPLE_HTML_DATA, + SAMPLE_JS_DATA, + SAMPLE_XML_DATA, + SAMPLE_QA_DATA + ]); + const recordedFiles = result[0]; + const mkdirStub = result[1]; + + const compactLayoutFields = buildTestCompactLayoutFields(); + const codeBuilder = new CodeBuilder( + extensionUri, + 'Account', + compactLayoutFields + ); + + await codeBuilder.generateCreate(); + assert.equal(recordedFiles.length, 5); + assert.equal(mkdirStub.callCount, 5); // for every file in test case + + // CSS file + var dirPath = 'force-app/main/default/lwc/createAccountRecord'; + assert.equal( + recordedFiles[0].filePath, + path.normalize(`${dirPath}/createAccountRecord.css`) + ); + assert.equal(recordedFiles[0].data, SAMPLE_CSS_DATA); + + // HTML file + assert.equal( + recordedFiles[1].filePath, + path.normalize(`${dirPath}/createAccountRecord.html`) + ); + assert.equal(recordedFiles[1].data, SAMPLE_HTML_DATA); + + // JS file + assert.equal( + recordedFiles[2].filePath, + path.normalize(`${dirPath}/createAccountRecord.js`) + ); + assert.equal(recordedFiles[2].data, SAMPLE_JS_DATA); + + // XML file + assert.equal( + recordedFiles[3].filePath, + path.normalize(`${dirPath}/createAccountRecord.js-meta.xml`) + ); + assert.equal(recordedFiles[3].data, SAMPLE_XML_DATA); + + // QA file + assert.equal( + recordedFiles[4].filePath, + path.normalize( + 'force-app/main/default/quickActions/Account.create.quickAction-meta.xml' + ) + ); + assert.equal(recordedFiles[4].data, SAMPLE_QA_DATA); + }); + + test('Field names are populated in constructor', () => { + const compactLayoutFields = buildTestCompactLayoutFields(); + const codeBuilder = new CodeBuilder( + extensionUri, + 'Account', + compactLayoutFields + ); + + const fieldNames = codeBuilder.fieldNames; + assert.equal(fieldNames.length, 1); + assert.equal(fieldNames[0], 'field1'); + }); + + test('Template variables are populated in constructor', () => { + const compactLayoutFields = buildTestCompactLayoutFields(); + const codeBuilder = new CodeBuilder( + extensionUri, + 'Account', + compactLayoutFields + ); + + const templateVars = codeBuilder.templateVariables; + + assert.equal( + templateVars[CodeBuilder.TEMPLATE_CREATE_LWC_LABEL], + 'LWC for creating a/an Account instance.' + ); + assert.equal( + templateVars[CodeBuilder.TEMPLATE_EDIT_LWC_LABEL], + 'LWC for editing a/an Account instance.' + ); + assert.equal( + templateVars[CodeBuilder.TEMPLATE_VIEW_LWC_LABEL], + 'LWC for viewing a/an Account instance.' + ); + + assert.equal( + templateVars[CodeBuilder.TEMPLATE_FIELDS], + 'FIELD1_FIELD, ' + ); + assert.ok( + templateVars[CodeBuilder.TEMPLATE_IMPORTS].includes( + 'import FIELD1_FIELD from "@salesforce/schema/Account.field1";' + ) + ); + assert.ok( + templateVars[ + CodeBuilder.TEMPLATE_LIGHTNING_INPUT_CREATE_FIELDS_HTML + ].includes( + '' + ) + ); + + assert.ok( + templateVars[ + CodeBuilder.TEMPLATE_LIGHTNING_INPUT_EDIT_FIELDS_HTML + ].includes( + '' + ) + ); + + assert.ok( + templateVars[CodeBuilder.TEMPLATE_VARIABLES].includes( + 'field1Field = FIELD1_FIELD;' + ) + ); + + assert.ok( + templateVars[CodeBuilder.TEMPLATE_VARIABLE_ASSIGNMENTS].includes( + 'field1 = "";' + ) + ); + }); + + function buildTestCompactLayoutFields() { + const compactLayoutFields: CompactLayoutField[] = []; + compactLayoutFields.push({ + editableForNew: true, + editableForUpdate: true, + label: 'Name', + layoutComponents: [ + { + value: 'field1' + } + ] + }); + return compactLayoutFields; + } + + /** + * Helper function to set up stubbing of the filesystem. + * + * @returns 2-item array where index 0 holds return values for invocations to + * the writeFileSync stub, and index 1 holds the mkdirSync stub. + */ + function stubFileSystem(fileContent: string[]) { + /** Stub all file system operations and ensure things are copied and calls are made */ + // mock the reads + const readFileStub = sinon.stub(fs, 'readFileSync'); + readFileStub.onCall(0).returns(fileContent[0]); + readFileStub.onCall(1).returns(fileContent[1]); + readFileStub.onCall(2).returns(fileContent[2]); + readFileStub.onCall(3).returns(fileContent[3]); + readFileStub.onCall(4).returns(fileContent[4]); + + // mock the writes + sinon.stub(fs, 'existsSync').returns(false); + const mkdirStub = sinon.stub(fs, 'mkdirSync'); + + // capture written out content + const writeStub = sinon.stub(fs, 'writeFileSync'); + var recordedFiles: any = []; + writeStub.callsFake((filePath, data, encoding) => { + // store values of all invocations + recordedFiles.push({ + filePath: filePath, + data: data + }); + assert.equal(encoding, 'utf8'); + }); + + // Return the recorded invocations of file write operations as well as the mkdirStub itself. + return [recordedFiles, mkdirStub]; + } +}); diff --git a/src/test/suite/utils/orgUtils.test.ts b/src/test/suite/utils/orgUtils.test.ts index b7df59e..a9f48be 100644 --- a/src/test/suite/utils/orgUtils.test.ts +++ b/src/test/suite/utils/orgUtils.test.ts @@ -165,17 +165,20 @@ suite('Org Utils Test Suite', () => { { editableForNew: true, editableForUpdate: true, - label: 'Name' + label: 'Name', + layoutComponents: [] }, { editableForNew: true, editableForUpdate: true, - label: 'Title' + label: 'Title', + layoutComponents: [] }, { editableForNew: false, editableForUpdate: false, - label: 'Contact Owner' + label: 'Contact Owner', + layoutComponents: [] } ], id: null, @@ -233,17 +236,20 @@ suite('Org Utils Test Suite', () => { { editableForNew: true, editableForUpdate: true, - label: 'Name' + label: 'Name', + layoutComponents: [] }, { editableForNew: true, editableForUpdate: true, - label: 'Title' + label: 'Title', + layoutComponents: [] }, { editableForNew: false, editableForUpdate: false, - label: 'Contact Owner' + label: 'Contact Owner', + layoutComponents: [] } ], id: null, diff --git a/src/utils/codeBuilder.ts b/src/utils/codeBuilder.ts new file mode 100644 index 0000000..798067c --- /dev/null +++ b/src/utils/codeBuilder.ts @@ -0,0 +1,307 @@ +/* + * Copyright (c) 2023, salesforce.com, inc. + * All rights reserved. + * SPDX-License-Identifier: MIT + * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT + */ +import * as fs from 'fs'; +import { Uri } from 'vscode'; +import * as path from 'path'; +import { CompactLayoutField } from './orgUtils'; +import { WorkspaceUtils } from './workspaceUtils'; + +type TemplateVariables = { [name: string]: string }; + +export class CodeBuilder { + static readonly QUICK_ACTION_TEMPLATE_NAME = 'quickAction.xml'; + + static readonly TEMPLATE_FILE_EXTENSIONS = [ + 'css', + 'html', + 'js', + 'js-meta.xml' + ]; + + // template variables + static readonly TEMPLATE_CREATE_LWC_LABEL = 'TEMPLATE_CREATE_LWC_LABEL'; + static readonly TEMPLATE_EDIT_LWC_LABEL = 'TEMPLATE_EDIT_LWC_LABEL'; + static readonly TEMPLATE_FIELDS = 'TEMPLATE_FIELDS'; + static readonly TEMPLATE_IMPORTS = 'TEMPLATE_IMPORTS'; + static readonly TEMPLATE_LIGHTNING_INPUT_CREATE_FIELDS_HTML = + 'TEMPLATE_LIGHTNING_INPUT_CREATE_FIELDS_HTML'; + static readonly TEMPLATE_LIGHTNING_INPUT_EDIT_FIELDS_HTML = + 'TEMPLATE_LIGHTNING_INPUT_EDIT_FIELDS_HTML'; + static readonly TEMPLATE_OBJECT_API_NAME = 'TEMPLATE_OBJECT_API_NAME'; + static readonly TEMPLATE_VARIABLES = 'TEMPLATE_VARIABLES'; + static readonly TEMPLATE_VARIABLE_ASSIGNMENTS = + 'TEMPLATE_VARIABLE_ASSIGNMENTS'; + static readonly TEMPLATE_VIEW_LWC_LABEL = 'TEMPLATE_VIEW_LWC_LABEL'; + static readonly TEMPLATE_LWC_NAME = 'TEMPLATE_LWC_NAME'; + static readonly TEMPLATE_QUICK_ACTION_ICON = 'TEMPLATE_QUICK_ACTION_ICON'; + static readonly TEMPLATE_QUICK_ACTION_LABEL = 'TEMPLATE_QUICK_ACTION_LABEL'; + + private extensionUri: Uri; + private objectApiName: string; + templateVariables: TemplateVariables; + fieldNames: string[]; + + constructor( + extensionUri: Uri, + objectApiName: string, + compactLayoutFields: CompactLayoutField[] + ) { + this.extensionUri = extensionUri; + this.objectApiName = objectApiName; + + this.fieldNames = this.getFieldNames(compactLayoutFields); + this.templateVariables = this.generateTemplateVariables( + this.fieldNames + ); + } + + async generateView(): Promise { + return new Promise(async (resolve) => { + const lwcName = `view${this.objectApiName}Record`; + this.copyTemplateFiles( + this.templateVariables, + 'viewRecord', + lwcName + ); + this.createQuickAction(this.templateVariables, 'View', lwcName); + resolve(true); + }); + } + + async generateEdit(): Promise { + return new Promise(async (resolve) => { + const lwcName = `edit${this.objectApiName}Record`; + this.copyTemplateFiles( + this.templateVariables, + 'editRecord', + lwcName + ); + this.createQuickAction( + this.templateVariables, + 'Edit', + lwcName, + 'editActionIcon' + ); + resolve(true); + }); + } + + async generateCreate(): Promise { + return new Promise(async (resolve) => { + const lwcName = `create${this.objectApiName}Record`; + this.copyTemplateFiles( + this.templateVariables, + 'createRecord', + lwcName + ); + this.createQuickAction(this.templateVariables, 'Create', lwcName); + resolve(true); + }); + } + + private getFieldNames(compactLayoutFields: CompactLayoutField[]) { + const fieldNames: string[] = []; + compactLayoutFields.forEach((field) => { + field.layoutComponents.forEach((component) => { + fieldNames.push(component.value); + }); + }); + return fieldNames; + } + + private createQuickAction( + templateVariables: TemplateVariables, + label: string, + name: string, + iconName?: string | undefined + ) { + const templateFilePath = path.join( + WorkspaceUtils.LWC_TEMPLATE_PATH, + CodeBuilder.QUICK_ACTION_TEMPLATE_NAME + ); + const fileContents = this.readFileContents(templateFilePath); + + const quickActionVariables: TemplateVariables = {}; + quickActionVariables[CodeBuilder.TEMPLATE_QUICK_ACTION_LABEL] = label; + quickActionVariables[CodeBuilder.TEMPLATE_LWC_NAME] = name; + if (iconName !== undefined && iconName !== '') { + quickActionVariables[ + CodeBuilder.TEMPLATE_QUICK_ACTION_ICON + ] = `${iconName}`; + } else { + quickActionVariables[CodeBuilder.TEMPLATE_QUICK_ACTION_ICON] = ''; + } + + // do substitutions + const newFileContents = this.replaceAllTemplateVariables(fileContents, { + ...templateVariables, + ...quickActionVariables + }); + + // copy to destination directory + const objectApiName = + templateVariables[CodeBuilder.TEMPLATE_OBJECT_API_NAME]; + // file name convention example: Account.view.quickAction-meta.xml + const destinationFile = `${objectApiName}.${label.toLocaleLowerCase()}.quickAction-meta.xml`; + + this.writeFileContents( + WorkspaceUtils.QUICK_ACTIONS_PATH, + destinationFile, + newFileContents + ); + } + + private copyTemplateFiles( + templateVariables: TemplateVariables, + template: string, + destinationLwc: string + ) { + CodeBuilder.TEMPLATE_FILE_EXTENSIONS.forEach((extension) => { + const templateFilePath = path.join( + WorkspaceUtils.LWC_TEMPLATE_PATH, + template, + `${template}.${extension}` + ); + const fileContents = this.readFileContents(templateFilePath); + + // do substitutions + const newFileContents = this.replaceAllTemplateVariables( + fileContents, + templateVariables + ); + + // copy to destination directory + const destinationDir = path.join( + WorkspaceUtils.LWC_PATH, + destinationLwc + ); + const destinationFile = `${destinationLwc}.${extension}`; + + this.writeFileContents( + destinationDir, + destinationFile, + newFileContents + ); + }); + } + + private replaceAllTemplateVariables( + contents: string, + templateVariables: TemplateVariables + ) { + var newFileContents = contents; + for (const key in templateVariables) { + if (templateVariables.hasOwnProperty(key)) { + const value = templateVariables[key]; + newFileContents = newFileContents.replace( + `///${key}///`, + value + ); + } + } + return newFileContents; + } + + private readFileContents(filePath: string): string { + const extensionFilePath = Uri.joinPath(this.extensionUri, filePath); + try { + return fs.readFileSync(extensionFilePath.fsPath, 'utf8'); + } catch (err) { + console.log(`Could not read file ${filePath}`, err); + return ''; + } + } + + private writeFileContents( + dirPath: string, + filename: string, + content: string + ) { + // ensure dirPath exists + if (!fs.existsSync(dirPath)) { + try { + fs.mkdirSync(dirPath, { recursive: true }); + } catch (err) { + console.log(`Unable to create directory: ${dirPath}`, err); + return; + } + } + // write the file + const filePath = path.join(dirPath, filename); + try { + fs.writeFileSync(filePath, content, 'utf8'); + } catch (err) { + console.error(`Error writing to file ${filePath}`, err); + } + } + + /** + * Ensure all the TEMPLATE_* variables have a value. + */ + private generateTemplateVariables(fieldNames: string[]): TemplateVariables { + const templateVariables: TemplateVariables = {}; + templateVariables[CodeBuilder.TEMPLATE_OBJECT_API_NAME] = + this.objectApiName; + + // Labels + templateVariables[ + CodeBuilder.TEMPLATE_CREATE_LWC_LABEL + ] = `LWC for creating a/an ${this.objectApiName} instance.`; + templateVariables[ + CodeBuilder.TEMPLATE_EDIT_LWC_LABEL + ] = `LWC for editing a/an ${this.objectApiName} instance.`; + templateVariables[ + CodeBuilder.TEMPLATE_VIEW_LWC_LABEL + ] = `LWC for viewing a/an ${this.objectApiName} instance.`; + + // We need to populate the following template variables: + // TEMPLATE_FIELDS - a comma separated list of field names from the import statements, used in viewRecord template. + // ie: return [NAME_FIELD, PHONE_FIELD, WEBSITE_FIELD, INDUSTRY_FIELD, TYPE_FIELD]; + // TEMPLATE_IMPORTS - a list of import statements that pulls in the @salesforce/schema fields: + // ie: import NAME_FIELD from "@salesforce/schema/Account.Name"; + // TEMPLATE_LIGHTNING_INPUT_CREATE_FIELDS_HTML - fields specified as lightning-input-field values in the create html: + // ie: + // TEMPLATE_LIGHTNING_INPUT_EDIT_FIELDS_HTML - fields specified as lightning-input-field values in the edit html + // ie: + // TEMPLATE_VARIABLES - aliases the imported fields to variables + // ie: nameField = NAME_FIELD; + // TEMPLATE_VARIABLE_ASSIGNMENTS - stores the value of the create fields: + // ie: name = ""; + + var fields = ''; + var imports = ''; + var createFieldsHtml = ''; + var editFieldsHtml = ''; + var importAliases = ''; + var variableAssignments = ''; + + fieldNames.forEach((field) => { + var fieldNameImport = `${field.toUpperCase()}_FIELD`; + fields += `${fieldNameImport}, `; + imports += `import ${fieldNameImport} from "@salesforce/schema/${this.objectApiName}.${field}";\n`; + + var fieldNameVariable = `${field.toLowerCase()}Field`; + importAliases += `${fieldNameVariable} = ${fieldNameImport};\n\t`; + variableAssignments += `${field.toLowerCase()} = "";\n\t`; + createFieldsHtml += `\n\t\t\t\t`; + editFieldsHtml += `\n\t\t\t\t`; + }); + templateVariables[CodeBuilder.TEMPLATE_FIELDS] = fields; + templateVariables[CodeBuilder.TEMPLATE_IMPORTS] = imports; + templateVariables[ + CodeBuilder.TEMPLATE_LIGHTNING_INPUT_CREATE_FIELDS_HTML + ] = createFieldsHtml; + templateVariables[ + CodeBuilder.TEMPLATE_LIGHTNING_INPUT_EDIT_FIELDS_HTML + ] = editFieldsHtml; + templateVariables[CodeBuilder.TEMPLATE_VARIABLES] = importAliases; + templateVariables[CodeBuilder.TEMPLATE_VARIABLE_ASSIGNMENTS] = + variableAssignments; + + return templateVariables; + } +} diff --git a/src/utils/orgUtils.ts b/src/utils/orgUtils.ts index e123e00..6e0af0d 100644 --- a/src/utils/orgUtils.ts +++ b/src/utils/orgUtils.ts @@ -19,10 +19,15 @@ export interface Field { type: string; } +export interface CompactLayoutFieldComponents { + value: string; +} + export interface CompactLayoutField { editableForNew: boolean; editableForUpdate: boolean; label: string; + layoutComponents: CompactLayoutFieldComponents[]; } export type SObjectCompactLayoutMapping = { diff --git a/src/utils/workspaceUtils.ts b/src/utils/workspaceUtils.ts index cfdf3a2..5dcc112 100644 --- a/src/utils/workspaceUtils.ts +++ b/src/utils/workspaceUtils.ts @@ -10,13 +10,29 @@ import { access } from 'fs/promises'; import * as path from 'path'; export class WorkspaceUtils { - static readonly STATIC_RESOURCES_PATH = path.join( + static readonly DEFAULT_APP_PATH = path.join( 'force-app', 'main', - 'default', + 'default' + ); + + static readonly STATIC_RESOURCES_PATH = path.join( + WorkspaceUtils.DEFAULT_APP_PATH, 'staticresources' ); + static readonly LWC_PATH = path.join( + WorkspaceUtils.DEFAULT_APP_PATH, + 'lwc' + ); + + static readonly QUICK_ACTIONS_PATH = path.join( + WorkspaceUtils.DEFAULT_APP_PATH, + 'quickActions' + ); + + static readonly LWC_TEMPLATE_PATH = path.join('resources', 'templates'); + static getWorkspaceDir(): string { const workspaceFolders = workspace.workspaceFolders; if (!workspaceFolders || workspaceFolders.length === 0) {