Mass deleting picklist values in Salesforce with AJAX javascript hack (2018 version)

More than 5 years ago I wrote an article on how to Mass delete Picklist values in Salesforce, this is still my most visited article and I have been meaning to get back to it for years. At least now it seems like it will be a part of the standard functionality sometime in the near future (mass delete picklist values (setup)) but today I had to do this at a customer so I had to solve it once again.

I tried using my old script but ended up with errors, Lightning doesn’t like loading external JavaScript things into itself. I respect that and switched to Classic, this is a Sysadmin Only exercise anyways.

Aura Content Security Policy directive

It works out of the box in Classic but it’s very quiet about what’s happening and you don’t really know what’s happening. Also if you accidentally clicked the bookmark in a production environment you’re going to have a bad time

Fuck It, We’ll Do It Live

Updated JavaScript looks like this:

javascript:

var allClear = function() {
    location.reload();
};
var links = document.getElementsByTagName("a");
var whatToDelete = prompt("Do you want to Delete 'Active' or 'Inactive' picklist values?'''", "Inactive");
if(!(whatToDelete === "Active" || whatToDelete === "Inactive")) {
    window.alert("Invalid choice, quitting");
} else {
    var onlyInactive = whatToDelete === "Inactive";

    var delLinks = new Array();
    for (var i = 0; i < links.length-1; i++) {
      var link = links[i];

      if(onlyInactive) {
        if(link.innerHTML === "Activate") {
            var link = links[i-1];
        } else {
            continue;
        }
      }
      if (link.innerHTML == "Del") {
        delLinks.push(link);
      }
    }

    if(delLinks.length == 0) {
        window.alert("Nothing to delete");
    } else {
        var goAhead = confirm("You're about to delete " + delLinks.length + " picklist values");
        if(goAhead) {
            for (var i = 0; i < delLinks.length; i++) {
              var delLink = delLinks[i].href
              // Synchronous AJAX style
              xmlhttp = new XMLHttpRequest();
              xmlhttp.open("GET",delLink, false);
              console.log("Deleting #" + i + ": " +  delLink);
              xmlhttp.send();
            }

            window.setTimeout(allClear, 2000);
        }
    }
}

You can still load it from where I store it by creating a bookmark with this URL:

javascript:(function()%7Bvar s = document.createElement("script"); s.src = "https://superfredag.com/massdelete_v2.js"; void(document.body.appendChild(s));%7D)()

Clicking it on a Global Value Set Detail page will give you a prompt:

Clicking ok will go ahead and select the inactive Picklist values and prompt again:

Clicking Cancel will abort at all times.
Clicking on on this last Confirmation dialogue will delete your Inactive picklist values and the page will refresh.

If you want to delete Active Picklist values you'll have to change the "Inactive" string to "Active" after clicking the bookmark:

Same thing will happen next, you're asked to confirm:

Picklists are deleted and page reloaded. If you have a lot of values it might take some time so starting up the Developer Console is not a bad idea:

Deleting #262: https://mydomain--sandboxname.csXX.my.salesforce.com/setup/ui/picklist_masterdelete.jsp?id=01J0E000006zu8g&tid=0Nt&pt=0Nt0E0000000RFx&retURL=%2F0Nt0E0000000RFx&deleteType=0&_CONFIRMATIONTOKEN=VmpFPSxNakF4T0Mwd09TMHhORlF4TXpvd016bzBPUzR4TmpWYSxEY29WbGQtQUZ2NFM1SFM0Y3ZvNUpmLFpXSXdPRFZq
massdelete_v2.js:39 
Deleting #263: https://mydomain--sandboxname.csXX.my.salesforce.com/setup/ui/picklist_masterdelete.jsp?id=01J0E000006zu8h&tid=0Nt&pt=0Nt0E0000000RFx&retURL=%2F0Nt0E0000000RFx&deleteType=0&_CONFIRMATIONTOKEN=VmpFPSxNakF4T0Mwd09TMHhORlF4TXpvd016bzBPUzR4TmpWYSxkbHZYWnlodjdFT0xLb0lUT3FaNzJlLFpqUTNNV1Uw
massdelete_v2.js:39 
Deleting #264: https://mydomain--sandboxname.csXX.my.salesforce.com/setup/ui/picklist_masterdelete.jsp?id=01J0E000006zu8i&tid=0Nt&pt=0Nt0E0000000RFx&retURL=%2F0Nt0E0000000RFx&deleteType=0&_CONFIRMATIONTOKEN=VmpFPSxNakF4T0Mwd09TMHhORlF4TXpvd016bzBPUzR4TmpWYSxmd0xkOFBUN1F0VHRkUmlVNmsxQUl0LFltVXlZVE16
massdelete_v2.js:39 
Deleting #265: https://mydomain--sandboxname.csXX.my.salesforce.com/setup/ui/picklist_masterdelete.jsp?id=01J0E000006zu8j&tid=0Nt&pt=0Nt0E0000000RFx&retURL=%2F0Nt0E0000000RFx&deleteType=0&_CONFIRMATIONTOKEN=VmpFPSxNakF4T0Mwd09TMHhORlF4TXpvd016bzBPUzR4TmpaYSwxdnZiTndrNFFIN0R4UEU0SzhBZDM3LE1qSXpZalpq
massdelete_v2.js:39 
Deleting #266: https://mydomain--sandboxname.csXX.my.salesforce.com/setup/ui/picklist_masterdelete.jsp?id=01J0E000006zu8k&tid=0Nt&pt=0Nt0E0000000RFx&retURL=%2F0Nt0E0000000RFx&deleteType=0&_CONFIRMATIONTOKEN=VmpFPSxNakF4T0Mwd09TMHhORlF4TXpvd016bzBPUzR4TmpaYSw5QmxpX1BueEF1SDdSUVRDUHhic2FsLE9XVXdNRFF3
massdelete_v2.js:39 
Deleting #267: https://mydomain--sandboxname.csXX.my.salesforce.com/setup/ui/picklist_masterdelete.jsp?id=01J0E000006zu8l&tid=0Nt&pt=0Nt0E0000000RFx&retURL=%2F0Nt0E0000000RFx&deleteType=0&_CONFIRMATIONTOKEN=VmpFPSxNakF4T0Mwd09TMHhORlF4TXpvd016bzBPUzR4TmpaYSx2bjdkSUxRamljYUR4dTBlZzlSMmYyLFlqY3pNR016 

I deleted 350 picklist values in just over 2 minutes so it will not take forever, next step for this script would be to add a spinning progressbar and some bells and whistles but for now this at least solves the problem.

Since you need to have at least 1 value in a Value Set it will not be able to delete all of the Active picklist values but at least you'll save some mouse clicks.

You'll get a warning in the browser that synchronous XML requests are deprecated:

Running the requests asynchronous works fine but the browser will be super swamped if you're deleting hundreds of picklist values so this one is better. When the day finally comes and support for synchronous XML requests are removed I'll make sure to update this but until then this hack is good enough.

Visualise Big Object data in a Lightning Component

Good evening,

In my previous post (Upgrade your Electric Imp IoT Trailhead Project to use Big Objects
) I showed how you can use Big Objects to archive data and now I will show how you can visualise the data in a Lightning Component.

So now we have big objects being created but the only way to see them is by executing a SOQL query in the Developer Console (SELECT DeviceId__c, Temperature__c, Humidity__c, ts__c FROM Fridge_Reading_History__b).

I have created a Lightning Component that uses an Apex Class to retrieve the data.

Lets start with a screen shot on how it looks and then post the wall of code

And in Salesforce1

And here’s the code:
Lightning Component




    
    
    
    
    
    
    
    

Controller

/**
 * Created by Johan Karlsteen on 2017-10-08.
 */
({
    doinit : function(component,event,helper){
        var today = new Date();
        component.set("v.today", today.toISOString());
        console.log(document.documentElement);
        component.set("v.width", document.documentElement.clientWidth);
        component.set("v.height", document.documentElement.clientHeight);
        helper.refreshData(component,event,helper);
    },
    refreshData : function(component,event,helper) {
        helper.refreshData(component,event,helper);
    }
})

Helper

/**
 * Created by Johan Karlsteen on 2017-10-08.
 */
({
        addData : function(chart, labels, data) {
            chart.data.labels = labels;
            chart.data.datasets[0] = data[0];
            chart.data.datasets[1] = data[1];
        },
        redrawData : function(component, event, helper, readings, chart, datasets) {
            helper.addData(chart, readings.ts, datasets);
            chart.update();
        },
        displayData : function(component, event, helper, readings) {
            var datasets = [readings.temperature, readings.humidity];
            var chart = window.myLine;
            if(chart != null) {
                helper.redrawData(component,event,helper,readings, chart, datasets);
            }
            var config = {
                type: 'line',
                data: {
                    labels: readings.ts,
                    datasets: [{
                                 label: 'Temperature',
                                 backgroundColor: 'red',
                                 borderColor: 'red',
                                 data: readings.temperature,
                                 yAxisID: "y-axis-1",
                                 fill: false,
                             },
                             {
                                 label: 'Humidity',
                                 backgroundColor: 'blue',
                                 borderColor: 'blue',
                                 data: readings.humidity,
                                 yAxisID: "y-axis-2",
                                 fill: false,
                             }]
                },
                options: {
                    maintainAspectRatio: true,
                    responsive: true,
                    title:{
                        display:false,
                        text:'Temperature'
                    },
                    tooltips: {
                        mode: 'index',
                        intersect: false,
                    },
                    hover: {
                        mode: 'nearest',
                        intersect: true
                    },
                    scales: {
                        yAxes: [{
                            type: "linear", // only linear but allow scale type registration. This allows extensions to exist solely for log scale for instance
                            display: true,
                            position: "left",
                            id: "y-axis-1",
                        }, {
                            type: "linear", // only linear but allow scale type registration. This allows extensions to exist solely for log scale for instance
                            display: true,
                            position: "right",
                            id: "y-axis-2",

                            // grid line settings
                            gridLines: {
                                drawOnChartArea: false, // only want the grid lines for one axis to show up
                            },
                        }],
                    }
                }
            };
            var ctx = document.getElementById("temperature").getContext("2d");
            window.myLine = new Chart(ctx, config);
        },
    refreshData : function(component,event,helper) {
        var spinner = component.find('spinner');
        $A.util.removeClass(spinner, "slds-hide");
        var action = component.get("c.getFridgeReadings");
        var endDate = component.get("v.today");
        var results = component.get("v.results");
        action.setParams({
        	deviceId : "2352fc042b6dc0ee",
        	results : results,
        	endDate : endDate
    	});
        action.setCallback(this, function(response){
            var state = response.getState();
            if (state === "SUCCESS") {
                var fridgereadings = JSON.parse(response.getReturnValue());
                helper.displayData(component,event,helper,fridgereadings);
            }
            var spinner = component.find('spinner');
            $A.util.addClass(spinner, "slds-hide");
        });
        $A.enqueueAction(action);
    }
})

And the Apex Class that fetches the data:

/**
 * Created by Johan Karlsteen on 2017-10-08.
 */

public with sharing class FridgeReadingHistoryController {

    public class FridgeReading {
        public String deviceId {get;set;}
        public List ts {get;set;}
        public List doorTs {get;set;}
        public List door {get;set;}
        public List temperature {get;set;}
        public List humidity {get;set;}
        public FridgeReading(String deviceId) {
            this.deviceId = deviceId;
            this.ts = new List();
            this.doorTs = new List();
            this.door = new List();
            this.temperature = new List();
            this.humidity = new List();
        }
        public void addReading(Fridge_Reading_History__b  fr) {
            addReading(fr.Temperature__c, fr.Humidity__c, fr.ts__c, fr.Door__c);
        }
        public void addReading(Decimal t, Decimal h, DateTime timeStamp, String d) {
            String tsString = timeStamp.format('HH:mm dd/MM');
            this.ts.add(tsString);
            temperature.add(t);
            humidity.add(h);
            Integer doorStatus = d == 'open' ? 1 : 0;
            if(door.size() == 0 || doorStatus != door.get(door.size()-1)) {
                door.add(doorStatus);
                doorTs.add(tsString);
            }
        }
    }

    @AuraEnabled
    public static String getFridgeReadings(String deviceId, Integer results, DateTime endDate) {
        if(results == null) {
            results = 200;
        }
        FridgeReading fr = new FridgeReading(deviceId);
        system.debug('RESULTS: ' +results);
        List frhs = [
                SELECT DeviceId__c, Temperature__c, Humidity__c, Door__c, ts__c
                FROM Fridge_Reading_History__b
                WHERE DeviceId__c = :deviceId AND ts__c <: endDate
                LIMIT :Integer.valueof(results)
        ];
        for (Integer i = frhs.size() - 1; i >= 0; i--) {
            Fridge_Reading_History__b frh = frhs[i];
            fr.addReading(frh);
        }
        return JSON.serialize(fr);
    }
}

The component assumes you have Charts.js as a static resource, mine is here.

There are no test cases anywhere and the code is probably not production grade.

The next step would be to use aggregate functions on the Big Objects to show data over a longer period of time.

Cheers,
Johan

Using SHA-1 to generate passwords

This is probably the worst thing about the Internet, remembering passwords.

Until about a year ago I had the same password for almost every service, sometimes I spiced it up by adding a character in the start representing the service (F for facebook, G for Gmail and so on).

A colleague had a different approach, he used simple “passwords” which he then computed SHA-1 on. He had developed a simple java script that performed the SHA-1 conversion.

I have changed his original script a bit to be able to generate different length passwords (11, 21 and 26 characters).

You can try it out yourself, just add the following url as a bookmark (works in all browsers, I think):

javascript:(function()%7Bvar%20s%20=%20document.createElement(%22script%22);%20s.src%20=%20%22https://superfredag.com/convert.js%22;%20void(document.body.appendChild(s));%7D)()

To use it, put a “-” in the password box and press the link, this will give you a glass pane with a password box, just write your simple password (Facebook1234 for example) and choose the length, the script will enter the password in the field where you put the “-“.
If you choose Facebook1234 as your simple password the 21 character SHA-1 will be fc1a17377c7ed19872037.

The convert.js script is YUI-compressed, the original one is convert.js.original.
There is also a form based version if you just want the password in cleartext:

https://superfredag.com/convert.html

Of course everything is on GitHub if you want to look at it and put it up for yourself. If you don’t have any web hosted you could just put the scripts in the Public part of your Dropbox.

Google Maps polygons with holes

The last couple of weeks I have been experimenting with Google Maps trying to draw filled polygons that look alright. I’m using matplotlib for making the polygons and I’ve figured out that the output from contourf(…) is like a plotting routine where you first get a polygon that should be filled with the current level and the following ones are holes in it.

Before I just draw them all, coloring the holes with a lower color. This forced me to sort the polygons according to size which worked ok but didn’t look good.

In Google Maps v2 there was something called encoded polygons but it seems as if they were removed in v3 (never supported by Chrome anyway).

Anyway, the correct way in v3 is to do like this:

  var paths = [[
    new google.maps.LatLng(38.872886, -77.054720),
    new google.maps.LatLng(38.872602, -77.058046),
    new google.maps.LatLng(38.870080, -77.058604),
    new google.maps.LatLng(38.868894, -77.055664),
    new google.maps.LatLng(38.870598, -77.053346)
  ], [
    new google.maps.LatLng(38.871684, -77.056780),
    new google.maps.LatLng(38.871867, -77.055449),
    new google.maps.LatLng(38.870915, -77.054891),
    new google.maps.LatLng(38.870113, -77.055836),
    new google.maps.LatLng(38.870581, -77.057037)
  ]];

  function initialize() {
    var map = new google.maps.Map(document.getElementById("map"), {
      zoom: 16,
      center: new google.maps.LatLng(38.8714, -77.0556),
      mapTypeId: google.maps.MapTypeId.SATELLITE
    });

    var poly = new google.maps.Polygon({
      paths: paths,
      strokeWeight: 3,
      fillColor: '#55FF55',
      fillOpacity: 0.5
    });

    poly.setMap(map);
  }

My project is now online at http://halvklart.se/ and here’s a screenshot:

There are some troubles with the polygons when applying the b-splines but I will try to take care of them in the near future.

I have briefly investigated Thrift and ProtoBuf but I’m still not sure that I will gain that much by switching from JSON. Also found something called BSON which is binary JSON. I think the next step will be to add some more parameters, wind direction is probably the hardest since I will have to draw the arrows myself.

 

Improving Google Maps polygons with b-splines

Google Maps is great, you get an extremely nice background map for free. I know that there are alternatives (Bing, OpenLayers, etc) out there but since I’m running Google App Engine it seems easier to go Google all the way.

I’m plotting polygons and polylines (that’s what weather is about) and it works great but my input data is kind of sparse so the polygons look very rough.

To improve them I’m using b-splines. Found a very nice article here. I just changed the javascript so it works with lat/lon-arrays and the output is an array of google.maps.LatLng.

function bspline(lats, lons) {
    var i, t, ax, ay, bx, by, cx, cy, dx, dy, lat, lon, points;
    points = [];
    // For every point
    for (i = 2; i < lats.length - 2; i++) {
        for (t = 0; t < 1; t += 0.2) {
            ax = (-lats[i - 2] + 3 * lats[i - 1] - 3 * lats[i] + lats[i + 1]) / 6;
            ay = (-lons[i - 2] + 3 * lons[i - 1] - 3 * lons[i] + lons[i + 1]) / 6;
            bx = (lats[i - 2] - 2 * lats[i - 1] + lats[i]) / 2;
            by = (lons[i - 2] - 2 * lons[i - 1] + lons[i]) / 2;
            cx = (-lats[i - 2] + lats[i]) / 2;
            cy = (-lons[i - 2] + lons[i]) / 2;
            dx = (lats[i - 2] + 4 * lats[i - 1] + lats[i]) / 6;
            dy = (lons[i - 2] + 4 * lons[i - 1] + lons[i]) / 6;
            lat = ax * Math.pow(t + 0.1, 3) + bx * Math.pow(t + 0.1, 2) + cx * (t + 0.1) + dx;
            lon = ay * Math.pow(t + 0.1, 3) + by * Math.pow(t + 0.1, 2) + cy * (t + 0.1) + dy;
            points.push(new google.maps.LatLng(lat, lon));
        }
    }
    return points;
}

There are some more things that you have to do, the original arrays have to be extended by adding the first 2 elements at the back and the last 2 at the front. Or else the polygon will look chopped.

Also the first point may have to be added at the end to close a polyline, a polygon will close itself.

This is what the picture looks like without b-splines:

And with b-splines:

There are some problems with incomplete polygons (polylines that enter and exit the area) but by skipping the first and last point they look ok.

Next step is to use Thrift for communication instead of JSON. Don’t know if it will actually make any difference but I have promised myself to learn either Google Protocol Buffers or Thrift.