Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@
{
"files": "*.{cmp,page,component}",
"options": { "parser": "html","printWidth":120 }
},
{
"files": "*.{cls,trigger,apex}",
"options": { "parser": "apex","printWidth":120 }
}
]
}
14 changes: 8 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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!*/
```
Expand All @@ -47,16 +49,16 @@ $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

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.
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
<aura:component implements="force:lightningQuickActionWithoutHeader,force:hasRecordId,force:hasSObjectName">
<aura:handler name="init" value="{!this}" action="{!c.doInit}" />
<c:jsButton aura:id="jsbutton" recordId="{!v.recordId}" cmdtName="{!v.sObjectName}"></c:jsButton>
<c:jsButtonLwc
aura:id="jsbutton"
recordId="{!v.recordId}"
cmdtName="{!v.sObjectName}"
oninitcomplete="{!c.doInit}"
></c:jsButtonLwc>
</aura:component>
Original file line number Diff line number Diff line change
@@ -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();
})
);
Expand Down
31 changes: 31 additions & 0 deletions force-app/main/default/classes/APICallController.cls
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/**
** 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<String, String> headers
) {
HttpRequest req = new HttpRequest();
req.setEndpoint(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);
Map<String, String> respHeaders = new Map<String, String>();
for (String key : resp.getHeaderKeys()) {
respHeaders.put(key, String.valueOf(resp.getHeader(key)));
}
return new HttpResponseWrapper(resp.getBody(), resp.getStatusCode(), respHeaders);
}
}
5 changes: 5 additions & 0 deletions force-app/main/default/classes/APICallController.cls-meta.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>48.0</apiVersion>
<status>Active</status>
</ApexClass>
38 changes: 38 additions & 0 deletions force-app/main/default/classes/APICallControllerTest.cls
Original file line number Diff line number Diff line change
@@ -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<String, String>{ '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;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>48.0</apiVersion>
<status>Active</status>
</ApexClass>
15 changes: 5 additions & 10 deletions force-app/main/default/classes/DynamicSOQLDMLControllerTest.cls
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ public with sharing class DynamicSOQLDMLControllerTest {
insert a;
a.Phone = '432424';
Account[] recordsToUpdate = new List<Account>{ 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);
}
Expand All @@ -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
Expand All @@ -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);
Expand All @@ -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);
}
Expand Down
14 changes: 14 additions & 0 deletions force-app/main/default/classes/HttpResponseWrapper.cls
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
public with sharing class HttpResponseWrapper {
@AuraEnabled
public String body;
@AuraEnabled
public Integer statusCode;
@AuraEnabled
public Map<String, String> headers;

public HttpResponseWrapper(String body, Integer statusCode, Map<String, String> headers) {
this.body = body;
this.statusCode = statusCode;
this.headers = headers;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>48.0</apiVersion>
<status>Active</status>
</ApexClass>
3 changes: 3 additions & 0 deletions force-app/main/default/lwc/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"extends": ["@salesforce/eslint-config-lwc/recommended"]
}
3 changes: 3 additions & 0 deletions force-app/main/default/lwc/httpRequest/httpRequest.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<template>

</template>
50 changes: 50 additions & 0 deletions force-app/main/default/lwc/httpRequest/httpRequest.js
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>48.0</apiVersion>
<isExposed>false</isExposed>
</LightningComponentBundle>
3 changes: 3 additions & 0 deletions force-app/main/default/lwc/jsButtonLwc/jsButtonLwc.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<template>

</template>
Loading