New Opportunity Page Layout – With Highlights Panel!
Yesterday, I enabled the new Opportunity page layout in my Developer Spring ’10 Preview org, and it took a few steps, so I thought I’d share them with you.
Firstly, you’ll need to contact salesforce.com to get this feature enabled.
Then be patient. It takes a minute or two for the update to propagate. Clearly, something was churning in the Force.com platform background!
Now we’ll navigate NOT to the Setup | Customize | User Interface screen (where this should be enabled). Instead, we’ll go to the Opportunity Page Layout screen.
Follow the cool prompts. They make it so easy, a … well, you know what I mean.
Note: You can only show fields in the Highlights Panel if they are in the page layout. (I have a feeling this has to do with Professional Edition or printable layouts, but I’m just guessing.)
Once you’ve done this for each page layout, click on the big button.
At this point, each user can enable the bar. I have no idea why the admin can’t just force this on all users – or maybe I missed something – but it seems to be an opt-in feature.
Here’s the link to enable the feature. Of course, you may wish to watch a video as well!
And here it is!
It’s interesting that if you have this enabled, certain user interface settings (yes, at Setup | Customize | User Interface) cannot be changed:
Here’s my prediction: We will start to see two major mistakes during Salesforce demos:
- We will continue to see the link asking if we want more information on inline editing (after more than a year, it’s time to turn that off, people).
- At the top of the Opportunity detail page, we will see this link.
And I will continue to think less of all demonstrators who make these mistakes.
Happy Spring 2010!
Get Documents and Attachments out of Salesforce
As Content will be included in all Salesforce licenses (for completeness, I'll add 'to some degree') with the Spring '10 release, orgs will be faced with the daunting prospect of getting their documents and attachments out of Salesforce and into Content.
I had this problem when Content was first released and I was asked to be one of the first SysAds to use it. At the time, we used Solution 1 (below), but since then, other products have been released to help with this.
Why is it even an issue?
- Surely we can download each file? Yes, but who wants to?
- Can't we do a Data Export and then upload those to Content? Yes, but all the files are renamed with their 15-character Ids, making renaming them all-but-impossible.
salesforce.com and DreamFactory to the rescue!
Solution 1
Summary: Use a script to rename all exported files. A (wonderful!) salesforce.com employee, Nick Marcantonio, wrote a Perl script to perform the transformation. Here it is, in all its glory:
# Nick Marcantonio
# nmarcantonio at salesforce.com
# 08/07
$file = 'Attachment.csv';
open (F, $file) || die ("Could not open $file!");
$line = <F>; #read first line which is nothing but column headers
while ($line = <F>)
{
($id,$name) = split ',', $line;
chomp($id);
$id =~ s/\"//g;
chomp($name);
$name =~ s/\"//g;
#print "$id : $name\n";
$result = rename($id, $name);
#print "$result\n";
}
close (F);
The instructions:
If you've done a data export you've noticed that all attachments are placed in the Attachments subfolder and named with their salesforce ID, not the actual file name or extension. One must then consult the Attachment.csv file included in the data export to find the name associated with the ID and rename the file. Attached to this solution is a Perl script that will rename all of the exported attachments to their proper names. Please follow these steps to run this: 1. Perform a data export and unzip the resulting zip file 2. Launch the data loader and export from the Attachments table ONLY the Id and Name column. This file must be named Attachment.csv. 3. Install ActivePerl. This will allow perl scripts to be run on a Windows machine. ActivePerl is available here (http://www.activestate.com/activeperl). 4. Copy the Attachment.csv file and the attached AttachmentParser.pl file to the Attachments subdirectory of the data export. 5. Double-click on AttachmentParser.pl. All of the files named with their salesforce IDs will be renamed with their proper names and file extensions. (This solution will work for documents as well. Follow the same procedure and be sure to name the extract from the Documents table Attachment.csv)
Note: This will not preserve folders, as far as I know. You may be able to recreate this by exporting the Folder table and doing some work on that, as the Document table does include a FolderId column.
A heartfelt thank-you to Nick Marcantonio for his help!
Solution 2
Install DreamFactory's FREE DreamTeam Document Management application from the AppExchange to drag-and-drop your Documents to your desktop.
This doesn't work with Attachments, though, so you may need to use another method for them.
Please let us know how it goes - good luck and enjoy Content!
Proof that salesforce.com listens – and comprehends
About two weeks ago, I posted a list of the many setup steps I took to make my Winter ’10 prerelease org useable. Those are the same steps I have to take whenever setting up any Developer Edition org for the first time as well.
An insider at salesforce.com forwarded me this email that a senior person sent to quite a few also-senior people on various product teams:
- Subject:
- Preparing a New Org. Each Pre-Release. In 27 steps.
- Body:
- Yikes. http://www.x2od.com/2009/09/16/preparing-a-new-org.html
This is not the most widely-read blog about Salesforce, but it feels good to know that salesforce.com does have its ear to the ground and is taking seriously even the indirect feedback that we bloggers provide.
Kudos, salesforce.com.
PS. While I’m at it, I may as well also mention that too many fields are invisible to every profile. Example on Account: Site, Ownership, SIC Code, and more. And Annual Revenue is invisible to Customer Portal users. It would be great to have a guide or documentation.
PPS. Turns out that there’s no rhyme nor reason why some orgs have various fields invisible. Between demo, developer, trial, prerelease, and more, some fields are available and some are not. It is a huge waste of time to find all the fields and enable them.
Preparing a New Org
With the impending arrival of the Winter 2010 (aka 162 or Winter'10) edition of Salesforce CRM, as with every other release, comes a prerelease org. (You can get one at https://www.salesforce.com/form/trial/prerelease_winter10.jsp.)
Every time one encounters a fresh org, there are maintenance tasks to perform. I usually go through an org (whether a Developer Edition org or a Prerelease version) and do the same tasks, generally in no particular order. This time, however, I wrote down what I did as I did it. Looking at the list, it's hardly in any "best practices" order at all - it's just how I did it.
There's no need to follow every step, and it is not a complete list of all possibilities, but this should give you some idea of the possibilities and available tweaks: (*** indicates some of the new features in WInter '10)
- Save login with 1Password/Roboform
- Reset (Set) Security Token
- Administration Setup | Security Controls
- Session time 8 hrs
- Passwords never expire
- Create Record Types (and Business Processes) for Lead, Opportunity, Case
- (Campaigns were not enabled in this prerelease org) - would have configured them here, similarly
- Activities section: Calendar link on sidebar
- Download latest versions of Connect for Outlook, Office Edition
- Opportunities:
- Enable Similar Opportunities
- Enable Opportunity Teams
- Create Account Master Record Type
- Enable Account Teams
- Create Contact Master Record Type
- Note: Asked to add to page layout. Not asked for Opportunities.
- Enable Case Teams
- Enable Public Solutions
- Solutions:
- Enable Solution Browsing
- Enable Solution HTML
- Could have created a Solution Process & Record Type
- Did not enable multilingual solutions
- Enable Self-Service
- Enable Web-to-Case
- Create default Owner, etc (auto prompted)
- Enable PRM and Partner Portal (though have no licenses)
- Salesforce to Salesforce
- Enabled S2S
- Set up S2S Connection Finder ***
- Added fields to page layout - Kept read-only for all profiles except System Administrator
- Enable Public & Private Tags
- Enable Console for all Profiles
- Search Settings - Enable Enhanced Lookup & Auto-Complete
- User Interface
- Separate loading of related lists
- Spell Checker on Tasks & Events
- Collapsible Sidebar
- Custom Sidebar on all Pages
- Enhanced Profile Management ***
- Set myself as default Workflow User
- Looked at Develop | Custom Settings ***
- Created a Default Queue and added myself
- Set all Sharing Rules to Private
- Update Home Page to the way I like it
- Order of wide section (top down): Calendar, Tasks, Items to Approve, Dashboard
- No changes to narrow section
Other things that may be possible in other orgs:
- Enable Customer Portal
- Customize Campaigns
- Set up Sites
Again, this is not meant to be a complete list. Also, it is not intended to be a how-to; for more information you may search the Help link at the top of every org page, check Salesforce Community, or Developer Force.
Happy configuring!
Visualforce Email Inbox
Sonny Cloward, SysAd at Rainforest Alliance, approached us about writing a Visualforce page to display all incoming emails for a given Case Queue. This led a few interesting discoveries. Here's how we handled the (donated time) project:
First, the page was built upon the template Sam Arjimandi built at Salesforce
| Property | Description |
|---|---|
| Autonumber | The API creates an autonumber. |
| Create | Value for the field can be specified during create using the API. |
| Defaulted on create | When created, a default value is supplied if no other value is specified. |
| Delete | Value for the field can be deleted using the API. |
| Filter | Can be used as filter criteria in a SOQL query FROM or WHERE clause. |
| idLookup | Can be used to specify a record in an upsert() call. The Id field of each object has this property and some Name fields. There are exceptions, so check for the property in any object you wish to upsert(). |
| Nillable | The field can contain a null value. |
| Query | The field can be queried with SOQL using the API. |
| Replicate | The value of the field can be replicated using the API. |
| Restricted picklist | A picklist that depends on the value of another picklist for the values it displays. |
| Retrieve | Value of the field can be retrieved using the API. |
| Search | Can be searched with SOSL using the API. |
| Update | Can be updated using the API. |
The important one here is "Filter" because (as the documentation states) this allows the field to be used in a WHERE clause. Also, however, (as the documentation does not state) it allows the field to be used in an ORDER BY clause. So all fields on the EmailMessage object that do not allow filtering/ordering had to be presented plainly, without Sam's cool PageBlockTable sorting features. Once this was done, Sonny had some great ideas:
- Show the email subject, but make that a hyperlink to the email message itself
- Link the Case Number (EmailMessage.ParentId) to the Case (EmailMessage.Parent)
- Show the Case Contact (EmailMessage.Parent.Contact.Name), linking to the Contact (EmailMessage.Parent.ContactId)
- Show the Case Account (EmailMessage.Parent.Account.Name), linking to the Account (EmailMessage.Parent.AccountId)
- Provide filters - Incoming only, Unread only, etc.
public with sharing class EmailMessageController {
public String EmailMessage { get; set; }
private List<EmailMessage> messages;
private String sortDirection = 'ASC';
private String sortExp = 'MessageDate';
public String wheretext;
public EmailMessageController(){
wheretext = '';
}
public String sortExpression { get {
return sortExp;
}
set {
//if the column is clicked on then switch between Ascending and Descending modes
if (value == sortExp)
sortDirection = (sortDirection == 'ASC')? 'DESC' : 'ASC';
else
sortDirection = 'ASC';
sortExp = value;
}
}
public void setWhereText(String value) {
whereText = value;
}
public string getWhereText(){
return wheretext;
}
public List<SelectOption> getViews(){
List<SelectOption> options = new List<SelectOption>();
options.add(new SelectOption('WHERE e.id != null','All'));
options.add(new SelectOption('WHERE e.Incoming = true AND e.Status = \'0\' ','Incoming Unread'));
options.add(new SelectOption('WHERE e.Incoming = true AND e.Status = \'1\' ','Incoming Read'));
options.add(new SelectOption('WHERE e.Incoming = true','All Incoming'));
options.add(new SelectOption('WHERE e.Incoming = false','All Outgoing'));
options.add(new SelectOption('WHERE e.ToAddress = \'support@x2od.com\'','Support Queue'));
options.add(new SelectOption('WHERE e.ToAddress = \'support2@x2od.com\'','Support Queue2')); //etc.
return options;
}
public String getSortDirection() {
//if not column is selected
if (sortExpression == null || sortExpression == '')
return 'ASC';
else
return sortDirection;
}
public void setSortDirection(String value) {
sortDirection = value;
}
public List<EmailMessage> getMessages() {
return messages;
}
public PageReference ViewData() {
//build the full sort expression
string sortFullExp = sortExpression + ' ' + sortDirection;
//query the database based on the sort expression
messages = Database.query('Select e.FromAddress, e.Parent.ContactId, e.Parent.Contact.Name, e.Parent.Account.Name, e.ToAddress, e.Parent.CaseNumber, e.Parent.AccountId, e.TextBody, e.SystemModstamp, e.Subject, e.Status, e.ParentId, e.MessageDate, e.LastModifiedDate, e.LastModifiedById, e.IsDeleted, e.Incoming, e.Id, e.HtmlBody, e.Headers, e.HasAttachment, e.FromName, e.CreatedDate, e.CreatedById, e.CcAddress, e.BccAddress, e.ActivityId From EmailMessage e ' + wheretext + ' order by ' + sortFullExp + ' limit 1000');
return null;
}
}
And here's the Visualforce Page:
<apex:page controller="EmailMessageController" action="{!ViewData}">
<apex:sectionHeader title="Email Messages" subtitle=""></apex:sectionHeader>
<apex:pageblock id="emailblock">
<apex:facet name="header">
<apex:form>
<apex:panelGrid styleClass="list"
columnClasses="pbTitle,pbButton,pbHelp" columns="3" border="0"
cellpadding="0" cellspacing="0">
<apex:outputLabel><h3>Messages</h3></apex:outputLabel>
<apex:commandButton value=" Refresh " styleClass="btn"
action="{!ViewData}" rerender="emailblock"></apex:commandButton>
<apex:SelectList value="{!wheretext}" size="1" id="controllerselectlist">
<apex:actionSupport event="onchange" action="{!ViewData}"
reRender="emailblock"></apex:actionSupport>
<apex:selectOptions value="{!views}" />
</apex:SelectList>
</apex:panelGrid>
</apex:form>
</apex:facet>
<apex:form>
<apex:pageblocktable value="{!Messages}" var="e" id="emailtable"
bgcolor="#F3F3EC" styleClass="list" rowClasses="dataRow"
onRowMouseOver="hiOn(this);" onRowMouseOut="hiOff(this);">
<apex:column>
<apex:facet name="header">
<apex:commandLink action="{!ViewData}"
value="{!$ObjectType.EmailMessage.fields.Subject.label}{!IF(sortExpression=='Subject',
IF(sortDirection='ASC','▼','▲'),'')}">
<apex:param value="Subject" name="column"
assignTo="{!sortExpression}"></apex:param>
</apex:commandLink>
</apex:facet>
<apex:outputLink value="/{!e.Id}" target="_blank">{!e.Subject}</apex:outputLink>
</apex:column>
<apex:column>
<apex:facet name="header">
{!$ObjectType.Contact.fields.Name.label}
</apex:facet>
<apex:outputLink value="/{!e.Parent.ContactId}" target="_blank"
rendered="{!IF(e.Parent.ContactId != '',true,false)}">{!e.FromName}</apex:outputLink>
<apex:outputtext value="{!e.FromName}"
rendered="{!IF(e.Parent.ContactId != '',false,true)}" />
</apex:column>
<apex:column>
<apex:facet name="header">
{!$ObjectType.Account.fields.Name.label}
</apex:facet>
<apex:outputLink value="/{!e.Parent.AccountId}" target="_blank">{!e.Parent.Account.Name}</apex:outputLink>
</apex:column>
<apex:column value="{!e.FromAddress}">
<apex:facet name="header">
<apex:commandLink action="{!ViewData}"
value="{!$ObjectType.EmailMessage.fields.FromAddress.label}{!IF(sortExpression=='FromAddress',
IF(sortDirection='ASC','▼','▲'),'')}">
<apex:param value="FromAddress" name="column"
assignTo="{!sortExpression}"></apex:param>
</apex:commandLink>
</apex:facet>
</apex:column>
<apex:column value="{!e.Status}">
<apex:facet name="header">
<apex:commandLink action="{!ViewData}"
value="{!$ObjectType.EmailMessage.fields.Status.label}{!IF(sortExpression=='Status',
IF(sortDirection='ASC','▼','▲'),'')}">
<apex:param value="Status" name="column"
assignTo="{!sortExpression}"></apex:param>
</apex:commandLink>
</apex:facet>
</apex:column>
<apex:column value="{!e.MessageDate}">
<apex:facet name="header">
<apex:commandLink action="{!ViewData}"
value="{!$ObjectType.EmailMessage.fields.MessageDate.label}{!IF(sortExpression=='MessageDate',
IF(sortDirection='ASC','▼','▲'),'')}">
<apex:param value="MessageDate" name="column"
assignTo="{!sortExpression}"></apex:param>
</apex:commandLink>
</apex:facet>
</apex:column>
<apex:column value="{!e.Incoming}">
<apex:facet name="header">
<apex:commandLink action="{!ViewData}"
value="{!$ObjectType.EmailMessage.fields.Incoming.label}{!IF(sortExpression=='Incoming',
IF(sortDirection='ASC','▼','▲'),'')}">
<apex:param value="Incoming" name="column"
assignTo="{!sortExpression}"></apex:param>
</apex:commandLink>
</apex:facet>
</apex:column>
<apex:column value="{!e.HasAttachment}">
<apex:facet name="header">
<apex:commandLink action="{!ViewData}"
value="{!$ObjectType.EmailMessage.fields.HasAttachment.label}{!IF(sortExpression=='HasAttachment',
IF(sortDirection='ASC','▼','▲'),'')}">
<apex:param value="HasAttachment" name="column"
assignTo="{!sortExpression}"></apex:param>
</apex:commandLink>
</apex:facet>
</apex:column>
<apex:column>
<apex:facet name="header">
<apex:commandLink action="{!ViewData}"
value="{!$ObjectType.Case.fields.CaseNumber.label}{!IF(sortExpression=='ParentId',
IF(sortDirection='ASC','▼','▲'),'')}">
<apex:param value="ParentId" name="column"
assignTo="{!sortExpression}"></apex:param>
</apex:commandLink>
</apex:facet>
<apex:outputLink value="/{!e.ParentId}">{!e.Parent.CaseNumber}</apex:outputLink>
</apex:column>
<apex:column value="{!e.ToAddress}">
<apex:facet name="header">
<apex:commandLink action="{!ViewData}"
value="{!$ObjectType.EmailMessage.fields.ToAddress.label}{!IF(sortExpression=='ToAddress',
IF(sortDirection='ASC','▼','▲'),'')}">
<apex:param value="ToAddress" name="column"
assignTo="{!sortExpression}"></apex:param>
</apex:commandLink>
</apex:facet>
</apex:column>
</apex:pageblocktable>
</apex:form>
</apex:pageblock>
</apex:page>
There are some other cool bits:
If there is no Case.Contact, the table will display the FromName, pulled from the email message.
An interesting point: You may notice that EmailStatus is presented in numerical form. For instance, Incoming Unread is 0, Incoming Read is 1, etc. The documentation, however, says, "Read only. The status of the email. For example, “New,” “Unread,” “Replied,” “Sent.”" So we're not sure of the exact mapping. 3 seems to be Sent, so 2 is probably Replied... but we're not sure.
Don't forget: EmailMessage has two lookups (foreign key): Case, and Activity. This Activity is the task created when Salesforce receives the email, and is - according to the documentation - assigned to the Case Owner. We're not sure what happens when the Case is owned by a Queue. Feel free to comment and share your experiences.
That's it! Enjoy.












