diff --git a/README.md b/README.md
index e101a39..3ebd4f6 100644
--- a/README.md
+++ b/README.md
@@ -20,12 +20,15 @@ No **development knowledge** required :x:
Just [log in](https://sfdc-happy-soup.herokuapp.com/) and start sipping the soup! :stew: :clap: :white_check_mark:
+[Watch full demo](https://www.youtube.com/watch?v=2asljhebqlY&t=6s)
-[Watch full demo](https://www.youtube.com/watch?v=NH4LGbdYaaE)
+
+
+
## Contents
@@ -191,6 +194,8 @@ Salesforce Happy Soup is built on top of the `MetadataComponentDependency` tooli
* Lookup filters are returned with cryptic names depending on whether they belong to a custom object or a standard one.
+* The app will tell you if a field is used in an apex class in read or write mode. For example, if a field is used in an assignment expression, then you know the class is assigning values to that field. The app will show you this with a visual indicator; something that the raw API cannot do.
+
As said above, Salesforce Happy Soup has **fixed all** this issues so that you can focus on learning about your dependencies rather than fighting the API! :facepunch:
[Back to top](#salesforce-happy-soup)
diff --git a/public/css/main.css b/public/css/main.css
index c80caaa..e80b8e7 100644
--- a/public/css/main.css
+++ b/public/css/main.css
@@ -455,4 +455,24 @@ select {
.metadata-info .prop{
font-weight: bold;
+}
+
+
+.class-info{
+ border: 1px solid white;
+ border-radius: 50px;
+ padding-left: 5px;
+ padding-right: 5px;
+ margin-left: 5px;
+ padding-top: 0px;
+ padding-bottom: 1px;
+ font-size: 11px;
+}
+
+.write{
+ background-color: #d63031;
+}
+
+.read{
+ background-color: #49b677;
}
\ No newline at end of file
diff --git a/public/index.html b/public/index.html
index 28269c2..95fb863 100644
--- a/public/index.html
+++ b/public/index.html
@@ -60,7 +60,7 @@
Happy Soup
-
diff --git a/public/js/tree.js b/public/js/tree.js
index cafc3ca..9743f73 100644
--- a/public/js/tree.js
+++ b/public/js/tree.js
@@ -80,6 +80,10 @@ function createTreeNodes(refs,parentNode){
if(member.namespace){
memberNodeName.appendChild(createWarningIcon());
}
+
+ if(member.fieldMode){
+ memberNodeName.appendChild(createPill(member.fieldMode));
+ }
memberNames.appendChild(memberNodeName);
metadataTypeNode.appendChild(memberNames);
@@ -102,6 +106,13 @@ function createTreeNodes(refs,parentNode){
return warningIcon;
}
+ function createPill(text){
+ let pill = document.createElement('span');
+ pill.classList.add(...['class-info',text]);
+ pill.innerText = text;
+ return pill;
+ }
+
export const treeApi = {
createDependencyTree,createUsageTree
};
\ No newline at end of file
diff --git a/sfdc_apis/usage.js b/sfdc_apis/usage.js
index 52d1a69..49803d8 100644
--- a/sfdc_apis/usage.js
+++ b/sfdc_apis/usage.js
@@ -64,6 +64,7 @@ function usageApi(connection,entryPoint,cache){
let customFields = [];
let layouts = [];
let lookupFilters = [];
+ let apexClasses = [];
let otherMetadata = [];
metadataArray.forEach(metadata => {
@@ -84,6 +85,9 @@ function usageApi(connection,entryPoint,cache){
else if(type == 'LOOKUPFILTER'){
lookupFilters.push(metadata);
}
+ else if(type == 'APEXCLASS' && entryPoint.type == 'CustomField'){
+ apexClasses.push(metadata);
+ }
else{
otherMetadata.push(metadata);
}
@@ -101,16 +105,72 @@ function usageApi(connection,entryPoint,cache){
if(lookupFilters.length){
lookupFilters = await getLookupFilterDetails(lookupFilters);
}
+ if(apexClasses.length){
+ apexClasses = await getFieldUsageMode(apexClasses);
+ }
otherMetadata.push(...customFields);
otherMetadata.push(...validationRules);
otherMetadata.push(...layouts);
otherMetadata.push(...lookupFilters);
+ otherMetadata.push(...apexClasses);
return otherMetadata;
}
+ /**
+ * We know that the field is referenced in this class, but is it
+ * used for reading or assignment? We determine that here by checking
+ * if the field is used in an assignment expression in the body of the class
+ */
+ async function getFieldUsageMode(apexClasses){
+
+ //i.e field_name__c without the object prefix
+ let refCustomField = entryPoint.name.split('.')[1];
+
+ /**
+ * This matches on custom_field__c = but it does NOT match
+ * on custom_field__c == because the latter is a boolean exp
+ * and we are searching for assignment expressions
+ * gi means global and case insensitive search
+ */
+ let assignmentExp = new RegExp(`${refCustomField}=(?!=)`,'gi');
+
+ let ids = apexClasses.map(ac => ac.id);
+ ids = utils.filterableId(ids);
+
+ let query = `SELECT Id,Name,Body FROM ApexClass WHERE Id IN ('${ids}')`;
+ let soqlQuery = {query,filterById:true};
+
+ let results = await toolingApi.query(soqlQuery);
+
+ let classBodyById = new Map();
+
+ results.records.forEach(rec => {
+ classBodyById.set(rec.Id,rec.Body);
+ });
+
+
+ apexClasses.forEach(ac => {
+
+ //by default we assume that the mode is read only
+ ac.fieldMode = 'read';
+
+ let body = classBodyById.get(ac.id);
+ if(body){
+ //remove all white space/new lines
+ body = body.replace(/\s/g,'');
+
+ if(body.match(assignmentExp)){
+ ac.fieldMode = 'write';
+ }
+ }
+ });
+
+ return apexClasses;
+ }
+
async function getLookupFilterDetails(lookupFilters){
let metadataRecordToEntityMap = new Map();