How do I render custom picklist values?

  • 8
  • Question
  • Updated 2 years ago
  • Answered
I have a page where users need to be able to change a picklist to change the status of a record, such as "working", "submitted", etc. However, normal users shouldn't be able to put it into the approved status.

I know I can add a validation rule that says "if the user's profile = x and the picklist value = "approved", then throw error message 'you can't select that' ", but the ideal way, of course, is to prevent a normal user from being able to select the "accepted" value in the first place.

This seems like a perfect opportunity for a custom Skuid renderer! I'm thinking that in read mode the picklist shows the current value, but in edit mode it shows only values that the current user has access to based on their profile. Something like:

if (field.mode == 'read') {
skuid.ui.fieldRenderers[field.metadata.displaytype][field.mode](field,value);
} else if (field.mode == 'edit') {
/*
if (profile != admin) {
add picklist value 'working';
add picklist value 'submitted';
} else if (profile = 'admin') {
add picklist value 'approved';
}
render picklist in edit mode
*/
}

How would I do this (or create an equivalent or similar solution)?

Bonus points: Maybe it can also prevent normal users from changing the status from "approved" to something else?
Photo of Peter Bender

Peter Bender, Champion

  • 6,256 Points 5k badge 2x thumb

Posted 5 years ago

  • 8
Photo of Zach McElrath

Zach McElrath, Employee

  • 49,262 Points 20k badge 2x thumb
One solution to this is, which doesn't require a custom renderer, is to add multiple RecordTypes to your Salesforce object. Then, different RecordTypes can be assigned to different Profiles, and each RecordType can have its own set of Picklist Values for each of your Picklist / Multi-select Picklist Fields. This functionality is supported by Skuid in the Spring 13 release. However, this strategy involves added setup effort, and adding RecordTypes is not always an ideal / desired solution, particularly if there's only one picklist that you want to have different values for. Also, you'll have to make sure that the correct RecordType is chosen on a given record in order for this functionality to take effect --- either by having a Condition on your model explicitly like RecordTypeId = 'School', or whatever the API name of the RecordType is, or by adding the RecordTypeId field to your Field Editor or Table and requesting the user to select the desired RecordType.

All that being said...

Here's how you could do it with a custom Field Renderer instead :)

If you want to do conditional picklist value adding based on Profile Name, instead of Profile Id, then you'll need to request the Running User's Profile information in a separate model on your page, e.g. a 'RunningUser' model.




var field = arguments[0],
value = arguments[1];

// Get the Running User's Profile Name from a 'RunningUser' model:
var userModel = skuid.model.getModel('RunningUser'),
user = userModel && userModel.getFirstRow(),
profileName = user && userModel.getFieldValue(user,'Profile.Name',true),
isAdmin = (profileName == 'System Administrator');

//
// Bonus points FIRST :)
//
// Prevent non-Admins from making any edits
// if the current status is 'Approved'.
//
if ((value === 'Approved') && !isAdmin) {
field.editable = false;
field.mode = 'read';
}

if (field.mode == 'edit') {

var picklistEntries = field.metadata.picklistEntries;

if (isAdmin) {
// add picklist values 'working' and 'submitted'
picklistEntries.push(
{ value: 'Working', label: 'Working', defaultValue: false, active: true },
{ value: 'Submitted', label: 'Submitted', defaultValue: false, active: true }
);
} else if (profileName == 'Sales Manager') {
picklistEntries.push(
{ value: 'In Review', label: 'In Review', defaultValue: false, active: true }
);
}

}

// Run the standard picklist renderer for the given mode
skuid.ui.fieldRenderers[field.metadata.displaytype][field.mode](field,value);

Photo of Peter Bender

Peter Bender, Champion

  • 6,256 Points 5k badge 2x thumb
Zach, thanks for this. I've used this to render conditional picklist values. I've got two issues, though:

1) How do I make the list default to the existing status if there is a row/record already existing? Right now it always appears as "--None--", even when the existing value is something else. (I've set up the field with an "Add None" property. Maybe this is related to #2...)

2) The page is rendering the standard set of picklist values, then rendering the "pushed" values from my JavaScript, resulting in two sets of identical values in the picklist. How do I get it to only render the values I'm trying to put into the list with my code?

Here's my code:


var field = arguments[0],
value = arguments[1];
var isTeacher = false;

var userModel = skuid.model.getModel('User'),
userRow = userModel.getFirstRow(),
userRole = userModel.getFieldValue(userRow,'Contact.Current_Role__c'),
isTeacher = (userRole == 'Teacher');

// Prevent teachers from making any edits if it is approved
if ((value === 'Reviewed & Approved') && isTeacher) {
field.editable = false;
field.mode = 'read';
}

if (field.mode == 'edit') {
var picklistEntries = field.metadata.picklistEntries;
picklistEntries.push(
{ value: 'Working', label: 'Working', defaultValue: false, active: true },
{ value: 'Ready for Review', label: 'Ready for Review', defaultValue: false, active: true }
);
if (!isTeacher) {
picklistEntries.push(
{ value: 'Reviewed & Approved', label: 'Reviewed & Approved', defaultValue: false, active: true },
{ value: 'Reviewed & Needs Changes', label: 'Reviewed & Needs Changes', defaultValue: false, active: true }
);
}
}
// Run the standard picklist renderer for the given mode
skuid.ui.fieldRenderers[field.metadata.displaytype][field.mode](field,value);
Photo of Zach McElrath

Zach McElrath, Employee

  • 49,130 Points 20k badge 2x thumb
For (1), my question is, does it still not pick the correct value if the status is "Working"?

For (2), try adding the following line after "var picklistEntries = field.metadata.picklistEntries;"

picklistEntries.length = 0;
Photo of Peter Bender

Peter Bender, Champion

  • 6,256 Points 5k badge 2x thumb
Thanks Zach. adding that line fixed (2).

For (1), the page saves the values fine. When the model refreshes or the page loads and the value is one of the first two (working, ready for review), it loads the correct value as the default. However, when the value is one of the second two (reviewed & approved, reviewed & needs changes), those values are not populated as an existing value. I can edit the field and change its value, though. The rest of the fields seem to load fine. So, something about the logic around building the picklist entries seems to be interfering with loading the existing value in to the field.
Photo of Zach McElrath

Zach McElrath, Employee

  • 49,130 Points 20k badge 2x thumb
Thanks, that helps me confirm what the problem is. Change value = arguments[1] to be value = skuid.utils.decodeHTML(arguments[1]), and that should resolve (1).

Here's the detailed explanation of why this is necessary: XSS vulnerability prevention. Whenever you do a Custom Field Renderer on a Text / Textarea / Picklist field, Skuid preemptively HTML encodes the current value stored for a particular field / row, so that, if you, the author of the Custom Field Renderer, happen to accidentally display this text to the screen without first properly encoding it, if necessary, then you won't inadvertently introduce an XSS vulnerability into your page. So, from the theoretical to your specific scenario: the reason that "Reviewed & Approved" and "Reviewed & Needs Changes" aren't working is that they contain a character, the &, which has to be HTML encoded. So the value that you are getting handed in arguments[1] is actually Reviewed & Approved. So by using skuid.utils.decodeHTML(), you're converting this back into the actual value. Then, because you're leveraging the skuid.ui.fieldRenderers to actually do your output (as opposed to hand-coding your own picklist), the XSS vulnerability prevention is taken care of.

So the lesson to learn here is this:

Whenever doing custom Field Renderers for Text/Textarea/Picklist fields, always decode the value passed in via arguments[1], like so:



var field = arguments[0],
value = skuid.utils.decodeHTML(arguments[1]);

Photo of Peter Bender

Peter Bender, Champion

  • 6,256 Points 5k badge 2x thumb
Excellent - that fixed it! Good lessons learned here. Thanks so much for your help on this, Zach. For anyone's future reference, below is the "final" version of my custom picklist renderer.


var field = arguments[0],
value = skuid.utils.decodeHTML(arguments[1]);
userModel = skuid.model.getModel('User'),
userRow = userModel.getFirstRow(),
userRole = userModel.getFieldValue(userRow,'Contact.Current_Role__c'),
isTeacher = (userRole == 'Teacher'),
grpModel = skuid.model.getModel('Guided Reading Plan'),
grpRow = grpModel.getFirstRow();

// Prevent teachers from making any edits if the status is approved by forcing the mode to read-only
if ((value === 'Reviewed & Approved') && isTeacher) {
field.editable = false;
field.mode = 'read';
}

if (field.mode == 'edit') {
var picklistEntries = field.metadata.picklistEntries;
picklistEntries.length = 0; // if you don't do this, then the "real" values are already in the picklist and the code below will add duplicate values

// if there is no record yet, add a default value. NOTE: when the page is saved, the grp model is only saved when the value in this field is not blank. I am NOT using the Skuid field property "Add 'None' Option" because I only want this option to appear when there isn't already a record.
if (skuid.model.isNewId(grpRow.Id)) {
picklistEntries.push(
{ value: '', label: '-- Select a Status to Create --', defaultValue: false, active: true }
);
}

// create picklist values for the basic statuses
picklistEntries.push(
{ value: 'Working', label: 'Working', defaultValue: false, active: true },
{ value: 'Ready for Review', label: 'Ready for Review', defaultValue: false, active: true }
);

// create picklist values for the review/approval statuses if the user is not a teacher
if (!isTeacher) {
picklistEntries.push(
{ value: 'Reviewed & Approved', label: 'Reviewed & Approved', defaultValue: false, active: true },
{ value: 'Reviewed & Needs Changes', label: 'Reviewed & Needs Changes', defaultValue: false, active: true }
);
}
}
// Run the standard picklist renderer for the given mode
skuid.ui.fieldRenderers[field.metadata.displaytype][field.mode](field,value);
Photo of Moshe Karmel

Moshe Karmel, Champion

  • 8,646 Points 5k badge 2x thumb
Hey Zach I'm trying to use this code but it doesn't seem to be doing anything for me. I'm trying to  remove a picklist value, but it keeps showing up... here is my snippet:
var field = arguments[0],
    value = skuid.utils.decodeHTML(arguments[1]);
    var picklistEntries = field.metadata.picklistEntries;
    picklistEntries.length = 0;     // if you don't do this, then the "real" values are already in the picklist and the code below will add duplicate values
	picklistEntries.push(
        { value: 'Fixed', label: 'Fixed', defaultValue: false, active: true },
		{ value: 'Indexed', label: 'Indexed', defaultValue: false, active: true }
	);
// Run the standard picklist renderer for the given mode
skuid.ui.fieldRenderers[field.metadata.displaytype][field.mode](field,value);
If I log it in the console I can see that my changes have been made, however it doesn't reflect on the page....
Photo of Zach McElrath

Zach McElrath, Employee

  • 49,262 Points 20k badge 2x thumb
Moshe, one "wrinkle" to the Picklist renderer is that if the Picklist is either (a) Dependent on the values of another picklist, or (b) Dependent on the record's Record Type, then field.metadata.picklistEntries is NOT considered when building the Picklist --- rather, an internal function is run that goes and determines the appropriate values to display based on the value of the controlling picklist and/or Record Type field on the current record.

If you want to completely override this behavior, you'll need to roll your own custom picklist renderer using our internal skuid.ui.renderers API, and as J describes in this community post.

If you want to rely on the internal renderer, but adjust the values AFTER the renderer has been run, you might try removing the displayed values directly from the output <select> element AFTER the renderer has been run:

// Run the standard picklist renderer for the given mode
skuid.ui.fieldRenderers[field.metadata.displaytype][field.mode](field,value);

var select = field.element.find('select');
if (select.length) {
// Remove unwanted entries
$.each(select.children('option'),function(){
if ($(this).val()==='blah')) $(this).remove();
});
// Add new entries
select.append($('<option value="cheese">Cheese</option>'));
}
Photo of Joseph Ortiz

Joseph Ortiz

  • 594 Points 500 badge 2x thumb
Thank you!
I was beating my head against the wall trying to figure out why overriding the skuid.metamodel.picklistEntries was not working.
Photo of Hasantha Liyanage

Hasantha Liyanage

  • 1,446 Points 1k badge 2x thumb
I get an errror saying "TypeError: field.metadata is undefined" am I missing something here?

var field = arguments[0],
value = skuid.utils.decodeHTML(arguments[1]);
skuid.ui.fieldRenderers[field.metadata.displaytype][field.mode](field,value);
var select = field.element.find('select');

if (select.length) {
   // Remove unwanted entries

   $.each(select.children('option'),function(){
        if ($(this).val() === 'Below Expectation'){
            $(this).remove();
        }

   });

   select.append($('<option value="cheese">Cheese</option>'));
}
(Edited)
Photo of Matt Sones

Matt Sones, Champion

  • 31,530 Points 20k badge 2x thumb
Hasantha,

It sounds like your snippet isn't getting passed the field as arguments[0]. How are you launching you snippet? This code will only work if you're running it as a custom field renderer.
Photo of Hasantha Liyanage

Hasantha Liyanage

  • 1,446 Points 1k badge 2x thumb
oops, my bad thanks for pointing it out Matt :)
Photo of Robin

Robin

  • 894 Points 500 badge 2x thumb
Hey team, just wondering how I would modify this snippet to render radio buttons instead of the picklist dropdown?

Seems so simple but I haven't been able to figure it out :-)

Thank you in advance!
Photo of Moshe Karmel

Moshe Karmel, Champion

  • 8,646 Points 5k badge 2x thumb
It works!
Photo of Arjun Kodidala

Arjun Kodidala

  • 90 Points 75 badge 2x thumb
Hi,

I have a picklist with 4 values, i would like to remove 2 values. I  am following the below code, but it is not removing those values.
skuid.ui.fieldRenderers[field.metadata.displaytype][field.mode](field,value);

var select = field.element.find('select'); if (select.length) {
// Remove unwanted entries
$.each(select.children('option'),function(){
if ($(this).val()==='BA') $(this).remove();   if ($(this).val()==='UA') $(this).remove(); }); }
(Edited)
Photo of Robin

Robin

  • 894 Points 500 badge 2x thumb
You guys rock! Thanks, this really helped :-)
Photo of Robin

Robin

  • 894 Points 500 badge 2x thumb
I asked this in a comment above, though have not found a reply and am still stuck with it. How do I modify the snippet to render radio buttons instead of the standard picklist dropdown?

Thank you!

var	$ = skuid.$;
var field = arguments[0],
value = skuid.utils.decodeHTML(arguments[1]);
skuid.ui.fieldRenderers[field.metadata.displaytype][field.mode](field,value);
var select = field.element.find('select');

if (select.length) {

   $.each(select.children('option'),function(){
        if ($(this).val() === 'Survey Complete - Qualified' || $(this).val() === 'Sent' || $(this).val() === 'Responded' || $(this).val() === 'Survey Complete - DNQ' || $(this).val() === 'Appointment Booked'){
            $(this).remove();
        }

   });

}
Photo of Jack Sanford

Jack Sanford, Champion

  • 8,136 Points 5k badge 2x thumb
I'm also trying to figure out how to render a picklist created by a custom field renderer snippet as Radio Buttons. 

I know based on this post: http://help.skuidify.com/m/11720/l/214170-skuid-ui-basic-renderers that I need to use renderas 'RADIO_BUTTONS' in some way, but not sure how. 

my snippet:
var field = arguments[0], 
    row = field.row,
    
    // set the render condition
    isPercentage = (row.Indicator__r.Benchmark_Type__c == 'Percentage') ? true : false;
    value = skuid.utils.decodeHTML(arguments[1]),
    metadata = field.metadata,
    element = field.item.element,
    $ = skuid.$;

if (isPercentage && field.mode =='edit'){

//create a blank variable for picklist entries
var picklistEntries = [];

// set the picklist entries. note defaultValue doesn't matter, the first one will be default
    picklistEntries.push(

 { value: '0', label: 'No', defaultValue: true, active: true },
 { value: '1', label: 'Yes', defaultValue: false, active: true  }
   
);
    field.metadata.picklistEntries = picklistEntries;

// render the field as a picklist
    skuid.ui.fieldRenderers.PICKLIST.edit(field,value);
    
} 

else{
    
    //use the default renderer
skuid.ui.fieldRenderers[field.metadata.displaytype][field.mode](field,value);
}
Photo of Jack Sanford

Jack Sanford, Champion

  • 8,136 Points 5k badge 2x thumb
ask and ye shall receive, h/t to https://community.skuidify.com/skuid/topics/readonly-picklist-radio-buttons-can-still-be-changed-on-...

add a line 
field.options.type = 'RADIO_BUTTONS';
before your skuid.ui.fieldRenderers

So Robin you might try
value = skuid.utils.decodeHTML(arguments[1]);
field.options.type = 'RADIO_BUTTONS';
skuid.ui.fieldRenderers[field.metadata.displaytype][field.mode]
Photo of Rob Hatch

Rob Hatch, Official Rep

  • 43,906 Points 20k badge 2x thumb
Thanks Jack! 
Photo of Robin

Robin

  • 894 Points 500 badge 2x thumb
Hi Jack and Rob, 

Amazing, thank you for the help! I can now render the options as radio buttons (YAY!). As an added challenge, I would still like to remove unwanted entries. The existing script as suggested earlier does not seem to do the trick any more once the list is rendered as radio buttons. How would I have to modify this code to remove them?

Thank you in advance!


if (select.length) {  
   $.each(select.children('option'),function(){
        if ($(this).val() === 'Survey Complete - Qualified' || $(this).val() === 'Sent' || $(this).val() === 'Responded' || $(this).val() === 'Survey Complete - DNQ' || $(this).val() === 'Appointment Booked'){
            $(this).remove();
        }
   });
(Edited)
Photo of Tami Lust

Tami Lust

  • 5,282 Points 5k badge 2x thumb
I am trying to render a custom picklist but the picklist is just rendering as normal and there are no errors in the console. Can some one point in the right 

Objective:
If the running user has a sales category of "Retail Sales" then only show the "Retail Sales" option in the picklist. Otherwise render as usual.

Notes:
This is a dependent picklist.

Code:
var $ = skuid.$;
var field = arguments[0],
    value = skuid.utils.decodeHTML(arguments[1]);
    var isRetailSales = [];



var userModel = skuid.model.getModel('RunningUser'),
    quoteModel = skuid.model.getModel('QuoteDetail'),
    userRow = userModel.getFirstRow(),
    userSalesCategory = userModel.getFieldValue(userRow,'AC_Sales_Category__c'),
    isRetailSales = (userSalesCategory == "Retail Sales");
    
    
    skuid.ui.fieldRenderers[field.metadata.displaytype][field.mode](field,value);
    var select = field.element.find('select');
    
    

if (isRetailSales && field.mode == 'edit') {

    if (select.length) {

   // Remove unwanted entries

    $.each(select.children('option'),function(){
        if ($(this).val() === 'Retail Sales'){
            $(this).remove();
        }


   });

    }
}
Photo of Tami Lust

Tami Lust

  • 5,282 Points 5k badge 2x thumb
The controlling value picklist value is set correctly it defaults to the correct value. There is not a record type issue and I am not getting any error on save. 

I made the field required to see what happens on save and it saves with the first value in the picklist not the value that is being displayed.

So for example I am only showing "Retail Sales" based on the snippet. When I hit save "Account Sales - Institutional Sales" is the value being saved which is the first value in the picklist.
Photo of Jack Sanford

Jack Sanford, Champion

  • 8,356 Points 5k badge 2x thumb
I've run into similar issues with picklists, particularly for reference fields, where it will default to a value I don't want and shouldn't even be available based on filters or security settings. 

Is it possible in your use case to default the value to Retail Sales on the model via a condition or model action? Or, if you make it required, can you add None as an option so people have to choose one? 

Also, I think it might work better to instead of removing all the values that don't equal Retail Sales, use a render snippet that declares these are all the values of my picklist: 'Retail Sales'. 

Just had another idea that might be a way around this:
Add two picklists, one that renders if the running user's Sales Category = 'Retail Sales', and one that renders if it doesn't. On the retails sales picklist, you could override the metadata so that the only option is Retail Sales, and make it required. 

Unless your field is in a table...
Photo of Tami Lust

Tami Lust

  • 5,282 Points 5k badge 2x thumb
Jack,

Thanks for all the great ideas. Your first suggestion wouldn't work for my use case, I tried your second suggestion without success. I have been working on this all morning trying to go the editing the picklist route, but the value wouldn't save.

So, I adjusted my thinking.

If the user is a retail sales user I want to set their category automatically and they don't really need to see the field on create. So I edited the script to do a row update. Attached the script to the save button. Now on every save if the user is a retail sales user then the category is set and I can hide the field from view for those sets of users.

Here is the final code
var params = arguments[0],
	$ = skuid.$;
    var isRetailSales = [];



var userModel = skuid.model.getModel('RunningUser'),
    model = skuid.model.getModel('QuoteDetail'),
    row = model.getFirstRow(),
    userRow = userModel.getFirstRow(),
    userSalesCategory = userModel.getFieldValue(userRow,'AC_Sales_Category__c'),
    isRetailSales = (userSalesCategory === "Retail Sales");
    console.log('Is Retail Sales? '+isRetailSales);
    
    
   // skuid.ui.fieldRenderers[field.metadata.displaytype][field.mode](field,value);
    //var select = field.element.find('select');
    
 var category = 'Retail Sales';   

if (isRetailSales) {

     model.updateRow(row, {
        AC_Category__c: category 

   });
} 
(Edited)
Photo of Jack Sanford

Jack Sanford, Champion

  • 8,356 Points 5k badge 2x thumb
Perfect! sounds like an even better solution, since it's more streamlined for retail sales users. Great work!
Photo of Tami Lust

Tami Lust

  • 5,282 Points 5k badge 2x thumb
Thank you for all your help today!
Photo of Matt Sones

Matt Sones, Champion

  • 31,530 Points 20k badge 2x thumb
Tami,

It looks like your code is removing the value is the value === 'Retail Sales', but from your description, you're looking to do the opposite. Try changing === to !==

if (isRetailSales && field.mode == 'edit') {
if (select.length) { // Remove unwanted entries $.each(select.children('option'),function(){ if ($(this).val() !== 'Retail Sales'){ $(this).remove(); } }); } }
(Edited)
Photo of Tami Lust

Tami Lust

  • 5,282 Points 5k badge 2x thumb
Thanks for that catch Matt, but it still is not working. Even when it was "===Retail Sales", retail sales was not removed. 
Photo of Tony Sirna

Tony Sirna

  • 976 Points 500 badge 2x thumb
I ran into a problem when I created a picklist with only one value. For some reason the value was not being save when the model was saved.

I looked in the console and the value in the model under data[0].fieldname was getting set but the value in the model under changes[id].fieldname was not getting set.

I add some lines to my code to set the value in the changes array and that worked. Not sure if this is good practice or if there is a better way but it seems to work.

Note that if I didn't check the typeof !==undefined I got errors in the console after the record was saved or when trying to cancel model changes or close the popup. Not sure why this code was getting run at that time but that seemed to fix it.

Suggestions of better code are certainly welcome.

var field = arguments[0],
value = skuid.utils.decodeHTML(arguments[1]);

if (typeof field.model.data[0] !== 'undefined') field.model.data[0].Endorser_Type__c ='Municipal';

var picklistEntries = field.metadata.picklistEntries;
picklistEntries.length = 0; // if you don't do this, then the "real" values are already in the picklist and the code below will add duplicate values
picklistEntries.push(
{ value: 'Municipal', label: 'Municipal', defaultValue: true, active: true }
);

id =-1;
if (typeof field.model.data[0] !== 'undefined'){
field.model.data[0].Endorser_Type__c = 'Municipal';
id = field.model.data[0].Id;

}

if ( typeof field.model.changes[id] !== 'undefined' && id != -1) field.model.changes[id].Endorser_Type__c = 'Municipal';


// Run the standard picklist renderer for the given mode
skuid.ui.fieldRenderers[field.metadata.displaytype][field.mode](field,value);
Photo of Segolene Nicoloff

Segolene Nicoloff

  • 488 Points 250 badge 2x thumb
Question along the same lines. I am a noob at code writing so it might have been answered and I haven't realised. I want to remove picklist values depending on if a checkbox is true or false. i.e if checkbox is true keep picklist entry, if checkbox is false, remove picklist entry. 
Thanks.