-
-
Save boxfoot/4166342 to your computer and use it in GitHub Desktop.
/* | |
* Apex doesn't expose dependent picklist info directly, but it's possible to expose. | |
* Approach: | |
* * Schema.PicklistEntry doesn't expose validFor tokens, but they are there, and can be accessed by serializing to JSON | |
* (and then for convenience, deserializing back into an Apex POJO) | |
* * validFor tokens are converted from base64 representations (e.g. gAAA) to binary (100000000000000000000) | |
* each character corresponds to 6 bits, determined by normal base64 encoding rules. | |
* * The binary bits correspond to controlling values that are active - e.g. in the example above, this dependent option | |
* is available for the first controlling field only. | |
* | |
* by Benj Kamm, 2017 | |
* CC BY-SA 3.0 (http://creativecommons.org/licenses/by-sa/3.0/us/) | |
*/ | |
public class HL_FieldDescribeUtil { | |
public static Map<String, List<String>> getDependentOptionsImpl(Schema.SObjectField theField, Schema.SObjectField ctrlField) { | |
// validFor property cannot be accessed via a method or a property, | |
// so we need to serialize the PicklistEntry object and then deserialize into a wrapper. | |
List<Schema.PicklistEntry> contrEntries = ctrlField.getDescribe().getPicklistValues(); | |
List<PicklistEntryWrapper> depEntries = | |
HL_FieldDescribeUtil.wrapPicklistEntries(theField.getDescribe().getPicklistValues()); | |
// Set up the return container - Map<ControllingValue, List<DependentValues>> | |
Map<String, List<String>> objResults = new Map<String, List<String>>(); | |
List<String> controllingValues = new List<String>(); | |
for (Schema.PicklistEntry ple : contrEntries) { | |
String label = ple.getLabel(); | |
objResults.put(label, new List<String>()); | |
controllingValues.add(label); | |
} | |
for (PicklistEntryWrapper plew : depEntries) { | |
String label = plew.label; | |
String validForBits = base64ToBits(plew.validFor); | |
for (Integer i = 0; i < validForBits.length(); i++) { | |
// For each bit, in order: if it's a 1, add this label to the dependent list for the corresponding controlling value | |
String bit = validForBits.mid(i, 1); | |
if (bit == '1') { | |
objResults.get(controllingValues.get(i)).add(label); | |
} | |
} | |
} | |
return objResults; | |
} | |
// Convert decimal to binary representation (alas, Apex has no native method :-( | |
// eg. 4 => '100', 19 => '10011', etc. | |
// Method: Divide by 2 repeatedly until 0. At each step note the remainder (0 or 1). | |
// These, in reverse order, are the binary. | |
public static String decimalToBinary(Integer val) { | |
String bits = ''; | |
while (val > 0) { | |
Integer remainder = Math.mod(val, 2); | |
val = Integer.valueOf(Math.floor(val / 2)); | |
bits = String.valueOf(remainder) + bits; | |
} | |
return bits; | |
} | |
// Convert a base64 token into a binary/bits representation | |
// e.g. 'gAAA' => '100000000000000000000' | |
public static String base64ToBits(String validFor) { | |
if (String.isEmpty(validFor)) return ''; | |
String validForBits = ''; | |
for (Integer i = 0; i < validFor.length(); i++) { | |
String thisChar = validFor.mid(i, 1); | |
Integer val = base64Chars.indexOf(thisChar); | |
String bits = decimalToBinary(val).leftPad(6, '0'); | |
validForBits += bits; | |
} | |
return validForBits; | |
} | |
private static final String base64Chars = '' + | |
'ABCDEFGHIJKLMNOPQRSTUVWXYZ' + | |
'abcdefghijklmnopqrstuvwxyz' + | |
'0123456789+/'; | |
private static List<PicklistEntryWrapper> wrapPicklistEntries(List<Schema.PicklistEntry> PLEs) { | |
return (List<PicklistEntryWrapper>) | |
JSON.deserialize(JSON.serialize(PLEs), List<PicklistEntryWrapper>.class); | |
} | |
public class PicklistEntryWrapper { | |
public String active {get; set;} | |
public String defaultValue {get; set;} | |
public String label {get; set;} | |
public String value {get; set;} | |
public String validFor {get; set;} | |
} | |
} |
/** | |
* getDependentPicklistOptions | |
* by Benj Kamm, 2012 | |
* (inspired by http://iwritecrappycode.wordpress.com/2012/02/23/dependent-picklists-in-salesforce-without-metadata-api-or-visualforce/) | |
* CC BY-SA 3.0 (http://creativecommons.org/licenses/by-sa/3.0/us/) | |
* | |
* Build an Object in which keys are valid options for the controlling field | |
* and values are lists of valid options for the dependent field. | |
* | |
* Method: dependent PickListEntry.validFor provides a base64 encoded | |
* string. After decoding, each of the bits (reading L to R) | |
* corresponds to the picklist values for the controlling field. | |
*/ | |
function getDependentOptions (objName, ctrlFieldName, depFieldName) { | |
// Isolate the Describe info for the relevant fields | |
var objDesc = sforce.connection.describeSObject(objName); | |
var ctrlFieldDesc, depFieldDesc; | |
var found = 0; | |
for (var i=0; i<objDesc.fields.length; i++) { | |
var f = objDesc.fields[i]; | |
if (f.name == ctrlFieldName) { | |
ctrlFieldDesc = f; | |
found++; | |
} else if (f.name == depFieldName) { | |
depFieldDesc = f; | |
found++; | |
} | |
if (found==2) break; | |
} | |
// Set up return object | |
var dependentOptions = {}; | |
var ctrlValues = ctrlFieldDesc.picklistValues; | |
for (var i=0; i<ctrlValues.length; i++) { | |
dependentOptions[ctrlValues[i].label] = []; | |
} | |
var base64 = new sforce.Base64Binary(""); | |
function testBit (validFor, pos) { | |
var byteToCheck = Math.floor(pos/8); | |
var bit = 7 - (pos % 8); | |
return ((Math.pow(2, bit) & validFor.charCodeAt(byteToCheck)) >> bit) == 1; | |
} | |
// For each dependent value, check whether it is valid for each controlling value | |
var depValues = depFieldDesc.picklistValues; | |
for (var i=0; i<depValues.length; i++) { | |
var thisOption = depValues[i]; | |
var validForDec = base64.decode(thisOption.validFor); | |
for (var ctrlValue=0; ctrlValue<ctrlValues.length; ctrlValue++) { | |
if (testBit(validForDec, ctrlValue)) { | |
dependentOptions[ctrlValues[ctrlValue].label].push(thisOption.label); | |
} | |
} | |
} | |
return dependentOptions; | |
} | |
var OBJ_NAME = 'Custom_Object__c'; | |
var CTRL_FIELD_NAME = "Controlling_Field__c"; | |
var DEP_FIELD_NAME = "Dependent_Field__c"; | |
var options = getDependentOptions(OBJ_NAME, CTRL_FIELD_NAME, DEP_FIELD_NAME); | |
console.debug(options); | |
@boxfoot can you explain me why you do the testBit? thank you. Thats helped me a lot.
@quoctc @bmodeprogrammer I just added a new approach that is easier to understand -- it uses a different approach instead of bitwise operators and is much easier to understand.
The new version is in Apex but should be easy enough for you to adapt a javascript version.
what is the validForBits.mid(i, 1);, what those that mean some string.mid what function is that in java c# ?
Why couldn't I retrieve this data var "objDesc = sforce.connection.describeSObject(objName)" Sforce is not defined?
@crowz4k you're taking each bit from validForBits
one at a time. the string.mid(position, number)
function takes number
bits from position position
@vinegrao95 you need to add the salesforce javascript sdk to your page - it will provide sforce
as a global variable.
In this stackexchange answer, you mentioned it would be rearranged as bytes: 10000000 00000000 00000000
but in your comments in this gist, it's e.g. 'gAAA' => '100000000000000000000'
.
Splitting that in 8ths like the SE answer, that's 10000000 00000000 00000
- is that missing 3 zeros at the end there? Trying to make sure I understand the logic correctly!
This is a great solution and it helped me a lot. Thank you
For JavaScript, this may be helpful:
Helper functions:
/*
Dependent picklists: https://developer.salesforce.com/docs/atlas.en-us.api.meta/api/sforce_api_calls_describesobjects_describesobjectresult.htm
Base64 characters: ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/
A is index 0 so : 000000
I is index 8 so : 001000
Q is index 16 so: 010000
o is index 40 so: 101000
QAAA is 010000 000000 000000 000000
Since binary 1 at 2nd position, not index, means the dependent field is valid option for the 2nd option in parent picklist
IAAA is 001000 000000 000000 000000
Since binary 1 at 3rd position, not index, means the dependent field is valid option for the 3rd option in parent picklist
oAAA is 101000 000000 000000 000000
Since binary 1 at 1st and 3rd position, not index, means the dependent field is valid option for the 1st and 3rd option in parent picklist
Given: 'QAAA', return string like: '010000000000000000000000'
*/
base64EncodingToBinaryBits: (value = '') => {
const base64CharacterSet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
const characterValues = [...value]; // 'QAAA' becomes ['Q', 'A', 'A', 'A']
const binaryValuesOfCharacterPosition = characterValues.map(character => {
const characterPosition = base64CharacterSet.indexOf(character);
const binaryRadix = 2;
const binaryRepresentation = characterPosition.toString(binaryRadix);
const base64Radix = 6; // 2^6 is 64, 6 is the target length of of string
const base64BinaryRepresentation = binaryRepresentation.padStart(base64Radix, '0'); // adds any needed leading 0's
return base64BinaryRepresentation;
});
const binaryBits = binaryValuesOfCharacterPosition.join('');
return binaryBits;
},
/*
Binary strings are interpreted from left to right
Consider on when the binary value is 1
*/
isBinaryValueOnAtIndex: (binaryStringValue = '', index = 0) => {
const bit = binaryStringValue[index];
const isOn = bit
? bit === '1'
: false;
return isOn;
}
Tests for helpers:
describe('Base64 string decoded to binary string', () => {
it('No position on', () => {
const base64PositionOn = undefined;
const binaryPositionOn = '';
const expectedBinaryPositionOn = stringUtilities.base64EncodingToBinaryBits(base64PositionOn);
expect(expectedBinaryPositionOn).toEqual(binaryPositionOn);
});
it('2nd position on', () => {
const base64PositionOn = 'QAAA';
const binaryPositionOn = '010000000000000000000000';
const expectedBinaryPositionOn = stringUtilities.base64EncodingToBinaryBits(base64PositionOn);
expect(expectedBinaryPositionOn).toEqual(binaryPositionOn);
});
it('1st and 3rd position on', () => {
const base64PositionOn = 'oAAA';
const binaryPositionOn = '101000000000000000000000';
const expectedBinaryPositionOn = stringUtilities.base64EncodingToBinaryBits(base64PositionOn);
expect(expectedBinaryPositionOn).toEqual(binaryPositionOn);
});
});
describe('Binary string values on or off', () => {
it('Value on at index for no binary value, index out of bounds', () => {
const binaryValue = '';
const someRandomOutOfBoundsIndex = 5;
const isIndexOn = stringUtilities.isBinaryValueOnAtIndex(binaryValue, someRandomOutOfBoundsIndex);
expect(isIndexOn).toEqual(false);
});
it('Value on at index', () => {
const binaryValue = '101000000000000000000000';
const isFirstIndexOn = stringUtilities.isBinaryValueOnAtIndex(binaryValue, 0);
const isThirdIndexOn = stringUtilities.isBinaryValueOnAtIndex(binaryValue, 2);
expect(isFirstIndexOn).toEqual(true);
expect(isThirdIndexOn).toEqual(true);
});
it('Value off at index', () => {
const binaryValue = '101000000000000000000000';
const isSecondIndexOn = stringUtilities.isBinaryValueOnAtIndex(binaryValue, 1);
const isFourthIndexOn = stringUtilities.isBinaryValueOnAtIndex(binaryValue, 3);
expect(isSecondIndexOn).toEqual(false);
expect(isFourthIndexOn).toEqual(false);
});
});
Hi @fahey252 I want to tweak your code. In my version I assume method may be used several time, so I added memorization. Another tune, is to use integers as bitwise arrays, since bitwise operation are very fast.
const base64CharacterSet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
const memory = new Map();
function base64EncodingToBinaryBits(value) {
if (memory.has(value)) return memory.get(value);
var v = value
.split('')
.map(character => base64CharacterSet
.indexOf(character)
.toString(2)
.padStart(6, '0')
.split('')
.reverse()
.join('')
)
.reverse()
.join('');
v = parseInt(v, 2);
memory.set(value, v);
return v;
}
function isValidFor(validFor, i) {
return base64EncodingToBinaryBits(validFor) & 1 << i;
}
It's not working for mobile SDK. the pos look wrong. how can i know the exactly pos of control field?