From c3f514c346230eef18fa9c19d6b58943e080e540 Mon Sep 17 00:00:00 2001 From: Dustin Breese Date: Wed, 8 Nov 2023 08:04:33 -0700 Subject: [PATCH 01/13] Now able to generate LWCs using template files. --- .../createSObjectLwcQuickActions.html | 61 +++-- src/commands/wizard/lwcGenerationCommand.ts | 76 ++++++- src/test/suite/utils/codeBuilder.test.ts | 74 +++++++ src/utils/codeBuilder.ts | 209 ++++++++++++++++++ 4 files changed, 396 insertions(+), 24 deletions(-) create mode 100644 src/test/suite/utils/codeBuilder.test.ts create mode 100644 src/utils/codeBuilder.ts diff --git a/resources/instructions/createSObjectLwcQuickActions.html b/resources/instructions/createSObjectLwcQuickActions.html index 973f646..c0fa894 100644 --- a/resources/instructions/createSObjectLwcQuickActions.html +++ b/resources/instructions/createSObjectLwcQuickActions.html @@ -56,18 +56,30 @@

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 03b5f03..0345b32 100644 --- a/src/commands/wizard/lwcGenerationCommand.ts +++ b/src/commands/wizard/lwcGenerationCommand.ts @@ -79,7 +79,7 @@ export class LwcGenerationCommand { 'resources/instructions/createSObjectLwcQuickActions.html', [ { - type: 'skipStepButton', + type: 'continueButton', action: (panel) => { panel.dispose(); return resolve(); @@ -87,7 +87,7 @@ export class LwcGenerationCommand { }, { type: 'generateLwcQuickActionsButton', - action: async (panel) => { + action: async (panel, _data, callback) => { // TODO: Hook this up to function that parses landing_page.json. const sobjects = [ 'Account', @@ -104,8 +104,15 @@ export class LwcGenerationCommand { extensionUri, quickActionStatus ); - panel.dispose(); - return resolve(); + + // send back updates so UI can be refreshed + if (callback) { + const quickActionStatus = + await LwcGenerationCommand.checkForExistingQuickActions( + sobjects + ); + callback(quickActionStatus); + } } }, { @@ -205,9 +212,11 @@ export class LwcGenerationCommand { } // Just double check now that things have been created. - return await LwcGenerationCommand.checkForExistingQuickActions( - Object.keys(quickActionStatus.sobjects) - ); + const newStatus = + await LwcGenerationCommand.checkForExistingQuickActions( + Object.keys(quickActionStatus.sobjects) + ); + resolve(newStatus); }); } diff --git a/src/utils/codeBuilder.ts b/src/utils/codeBuilder.ts index 092456a..b077f68 100644 --- a/src/utils/codeBuilder.ts +++ b/src/utils/codeBuilder.ts @@ -12,7 +12,10 @@ type TemplateVariables = { [name: string]: string }; export class CodeBuilder { static readonly TEMPLATE_DIR = './resources/templates'; - static readonly DESTINATION_DIR = './force-app/main/default/lwc'; + static readonly QUICK_ACTION_TEMPLATE_NAME = 'quickAction.xml'; + static readonly LWC_DESTINATION_DIR = './force-app/main/default/lwc'; + static readonly QA_DESTINATION_DIR = + './force-app/main/default/quickActions'; static readonly TEMPLATE_FILE_EXTENSIONS = [ 'css', 'html', @@ -39,34 +42,72 @@ export class CodeBuilder { async generateView(): Promise { return new Promise(async (resolve) => { - this.copyTemplateFiles( - 'viewRecord', - `view${this.objectApiName}Record` - ); + const lwcName = `view${this.objectApiName}Record`; + this.copyTemplateFiles('viewRecord', lwcName); + this.createQuickAction('View', lwcName); resolve(true); }); } async generateEdit(): Promise { return new Promise(async (resolve) => { - this.copyTemplateFiles( - 'editRecord', - `edit${this.objectApiName}Record` - ); + const lwcName = `edit${this.objectApiName}Record`; + this.copyTemplateFiles('editRecord', lwcName); + this.createQuickAction('Edit', lwcName, 'editActionIcon'); resolve(true); }); } async generateCreate(): Promise { return new Promise(async (resolve) => { - this.copyTemplateFiles( - 'createRecord', - `create${this.objectApiName}Record` - ); + const lwcName = `create${this.objectApiName}Record`; + this.copyTemplateFiles('createRecord', lwcName); + this.createQuickAction('Create', lwcName); resolve(true); }); } + private createQuickAction( + label: string, + name: string, + iconName: string = '' + ) { + const templateFilePath = path.join( + CodeBuilder.TEMPLATE_DIR, + CodeBuilder.QUICK_ACTION_TEMPLATE_NAME + ); + const fileContents = this.readFileContents(templateFilePath); + + const quickActionVariables: TemplateVariables = {}; + quickActionVariables['TEMPLATE_QUICK_ACTION_LABEL'] = label; + quickActionVariables['TEMPLATE_LWC_NAME'] = name; + if (iconName !== '') { + quickActionVariables[ + 'TEMPLATE_QUICK_ACTION_ICON' + ] = `${iconName}`; + } else { + quickActionVariables['TEMPLATE_QUICK_ACTION_ICON'] = ''; + } + + // do substitutions + const newFileContents = this.replaceAllTemplateVariables(fileContents, { + ...this.templateVariables, + ...quickActionVariables + }); + + // copy to destination directory + const objectApiName = + this.templateVariables['TEMPLATE_OBJECT_API_NAME']; + // file name convention example: Account.view.quickAction-meta.xml + const destinationFile = `${objectApiName}.${label.toLocaleLowerCase()}.quickAction-meta.xml`; + + this.writeFileContents( + CodeBuilder.QA_DESTINATION_DIR, + destinationFile, + newFileContents + ); + } + private copyTemplateFiles(template: string, destinationLwc: string) { CodeBuilder.TEMPLATE_FILE_EXTENSIONS.forEach((extension) => { const templateFilePath = path.join( @@ -84,7 +125,7 @@ export class CodeBuilder { // copy to destination directory const destinationDir = path.join( - CodeBuilder.DESTINATION_DIR, + CodeBuilder.LWC_DESTINATION_DIR, destinationLwc ); const destinationFile = `${destinationLwc}.${extension}`; From 43ed6ced2bc1e9842090f0c2080225915bd7d314 Mon Sep 17 00:00:00 2001 From: Dustin Breese Date: Thu, 9 Nov 2023 09:43:39 -0700 Subject: [PATCH 03/13] Adding unit tests for file system operations. --- src/test/suite/utils/codeBuilder.test.ts | 267 +++++++++++++++++++++++ 1 file changed, 267 insertions(+) diff --git a/src/test/suite/utils/codeBuilder.test.ts b/src/test/suite/utils/codeBuilder.test.ts index 8cac143..ed00652 100644 --- a/src/test/suite/utils/codeBuilder.test.ts +++ b/src/test/suite/utils/codeBuilder.test.ts @@ -10,16 +10,247 @@ import * as sinon from 'sinon'; import { CodeBuilder } from '../../../utils/codeBuilder'; import { Uri } from 'vscode'; import { afterEach, beforeEach } from 'mocha'; +import * as fs from 'fs'; 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 subtituted 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 = [ + 'TEMPLATE_CREATE_LWC_LABEL', + 'TEMPLATE_EDIT_LWC_LABEL', + 'TEMPLATE_FIELDS', + 'TEMPLATE_IMPORTS', + 'TEMPLATE_LIGHTNING_INPUT_CREATE_FIELDS_HTML', + 'TEMPLATE_LIGHTNING_INPUT_EDIT_FIELDS_HTML', + 'TEMPLATE_OBJECT_API_NAME', + 'TEMPLATE_VARIABLES', + 'TEMPLATE_VARIABLE_ASSIGNMENTS', + 'TEMPLATE_VIEW_LWC_LABEL' + ]; + + // but only these are substituted in a qa + const allQaTemplateFields = [ + 'TEMPLATE_LWC_NAME', + 'TEMPLATE_QUICK_ACTION_ICON', + '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 codeBuilder = new CodeBuilder(extensionUri, 'Account', [ + 'field1' + ]); + + 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 codeBuilder = new CodeBuilder(extensionUri, 'Account', [ + 'field1' + ]); + + 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, + `${dirPath}/viewAccountRecord.css` + ); + assert.equal(recordedFiles[0].data, SAMPLE_CSS_DATA); + + // HTML file + assert.equal( + recordedFiles[1].filePath, + `${dirPath}/viewAccountRecord.html` + ); + assert.equal(recordedFiles[1].data, SAMPLE_HTML_DATA); + + // JS file + assert.equal( + recordedFiles[2].filePath, + `${dirPath}/viewAccountRecord.js` + ); + assert.equal(recordedFiles[2].data, SAMPLE_JS_DATA); + + // XML file + assert.equal( + recordedFiles[3].filePath, + `${dirPath}/viewAccountRecord.js-meta.xml` + ); + assert.equal(recordedFiles[3].data, SAMPLE_XML_DATA); + + // QA file + assert.equal( + recordedFiles[4].filePath, + '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 codeBuilder = new CodeBuilder(extensionUri, 'Account', [ + 'field1' + ]); + + 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, + `${dirPath}/editAccountRecord.css` + ); + assert.equal(recordedFiles[0].data, SAMPLE_CSS_DATA); + + // HTML file + assert.equal( + recordedFiles[1].filePath, + `${dirPath}/editAccountRecord.html` + ); + assert.equal(recordedFiles[1].data, SAMPLE_HTML_DATA); + + // JS file + assert.equal( + recordedFiles[2].filePath, + `${dirPath}/editAccountRecord.js` + ); + assert.equal(recordedFiles[2].data, SAMPLE_JS_DATA); + + // XML file + assert.equal( + recordedFiles[3].filePath, + `${dirPath}/editAccountRecord.js-meta.xml` + ); + assert.equal(recordedFiles[3].data, SAMPLE_XML_DATA); + + // QA file + assert.equal( + recordedFiles[4].filePath, + '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 codeBuilder = new CodeBuilder(extensionUri, 'Account', [ + 'field1' + ]); + + 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, + `${dirPath}/createAccountRecord.css` + ); + assert.equal(recordedFiles[0].data, SAMPLE_CSS_DATA); + + // HTML file + assert.equal( + recordedFiles[1].filePath, + `${dirPath}/createAccountRecord.html` + ); + assert.equal(recordedFiles[1].data, SAMPLE_HTML_DATA); + + // JS file + assert.equal( + recordedFiles[2].filePath, + `${dirPath}/createAccountRecord.js` + ); + assert.equal(recordedFiles[2].data, SAMPLE_JS_DATA); + + // XML file + assert.equal( + recordedFiles[3].filePath, + `${dirPath}/createAccountRecord.js-meta.xml` + ); + assert.equal(recordedFiles[3].data, SAMPLE_XML_DATA); + + // QA file + assert.equal( + recordedFiles[4].filePath, + 'force-app/main/default/quickActions/Account.create.quickAction-meta.xml' + ); + assert.equal(recordedFiles[4].data, SAMPLE_QA_DATA); + }); + test('Template variables are populated in constructor', async () => { const codeBuilder = new CodeBuilder(extensionUri, 'Account', [ 'field1' @@ -71,4 +302,40 @@ suite('CodeBuilder Test Suite', () => { ) ); }); + + /** + * 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]; + } }); From f5595a8509fb09ebb8bd7553d1bdf033dbf46f42 Mon Sep 17 00:00:00 2001 From: Dustin Breese Date: Thu, 9 Nov 2023 11:06:57 -0700 Subject: [PATCH 04/13] Making template vars constants. --- src/test/suite/utils/codeBuilder.test.ts | 62 +++++++++++++----------- src/utils/codeBuilder.ts | 55 ++++++++++++++------- 2 files changed, 73 insertions(+), 44 deletions(-) diff --git a/src/test/suite/utils/codeBuilder.test.ts b/src/test/suite/utils/codeBuilder.test.ts index ed00652..ecec5a8 100644 --- a/src/test/suite/utils/codeBuilder.test.ts +++ b/src/test/suite/utils/codeBuilder.test.ts @@ -32,35 +32,35 @@ suite('CodeBuilder Test Suite', () => { // 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 = [ - 'TEMPLATE_CREATE_LWC_LABEL', - 'TEMPLATE_EDIT_LWC_LABEL', - 'TEMPLATE_FIELDS', - 'TEMPLATE_IMPORTS', - 'TEMPLATE_LIGHTNING_INPUT_CREATE_FIELDS_HTML', - 'TEMPLATE_LIGHTNING_INPUT_EDIT_FIELDS_HTML', - 'TEMPLATE_OBJECT_API_NAME', - 'TEMPLATE_VARIABLES', - 'TEMPLATE_VARIABLE_ASSIGNMENTS', - 'TEMPLATE_VIEW_LWC_LABEL' + 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 = [ - 'TEMPLATE_LWC_NAME', - 'TEMPLATE_QUICK_ACTION_ICON', - 'TEMPLATE_QUICK_ACTION_LABEL' + CodeBuilder.TEMPLATE_LWC_NAME, + CodeBuilder.TEMPLATE_QUICK_ACTION_ICON, + CodeBuilder.TEMPLATE_QUICK_ACTION_LABEL ]; - var allTemplateFieldsContent = ""; - allTemplateFields.forEach( (field) => { + var allTemplateFieldsContent = ''; + allTemplateFields.forEach((field) => { allTemplateFieldsContent += `///${field}///\n`; }); var allQaTemplateFieldsContent = allTemplateFieldsContent; - allQaTemplateFields.forEach( (field) => { + allQaTemplateFields.forEach((field) => { allQaTemplateFieldsContent += `///${field}///\n`; }); - + var result = stubFileSystem([ allTemplateFieldsContent, allTemplateFieldsContent, @@ -79,7 +79,10 @@ suite('CodeBuilder Test Suite', () => { // 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}!`); + assert.ok( + !file.data.includes('///'), + `All values should have been replaced in file ${file.filePath}!` + ); }); }); @@ -258,46 +261,51 @@ suite('CodeBuilder Test Suite', () => { const templateVars = codeBuilder.templateVariables; assert.equal( - templateVars['TEMPLATE_CREATE_LWC_LABEL'], + templateVars[CodeBuilder.TEMPLATE_CREATE_LWC_LABEL], 'LWC for creating a/an Account instance.' ); assert.equal( - templateVars['TEMPLATE_EDIT_LWC_LABEL'], + templateVars[CodeBuilder.TEMPLATE_EDIT_LWC_LABEL], 'LWC for editing a/an Account instance.' ); assert.equal( - templateVars['TEMPLATE_VIEW_LWC_LABEL'], + templateVars[CodeBuilder.TEMPLATE_VIEW_LWC_LABEL], 'LWC for viewing a/an Account instance.' ); - assert.equal(templateVars['TEMPLATE_FIELDS'], 'FIELD1_FIELD, '); + assert.equal( + templateVars[CodeBuilder.TEMPLATE_FIELDS], + 'FIELD1_FIELD, ' + ); assert.ok( - templateVars['TEMPLATE_IMPORTS'].includes( + templateVars[CodeBuilder.TEMPLATE_IMPORTS].includes( 'import FIELD1_FIELD from "@salesforce/schema/Account.field1";' ) ); assert.ok( templateVars[ - 'TEMPLATE_LIGHTNING_INPUT_CREATE_FIELDS_HTML' + CodeBuilder.TEMPLATE_LIGHTNING_INPUT_CREATE_FIELDS_HTML ].includes( '' ) ); assert.ok( - templateVars['TEMPLATE_LIGHTNING_INPUT_EDIT_FIELDS_HTML'].includes( + templateVars[ + CodeBuilder.TEMPLATE_LIGHTNING_INPUT_EDIT_FIELDS_HTML + ].includes( '' ) ); assert.ok( - templateVars['TEMPLATE_VARIABLES'].includes( + templateVars[CodeBuilder.TEMPLATE_VARIABLES].includes( 'field1Field = FIELD1_FIELD;' ) ); assert.ok( - templateVars['TEMPLATE_VARIABLE_ASSIGNMENTS'].includes( + templateVars[CodeBuilder.TEMPLATE_VARIABLE_ASSIGNMENTS].includes( 'field1 = "";' ) ); diff --git a/src/utils/codeBuilder.ts b/src/utils/codeBuilder.ts index b077f68..12f1c37 100644 --- a/src/utils/codeBuilder.ts +++ b/src/utils/codeBuilder.ts @@ -23,6 +23,24 @@ export class CodeBuilder { '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; private fieldNames: string[]; @@ -79,14 +97,14 @@ export class CodeBuilder { const fileContents = this.readFileContents(templateFilePath); const quickActionVariables: TemplateVariables = {}; - quickActionVariables['TEMPLATE_QUICK_ACTION_LABEL'] = label; - quickActionVariables['TEMPLATE_LWC_NAME'] = name; + quickActionVariables[CodeBuilder.TEMPLATE_QUICK_ACTION_LABEL] = label; + quickActionVariables[CodeBuilder.TEMPLATE_LWC_NAME] = name; if (iconName !== '') { quickActionVariables[ - 'TEMPLATE_QUICK_ACTION_ICON' + CodeBuilder.TEMPLATE_QUICK_ACTION_ICON ] = `${iconName}`; } else { - quickActionVariables['TEMPLATE_QUICK_ACTION_ICON'] = ''; + quickActionVariables[CodeBuilder.TEMPLATE_QUICK_ACTION_ICON] = ''; } // do substitutions @@ -97,7 +115,7 @@ export class CodeBuilder { // copy to destination directory const objectApiName = - this.templateVariables['TEMPLATE_OBJECT_API_NAME']; + this.templateVariables[CodeBuilder.TEMPLATE_OBJECT_API_NAME]; // file name convention example: Account.view.quickAction-meta.xml const destinationFile = `${objectApiName}.${label.toLocaleLowerCase()}.quickAction-meta.xml`; @@ -192,17 +210,18 @@ export class CodeBuilder { * Ensure all the TEMPLATE_* variables have a value. */ private generateTemplateVariables() { - this.templateVariables['TEMPLATE_OBJECT_API_NAME'] = this.objectApiName; + this.templateVariables[CodeBuilder.TEMPLATE_OBJECT_API_NAME] = + this.objectApiName; // Labels this.templateVariables[ - 'TEMPLATE_CREATE_LWC_LABEL' + CodeBuilder.TEMPLATE_CREATE_LWC_LABEL ] = `LWC for creating a/an ${this.objectApiName} instance.`; this.templateVariables[ - 'TEMPLATE_EDIT_LWC_LABEL' + CodeBuilder.TEMPLATE_EDIT_LWC_LABEL ] = `LWC for editing a/an ${this.objectApiName} instance.`; this.templateVariables[ - 'TEMPLATE_VIEW_LWC_LABEL' + CodeBuilder.TEMPLATE_VIEW_LWC_LABEL ] = `LWC for viewing a/an ${this.objectApiName} instance.`; // We need to populate the following template variables: @@ -237,14 +256,16 @@ export class CodeBuilder { createFieldsHtml += `\n\t\t\t\t`; editFieldsHtml += `\n\t\t\t\t`; }); - this.templateVariables['TEMPLATE_FIELDS'] = fields; - this.templateVariables['TEMPLATE_IMPORTS'] = imports; - this.templateVariables['TEMPLATE_LIGHTNING_INPUT_CREATE_FIELDS_HTML'] = - createFieldsHtml; - this.templateVariables['TEMPLATE_LIGHTNING_INPUT_EDIT_FIELDS_HTML'] = - editFieldsHtml; - this.templateVariables['TEMPLATE_VARIABLES'] = importAliases; - this.templateVariables['TEMPLATE_VARIABLE_ASSIGNMENTS'] = + this.templateVariables[CodeBuilder.TEMPLATE_FIELDS] = fields; + this.templateVariables[CodeBuilder.TEMPLATE_IMPORTS] = imports; + this.templateVariables[ + CodeBuilder.TEMPLATE_LIGHTNING_INPUT_CREATE_FIELDS_HTML + ] = createFieldsHtml; + this.templateVariables[ + CodeBuilder.TEMPLATE_LIGHTNING_INPUT_EDIT_FIELDS_HTML + ] = editFieldsHtml; + this.templateVariables[CodeBuilder.TEMPLATE_VARIABLES] = importAliases; + this.templateVariables[CodeBuilder.TEMPLATE_VARIABLE_ASSIGNMENTS] = variableAssignments; } } From 691e3de26b3eac8479d7d75b10ab76a88a5fd386 Mon Sep 17 00:00:00 2001 From: Dustin Breese Date: Thu, 9 Nov 2023 11:18:56 -0700 Subject: [PATCH 05/13] Normalizing path in unit tests for Windows. --- src/test/suite/utils/codeBuilder.test.ts | 37 ++++++++++++++---------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/src/test/suite/utils/codeBuilder.test.ts b/src/test/suite/utils/codeBuilder.test.ts index ecec5a8..99933e1 100644 --- a/src/test/suite/utils/codeBuilder.test.ts +++ b/src/test/suite/utils/codeBuilder.test.ts @@ -11,6 +11,7 @@ import { CodeBuilder } from '../../../utils/codeBuilder'; import { Uri } from 'vscode'; import { afterEach, beforeEach } from 'mocha'; import * as fs from 'fs'; +import * as path from 'path'; suite('CodeBuilder Test Suite', () => { var extensionUri = Uri.parse('file:///tmp/'); @@ -109,35 +110,37 @@ suite('CodeBuilder Test Suite', () => { var dirPath = 'force-app/main/default/lwc/viewAccountRecord'; assert.equal( recordedFiles[0].filePath, - `${dirPath}/viewAccountRecord.css` + path.normalize(`${dirPath}/viewAccountRecord.css`) ); assert.equal(recordedFiles[0].data, SAMPLE_CSS_DATA); // HTML file assert.equal( recordedFiles[1].filePath, - `${dirPath}/viewAccountRecord.html` + path.normalize(`${dirPath}/viewAccountRecord.html`) ); assert.equal(recordedFiles[1].data, SAMPLE_HTML_DATA); // JS file assert.equal( recordedFiles[2].filePath, - `${dirPath}/viewAccountRecord.js` + path.normalize(`${dirPath}/viewAccountRecord.js`) ); assert.equal(recordedFiles[2].data, SAMPLE_JS_DATA); // XML file assert.equal( recordedFiles[3].filePath, - `${dirPath}/viewAccountRecord.js-meta.xml` + path.normalize(`${dirPath}/viewAccountRecord.js-meta.xml`) ); assert.equal(recordedFiles[3].data, SAMPLE_XML_DATA); // QA file assert.equal( recordedFiles[4].filePath, - 'force-app/main/default/quickActions/Account.view.quickAction-meta.xml' + path.normalize( + 'force-app/main/default/quickActions/Account.view.quickAction-meta.xml' + ) ); assert.equal(recordedFiles[4].data, SAMPLE_QA_DATA); }); @@ -165,35 +168,37 @@ suite('CodeBuilder Test Suite', () => { var dirPath = 'force-app/main/default/lwc/editAccountRecord'; assert.equal( recordedFiles[0].filePath, - `${dirPath}/editAccountRecord.css` + path.normalize(`${dirPath}/editAccountRecord.css`) ); assert.equal(recordedFiles[0].data, SAMPLE_CSS_DATA); // HTML file assert.equal( recordedFiles[1].filePath, - `${dirPath}/editAccountRecord.html` + path.normalize(`${dirPath}/editAccountRecord.html`) ); assert.equal(recordedFiles[1].data, SAMPLE_HTML_DATA); // JS file assert.equal( recordedFiles[2].filePath, - `${dirPath}/editAccountRecord.js` + path.normalize(`${dirPath}/editAccountRecord.js`) ); assert.equal(recordedFiles[2].data, SAMPLE_JS_DATA); // XML file assert.equal( recordedFiles[3].filePath, - `${dirPath}/editAccountRecord.js-meta.xml` + path.normalize(`${dirPath}/editAccountRecord.js-meta.xml`) ); assert.equal(recordedFiles[3].data, SAMPLE_XML_DATA); // QA file assert.equal( recordedFiles[4].filePath, - 'force-app/main/default/quickActions/Account.edit.quickAction-meta.xml' + path.normalize( + 'force-app/main/default/quickActions/Account.edit.quickAction-meta.xml' + ) ); assert.equal(recordedFiles[4].data, SAMPLE_QA_DATA); }); @@ -221,35 +226,37 @@ suite('CodeBuilder Test Suite', () => { var dirPath = 'force-app/main/default/lwc/createAccountRecord'; assert.equal( recordedFiles[0].filePath, - `${dirPath}/createAccountRecord.css` + path.normalize(`${dirPath}/createAccountRecord.css`) ); assert.equal(recordedFiles[0].data, SAMPLE_CSS_DATA); // HTML file assert.equal( recordedFiles[1].filePath, - `${dirPath}/createAccountRecord.html` + path.normalize(`${dirPath}/createAccountRecord.html`) ); assert.equal(recordedFiles[1].data, SAMPLE_HTML_DATA); // JS file assert.equal( recordedFiles[2].filePath, - `${dirPath}/createAccountRecord.js` + path.normalize(`${dirPath}/createAccountRecord.js`) ); assert.equal(recordedFiles[2].data, SAMPLE_JS_DATA); // XML file assert.equal( recordedFiles[3].filePath, - `${dirPath}/createAccountRecord.js-meta.xml` + path.normalize(`${dirPath}/createAccountRecord.js-meta.xml`) ); assert.equal(recordedFiles[3].data, SAMPLE_XML_DATA); // QA file assert.equal( recordedFiles[4].filePath, - 'force-app/main/default/quickActions/Account.create.quickAction-meta.xml' + path.normalize( + 'force-app/main/default/quickActions/Account.create.quickAction-meta.xml' + ) ); assert.equal(recordedFiles[4].data, SAMPLE_QA_DATA); }); From 87787baa7ecbefa2cdafc6442761bc5b3c7df726 Mon Sep 17 00:00:00 2001 From: Dustin Breese Date: Wed, 15 Nov 2023 11:22:21 -0700 Subject: [PATCH 06/13] Rebase working now. Need to debug more. --- .../createSObjectLwcQuickActions.html | 20 ++--- src/commands/wizard/lwcGenerationCommand.ts | 87 ++++++++----------- 2 files changed, 44 insertions(+), 63 deletions(-) diff --git a/resources/instructions/createSObjectLwcQuickActions.html b/resources/instructions/createSObjectLwcQuickActions.html index 5ba90e2..a967d74 100644 --- a/resources/instructions/createSObjectLwcQuickActions.html +++ b/resources/instructions/createSObjectLwcQuickActions.html @@ -84,11 +84,13 @@

Create sObject LWC Quick Actions

{}, handleQuickActionStatusResponse ); - generateQuickActionsButtonElement.addEventListener('click', () => { - webviewMessaging.sendMessageRequest( - 'generateLwcQuickActionsButton' - ); - }); + }); + + generateQuickActionsButtonElement.addEventListener('click', () => { + webviewMessaging.sendMessageRequest( + 'generateLwcQuickActionsButton' + ); + }); continueButtonElement.addEventListener('click', () => { webviewMessaging.sendMessageRequest( @@ -105,11 +107,8 @@

Create sObject LWC Quick Actions

{}, handleQuickActionStatusResponse ); - - function handleQuickActionStatusResponse(response) { - const table = document.getElementById('quickActionStatusTable'); - for (const sobject in response.sobjects) { - const quickActions = response.sobjects[sobject]; + + }); function handleQuickActionStatusResponse(response) { const table = document.getElementById('quickActionStatusTable'); @@ -135,6 +134,7 @@

Create sObject LWC Quick Actions

edit.innerHTML = quickActions.edit == true ? "✅" : "❌"; create.innerHTML = quickActions.create == true ? "✅" : "❌"; } + } diff --git a/src/commands/wizard/lwcGenerationCommand.ts b/src/commands/wizard/lwcGenerationCommand.ts index 0345b32..b5f0a1e 100644 --- a/src/commands/wizard/lwcGenerationCommand.ts +++ b/src/commands/wizard/lwcGenerationCommand.ts @@ -35,42 +35,7 @@ export type GetSObjectsStatus = { }; export class LwcGenerationCommand { - extensionUri: Uri; - - constructor(extensionUri: Uri) { - this.extensionUri = extensionUri; - } - - static async getSObjectsFromLandingPage(): Promise { - return new Promise(async (resolve) => { - const staticResourcesPath = - await WorkspaceUtils.getStaticResourcesDir(); - const landingPageJson = 'landing_page.json'; - const landingPagePath = path.join( - staticResourcesPath, - landingPageJson - ); - - const getSObjectsStatus: GetSObjectsStatus = { - sobjects: [] - }; - - try { - await access(landingPagePath); - const uem = CommonUtils.loadJsonFromFile(landingPagePath); - getSObjectsStatus.sobjects = UEMParser.findSObjects(uem); - } catch (err) { - console.warn( - `File '${landingPageJson}' does not exist at '${staticResourcesPath}'.` - ); - getSObjectsStatus.error = (err as Error).message; - } - - resolve(getSObjectsStatus); - }); - } - - async createSObjectLwcQuickActions() { + static async createSObjectLwcQuickActions(extensionUri: Uri) { return new Promise((resolve) => { new InstructionsWebviewProvider( extensionUri @@ -88,19 +53,10 @@ export class LwcGenerationCommand { { type: 'generateLwcQuickActionsButton', action: async (panel, _data, callback) => { - // TODO: Hook this up to function that parses landing_page.json. - const sobjects = [ - 'Account', - 'Contact', - 'Opportunity', - 'SomeOther' - ]; const quickActionStatus = - await LwcGenerationCommand.checkForExistingQuickActions( - sobjects - ); + await LwcGenerationCommand.checkForExistingQuickActions(); - await this.generateMissingLwcsAndQuickActions( + await LwcGenerationCommand.generateMissingLwcsAndQuickActions( extensionUri, quickActionStatus ); @@ -108,9 +64,7 @@ export class LwcGenerationCommand { // send back updates so UI can be refreshed if (callback) { const quickActionStatus = - await LwcGenerationCommand.checkForExistingQuickActions( - sobjects - ); + await LwcGenerationCommand.checkForExistingQuickActions(); callback(quickActionStatus); } } @@ -137,6 +91,35 @@ export class LwcGenerationCommand { ); }); } + + static async getSObjectsFromLandingPage(): Promise { + return new Promise(async (resolve) => { + const staticResourcesPath = + await WorkspaceUtils.getStaticResourcesDir(); + const landingPageJson = 'landing_page.json'; + const landingPagePath = path.join( + staticResourcesPath, + landingPageJson + ); + + const getSObjectsStatus: GetSObjectsStatus = { + sobjects: [] + }; + + try { + await access(landingPagePath); + const uem = CommonUtils.loadJsonFromFile(landingPagePath); + getSObjectsStatus.sobjects = UEMParser.findSObjects(uem); + } catch (err) { + console.warn( + `File '${landingPageJson}' does not exist at '${staticResourcesPath}'.` + ); + getSObjectsStatus.error = (err as Error).message; + } + + resolve(getSObjectsStatus); + }); + } static async checkForExistingQuickActions(): Promise { return new Promise(async (resolve) => { @@ -213,9 +196,7 @@ export class LwcGenerationCommand { // Just double check now that things have been created. const newStatus = - await LwcGenerationCommand.checkForExistingQuickActions( - Object.keys(quickActionStatus.sobjects) - ); + await LwcGenerationCommand.checkForExistingQuickActions(); resolve(newStatus); }); } From f36c39334cecc705a93d5738bd08bb5fd03972d6 Mon Sep 17 00:00:00 2001 From: Dustin Breese Date: Wed, 15 Nov 2023 12:49:32 -0700 Subject: [PATCH 07/13] Now using api name for fields and correctly generating cases LWCs and QAs. --- src/commands/wizard/lwcGenerationCommand.ts | 10 +-- src/utils/codeBuilder.ts | 82 +++++++++++++-------- src/utils/orgUtils.ts | 7 +- 3 files changed, 62 insertions(+), 37 deletions(-) diff --git a/src/commands/wizard/lwcGenerationCommand.ts b/src/commands/wizard/lwcGenerationCommand.ts index b5f0a1e..cb46c5a 100644 --- a/src/commands/wizard/lwcGenerationCommand.ts +++ b/src/commands/wizard/lwcGenerationCommand.ts @@ -173,12 +173,10 @@ export class LwcGenerationCommand { !quickActions.edit || !quickActions.view ) { - // at least 1 needs to be creaed - // TODO: Hook up to compact layout to obtain list of field names to use - const codeBuilder = new CodeBuilder(extensionUri, sobject, [ - 'Name', - 'AccountId' - ]); + // 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(); diff --git a/src/utils/codeBuilder.ts b/src/utils/codeBuilder.ts index 12f1c37..7847c68 100644 --- a/src/utils/codeBuilder.ts +++ b/src/utils/codeBuilder.ts @@ -7,6 +7,7 @@ import * as fs from 'fs'; import { Uri } from 'vscode'; import * as path from 'path'; +import { CompactLayoutField } from './orgUtils'; type TemplateVariables = { [name: string]: string }; @@ -43,35 +44,46 @@ export class CodeBuilder { private extensionUri: Uri; private objectApiName: string; - private fieldNames: string[]; - - templateVariables: TemplateVariables = {}; + private templateVariables: TemplateVariables; + private fieldNames: string[] constructor( extensionUri: Uri, objectApiName: string, - fieldNames: string[] + compactLayoutFields: CompactLayoutField[] ) { this.extensionUri = extensionUri; this.objectApiName = objectApiName; - this.fieldNames = fieldNames; - this.generateTemplateVariables(); + + 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('viewRecord', lwcName); - this.createQuickAction('View', lwcName); + this.copyTemplateFiles(this.templateVariables, 'viewRecord', lwcName); + this.createQuickAction(this.templateVariables, 'View', lwcName); resolve(true); }); } + getFieldNames(compactLayoutFields: CompactLayoutField[]) { + const fieldNames: string[] = []; + compactLayoutFields.forEach((field) => { + + field.layoutComponents.forEach((component) => { + fieldNames.push(component.value); + }); + }); + return fieldNames; + } + async generateEdit(): Promise { return new Promise(async (resolve) => { const lwcName = `edit${this.objectApiName}Record`; - this.copyTemplateFiles('editRecord', lwcName); - this.createQuickAction('Edit', lwcName, 'editActionIcon'); + this.copyTemplateFiles(this.templateVariables, 'editRecord', lwcName); + this.createQuickAction(this.templateVariables, 'Edit', lwcName, 'editActionIcon'); resolve(true); }); } @@ -79,16 +91,17 @@ export class CodeBuilder { async generateCreate(): Promise { return new Promise(async (resolve) => { const lwcName = `create${this.objectApiName}Record`; - this.copyTemplateFiles('createRecord', lwcName); - this.createQuickAction('Create', lwcName); + this.copyTemplateFiles(this.templateVariables, 'createRecord', lwcName); + this.createQuickAction(this.templateVariables, 'Create', lwcName); resolve(true); }); } private createQuickAction( + templateVariables: TemplateVariables, label: string, name: string, - iconName: string = '' + iconName: string | undefined = undefined ) { const templateFilePath = path.join( CodeBuilder.TEMPLATE_DIR, @@ -99,7 +112,7 @@ export class CodeBuilder { const quickActionVariables: TemplateVariables = {}; quickActionVariables[CodeBuilder.TEMPLATE_QUICK_ACTION_LABEL] = label; quickActionVariables[CodeBuilder.TEMPLATE_LWC_NAME] = name; - if (iconName !== '') { + if (iconName != undefined && iconName !== '') { quickActionVariables[ CodeBuilder.TEMPLATE_QUICK_ACTION_ICON ] = `${iconName}`; @@ -109,13 +122,13 @@ export class CodeBuilder { // do substitutions const newFileContents = this.replaceAllTemplateVariables(fileContents, { - ...this.templateVariables, + ...templateVariables, ...quickActionVariables }); // copy to destination directory const objectApiName = - this.templateVariables[CodeBuilder.TEMPLATE_OBJECT_API_NAME]; + templateVariables[CodeBuilder.TEMPLATE_OBJECT_API_NAME]; // file name convention example: Account.view.quickAction-meta.xml const destinationFile = `${objectApiName}.${label.toLocaleLowerCase()}.quickAction-meta.xml`; @@ -126,7 +139,11 @@ export class CodeBuilder { ); } - private copyTemplateFiles(template: string, destinationLwc: string) { + private copyTemplateFiles( + templateVariables: TemplateVariables, + template: string, + destinationLwc: string + ) { CodeBuilder.TEMPLATE_FILE_EXTENSIONS.forEach((extension) => { const templateFilePath = path.join( CodeBuilder.TEMPLATE_DIR, @@ -138,7 +155,7 @@ export class CodeBuilder { // do substitutions const newFileContents = this.replaceAllTemplateVariables( fileContents, - this.templateVariables + templateVariables ); // copy to destination directory @@ -209,18 +226,21 @@ export class CodeBuilder { /** * Ensure all the TEMPLATE_* variables have a value. */ - private generateTemplateVariables() { - this.templateVariables[CodeBuilder.TEMPLATE_OBJECT_API_NAME] = + private generateTemplateVariables( + fieldNames: string[] + ): TemplateVariables { + const templateVariables: TemplateVariables = {}; + templateVariables[CodeBuilder.TEMPLATE_OBJECT_API_NAME] = this.objectApiName; // Labels - this.templateVariables[ + templateVariables[ CodeBuilder.TEMPLATE_CREATE_LWC_LABEL ] = `LWC for creating a/an ${this.objectApiName} instance.`; - this.templateVariables[ + templateVariables[ CodeBuilder.TEMPLATE_EDIT_LWC_LABEL ] = `LWC for editing a/an ${this.objectApiName} instance.`; - this.templateVariables[ + templateVariables[ CodeBuilder.TEMPLATE_VIEW_LWC_LABEL ] = `LWC for viewing a/an ${this.objectApiName} instance.`; @@ -245,7 +265,7 @@ export class CodeBuilder { var importAliases = ''; var variableAssignments = ''; - this.fieldNames.forEach((field) => { + fieldNames.forEach((field) => { var fieldNameImport = `${field.toUpperCase()}_FIELD`; fields += `${fieldNameImport}, `; imports += `import ${fieldNameImport} from "@salesforce/schema/${this.objectApiName}.${field}";\n`; @@ -256,16 +276,18 @@ export class CodeBuilder { createFieldsHtml += `\n\t\t\t\t`; editFieldsHtml += `\n\t\t\t\t`; }); - this.templateVariables[CodeBuilder.TEMPLATE_FIELDS] = fields; - this.templateVariables[CodeBuilder.TEMPLATE_IMPORTS] = imports; - this.templateVariables[ + templateVariables[CodeBuilder.TEMPLATE_FIELDS] = fields; + templateVariables[CodeBuilder.TEMPLATE_IMPORTS] = imports; + templateVariables[ CodeBuilder.TEMPLATE_LIGHTNING_INPUT_CREATE_FIELDS_HTML ] = createFieldsHtml; - this.templateVariables[ + templateVariables[ CodeBuilder.TEMPLATE_LIGHTNING_INPUT_EDIT_FIELDS_HTML ] = editFieldsHtml; - this.templateVariables[CodeBuilder.TEMPLATE_VARIABLES] = importAliases; - this.templateVariables[CodeBuilder.TEMPLATE_VARIABLE_ASSIGNMENTS] = + 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..4e2346f 100644 --- a/src/utils/orgUtils.ts +++ b/src/utils/orgUtils.ts @@ -19,11 +19,16 @@ export interface Field { type: string; } +export interface CompactLayoutFieldComponents { + value: string; +}; + export interface CompactLayoutField { editableForNew: boolean; editableForUpdate: boolean; label: string; -} + layoutComponents: CompactLayoutFieldComponents[]; +}; export type SObjectCompactLayoutMapping = { compactLayoutId: string | null; From f87a0e1700b1aa31cc0d7ea77dbeb80250806518 Mon Sep 17 00:00:00 2001 From: Dustin Breese Date: Wed, 15 Nov 2023 13:21:39 -0700 Subject: [PATCH 08/13] Updated to always use the same field names for create/edit/view LWCs. Added unit tests and fixed broken ones. --- src/commands/wizard/lwcGenerationCommand.ts | 13 +++- src/test/suite/utils/codeBuilder.test.ts | 80 ++++++++++++++++----- src/test/suite/utils/orgUtils.test.ts | 18 +++-- src/utils/codeBuilder.ts | 64 ++++++++++------- src/utils/orgUtils.ts | 4 +- 5 files changed, 126 insertions(+), 53 deletions(-) diff --git a/src/commands/wizard/lwcGenerationCommand.ts b/src/commands/wizard/lwcGenerationCommand.ts index cb46c5a..7ace2b8 100644 --- a/src/commands/wizard/lwcGenerationCommand.ts +++ b/src/commands/wizard/lwcGenerationCommand.ts @@ -91,7 +91,7 @@ export class LwcGenerationCommand { ); }); } - + static async getSObjectsFromLandingPage(): Promise { return new Promise(async (resolve) => { const staticResourcesPath = @@ -174,9 +174,16 @@ export class LwcGenerationCommand { !quickActions.view ) { // at least 1 needs to be created - const compactLayoutFields = await OrgUtils.getCompactLayoutFieldsForSObject(sobject); + const compactLayoutFields = + await OrgUtils.getCompactLayoutFieldsForSObject( + sobject + ); - const codeBuilder = new CodeBuilder(extensionUri, sobject, compactLayoutFields); + const codeBuilder = new CodeBuilder( + extensionUri, + sobject, + compactLayoutFields + ); if (!quickActions.view) { await codeBuilder.generateView(); diff --git a/src/test/suite/utils/codeBuilder.test.ts b/src/test/suite/utils/codeBuilder.test.ts index 99933e1..b084ba5 100644 --- a/src/test/suite/utils/codeBuilder.test.ts +++ b/src/test/suite/utils/codeBuilder.test.ts @@ -12,6 +12,7 @@ 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/'); @@ -28,7 +29,7 @@ suite('CodeBuilder Test Suite', () => { sinon.restore(); }); - test('All values subtituted before writing', async () => { + 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. @@ -70,10 +71,12 @@ suite('CodeBuilder Test Suite', () => { allQaTemplateFieldsContent ]); const recordedFiles = result[0]; - - const codeBuilder = new CodeBuilder(extensionUri, 'Account', [ - 'field1' - ]); + const compactLayoutFields = buildTestCompactLayoutFields(); + const codeBuilder = new CodeBuilder( + extensionUri, + 'Account', + compactLayoutFields + ); await codeBuilder.generateView(); assert.equal(recordedFiles.length, 5); @@ -98,9 +101,12 @@ suite('CodeBuilder Test Suite', () => { const recordedFiles = result[0]; const mkdirStub = result[1]; - const codeBuilder = new CodeBuilder(extensionUri, 'Account', [ - 'field1' - ]); + const compactLayoutFields = buildTestCompactLayoutFields(); + const codeBuilder = new CodeBuilder( + extensionUri, + 'Account', + compactLayoutFields + ); await codeBuilder.generateView(); assert.equal(recordedFiles.length, 5); @@ -156,9 +162,12 @@ suite('CodeBuilder Test Suite', () => { const recordedFiles = result[0]; const mkdirStub = result[1]; - const codeBuilder = new CodeBuilder(extensionUri, 'Account', [ - 'field1' - ]); + const compactLayoutFields = buildTestCompactLayoutFields(); + const codeBuilder = new CodeBuilder( + extensionUri, + 'Account', + compactLayoutFields + ); await codeBuilder.generateEdit(); assert.equal(recordedFiles.length, 5); @@ -214,9 +223,12 @@ suite('CodeBuilder Test Suite', () => { const recordedFiles = result[0]; const mkdirStub = result[1]; - const codeBuilder = new CodeBuilder(extensionUri, 'Account', [ - 'field1' - ]); + const compactLayoutFields = buildTestCompactLayoutFields(); + const codeBuilder = new CodeBuilder( + extensionUri, + 'Account', + compactLayoutFields + ); await codeBuilder.generateCreate(); assert.equal(recordedFiles.length, 5); @@ -261,10 +273,27 @@ suite('CodeBuilder Test Suite', () => { assert.equal(recordedFiles[4].data, SAMPLE_QA_DATA); }); - test('Template variables are populated in constructor', async () => { - const codeBuilder = new CodeBuilder(extensionUri, 'Account', [ - 'field1' - ]); + 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( @@ -318,6 +347,21 @@ suite('CodeBuilder Test Suite', () => { ); }); + 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. * 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 index 7847c68..03c960d 100644 --- a/src/utils/codeBuilder.ts +++ b/src/utils/codeBuilder.ts @@ -44,8 +44,8 @@ export class CodeBuilder { private extensionUri: Uri; private objectApiName: string; - private templateVariables: TemplateVariables; - private fieldNames: string[] + templateVariables: TemplateVariables; + fieldNames: string[]; constructor( extensionUri: Uri, @@ -56,34 +56,38 @@ export class CodeBuilder { this.objectApiName = objectApiName; this.fieldNames = this.getFieldNames(compactLayoutFields); - this.templateVariables = this.generateTemplateVariables(this.fieldNames); + 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.copyTemplateFiles( + this.templateVariables, + 'viewRecord', + lwcName + ); this.createQuickAction(this.templateVariables, 'View', lwcName); resolve(true); }); } - getFieldNames(compactLayoutFields: CompactLayoutField[]) { - const fieldNames: string[] = []; - compactLayoutFields.forEach((field) => { - - field.layoutComponents.forEach((component) => { - fieldNames.push(component.value); - }); - }); - return fieldNames; - } - 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'); + this.copyTemplateFiles( + this.templateVariables, + 'editRecord', + lwcName + ); + this.createQuickAction( + this.templateVariables, + 'Edit', + lwcName, + 'editActionIcon' + ); resolve(true); }); } @@ -91,12 +95,26 @@ export class CodeBuilder { async generateCreate(): Promise { return new Promise(async (resolve) => { const lwcName = `create${this.objectApiName}Record`; - this.copyTemplateFiles(this.templateVariables, 'createRecord', lwcName); + 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, @@ -112,7 +130,7 @@ export class CodeBuilder { const quickActionVariables: TemplateVariables = {}; quickActionVariables[CodeBuilder.TEMPLATE_QUICK_ACTION_LABEL] = label; quickActionVariables[CodeBuilder.TEMPLATE_LWC_NAME] = name; - if (iconName != undefined && iconName !== '') { + if (iconName !== undefined && iconName !== '') { quickActionVariables[ CodeBuilder.TEMPLATE_QUICK_ACTION_ICON ] = `${iconName}`; @@ -143,7 +161,7 @@ export class CodeBuilder { templateVariables: TemplateVariables, template: string, destinationLwc: string - ) { + ) { CodeBuilder.TEMPLATE_FILE_EXTENSIONS.forEach((extension) => { const templateFilePath = path.join( CodeBuilder.TEMPLATE_DIR, @@ -226,9 +244,7 @@ export class CodeBuilder { /** * Ensure all the TEMPLATE_* variables have a value. */ - private generateTemplateVariables( - fieldNames: string[] - ): TemplateVariables { + private generateTemplateVariables(fieldNames: string[]): TemplateVariables { const templateVariables: TemplateVariables = {}; templateVariables[CodeBuilder.TEMPLATE_OBJECT_API_NAME] = this.objectApiName; @@ -287,7 +303,7 @@ export class CodeBuilder { 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 4e2346f..6e0af0d 100644 --- a/src/utils/orgUtils.ts +++ b/src/utils/orgUtils.ts @@ -21,14 +21,14 @@ export interface Field { export interface CompactLayoutFieldComponents { value: string; -}; +} export interface CompactLayoutField { editableForNew: boolean; editableForUpdate: boolean; label: string; layoutComponents: CompactLayoutFieldComponents[]; -}; +} export type SObjectCompactLayoutMapping = { compactLayoutId: string | null; From 6b920d504f8c7afc07c3b39993822100d231aecb Mon Sep 17 00:00:00 2001 From: Dustin Breese Date: Wed, 15 Nov 2023 13:26:44 -0700 Subject: [PATCH 09/13] Getting rid of unnecessary call. --- resources/instructions/createSObjectLwcQuickActions.html | 8 +++++--- src/commands/wizard/lwcGenerationCommand.ts | 8 -------- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/resources/instructions/createSObjectLwcQuickActions.html b/resources/instructions/createSObjectLwcQuickActions.html index a967d74..ab2558a 100644 --- a/resources/instructions/createSObjectLwcQuickActions.html +++ b/resources/instructions/createSObjectLwcQuickActions.html @@ -78,6 +78,10 @@

Create sObject LWC Quick Actions

'continueButton' ); + const quickActionStatusTable = document.getElementById( + 'quickActionStatusTable' + ); + generateQuickActionsButtonElement.addEventListener('click', () => { webviewMessaging.sendMessageRequest( 'generateLwcQuickActionsButton', @@ -107,12 +111,10 @@

Create sObject LWC Quick Actions

{}, handleQuickActionStatusResponse ); - }); function handleQuickActionStatusResponse(response) { - const table = document.getElementById('quickActionStatusTable'); - var tbody = table.getElementsByTagName('tbody')[0]; + var tbody = quickActionStatusTable.getElementsByTagName('tbody')[0]; // remove all table rows first while (tbody.firstChild) { diff --git a/src/commands/wizard/lwcGenerationCommand.ts b/src/commands/wizard/lwcGenerationCommand.ts index 7ace2b8..b59f325 100644 --- a/src/commands/wizard/lwcGenerationCommand.ts +++ b/src/commands/wizard/lwcGenerationCommand.ts @@ -75,14 +75,6 @@ export class LwcGenerationCommand { if (callback) { const quickActionStatus = await LwcGenerationCommand.checkForExistingQuickActions(); - - for (const key in quickActionStatus.sobjects) { - const layoutFields = - await OrgUtils.getCompactLayoutFieldsForSObject( - key - ); - } - callback(quickActionStatus); } } From d3a09b3eef63c483bc3654876182c167af38e96a Mon Sep 17 00:00:00 2001 From: Dustin Breese Date: Wed, 15 Nov 2023 13:38:42 -0700 Subject: [PATCH 10/13] Linking in the lwc code generation command. --- src/commands/wizard/onboardingWizard.ts | 2 ++ 1 file changed, 2 insertions(+) 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(); From 7f16b943f920fbdee1300d38585c17ba60883476 Mon Sep 17 00:00:00 2001 From: Dustin Breese Date: Wed, 15 Nov 2023 15:28:01 -0700 Subject: [PATCH 11/13] Moved constants to workspaceUtils and some cleanup. --- .../createSObjectLwcQuickActions.html | 8 +------- src/commands/wizard/lwcGenerationCommand.ts | 4 ++-- src/utils/codeBuilder.ts | 14 ++++++------- src/utils/workspaceUtils.ts | 20 +++++++++++++++++-- 4 files changed, 27 insertions(+), 19 deletions(-) diff --git a/resources/instructions/createSObjectLwcQuickActions.html b/resources/instructions/createSObjectLwcQuickActions.html index ab2558a..9756cdd 100644 --- a/resources/instructions/createSObjectLwcQuickActions.html +++ b/resources/instructions/createSObjectLwcQuickActions.html @@ -84,18 +84,12 @@

Create sObject LWC Quick Actions

generateQuickActionsButtonElement.addEventListener('click', () => { webviewMessaging.sendMessageRequest( - 'generateLwcQuickActionsButton', + 'generateLwcQuickActions', {}, handleQuickActionStatusResponse ); }); - generateQuickActionsButtonElement.addEventListener('click', () => { - webviewMessaging.sendMessageRequest( - 'generateLwcQuickActionsButton' - ); - }); - continueButtonElement.addEventListener('click', () => { webviewMessaging.sendMessageRequest( 'continueButton' diff --git a/src/commands/wizard/lwcGenerationCommand.ts b/src/commands/wizard/lwcGenerationCommand.ts index b59f325..2a9cebf 100644 --- a/src/commands/wizard/lwcGenerationCommand.ts +++ b/src/commands/wizard/lwcGenerationCommand.ts @@ -51,8 +51,8 @@ export class LwcGenerationCommand { } }, { - type: 'generateLwcQuickActionsButton', - action: async (panel, _data, callback) => { + type: 'generateLwcQuickActions', + action: async (_panel, _data, callback) => { const quickActionStatus = await LwcGenerationCommand.checkForExistingQuickActions(); diff --git a/src/utils/codeBuilder.ts b/src/utils/codeBuilder.ts index 03c960d..f8b02af 100644 --- a/src/utils/codeBuilder.ts +++ b/src/utils/codeBuilder.ts @@ -8,15 +8,13 @@ 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 TEMPLATE_DIR = './resources/templates'; static readonly QUICK_ACTION_TEMPLATE_NAME = 'quickAction.xml'; - static readonly LWC_DESTINATION_DIR = './force-app/main/default/lwc'; - static readonly QA_DESTINATION_DIR = - './force-app/main/default/quickActions'; + static readonly TEMPLATE_FILE_EXTENSIONS = [ 'css', 'html', @@ -122,7 +120,7 @@ export class CodeBuilder { iconName: string | undefined = undefined ) { const templateFilePath = path.join( - CodeBuilder.TEMPLATE_DIR, + WorkspaceUtils.LWC_TEMPLATE_PATH, CodeBuilder.QUICK_ACTION_TEMPLATE_NAME ); const fileContents = this.readFileContents(templateFilePath); @@ -151,7 +149,7 @@ export class CodeBuilder { const destinationFile = `${objectApiName}.${label.toLocaleLowerCase()}.quickAction-meta.xml`; this.writeFileContents( - CodeBuilder.QA_DESTINATION_DIR, + WorkspaceUtils.QUICK_ACTIONS_PATH, destinationFile, newFileContents ); @@ -164,7 +162,7 @@ export class CodeBuilder { ) { CodeBuilder.TEMPLATE_FILE_EXTENSIONS.forEach((extension) => { const templateFilePath = path.join( - CodeBuilder.TEMPLATE_DIR, + WorkspaceUtils.LWC_TEMPLATE_PATH, template, `${template}.${extension}` ); @@ -178,7 +176,7 @@ export class CodeBuilder { // copy to destination directory const destinationDir = path.join( - CodeBuilder.LWC_DESTINATION_DIR, + WorkspaceUtils.LWC_PATH, destinationLwc ); const destinationFile = `${destinationLwc}.${extension}`; 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) { From 2823dad0abbf98a8ae8e7cc19b02801f7cc065e9 Mon Sep 17 00:00:00 2001 From: Dustin Breese Date: Wed, 15 Nov 2023 16:12:37 -0700 Subject: [PATCH 12/13] Changing it so that a single sobject error (ie, Visit) doesnt fail all generation. --- src/commands/wizard/lwcGenerationCommand.ts | 70 +++++++++++---------- 1 file changed, 38 insertions(+), 32 deletions(-) diff --git a/src/commands/wizard/lwcGenerationCommand.ts b/src/commands/wizard/lwcGenerationCommand.ts index 2a9cebf..819fe2d 100644 --- a/src/commands/wizard/lwcGenerationCommand.ts +++ b/src/commands/wizard/lwcGenerationCommand.ts @@ -56,16 +56,15 @@ export class LwcGenerationCommand { const quickActionStatus = await LwcGenerationCommand.checkForExistingQuickActions(); - await LwcGenerationCommand.generateMissingLwcsAndQuickActions( - extensionUri, - quickActionStatus - ); + const newLwcQuickActionStatus = + await LwcGenerationCommand.generateMissingLwcsAndQuickActions( + extensionUri, + quickActionStatus + ); // send back updates so UI can be refreshed if (callback) { - const quickActionStatus = - await LwcGenerationCommand.checkForExistingQuickActions(); - callback(quickActionStatus); + callback(newLwcQuickActionStatus); } } }, @@ -158,36 +157,43 @@ export class LwcGenerationCommand { ): Promise { return new Promise(async (resolve) => { for (const sobject in quickActionStatus.sobjects) { - 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 - ); + 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 - ); + const codeBuilder = new CodeBuilder( + extensionUri, + sobject, + compactLayoutFields + ); - if (!quickActions.view) { - await codeBuilder.generateView(); - } + if (!quickActions.view) { + await codeBuilder.generateView(); + } - if (!quickActions.edit) { - await codeBuilder.generateEdit(); - } + if (!quickActions.edit) { + await codeBuilder.generateEdit(); + } - if (!quickActions.create) { - await codeBuilder.generateCreate(); + if (!quickActions.create) { + await codeBuilder.generateCreate(); + } } + } catch (err) { + console.error( + `Could not generate quick actions for sobject ${sobject}, so skipping`, + err + ); } } From e59258932a0b64259b0a019164cba4e2f190c7d2 Mon Sep 17 00:00:00 2001 From: Dustin Breese Date: Wed, 15 Nov 2023 16:31:41 -0700 Subject: [PATCH 13/13] Updating as optional with undefined default value. --- src/utils/codeBuilder.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/codeBuilder.ts b/src/utils/codeBuilder.ts index f8b02af..798067c 100644 --- a/src/utils/codeBuilder.ts +++ b/src/utils/codeBuilder.ts @@ -117,7 +117,7 @@ export class CodeBuilder { templateVariables: TemplateVariables, label: string, name: string, - iconName: string | undefined = undefined + iconName?: string | undefined ) { const templateFilePath = path.join( WorkspaceUtils.LWC_TEMPLATE_PATH,