Direkt zum Hauptbereich

Zugriff auf "private" Daten mit Visualforce ermöglichen

Die Organisationsübergreifenden Sicherheitseinstellungen können den allgemeinen Zugriff auf den Datenbestand verhindern. Ist der Standardzugriff auf "Privat" gesetzt (s. Screenshot), hat nur der Eigentümer inklusive seines Vorgesetzten den Zugriff auf die Daten, wo er als Inhaber hinterlegt ist.
Insbesondere Vertriebsmitarbeiter arbeiten intensiv mit Account-Informationen. Hier werden unter anderem sensible Daten erfasst. In diesem Zusammenhang entstehen Fragen folgender Art:
Wie soll ich meine Salesforce-Organisation konfigurieren, wenn ich nur bestimmte Daten trotz "Privat" Einstellung allen Vertriebsmitarbeiter zur Verfügung stellen möchte?
Eine potenzielle Lösung ist die Entwicklung einer Visualforce-Seite, die relevante Daten darstellt. Basierend auf dem Beitrag von Jeff Douglas habe ich ein Suchformular gebaut. Damit lassen sich alle Accounts, unabhängig von den "Freigabeeinstellungen" durchsuchen.
Im folgenden Video ist die Funktionalität dargestellt.

Vorteile:
- der Zugriff auf die neue Seite und somit auf alle Daten ist konfigurierbar
- die Darstellung der Account-Felder lässt sich über ein Feldset steuern (keine Programmierung notwendig)
- schnelle Umsetzung
- flexibel für Anpassungen

Bausteine der Visualforce-Seite:
- CSS: was fürs Auge
- JavaScript:
   * Übergabe der Suchparameter an den Controller
   * Identifikation der Enter-Taste mit Ausführung der Suchfunktion
- ActionStatus: visuelle Darstellung für die Ausführung der Suchfunktion

Jetzt wird's technisch ;-)

------------------------------- VUSUALFORCE SEITE -------------------------------
<apex:page controller="f42_Search_Controller" sidebar="false">
 <style>
  .linkSearchResult{
   text-decoration:none !important;
   color:#015ba7 !important;
  }
  .loadingContainer{
   width:84%;
   height:88%;
   border:6px solid #CCCCCC;
   border-radius: 2px;
   z-index:1000;
   position:absolute;
   background-color: #efefef;
   filter: alpha(opacity=90);
   opacity: 0.8;
   -moz-opacity:0.8;
   -khtml-opacity:0.8;
  }
  .btn:hover {
   box-shadow: 0px 15px 20px -7px #fff inset, 0px 0px 5px -1px #333 !important;
  }
  .btn {
   display: block !important;
   padding: 5px 10px !important;
   background: #3ba9cd !important;
   border-radius: 5px !important;
   box-shadow: 0px 15px 20px -10px #fff inset, 0px 0px 5px -1px #333 !important;
   border: solid 2px #fff !important;
   margin: 5px !important;
   color: #fff !important;
   cursor: pointer !important;
   text-shadow: 0px 0px 2px #185375 !important;
   width:95%;
  }
  .resultsContainer{
   border-radius: 2px;
   box-shadow: 0px 10px 10px 0px #efefef inset;
   padding:4px;
   margin-top:10px;
   margin-left:2px;
  }
  .labelCol{
   text-align:left !important;
  }
  .buttonSection{
   border-top:1px solid #E0E0E0;
  }
  INPUT{
   width:100%;
  }
  </style>
  <script type="text/javascript">
   /************************
   doSearch
   *************************/
  function doSearch() {
   doSearchApex(
    document.getElementById("Name").value,
    document.getElementById("BillingCity").value
   );
  }

  /************************
  initSearchOnEnter
  *************************/
  function initSearchOnEnter(e){
   if(window.event){
    key = window.event.keyCode;
   }else{
    key = e.which; //firefox
   }
   if(key == 13) {
    var ele=document.getElementById("search-button");
    ele.click();
    return false;
   }else{
    return true;
   }
  }
 </script>
 <apex:form >
<!-- -------------------------------------------------------------------- -->
<!-- APEX FUNCTIONS -->
<!-- Getting the Daschboard ID find the corresponding Monthly KAM Review -->
 <apex:actionFunction name="doSearchApex" action="{!runSearch}" rerender="results,debug,errors,noSearchResults" status="ajaxLoading">
  <apex:param name="Name" value="" />
  <apex:param name="BillingCity" value="" />
 </apex:actionFunction>
<!-- -------------------------------------------------------------------- -->
 <table width="100%" border="0">
  <tr>
<!-- SEARCH SETTINGS -->
<!-- ---------------- -->
   <td width="200" valign="top">
    <apex:pageBlock title="{!$Label.f42_Search_Parameter}" mode="edit" id="criteria">
     <table cellpadding="2" cellspacing="2" width="100%">
<!-- ACCOUNT NAME -->
      <tr>
       <th class="labelCol">
        <apex:outputLabel value="{!$ObjectType.Account.fields.Name.label}" /><br />
        <input type="text" id="Name" onkeypress="return initSearchOnEnter(event);" />
       </th>
      </tr>
<!-- CITY -->
      <tr>
       <th class="labelCol">
        <apex:outputLabel value="{!$ObjectType.Account.fields.BillingCity.label}" /><br/>
        <input type="text" id="BillingCity" onkeypress="return initSearchOnEnter(event);" />
       </th>
      </tr>
     </table>
    </apex:pageBlock>
<!-- SEARCH BUTTON -->
    <input type="button" id="search-button" class="btn" name="btnSearch" value="{!$LABEL.f42_Search_Button}" onclick="doSearch();" />
<!-- NUMBER OF SEARCH RESULTS -->
    <apex:outputPanel id="noSearchResults">
     <div class="resultsContainer">
      <apex:outputLabel styleclass="labelCol" value="{!$Label.f42_Search_NumberOfResults} {!numberSearchResults}" />
     </div>
    </apex:outputPanel>
   </td>
   <td valign="top">
    <apex:pageMessages id="errors" />
<!-- ********** AJAX LOADING STATUS ************-->
     <apex:outputPanel >
      <apex:actionStatus id="ajaxLoading">
       <apex:facet name="start">
        <div class="loadingContainer">
         <div style="text-align:center; padding-top:50px;"><img src="/img/loading32.gif" /> </div>
        </div>
       </apex:facet>
      </apex:actionStatus>
     </apex:outputPanel>
<!-- ----------------------- -->
       <apex:pageBlock mode="edit" id="results">
<!-- LIST with ACCOUNTS -->
<!------------------------>
       <apex:PageBlockTable value="{!lstAccounts}" var="obj">
<!-- Link View Account -->
<!-- ----------------- -->
        <apex:column headerValue="">
         <apex:outputLink styleClass="linkSearchResult" target="_top" value="/{!obj.ID}?retURL=%2F{!obj.id}" rendered="{!obj.OwnerId = idCurrentUser}">{!$Label.f42_Search_View}</apex:outputLink>
        </apex:column>
<!-- Get fields for displaying from FieldSet -->
<!------------------------>
        <apex:repeat value="{!$ObjectType.Account.FieldSets.SearchForm}" var="item">
         <apex:column >
          <apex:facet name="header">
           <apex:commandLink value="{!$ObjectType.Account.fields[item].label}" action="{!toggleSort}" rerender="results" status="ajaxLoading">
            <apex:param name="sortField" value="{!item}" assignTo="{!sortField}"/>
           </apex:commandLink>
          </apex:facet>
          <apex:outputField value="{!obj[item]}" />
         </apex:column>
        </apex:repeat>
       </apex:PageBlockTable>
      </apex:pageBlock>
     </td>
    </tr>
   </table>
   <apex:pageBlock title="Debug - SOQL" id="debug" rendered="false">
    <apex:outputText value="{!debugSoql}" /> 
  </apex:pageBlock>
 </apex:form>

</apex:page>

------------------------------- CONTROLLER -------------------------------

public without sharing class f42_Search_Controller {
public List<Account> lstAccounts {get;set;} // the collection of accounts to display
public String idCurrentUser{get; set;}
public Integer numberSearchResults{get; set;}
// PRIVATE --------------------------------------------------
private String API_NAME_ACCOUNT = 'Account';
private String STANDARD_ACCOUNT_SORTFIELD = 'Name';
//-----------------------------------------------------------
private String soqlInit;
private String soql; // the soql without the order and limit
private map<String, Schema.SObjectField> mapFieldnamesAccount;
//-----------------------------------------------------------
public Boolean runIntoException = false; // used by Test Class in order to run into exceptions and reach more coverage
/************************
CONSTRUCTOR
***/
// init the controller and display some sample data when the page loads
public f42_Search_Controller() {
idCurrentUser = UserInfo.getUserId();
// get all field names and build a query
mapFieldnamesAccount = f42_Helper.getFieldMapFromObject(API_NAME_ACCOUNT);
soqlInit = 'SELECT ' + f42_Helper.getFieldsAsStringFromObject(mapFieldnamesAccount) + ' ' +
'FROM ' + API_NAME_ACCOUNT + ' ' +
'WHERE IsDeleted=false';
soql = soqlInit;
}


/***********************
sortDir
***/
// the current sort direction. defaults to asc
public String sortDir {
get{
if(sortDir == null)
sortDir = 'asc';
return sortDir;
}set;
}
/***********************
sortField
***/
// the current field to sort by. defaults to Account Name
public String sortField {
get{
if(sortField == null)
sortField = STANDARD_ACCOUNT_SORTFIELD;
return sortField;
}set;
}
/***********************
debugSoql
***/
// format the soql for display on the visualforce page
public String debugSoql {
get{return soql + ' order by ' + sortField + ' ' + sortDir;}
set;
}
/***********************
toggleSort
***/
// toggles the sorting of query from asc<-->desc
public void toggleSort() {
// simply toggle the direction
sortDir = sortDir.equals('asc') ? 'desc' : 'asc';
// run the query again
runQuery();
}
/***********************
runSearch
***/
// build search string with parameters passed via Javascript
public PageReference runSearch() {
soql = soqlInit;
// URL Parameters
Map<String, String> mapParam = Apexpages.currentPage().getParameters();
if(mapParam != null){
for(String param : mapParam.keySet()){
// find the parameter in Account Fields Map
if(mapFieldnamesAccount.containsKey(param.toLowerCase())){
String val = mapParam.get(param);
if(!val.equals(''))
soql += ' AND ' + param + ' LIKE \'' + String.escapeSingleQuotes(val) + '%\'';
}
}
}
// run the query again
runQuery();
return null;
}
/***********************
runQuery
***/
// runs the actual query
private void runQuery() {
try {
if(runIntoException)
soql += 'invalid soql param';
lstAccounts = Database.query(soql + ' order by ' + sortField + ' ' + sortDir);
numberSearchResults = lstAccounts.size();
if(numberSearchResults >= f42_Helper.MAX_RECORDS_DISPLAY){
list<Account> lstTemp = new list<Account>();
for(Account acc : lstAccounts){
if(lstTemp.size() >= f42_Helper.MAX_RECORDS_DISPLAY)
break;
else
lstTemp.add(acc);
}
lstAccounts.clear();
lstAccounts.addAll(lstTemp);
ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.Info, LABEL.f42_Search_ToManyResult));
}
}catch (Exception e) {
ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.ERROR, 'ERROR: ' + e.getMessage()));
}
}

}


------------------------------- TEST CLASS -------------------------------

@isTest
private class f42_Test_Search_Controller {
private static Integer maxAccount = f42_Helper.MAX_RECORDS_DISPLAY + 1;
static testMethod void testSearchForm() {
// List with accounts (more than allowed for the search)
List<Account> lstAccounts = new list<Account>();
for(Integer i=0; i<=maxAccount; i++ ){
Account a = f42_TestDataGenerator.createTestAccount(i, false);
lstAccounts.add(a);
}
insert lstAccounts;
test.startTest();
//******* CHECK
system.assertnotequals(null, lstAccounts[0].id);
// init controller
f42_Search_Controller cnt = new f42_Search_Controller();
// load the search page
Pagereference pg = Page.f42_Search;
Test.setCurrentPage(pg);
// test with to many accounts
cnt.runSearch();
//******* CHECK
for(Apexpages.Message msg : ApexPages.getMessages()){
system.assertequals(Label.f42_Search_ToManyResult, msg.getDetail());
}
// Number of displayed accounts is reduced to the defined max number
system.assertEquals(f42_Helper.MAX_RECORDS_DISPLAY, cnt.lstAccounts.size());
// search for just one account
pg.getParameters().put('Name', lstAccounts[0].Name);
cnt.runSearch();
//******* CHECK
system.assertEquals(1, cnt.lstAccounts.size());
// run other methods to reach more test coverage
cnt.toggleSort();
String s = cnt.debugSoql;
cnt.runIntoException = true;
cnt.runSearch();
test.stopTest();
}
}

------------------------------- CLASS f42_TestDataGenerator -------------------------------

public with sharing class f42_TestDataGenerator {
// Account
public static Account createTestAccount(Integer i, Boolean insertObject){
Account acc = new Account();
acc.Name = 'TestAccount' + i;
if(insertObject) insert acc;
return acc;
}
}

------------------------------- CLASS f42_Helper -------------------------------
public with sharing class f42_Helper {
public static Integer MAX_RECORDS_DISPLAY = 250; // max number of search results for the page "f42_Search"
/********************
getFieldMapFromObject
***/
public static Map<String, Schema.SObjectField> getFieldMapFromObject(String objName){
return Schema.getGlobalDescribe().get(objName).getDescribe().fields.getMap();
}
/********************
getFieldListFromObject
***/
public static String getFieldsAsStringFromObject(Map<String, Schema.SObjectField> mapFields){
String strFields='';
for ( String f : mapFields.keySet() ){
strFields += f;
strFields += ', ';
}
strFields = strFields.subString(0,strFields.Length() -2);
return strFields;
}
}

Kommentare

Beliebte Posts aus diesem Blog

Zeitgesteuerter Flow blockiert Custom Leadkonvertierung

Die programmierte Konvertierung eines Leads bricht mit der Fehlermeldung "Unable to convert lead that is in use by workflow" ab. Der Grund ist ein Prozess, der automatisiert und zeitgesteuert ausgeführt wird. Dieser Prozess ruft zu einem späteren Zeitpunkt einen Flow auf. Während der Speicherung eines Leads wird dabei automatisch ein Flow Interview erstellt. Dieser Datensatz vom Typ "FlowInterview" blockiert die Leadkonvertierung. Lösung: Unmittelbar vor der Leadkonvertierung eine Checkbox auf dem Lead auf TRUE setzen. Da dieselbe Checkbox in den Process Builder Kriterien eingebunden ist und der Prozess nur auf den FALSE Wert reagiert, löscht das System automatisch das entsprechende Flow Interview.

Bad value for restricted picklist field

Der Einsatz von "Restricted Picklists" bereitet spätestens im Deployment Kopfschmerzen. Basiert das Deployment auf Basis eines Drittanbietertools, dann sind die Kopfschmerzen noch intensiver. In meinem Fall habe ich versucht, ein neues Picklist-Feld mit Copado zu deployen. Während der Bereitstellung bekomme ich die folgende Fehlermeldung: System.DmlException: Insert failed. First exception on row 0; first error: INVALID_OR_NULL_FOR_RESTRICTED_PICKLIST, bad value for restricted picklist field: Z012: [CountryGroup__c] Das neue Picklist-Feld übernimmt alle Werte aus einem Global Value Set. Das bedeutet, die Option "Restrict to the values defined in the value set" ist automatisch aktiv und lässt sich nicht deaktivieren. Eine APEX-Testklasse beschreibt ebenfalls die neue Pickliste. Mit dem folgenden Workaround konnte ich das Deployment-Problem lösen: 1) Global Value Set samt Pickliste per Changeset in die Zielorg übertragen und bereitstellen ggf. Prof

Salesforce Community URL Settings

Ich habe mich in den letzten Tagen etwas ausführlicher mit Salesforce Communities in Kombination mit der API beschäftigt. Ein Problem dabei war, den richtigen Endpoint zu berechnen, wie im letzten Beitrag beschrieben API im Salesforce Partner Portal. Um die Weichen im Code für Community Benutzer einzubauen, muss während der Laufzeit berechnet werden, in welchem Context sich der aktuell eingeloggte Benutzer befindet. Dabei muss man sich zwangsweise mit den Fragen folgender Art beschäftigen: ist der eingeloggte Benuter ein Community Benutzer? ob und welche Community ist gerade aktiv? wie sieht die definierte Community URL aus? Antwort auf die Frage 1: private Boolean isCommunityUser(){         Boolean bIsCommunityUser = false;         String sUserType = UserInfo.getUserType();         sUserType = sUserType.toUpperCase();         if(sUserType == 'STANDARD')                 bIsCommunityUser = false;         if(sUserType == 'PARTNER')                  bIsCommunity