-
-
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); | |
It's not working for mobile SDK. the pos look wrong. how can i know the exactly pos of control field?
@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;
}
This helped me a lot! Thank you. I was wondering why you would store everything in an object rather than an array?