From 1dbdc1fb28d99827c493fc1c53251d02c94e91b8 Mon Sep 17 00:00:00 2001 From: Suraj Pillai <85.suraj@gmail.com> Date: Tue, 2 Jun 2020 23:51:42 -0400 Subject: [PATCH 1/7] WIP: Move jsbutton to LWC --- .../default/lwc/jsButtonLwc/jsButtonLwc.html | 3 + .../default/lwc/jsButtonLwc/jsButtonLwc.js | 62 +++++++++++++++++++ .../lwc/jsButtonLwc/jsButtonLwc.js-meta.xml | 5 ++ 3 files changed, 70 insertions(+) create mode 100644 force-app/main/default/lwc/jsButtonLwc/jsButtonLwc.html create mode 100644 force-app/main/default/lwc/jsButtonLwc/jsButtonLwc.js create mode 100644 force-app/main/default/lwc/jsButtonLwc/jsButtonLwc.js-meta.xml diff --git a/force-app/main/default/lwc/jsButtonLwc/jsButtonLwc.html b/force-app/main/default/lwc/jsButtonLwc/jsButtonLwc.html new file mode 100644 index 0000000..27e0f69 --- /dev/null +++ b/force-app/main/default/lwc/jsButtonLwc/jsButtonLwc.html @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/force-app/main/default/lwc/jsButtonLwc/jsButtonLwc.js b/force-app/main/default/lwc/jsButtonLwc/jsButtonLwc.js new file mode 100644 index 0000000..3ffacea --- /dev/null +++ b/force-app/main/default/lwc/jsButtonLwc/jsButtonLwc.js @@ -0,0 +1,62 @@ +import { LightningElement, api, wire } from "lwc"; +import fetchJSFromCmdt from '@salesforce/apex/DynamicSOQLDMLController.getJSFromCmdt'; +import executeSoql from '@salesforce/apex/DynamicSOQLDMLController.executeSoqlQuery'; +import runDml from '@salesforce/apex/DynamicSOQLDMLController.executeDml'; +import getSObjectType from '@salesforce/apex/DynamicSOQLDMLController.getSObjectTypeFromId'; + +const REGEX_SOQL = "\\|\\|\\s?(select\\s+[^|]+)\\s?\\|\\|"; +const REGEX_UPDATE = "\\|\\|\\s?update\\s([^|;]+);?\\s*\\|\\|"; +const REGEX_INSERT_UPSERT = + "\\|\\|\\s?(insert|upsert)\\s([\\w\\d_]+)\\s?\\(\\s?(\\w+).*\\|\\|"; +const AsyncFunction = Object.getPrototypeOf(async () => {}).constructor; +export default class JsButtonLwc extends LightningElement { + @api js; + @api cmdtName; + @api recordId; + + @api + async invoke() { + if (!this.js && this.cmdtName) { + this.js = await fetchJSFromCmdt({ cmdtName }); + return await this.runJS(); + } else if (this.js) { + return await this.runJS(); + } + throw Error("No script found to execute"); + } + + executeSoql(){ + //TODO: implement + } + + executeDml(){ + //TODO: implement + } + + + runJS() { + //replace consecutive spaces + this.js = this.js.replace(/\s+/g, " "); + + //parse soql + this.js = this.js.replace( + new RegExp(helper.REGEX_SOQL, "gi"), + "await this.executeSoql(cmp,`$1`);" + ); + + //parse updates + this.js = this.js.replace( + new RegExp(helper.REGEX_UPDATE, "gi"), + "await this.executeDml(cmp,'update',$1);" + ); + + //parse inserts + this.js = this.js.replace( + new RegExp(helper.REGEX_INSERT_UPSERT, "gi"), + "await this.executeDml(cmp,'$1',$3,'$2');" + ); + + return await AsyncFunction("recordId", `return ${js}`).bind(this)(recordId); + + } +} diff --git a/force-app/main/default/lwc/jsButtonLwc/jsButtonLwc.js-meta.xml b/force-app/main/default/lwc/jsButtonLwc/jsButtonLwc.js-meta.xml new file mode 100644 index 0000000..605287e --- /dev/null +++ b/force-app/main/default/lwc/jsButtonLwc/jsButtonLwc.js-meta.xml @@ -0,0 +1,5 @@ + + + 48.0 + false + \ No newline at end of file From 64ed5abbb54b8fc776e3ceb221a5eda7fd30dc5c Mon Sep 17 00:00:00 2001 From: Suraj Pillai <85.suraj@gmail.com> Date: Fri, 5 Jun 2020 14:20:43 -0400 Subject: [PATCH 2/7] Add LWC Button --- .../jsButtonQuickAction.cmp | 8 +- .../jsButtonQuickActionController.js | 6 +- force-app/main/default/lwc/.eslintrc.json | 3 + .../default/lwc/jsButtonLwc/jsButtonLwc.js | 95 +++++++++++++------ package-lock.json | 18 ++-- package.json | 11 ++- 6 files changed, 97 insertions(+), 44 deletions(-) create mode 100755 force-app/main/default/lwc/.eslintrc.json diff --git a/force-app/main/default/aura/jsButtonQuickAction/jsButtonQuickAction.cmp b/force-app/main/default/aura/jsButtonQuickAction/jsButtonQuickAction.cmp index 78054e7..28f4c15 100644 --- a/force-app/main/default/aura/jsButtonQuickAction/jsButtonQuickAction.cmp +++ b/force-app/main/default/aura/jsButtonQuickAction/jsButtonQuickAction.cmp @@ -1,4 +1,8 @@ - - + diff --git a/force-app/main/default/aura/jsButtonQuickAction/jsButtonQuickActionController.js b/force-app/main/default/aura/jsButtonQuickAction/jsButtonQuickActionController.js index 7fec0c9..ad9a7bd 100644 --- a/force-app/main/default/aura/jsButtonQuickAction/jsButtonQuickActionController.js +++ b/force-app/main/default/aura/jsButtonQuickAction/jsButtonQuickActionController.js @@ -1,16 +1,16 @@ ({ - doInit: function (component, event, helper) { + doInit: function (component) { component .find("jsbutton") .invoke() .then( - $A.getCallback(() => { + $A.getCallback((resp) => { + console.log('>> resp '+JSON.stringify(resp)); $A.get("e.force:closeQuickAction").fire(); }) ) .catch( $A.getCallback((err) => { - alert("An error occurred " + err); $A.get("e.force:closeQuickAction").fire(); }) ); diff --git a/force-app/main/default/lwc/.eslintrc.json b/force-app/main/default/lwc/.eslintrc.json new file mode 100755 index 0000000..961d192 --- /dev/null +++ b/force-app/main/default/lwc/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": ["@salesforce/eslint-config-lwc/recommended"] +} diff --git a/force-app/main/default/lwc/jsButtonLwc/jsButtonLwc.js b/force-app/main/default/lwc/jsButtonLwc/jsButtonLwc.js index 3ffacea..e3d743f 100644 --- a/force-app/main/default/lwc/jsButtonLwc/jsButtonLwc.js +++ b/force-app/main/default/lwc/jsButtonLwc/jsButtonLwc.js @@ -1,62 +1,99 @@ -import { LightningElement, api, wire } from "lwc"; -import fetchJSFromCmdt from '@salesforce/apex/DynamicSOQLDMLController.getJSFromCmdt'; -import executeSoql from '@salesforce/apex/DynamicSOQLDMLController.executeSoqlQuery'; -import runDml from '@salesforce/apex/DynamicSOQLDMLController.executeDml'; -import getSObjectType from '@salesforce/apex/DynamicSOQLDMLController.getSObjectTypeFromId'; +import { LightningElement, api } from "lwc"; +import fetchJSFromCmdt from "@salesforce/apex/DynamicSOQLDMLController.getJSFromCmdt"; +import executeSoql from "@salesforce/apex/DynamicSOQLDMLController.executeSoqlQuery"; +import executeDml from "@salesforce/apex/DynamicSOQLDMLController.executeDml"; +import getSObjectType from "@salesforce/apex/DynamicSOQLDMLController.getSObjectTypeFromId"; +import { ShowToastEvent } from "lightning/platformShowToastEvent"; const REGEX_SOQL = "\\|\\|\\s?(select\\s+[^|]+)\\s?\\|\\|"; const REGEX_UPDATE = "\\|\\|\\s?update\\s([^|;]+);?\\s*\\|\\|"; const REGEX_INSERT_UPSERT = "\\|\\|\\s?(insert|upsert)\\s([\\w\\d_]+)\\s?\\(\\s?(\\w+).*\\|\\|"; -const AsyncFunction = Object.getPrototypeOf(async () => {}).constructor; + export default class JsButtonLwc extends LightningElement { @api js; @api cmdtName; @api recordId; + _notifiedParent = false; + + renderedCallback() { + if (!this._notifiedParent) + this.dispatchEvent(new CustomEvent("initcomplete")); + } @api async invoke() { if (!this.js && this.cmdtName) { - this.js = await fetchJSFromCmdt({ cmdtName }); - return await this.runJS(); - } else if (this.js) { - return await this.runJS(); + let js = await fetchJSFromCmdt({ cmdtName: this.cmdtName }); + await this.runJS(js); + }else if(this.js){ + await this.runJS(this.js) } - throw Error("No script found to execute"); } - executeSoql(){ - //TODO: implement + _showError(message) { + this.dispatchEvent(new ShowToastEvent({ message, variant: "error" })); } - executeDml(){ - //TODO: implement + async executeSoql(query) { + try { + let results = await executeSoql({ query }); + return results; + } catch (err) { + this._showError(err); + } + return null; } + async executeDml(dmlType, records, sObjectType) { + try { + if(records && !Array.isArray(records)){ + records = [records]; + } + if (!sObjectType) + sObjectType = await getSObjectType({ recordId: records[0].Id }); + records = records.map((rec) => ({ + ...rec, + attributes: { type: sObjectType } + })); + let results = executeDml({ + operation: dmlType, + strData: sObjectType + ? JSON.stringify(records, (k, v) => { + return typeof v === "number" ? "" + v : v; + }) + : null, + sObjectType + }); + return results; + } catch (err) { + this._showError(err); + } + return null; + } - runJS() { + async runJS(js) { //replace consecutive spaces - this.js = this.js.replace(/\s+/g, " "); + js = js.replace(/\s+/g, " "); //parse soql - this.js = this.js.replace( - new RegExp(helper.REGEX_SOQL, "gi"), - "await this.executeSoql(cmp,`$1`);" + js = js.replace( + new RegExp(REGEX_SOQL, "gi"), + "await this.executeSoql(`$1`);" ); //parse updates - this.js = this.js.replace( - new RegExp(helper.REGEX_UPDATE, "gi"), - "await this.executeDml(cmp,'update',$1);" + js = js.replace( + new RegExp(REGEX_UPDATE, "gi"), + "await this.executeDml('update',$1);" ); //parse inserts - this.js = this.js.replace( - new RegExp(helper.REGEX_INSERT_UPSERT, "gi"), - "await this.executeDml(cmp,'$1',$3,'$2');" + js = js.replace( + new RegExp(REGEX_INSERT_UPSERT, "gi"), + "await this.executeDml('$1',$3,'$2');" ); - - return await AsyncFunction("recordId", `return ${js}`).bind(this)(recordId); - + let op = await (Function("recordId", `return (async ()=>{${js}})()`).bind(this))(this.recordId); + return op; } } diff --git a/package-lock.json b/package-lock.json index b74db00..535a2d5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2971,9 +2971,9 @@ "dev": true }, "eslint-scope": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.0.0.tgz", - "integrity": "sha512-oYrhJW7S0bxAFDvWqzvMPRm6pcgcnWc4QnofCAqRTRfQC0JcwenzGglTtsLyIuuWFfkqDG9vz67cnttSd53djw==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.0.tgz", + "integrity": "sha512-iiGRvtxWqgtx5m8EyQUJihBloE4EnYeGE/bz1wSPwJE6tZuJUtHlhqDM4Xj2ukE8Dyy1+HCZ4hE0fzIVMzb58w==", "dev": true, "requires": { "esrecurse": "^4.1.0", @@ -2990,9 +2990,9 @@ } }, "eslint-visitor-keys": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.1.0.tgz", - "integrity": "sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.2.0.tgz", + "integrity": "sha512-WFb4ihckKil6hu3Dp798xdzSfddwKKU3+nGniKF6HfeW6OLd2OUDEPP7TcHtB5+QXOKg2s6B2DaMPE1Nn/kxKQ==", "dev": true }, "espree": { @@ -9389,9 +9389,9 @@ "dev": true }, "v8-compile-cache": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.1.0.tgz", - "integrity": "sha512-usZBT3PW+LOjM25wbqIlZwPeJV+3OSz3M1k1Ws8snlW39dZyYL9lOGC5FgPVHfk0jKmjiDV8Z0mIbVQPiwFs7g==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.1.1.tgz", + "integrity": "sha512-8OQ9CL+VWyt3JStj7HX7/ciTL2V3Rl1Wf5OL+SNTm0yK1KvtReVulksyeRnCANHHuUxHlQig+JJDlUhBt1NQDQ==", "dev": true }, "validate-npm-package-license": { diff --git a/package.json b/package.json index 7a1eda9..2d2efe4 100644 --- a/package.json +++ b/package.json @@ -21,5 +21,14 @@ "eslint": "^6.8.0", "prettier": "^2.0.5", "prettier-plugin-apex": "^1.4.0" - } + }, + "main": ".eslintrc.js", + "dependencies": {}, + "repository": { + "type": "git", + "url": "git@suraj.github.com:surajp/lightning-js-button.git" + }, + "keywords": [], + "author": "", + "license": "ISC" } From e1979f442093705e30b4089491b22899b51e6eab Mon Sep 17 00:00:00 2001 From: Suraj Pillai <85.suraj@gmail.com> Date: Sun, 7 Jun 2020 00:19:46 -0400 Subject: [PATCH 3/7] Add ability to make callouts through apex Users may leverage named credentials to hit secure endpoints --- .prettierrc | 4 ++ .../default/classes/APICallController.cls | 33 ++++++++++++ .../classes/APICallController.cls-meta.xml | 5 ++ .../classes/DynamicSOQLDMLControllerTest.cls | 15 ++---- .../default/classes/HttpResponseWrapper.cls | 14 ++++++ .../classes/HttpResponseWrapper.cls-meta.xml | 5 ++ .../default/lwc/httpRequest/httpRequest.html | 3 ++ .../default/lwc/httpRequest/httpRequest.js | 50 +++++++++++++++++++ .../lwc/httpRequest/httpRequest.js-meta.xml | 5 ++ .../default/lwc/jsButtonLwc/jsButtonLwc.js | 13 +++-- 10 files changed, 133 insertions(+), 14 deletions(-) create mode 100644 force-app/main/default/classes/APICallController.cls create mode 100644 force-app/main/default/classes/APICallController.cls-meta.xml create mode 100644 force-app/main/default/classes/HttpResponseWrapper.cls create mode 100644 force-app/main/default/classes/HttpResponseWrapper.cls-meta.xml create mode 100644 force-app/main/default/lwc/httpRequest/httpRequest.html create mode 100644 force-app/main/default/lwc/httpRequest/httpRequest.js create mode 100644 force-app/main/default/lwc/httpRequest/httpRequest.js-meta.xml diff --git a/.prettierrc b/.prettierrc index c44bc43..dc50104 100755 --- a/.prettierrc +++ b/.prettierrc @@ -8,6 +8,10 @@ { "files": "*.{cmp,page,component}", "options": { "parser": "html","printWidth":120 } + }, + { + "files": "*.{cls,trigger,apex}", + "options": { "parser": "apex","printWidth":120 } } ] } diff --git a/force-app/main/default/classes/APICallController.cls b/force-app/main/default/classes/APICallController.cls new file mode 100644 index 0000000..c2b0342 --- /dev/null +++ b/force-app/main/default/classes/APICallController.cls @@ -0,0 +1,33 @@ +/** + ** description: Controller for making api calls and sending the response back + **/ + +public with sharing class APICallController { + @AuraEnabled + public static HttpResponseWrapper makeApiCall( + String endPoint, + String method, + String bodyStr, + Map headers + ) { + HttpRequest req = new HttpRequest(); + req.setEndpoint(endPoint); + system.debug('endpoint ' + endpoint); + req.setMethod(method); + if (method != 'GET') { + req.setBody(bodyStr); + } + if (headers != null) { + for (String key : headers.keySet()) { + req.setHeader(key, headers.get(key)); + } + } + HttpResponse resp = new Http().send(req); + system.debug('response ' + resp.getBody()); + Map respHeaders = new Map(); + for (String key : resp.getHeaderKeys()) { + respHeaders.put(key, String.valueOf(resp.getHeader(key))); + } + return new HttpResponseWrapper(resp.getBody(), resp.getStatusCode(), respHeaders); + } +} diff --git a/force-app/main/default/classes/APICallController.cls-meta.xml b/force-app/main/default/classes/APICallController.cls-meta.xml new file mode 100644 index 0000000..db9bf8c --- /dev/null +++ b/force-app/main/default/classes/APICallController.cls-meta.xml @@ -0,0 +1,5 @@ + + + 48.0 + Active + diff --git a/force-app/main/default/classes/DynamicSOQLDMLControllerTest.cls b/force-app/main/default/classes/DynamicSOQLDMLControllerTest.cls index 9cda885..129c387 100644 --- a/force-app/main/default/classes/DynamicSOQLDMLControllerTest.cls +++ b/force-app/main/default/classes/DynamicSOQLDMLControllerTest.cls @@ -6,7 +6,7 @@ public with sharing class DynamicSOQLDMLControllerTest { insert a; a.Phone = '432424'; Account[] recordsToUpdate = new List{ a }; - DynamicSOQLDMLController.executeDml('update', recordsToUpdate, null, null); + DynamicSOQLDMLController.executeDml('update', JSON.serialize(recordsToUpdate), 'Account'); a = [SELECT Phone FROM Account WHERE Id = :a.Id]; System.assertEquals('432424', a.Phone); } @@ -16,11 +16,8 @@ public with sharing class DynamicSOQLDMLControllerTest { // we won't test fetching cmdt DynamicSOQLDMLController.getJSFromCmdt('Account'); String acctString = '[{"attributes":{"type":"Account"},"Name":"Test Account"}]'; - DynamicSOQLDMLController.executeDml('insert', null, acctString, 'Account'); - System.assertEquals( - 1, - [SELECT ID FROM Account WHERE Name = 'Test Account'].size() - ); + DynamicSOQLDMLController.executeDml('insert', acctString, 'Account'); + System.assertEquals(1, [SELECT ID FROM Account WHERE Name = 'Test Account'].size()); } @isTest @@ -34,7 +31,7 @@ public with sharing class DynamicSOQLDMLControllerTest { accountsToUpdate.add(a1); String acctString = JSON.serialize(accountsToUpdate); - DynamicSOQLDMLController.executeDml('upsert', null, acctString, 'Account'); + DynamicSOQLDMLController.executeDml('upsert', acctString, 'Account'); System.assertEquals(2, [SELECT ID FROM Account].size()); a = [SELECT Phone FROM Account WHERE Id = :a.Id]; System.assertEquals('432343', a.Phone); @@ -44,9 +41,7 @@ public with sharing class DynamicSOQLDMLControllerTest { public static void testSoql() { Account a = new Account(Name = 'Test Account'); insert a; - Account[] acctsResult = DynamicSOQLDMLController.executeSoqlQuery( - 'Select Name from Account' - ); + Account[] acctsResult = DynamicSOQLDMLController.executeSoqlQuery('Select Name from Account'); System.assertEquals(1, acctsResult.size()); System.assertEquals('Test Account', acctsResult[0].Name); } diff --git a/force-app/main/default/classes/HttpResponseWrapper.cls b/force-app/main/default/classes/HttpResponseWrapper.cls new file mode 100644 index 0000000..df36a1a --- /dev/null +++ b/force-app/main/default/classes/HttpResponseWrapper.cls @@ -0,0 +1,14 @@ +public with sharing class HttpResponseWrapper { + @AuraEnabled + public String body; + @AuraEnabled + public Integer statusCode; + @AuraEnabled + public Map headers; + + public HttpResponseWrapper(String body, Integer statusCode, Map headers) { + this.body = body; + this.statusCode = statusCode; + this.headers = headers; + } +} diff --git a/force-app/main/default/classes/HttpResponseWrapper.cls-meta.xml b/force-app/main/default/classes/HttpResponseWrapper.cls-meta.xml new file mode 100644 index 0000000..db9bf8c --- /dev/null +++ b/force-app/main/default/classes/HttpResponseWrapper.cls-meta.xml @@ -0,0 +1,5 @@ + + + 48.0 + Active + diff --git a/force-app/main/default/lwc/httpRequest/httpRequest.html b/force-app/main/default/lwc/httpRequest/httpRequest.html new file mode 100644 index 0000000..27e0f69 --- /dev/null +++ b/force-app/main/default/lwc/httpRequest/httpRequest.html @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/force-app/main/default/lwc/httpRequest/httpRequest.js b/force-app/main/default/lwc/httpRequest/httpRequest.js new file mode 100644 index 0000000..78e9aad --- /dev/null +++ b/force-app/main/default/lwc/httpRequest/httpRequest.js @@ -0,0 +1,50 @@ +import { api } from "lwc"; +import makeApiCall from "@salesforce/apex/APICallController.makeApiCall"; + +export default class HttpRequest { + endPoint = ""; + method = "GET"; + body = null; + headers = {}; + + @api + setEndpoint(val) { + this.endPoint = val; + } + + @api + setMethod(val) { + this.method = val; + } + + @api + setBody(val) { + this.body = val; + } + + @api + addHeader(key, value) { + if (typeof value !== "string") + throw "You may only set string values for headers"; + this.headers[key] = value; + } + + @api + clear() { + this.endPoint = ""; + this.method = "GET"; + this.body = null; + this.headers = {}; + } + + @api + async send() { + let resp = await makeApiCall({ + endPoint: this.endPoint, + method: this.method, + bodyStr: this.body ? JSON.stringify(this.body) : "", + headers: this.headers + }); + return resp; + } +} diff --git a/force-app/main/default/lwc/httpRequest/httpRequest.js-meta.xml b/force-app/main/default/lwc/httpRequest/httpRequest.js-meta.xml new file mode 100644 index 0000000..605287e --- /dev/null +++ b/force-app/main/default/lwc/httpRequest/httpRequest.js-meta.xml @@ -0,0 +1,5 @@ + + + 48.0 + false + \ No newline at end of file diff --git a/force-app/main/default/lwc/jsButtonLwc/jsButtonLwc.js b/force-app/main/default/lwc/jsButtonLwc/jsButtonLwc.js index e3d743f..5307039 100644 --- a/force-app/main/default/lwc/jsButtonLwc/jsButtonLwc.js +++ b/force-app/main/default/lwc/jsButtonLwc/jsButtonLwc.js @@ -4,6 +4,7 @@ import executeSoql from "@salesforce/apex/DynamicSOQLDMLController.executeSoqlQu import executeDml from "@salesforce/apex/DynamicSOQLDMLController.executeDml"; import getSObjectType from "@salesforce/apex/DynamicSOQLDMLController.getSObjectTypeFromId"; import { ShowToastEvent } from "lightning/platformShowToastEvent"; +import HttpRequest from "c/httpRequest"; const REGEX_SOQL = "\\|\\|\\s?(select\\s+[^|]+)\\s?\\|\\|"; const REGEX_UPDATE = "\\|\\|\\s?update\\s([^|;]+);?\\s*\\|\\|"; @@ -16,6 +17,8 @@ export default class JsButtonLwc extends LightningElement { @api recordId; _notifiedParent = false; + httpRequest = new HttpRequest(); + renderedCallback() { if (!this._notifiedParent) this.dispatchEvent(new CustomEvent("initcomplete")); @@ -26,8 +29,8 @@ export default class JsButtonLwc extends LightningElement { if (!this.js && this.cmdtName) { let js = await fetchJSFromCmdt({ cmdtName: this.cmdtName }); await this.runJS(js); - }else if(this.js){ - await this.runJS(this.js) + } else if (this.js) { + await this.runJS(this.js); } } @@ -47,7 +50,7 @@ export default class JsButtonLwc extends LightningElement { async executeDml(dmlType, records, sObjectType) { try { - if(records && !Array.isArray(records)){ + if (records && !Array.isArray(records)) { records = [records]; } if (!sObjectType) @@ -93,7 +96,9 @@ export default class JsButtonLwc extends LightningElement { new RegExp(REGEX_INSERT_UPSERT, "gi"), "await this.executeDml('$1',$3,'$2');" ); - let op = await (Function("recordId", `return (async ()=>{${js}})()`).bind(this))(this.recordId); + let op = await Function("recordId", `return (async ()=>{${js}})()`).bind( + this + )(this.recordId); return op; } } From cdae809e24956510ce148974f3ad6ce53eb1a8c7 Mon Sep 17 00:00:00 2001 From: Suraj Pillai <85.suraj@gmail.com> Date: Sun, 7 Jun 2020 02:51:11 -0400 Subject: [PATCH 4/7] Add sample scripts --- README.md | 7 +- scripts/jsButton/README.md | 4 + scripts/jsButton/createNewJSButton.js | 88 +++++++++++++++++++ .../jsButton/deleteInactiveFlowVersions.js | 25 ++++++ 4 files changed, 122 insertions(+), 2 deletions(-) create mode 100644 scripts/jsButton/README.md create mode 100644 scripts/jsButton/createNewJSButton.js create mode 100644 scripts/jsButton/deleteInactiveFlowVersions.js diff --git a/README.md b/README.md index 0e8c239..683638c 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,9 @@ alert(Array(5).fill(0).map((e,i)=>'Hello, '+i)); ```javascript let accts=|| Select Name,(Select Id from Contacts) from Account order by createddate desc limit 100 ||; -let contacts = accts.filter((a)=>!a.Contacts || a.Contacts.length===0).slice(0,10).map((a)=>({LastName: a.Name+'-Contact', AccountId: a.Id})); +let contacts = accts.filter((a)=>!a.Contacts || a.Contacts.length===0) + .slice(0,10) + .map((a)=>({LastName: a.Name+'-Contact', AccountId: a.Id})); let contactIds = || insert Contact(contacts) ||; /*Note how the SObjectType has been specified. This is required for insert and upsert*/ $A.get('e.force:refreshView').fire(); /* $A is supported!*/ ``` @@ -47,13 +49,14 @@ $A.get('e.force:refreshView').fire(); * Upsert and Update statements must be qualified with the SObjectType thus `|| insert Account(accts) ||;` * SOQL statements are parsed using template literals. Any arguments should follow the appropriate syntax `${argument}` * SOQL and DML statements may not be wrapped in a function. +* All statements must be strictly terminated by a semicolon. ### Known Limitations * Support for delete has been intentionally withheld. * Single-line comments are not supported. * Haven't tested DML with date, datetime, boolean, geolocation and other compound fields. I will update this section as I do so. -* Explicit use of async/await, Promises and Generators is not supported, atm. +* SOQL and DML statements should be enclosed in async functions, if they are required to be contained in functions. The program automatically adds `await` to SOQL and DML statements * DML on Files, Attachments, Documents, etc. is not supported ### For Developers: Extending to more than one Button per SObjectType diff --git a/scripts/jsButton/README.md b/scripts/jsButton/README.md new file mode 100644 index 0000000..dcc8122 --- /dev/null +++ b/scripts/jsButton/README.md @@ -0,0 +1,4 @@ +## These scripts assume you've a named credential named 'salesforce' created for your instance. The detailed steps for doing so are below: + +* Create an Auth Provider of type 'Salesforce'. You can either create your own Connected App or use the following consumer key and secret. The scopes should include `api` and `refresh_token` +* Create a named credential using the Auth Provider above and name it `salesforce`. The authentication type should be `OAuth`. For better security, I suggest you used `Per User` authentication so only authorized users can use the Salesforce APIs through the button diff --git a/scripts/jsButton/createNewJSButton.js b/scripts/jsButton/createNewJSButton.js new file mode 100644 index 0000000..d19767d --- /dev/null +++ b/scripts/jsButton/createNewJSButton.js @@ -0,0 +1,88 @@ +try { + let cmpName = prompt( + "Enter the name for your aura bundle. This will be the name of the custom metadata record backing this bundle as well" + ); + + if (!cmpName) return; + + this.httpRequest.setEndpoint( + "callout:salesforce/services/data/v48.0/tooling/sobjects/AuraDefinitionBundle/" + ); + + this.httpRequest.setMethod("POST"); + + this.httpRequest.addHeader("Content-Type", "application/json"); + + this.httpRequest.setBody({ + MasterLabel: cmpName, + Description: "created by js button", + ApiVersion: 48.0, + DeveloperName: cmpName + }); + + let resp = await this.httpRequest.send(); + + let auraBundleId = JSON.parse(resp.body).id; + + alert(auraBundleId); + + this.httpRequest.clear(); + + this.httpRequest.setEndpoint( + "callout:salesforce/services/data/v48.0/tooling/sobjects/AuraDefinition/" + ); + + this.httpRequest.setMethod("POST"); + this.httpRequest.addHeader("Content-Type", "application/json"); + + let source = ` + + `; + + this.httpRequest.setBody({ + AuraDefinitionBundleId: auraBundleId, + DefType: "COMPONENT", + Format: "XML", + Source: source + }); + + resp = await this.httpRequest.send(); + + alert(resp.statusCode); + + source = ` ({ + doInit: function (component) { + component + .find("jsbutton") + .invoke() + .then( + $A.getCallback((resp) => { + $A.get("e.force:closeQuickAction").fire(); + }) + ) + .catch( + $A.getCallback((err) => { + $A.get("e.force:closeQuickAction").fire(); + }) + ); + } + });`; + + this.httpRequest.setBody({ + AuraDefinitionBundleId: auraBundleId, + DefType: "CONTROLLER", + Format: "JS", + Source: source + }); + + resp = await this.httpRequest.send(); + + alert(resp.statusCode); +} catch (e) { + alert(JSON.stringify(e)); +} diff --git a/scripts/jsButton/deleteInactiveFlowVersions.js b/scripts/jsButton/deleteInactiveFlowVersions.js new file mode 100644 index 0000000..b782acd --- /dev/null +++ b/scripts/jsButton/deleteInactiveFlowVersions.js @@ -0,0 +1,25 @@ +try { + this.httpRequest.setEndpoint( + "callout:salesforce/services/data/v48.0/tooling/query/?q=Select+Id,FullName+from+Flow+where+status+!=+'Active'" + ); + + let resp = await this.httpRequest.send(); + let respJson = JSON.parse(resp.body); + + let results = await Promise.all( + respJson.records.map(async (rec) => { + this.httpRequest.clear(); + let flowId = rec.Id; + this.httpRequest.setEndpoint( + "callout:salesforce/services/data/v48.0/tooling/sobjects/Flow/" + flowId + "/" + ); + this.httpRequest.setMethod("DELETE"); + resp = await this.httpRequest.send(); + return resp.statusCode; + }) + ); + + alert(results); +} catch (e) { + alert(JSON.stringify(e)); +} From 8b131ebae1a9fb15f5c22d123da984eaa1deb93f Mon Sep 17 00:00:00 2001 From: Suraj Pillai <85.suraj@gmail.com> Date: Sun, 7 Jun 2020 01:28:56 -0400 Subject: [PATCH 5/7] Add scripts --- scripts/jsButton/addDefaultOpportunityLineItems.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 scripts/jsButton/addDefaultOpportunityLineItems.js diff --git a/scripts/jsButton/addDefaultOpportunityLineItems.js b/scripts/jsButton/addDefaultOpportunityLineItems.js new file mode 100644 index 0000000..da8ec65 --- /dev/null +++ b/scripts/jsButton/addDefaultOpportunityLineItems.js @@ -0,0 +1,14 @@ +let family=prompt('What family of products would you like to add?'); +if(!family) + return; +try{ + let items = || select Id,UnitPrice from PricebookEntry where Pricebook2.Name='Standard Price Book' and IsActive=true and Product2.Family='${family}' ||; + if(!items || items.length===0){ + alert('No Products found'); + } + let oli = items.map(item => ({PriceBookEntryId: item.Id, OpportunityId: recordId, Quantity: 1, ListPrice: item.UnitPrice, TotalPrice: item.UnitPrice})); + || insert OpportunityLineItem(oli) ||; + $A.get('e.force:refreshView').fire(); +}catch(e){ + alert(JSON.stringify(e)); +} From a5006a5e10a498650baf4c80f2e3ce85b16935bd Mon Sep 17 00:00:00 2001 From: Suraj Pillai <85.suraj@gmail.com> Date: Sun, 7 Jun 2020 02:01:32 -0400 Subject: [PATCH 6/7] Update Readme --- README.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 683638c..7be5888 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Pure JS Buttons in Lightning -JS buttons are back in Lightning! (For now, at least) And they are even more powerful than JS buttons in classic, in some respects. SOQL and DML statements supported! +JS buttons are back in Lightning! (For now, at least) And they are even more powerful than JS buttons in classic. Run SOQL and DML statements seamlessly. Make callouts to APIs, including Salesforce APIs using named credentials directly from JavaScript! This would allow you to build buttons that do amazing things, just using JavaScript. Check out the `scripts` folder for examples. Feel free to raise a PR to contribute your own scripts. ### The Setup @@ -59,7 +59,6 @@ $A.get('e.force:refreshView').fire(); * SOQL and DML statements should be enclosed in async functions, if they are required to be contained in functions. The program automatically adds `await` to SOQL and DML statements * DML on Files, Attachments, Documents, etc. is not supported -### For Developers: Extending to more than one Button per SObjectType - -If you need more than one button on an SObjectType, you may create a lightning component quickAction with the name of the custom metadata record containing your JS passed in to the `jsButton` child component. You will also need to implement an `init` method to invoke the controller method in `jsButton`. Refer to the `jsButtonQuickAction` component for implementation details +### Using Salesforce (and other) APIs in your script +You can use any of Salesforce's APIs (REST, Tooling, Metadata) by setting up a named credential for your own Salesforce instance. This allows you to write scripts for admins to perform tasks like [deleting inactive versions of flows](scripts/jsButton/deleteInactiveFlowVersions.js), or [creating new JS Buttons](scripts/jsButton/createNewJSButton.js)! You can also use named credentials to interact with other APIs as well, of course. Although, for Public APIs, you can just use `fetch` directly. The Salesforce named credential set up would need to have the following scopes (api refresh_token offline_access web). You would need to set up your own Connected App and a Salesforce Auth. Provider that uses this connected app. From ba1a9b08dd79cc5713bd209c73ebba7fe315c425 Mon Sep 17 00:00:00 2001 From: Suraj Pillai <85.suraj@gmail.com> Date: Sun, 7 Jun 2020 04:09:32 -0400 Subject: [PATCH 7/7] Add test class for APICallController --- .../default/classes/APICallController.cls | 2 - .../default/classes/APICallControllerTest.cls | 38 +++++++++++++++++++ .../APICallControllerTest.cls-meta.xml | 5 +++ 3 files changed, 43 insertions(+), 2 deletions(-) create mode 100644 force-app/main/default/classes/APICallControllerTest.cls create mode 100644 force-app/main/default/classes/APICallControllerTest.cls-meta.xml diff --git a/force-app/main/default/classes/APICallController.cls b/force-app/main/default/classes/APICallController.cls index c2b0342..c95d9f8 100644 --- a/force-app/main/default/classes/APICallController.cls +++ b/force-app/main/default/classes/APICallController.cls @@ -12,7 +12,6 @@ public with sharing class APICallController { ) { HttpRequest req = new HttpRequest(); req.setEndpoint(endPoint); - system.debug('endpoint ' + endpoint); req.setMethod(method); if (method != 'GET') { req.setBody(bodyStr); @@ -23,7 +22,6 @@ public with sharing class APICallController { } } HttpResponse resp = new Http().send(req); - system.debug('response ' + resp.getBody()); Map respHeaders = new Map(); for (String key : resp.getHeaderKeys()) { respHeaders.put(key, String.valueOf(resp.getHeader(key))); diff --git a/force-app/main/default/classes/APICallControllerTest.cls b/force-app/main/default/classes/APICallControllerTest.cls new file mode 100644 index 0000000..74f3f40 --- /dev/null +++ b/force-app/main/default/classes/APICallControllerTest.cls @@ -0,0 +1,38 @@ +@isTest +public with sharing class APICallControllerTest { + @isTest + public static void testAPICall() { + Test.setMock(HttpCalloutMock.class, new APICallMock()); + HttpResponseWrapper resp = APICallController.makeApiCall( + 'https://api.example.com', + 'POST', + '{"message":"sample_request"}', + new Map{ 'Accept' => 'application/json', 'Content-Type' => 'application/json' } + ); + system.assertEquals('{"message": "sample response"}', resp.body, 'Unexpected Response'); + system.assertEquals(200, resp.statusCode, 'Incorrect value for status code'); + system.assertEquals(2, resp.headers.size(), 'Mismatch in the number of response headers expected'); + system.assertEquals('sample_value1', resp.headers.get('custom_header1'), 'Incorrect value for first header'); + system.assertEquals('sample_value2', resp.headers.get('custom_header2'), 'Incorrect value for second header'); + } + + class APICallMock implements HttpCalloutMock { + public HttpResponse respond(HttpRequest req) { + HttpResponse resp = new HttpResponse(); + if ( + req.getBody() == '{"message":"sample_request"}' && + req.getHeader('Accept') == 'application/json' && + req.getHeader('Content-Type') == 'application/json' + ) { + resp.setBody('{"message": "sample response"}'); + resp.setHeader('custom_header1', 'sample_value1'); + resp.setHeader('custom_header2', 'sample_value2'); + resp.setStatusCode(200); + } else { + resp.setStatusCode(400); + resp.setBody('{"message":"Bad Request"}'); + } + return resp; + } + } +} diff --git a/force-app/main/default/classes/APICallControllerTest.cls-meta.xml b/force-app/main/default/classes/APICallControllerTest.cls-meta.xml new file mode 100644 index 0000000..db9bf8c --- /dev/null +++ b/force-app/main/default/classes/APICallControllerTest.cls-meta.xml @@ -0,0 +1,5 @@ + + + 48.0 + Active +