LWC Editor using Tooling API
Currently, Salesforce is not allowing the creation or editing of LWC files on the developer
console. To make simple changes to LWC files within salesforce without using
Visual Studio Code.
Below are
the steps I followed to create the component:
- Created a connected app for using
tooling API within the salesforce.
- Created Named credentials with
details from step 1.
- Created Aura enabled methods in Apex
class to get, create or update LWC components.
- Created Future methods to overcome
timeout error while making a call to tooling API.
- Created LWC component to
imperatively call apex methods from step 4 and edit or create the same source.
public class LWCEditor { @AuraEnabled public static String getLWCs() { String query='SELECT+Id,Description,DeveloperName,IsExposed,TargetConfigs+FROM+LightningComponentBundle'; String s = 'services/data/v49.0/tooling/query/?q='+query; HttpRequest req = new HttpRequest(); req.setEndpoint('callout:LWCToolingAPI/'+s); req.setMethod('GET'); req.setHeader('Content-Type', 'application/json'); Http http = new Http(); HTTPResponse res = http.send(req); while (res.getStatusCode() == 302) { req.setEndpoint(res.getHeader('Location')); res = new Http().send(req); } System.debug(res.getBody()); return res.getBody(); } @AuraEnabled public static String getLWCResource(String bundleId) { String query='SELECT+Id,LightningComponentBundleId,Format,FilePath,Source+FROM+LightningComponentResource+WHERE+LightningComponentBundleId=\'' + bundleId + '\''; String s = 'services/data/v49.0/tooling/query/?q='+query; HttpRequest req = new HttpRequest(); req.setEndpoint('callout:LWCToolingAPI/'+s); req.setMethod('GET'); req.setHeader('Content-Type', 'application/json'); Http http = new Http(); HTTPResponse res = http.send(req); while (res.getStatusCode() == 302) { req.setEndpoint(res.getHeader('Location')); res = new Http().send(req); } System.debug(res.getBody()); return res.getBody(); } @AuraEnabled public static String getLWCList() { //Listclass FutureToolClasslwcList = [SELECT Id, DeveloperName, CreatedBy.Name, CreatedDate, LastModifiedDate FROM LightningComponentBundle]; //system.debug('lwcList : '+lwcList.size()); return ''; } @AuraEnabled public static String updateLWCResource(String id,String fpath,String bundleId,String format, String source) { //FutureToolClass.createResource(fpath, bundleId, format, source); FutureToolClass.updateResource(id,fpath, bundleId, format, source); return 'update call sent'; //return ''; } @AuraEnabled public static String createLWCResource(String description,String developerName,String isExposed,String includeCSS,List targets,String sourceHTML,String sourceJS,String sourceMETA) { //FutureToolClass.createResource(fpath, bundleId, format, source); //FutureToolClass.createResource(fpath, bundleId, format, source); FutureToolClass.createResource(description, developerName, isExposed, includeCSS, targets,sourceHTML,sourceJS,sourceMETA); return 'update call sent'; //return ''; } }
global class FutureToolClass { @future(callout=true) public static void createResource(String description,String developerName,String isExposed,String includeCSS,ListLWC component LWC Editor html:targets,String sourceHTML,String sourceJS,String sourceMETA) { system.debug(targets); String sb = 'services/data/v49.0/tooling/sobjects/LightningComponentBundle'; HttpRequest reqSB = new HttpRequest(); String trag = ''; String targForMetafile=''; for(integer i=0;i '; } String targ = '"targets": {"target": ['+trag+']}'; //String targ = '"targets": {"target": ["lightning__RecordPage","lightning__AppPage","lightning__HomePage"]}'; String bodySB = '{"FullName":"'+developerName+'","Metadata":{"apiVersion":48,"description": null,"isExplicitImport": false,"isExposed": '+isExposed+',"masterLabel": "'+developerName+'",'+targ+'}}'; system.debug('body : '+bodySB); reqSB.setEndpoint('callout:LWCToolingAPI/'+sb); reqSB.setMethod('POST'); reqSB.setBody(bodySB); reqSB.setHeader('Content-Type', 'application/json'); Http http = new Http(); HTTPResponse res = http.send(reqSB); system.debug(res.getBody()); //{"id":"0Rb7F000000XzcqSAC","success":true,"errors":[],"warnings":[],"infos":[]} JSONParser parser = JSON.createParser(res.getBody()); System.JSONToken jt; String bundleId; //system.debug(resMc.getBody()); while (parser.nextToken() != null) { if(parser.getCurrentToken()==JSONToken.FIELD_NAME && parser.getText() == 'id'){ jt = parser.nextToken(); } if(parser.getCurrentToken()==JSONToken.VALUE_STRING && jt== parser.getCurrentToken()){ bundleId = parser.getText(); } } String source; String fpath; String format; system.debug('source'); system.debug(source); system.debug('bundleId'); system.debug(bundleId); //create js file //se format format = 'js'; //se format //create FilePath fpath = 'lwc/'+developerName+'/'+developerName+'.'+format; //create FilePath //set Dummy Source source = sourceJS; //source = '"import { LightningElement } from \'lwc\'; \n export default class Empty extends LightningElement { \n }"'; //set Dummy Source String s = 'services/data/v49.0/tooling/sobjects/LightningComponentResource'; HttpRequest reqJS = new HttpRequest(); String body = '{"FilePath": "'+fpath+'","LightningComponentBundleId":"'+bundleId+'","Format":"'+format+'", "Source" : ' + source + '}'; system.debug('body : '+body); reqJS.setEndpoint('callout:LWCToolingAPI/'+s); reqJS.setMethod('POST'); reqJS.setBody(body); reqJS.setHeader('Content-Type', 'application/json'); http = new Http(); res = http.send(reqJS); System.debug(res.getBody()); //create js file // //create html file //create FilePath fpath = 'lwc/'+developerName+'/'+developerName+'.html'; //create FilePath //se format format = 'html'; //se format //set Dummy Source source = sourceHTML; //source = '"\n\n"'; //set Dummy Source s = 'services/data/v49.0/tooling/sobjects/LightningComponentResource'; HttpRequest reqHtml = new HttpRequest(); body = '{"FilePath": "'+fpath+'","LightningComponentBundleId":"'+bundleId+'","Format":"'+format+'", "Source" : ' + source + '}'; system.debug('body : '+body); reqHtml.setEndpoint('callout:LWCToolingAPI/'+s); reqHtml.setMethod('POST'); reqHtml.setBody(body); reqHtml.setHeader('Content-Type', 'application/json'); Http httpH = new Http(); res = httpH.send(reqHtml); System.debug(res.getBody()); //create html file //create meta file //se format format = 'js-meta.xml'; //se format //create FilePath fpath = 'lwc/'+developerName+'/'+developerName+'.'+format; //create FilePath //set Dummy Source source = sourceMETA; //source = '"\n'+targets.get(i)+' \n "'; //set Dummy Source s = 'services/data/v49.0/tooling/sobjects/LightningComponentResource'; HttpRequest reqMeta = new HttpRequest(); body = '{"FilePath": "'+fpath+'","LightningComponentBundleId":"'+bundleId+'","Format":"'+format+'", "Source" : ' + source + '}'; system.debug('body : '+body); reqMeta.setEndpoint('callout:LWCToolingAPI/'+s); reqMeta.setMethod('POST'); reqMeta.setBody(body); reqMeta.setHeader('Content-Type', 'application/json'); http = new Http(); res = http.send(reqMeta); System.debug(res.getBody()); //create meta file if(includeCSS == 'true' || includeCSS == 'TRUE' || includeCSS == 'True'){ //create css file if needed //se format format = 'css'; //se format //create FilePath fpath = 'lwc/'+developerName+'/'+developerName+'.'+format; //create FilePath //set Dummy Source source = '"#dummy css"'; //set Dummy Source s = 'services/data/v49.0/tooling/sobjects/LightningComponentResource'; HttpRequest reqCSS = new HttpRequest(); body = '{"FilePath": "'+fpath+'","LightningComponentBundleId":"'+bundleId+'","Format":"'+format+'", "Source" : ' + source + '}'; system.debug('body : '+body); reqCSS.setEndpoint('callout:LWCToolingAPI/'+s); reqCSS.setMethod('POST'); reqCSS.setBody(body); reqCSS.setHeader('Content-Type', 'application/json'); http = new Http(); res = http.send(reqCSS); System.debug(res.getBody()); //create css file if needed } //return res.getBody(); } @future(callout=true) public static void updateResource(String id,String fpath,String bundleId,String format, String source) { system.debug('source'); system.debug(source); system.debug('bundleId'); system.debug(bundleId); String s = 'services/data/v49.0/tooling/sobjects/LightningComponentResource/'+id; HttpRequest req = new HttpRequest(); String body = '{"Source" : ' + source + '}'; system.debug('body : '+body); req.setEndpoint('callout:LWCToolingAPI/'+s); req.setMethod('PATCH'); req.setBody(body); req.setHeader('Content-Type', 'application/json'); Http http = new Http(); HTTPResponse res = http.send(req); while (res.getStatusCode() == 302) { req.setEndpoint(res.getHeader('Location')); res = new Http().send(req); } System.debug(res.getBody()); //return res.getBody(); } }50 \n'+isExposed+' \n'+targForMetafile+'\n \n
<template> <lightning-card title="LWC Editor"> <lightning-button label="New" slot="actions" variant="brand" onclick={handleNewLWC}></lightning-button> <lightning-button label="Save" slot="actions" variant="success" onclick={handleSaveLWC}></lightning-button> <div class="c-container"> <lightning-layout multiple-rows="true"> <lightning-layout-item padding="around-small" size="12"> <lightning-layout> <lightning-layout-item padding="around-small" size="3"> <div class="page-section page-right"> <h2>Existing Components</h2> <template if:true={listLWCs}> <lightning-accordion class="example-accordion"> <template for:each={listLWCs} for:item="bear" > <lightning-accordion-section name={bear.Id} label={bear.DeveloperName} key={bear.Id} onclick={loadResources}> <template if:true={listResources}> <template for:each={listResources} for:item="lr" > <span key={lr.Id}> <p id={lr.Id} onclick={loadSource}>{lr.FilePath}</p><br/> </span> </template> </template> </lightning-accordion-section> </template> </lightning-accordion> </template> </div> </lightning-layout-item> <lightning-layout-item padding="around-small" size="9"> <div class="page-section page-main"> <h2>Editor</h2><!-- <lightning-formatted-rich-text value={source}> </lightning-formatted-rich-text> 123--> <textarea id="codeContainer" name="codeContainer" rows="15" cols="100" onblur={updateSource}> {source} </textarea><!-- <lightning-input-rich-text value={source} formats={formats}> <lightning-rich-text-toolbar-button-group slot="toolbar" aria-label="Save Button Group"> <lightning-rich-text-toolbar-button icon-name="utility:save" icon-alternative-text="Save" onclick={handleSave}> </lightning-rich-text-toolbar-button> </lightning-rich-text-toolbar-button-group> </lightning-input-rich-text>--> </div> </lightning-layout-item> </lightning-layout> </lightning-layout-item> </lightning-layout> </div> <p slot="footer">Card Footer</p> </lightning-card> <template if:true={isModalOpen}> <section role="dialog" tabindex="-1" aria-labelledby="modal-heading-01" aria-modal="true" aria-describedby="modal-content-id-1" class="slds-modal slds-fade-in-open"> <div class="slds-modal__container"> <!-- Modal/Popup Box LWC header here --> <header class="slds-modal__header"> <button class="slds-button slds-button_icon slds-modal__close slds-button_icon-inverse" title="Close" onclick={closeModal}> <lightning-icon icon-name="utility:close" alternative-text="close" variant="inverse" size="small" ></lightning-icon> <span class="slds-assistive-text">Close</span> </button> <h2 id="modal-heading-01" class="slds-text-heading_medium slds-hyphenate">Modal/PopUp Box header LWC</h2> </header> <!-- Modal/Popup Box LWC body starts here --> <div class="slds-modal__content slds-p-around_medium" id="modal-content-id-1"> <lightning-input type="text" label="Component name" onblur={sCompName}></lightning-input><br/> <lightning-input type="checkbox" label="Include CSS file" name="cssOption" onchange={sCheckboxValue}></lightning-input><br/> <lightning-input type="checkbox" label="Is Exposed?" name="exposedOption" onchange={sCheckboxValue}></lightning-input><br/> <h2 class="header">Target</h2><br/> <lightning-input type="checkbox" label="lightning__HomePage" name="lightning__HomePage" onchange={sCheckboxValue}></lightning-input><br/> <lightning-input type="checkbox" label="lightning__RecordPage" name="lightning__RecordPage" onchange={sCheckboxValue}></lightning-input><br/> <lightning-input type="checkbox" label="lightning__AppPage" name="lightning__AppPage" onchange={sCheckboxValue}></lightning-input><br/> <lightning-input type="checkbox" label="lightning__Tab" name="lightning__Tab" onchange={sCheckboxValue}></lightning-input><br/> </div> <footer class="slds-modal__footer"> <button class="slds-button slds-button_neutral" onclick={closeModal} title="Cancel">Cancel</button> <button class="slds-button slds-button_brand" onclick={submitDetails} title="OK">OK</button> </footer> </div> </section> <div class="slds-backdrop slds-backdrop_open"></div> </template> </template>LWC component LWC Editor JS
1 import { LightningElement } from 'lwc'; import getLWCs from '@salesforce/apex/LWCEditor.getLWCs'; import getResources from '@salesforce/apex/LWCEditor.getLWCResource'; import updateResources from '@salesforce/apex/LWCEditor.updateLWCResource'; import createLWCResource from '@salesforce/apex/LWCEditor.createLWCResource'; export default class LWCEditor extends LightningElement { listLWCs; listResources; error; successMessage; bundleId = ''; resourceId = ''; filePath=''; source = ''; format = ''; isModalOpen = false; targetsList = ''; //new LWC component variables developerName; includeCSS=false; isExposed=false; targets = []; //new LWC component variables formats = ['font', 'size', 'bold', 'italic', 'underline', 'strike', 'list', 'indent', 'align', 'link', 'clean', 'table', 'header', 'color','code']; connectedCallback() { this.loadLWCs(); } loadLWCs() { getLWCs() .then(result => { console.log(result); let parsedValue = JSON.parse(result) this.listLWCs = parsedValue.records; }) .catch(error => { this.error = error; }); } loadResources(event) { if(event.target.name!==undefined){ this.bundleId = event.target.name; getResources({bundleId:event.target.name}) .then(result => { console.log(result); let parsedValue1 = JSON.parse(result) this.listResources = parsedValue1.records; }) .catch(error => { console.log(error); this.error = error; }); } } loadSource(event) { for(let acc of this.listResources){ if(acc.Id === event.target.id.substring(0,18)){ console.log('334455' ); console.log(acc.FilePath ); console.log(acc.Source ); this.resourceId = acc.Id; this.filePath = acc.FilePath; this.source = acc.Source; this.format = acc.Format; }else{ console.log('error'); console.log('else'); } } } handleSaveLWC() { console.log('111222' ); console.log(this.bundleId ); console.log(this.filePath ); console.log('111222' ); updateResources({id:this.resourceId ,fpath:this.filePath,bundleId:this.bundleId,format:this.format, source:JSON.stringify(this.source)}) .then(result => { console.log(result); //let parsedValue1 = JSON.parse(result) //this.listResources = parsedValue1.records; }) .catch(error => { console.log(error); this.error = error; }); } updateSource(event) { console.log('update' ); console.log(event.target.value ); this.source = event.target.value; } sCheckboxValue(event) { console.log('setCheckboxValue' ); console.log(event.target.name ); if(event.target.name === 'cssOption'){ this.includeCSS = true; } if(event.target.name === 'exposedOption'){ this.isExposed = true; } if(event.target.name === 'lightning__HomePage' || event.target.name === 'lightning__RecordPage' || event.target.name === 'lightning__AppPage' || event.target.name === 'lightning__Tab'){ this.targets.push(event.target.name); this.targetsList = this.targetsList + '\n <target>'+event.target.name+'</target>'; } } sCompName(event) { console.log('sCompName' ); this.developerName = event.target.value; } handleNewLWC() { this.isModalOpen = true; } closeModal() { this.isModalOpen = false; } submitDetails() { const d = new Date(); let sourceHTML = '<!-- @description: \n @author: ChangeMeIn@UserSettingsUnder.SFDoc \n @last modified on: '+d.toDateString()+' \n @last modified by: ChangeMeIn@UserSettingsUnder.SFDoc-->\n<template>\n</template>'; let sourceJS = 'import { LightningElement } from \'lwc\'; \n export default class Empty extends LightningElement { \n }'; let sourceMETA = '<?xml version="1.0"?>\n <LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata"> \n <apiVersion>50</apiVersion> \n <isExposed>'+this.isExposed+'</isExposed> \n <targets> '+this.targetsList+'\n </targets> \n </LightningComponentBundle>'; createLWCResource({description:'' ,developerName:this.developerName,isExposed:this.isExposed,includeCSS:this.includeCSS,targets:this.targets,sourceHTML:JSON.stringify(sourceHTML),sourceJS:JSON.stringify(sourceJS),sourceMETA:JSON.stringify(sourceMETA) }) .then(result => { console.log(result); this.successMessage = result; setTimeout(function () { window.location.reload(); }, 5000); //let parsedValue1 = JSON.parse(result) //this.listResources = parsedValue1.records; }) .catch(error => { console.log(error); this.error = error; }); //createLWCResource(String description,String developerName,String isExposed,List<String> targets this.isModalOpen = false; } } 2
No comments: