I’ve wanted to redesign the Highrise sidebars for a long time. They’ve felt cluttered and messy to me, and as we add more features to Highrise the mess will only multiply. So I was glad to have the chance this week to redesign the sidebar modules. The visual side of the redesign was straightforward, but implementing the design in code required a few tricks. Here’s a look behind the scenes at the coding decisions we made for the new Highrise sidebars.
“Subjects” in Highrise
Which sidebar modules am I talking about? In Highrise you can keep track of People, Companies, and Cases. These all have the same basic code and UI. You can keep notes about them, set tasks for the future, and manage some common types of metadata. Since People, Companies and Cases share so much plumbing, we’ve abstracted them as subjects
. A subject
is anything in Highrise that you can attach notes and tasks to. When you look at a subject
’s page, you see a sidebar with some modules for adding or editing metadata such as contact information, background information (a kind of static text description), dates to remember for that subject, and more. The screenshot below shows a subject
page with the sidebar modules highlighted.
Redesigning the modules
Each module has a header like “Contact Bob” or “Dates to remember” and data below. In the original design, modules can be either “active” or “empty” based on whether they have any data in them. Empty modules have a grey header and an “add” link floated right. Active modules have a light blue header and an “edit” link on the right. We made this distinction so your eye would more easily catch active modules when you’re looking for information. The idea was good, but the original implementation looked messy with its mix of grey and blue, scattered red action links, and lack of separation between modules.
For the first redesign (above) we cleaned up the modules. Active modules are now wrapped entirely in a light grey box with a tiny drop shadow. We killed the blue header style, relying instead on the space between modules to separate them. Empty modules no longer have a header. They are grey boxes collapsed down to a single link to add the content relevant to that module. Finally we replaced all the red links with grey links in order to put the focus on the data within active modules rather than all the possible actions. One last tweak: we changed the text for “About [subject’s name]” to “Add background information.” We’ve gone back and forth a number of times on the language for this feature, and at this stage we decided to try “background info” on for size again.
The first redesign was a big improvement. But we didn’t like the way active and empty modules looked mixed together. The dim bar in between those two active modules creates a kind of striped look that we want to avoid. The problem was worse on subjects
with more sidebar modules, like companies or cases. So we decided to group all the active modules together on the top, and then group the empty modules on the bottom. The result is much cleaner, and it’s easier to scan when you load up a subject
in order to quickly grab some info like an email address or birthday.
The re-ordered sidebar was a winner. But it came at a price. We couldn’t just change the CSS and call it a day. Now we also had to write code to re-order the sidebar modules dynamically based on whether they were empty or active. Ruby’s power and flexibility really came in handy for this job.
The code
I said earlier that people, companies, and cases are handled by the same plumbing because we abstracted them as subjects
. The result of this abstraction is that whether you are looking at a person, a company or a case, the sidebar is rendered by the same template: subjects/_sidebar.rhtml
.
(This kind of “view polymorphism” has been subject to a lot of internal debate since we first released the app. It makes maintenance both easier and harder because the code has less repetition on one hand but on the other it is less intention-revealing due to the abstractions and indirection.)
This is what the original template code looked like to render the subject
sidebars:
in app/views/subjects/_sidebar.rhtml:
<% if @subject.is_a?(Party) %>
<%= render(:partial => 'parties/contact_info') %>
<% end %>
<% if show_company_contact_info?(@subject) %>
<%= render(:partial => 'parties/contact_info', :object => @subject.company) %>
<% end %>
<%= render :partial => 'backgrounds/show' %>
<%= render :partial => 'contact_dates/index' %>
<% if @subject.is_a?(Kase) %>
<%= render :partial => 'kases/parties' %>
<% end %>
<% if @subject.is_a?(Company) %>
<%= render :partial => 'companies/people' %>
<% end %>
Don’t worry too much about the individual partials and conditions. The key point is that each partial is a sidebar module, and each module is conditioned based on the particular subject
we are rendering. A different mixture of partials will be rendered depending on whether the subject
is a person, a company or a case, but they’ll always render in the same order.
We want to re-order these partials dynamically based on whether each module is active or empty. That means we need to represent the possible partials, the conditions for displaying them, and also the conditions for determining whether they are active or empty within some kind of data structure. So we popped open our Rails subjects_helper.rb
and represented this information in an array.
in app/views/helpers/subjects_helper.rb:
def sidebar_modules_to_sort
returning [] do |m|
# partial to render module_is_active? options render the module for this subject?
m << ['parties/contact_info' , show_contact_info_module_on_top?, {} ] if @subject.is_a?(Party)
m << ['parties/contact_info' , true , {:object => @subject.company} ] if show_company_contact_info?(@subject)
#necessarily true per the condition at right
m << ['backgrounds/show' , [email protected]? , {} ]
m << ['contact_dates/index' , @contact_dates.any? , {} ]
m << ['collections/parties' , @subject.parties.any? , {} ] if looking_at_collection?
m << ['companies/people' , @subject.people.any? , {} ] if @subject.is_a?(Company)
end
end
The helper method sidebar_modules_to_sort
returns a parent array full of child arrays, one for each module with an element for the template path, a true/false value to show if it is active, and an options hash for the render method. The conditions that used to determine whether each partial should be rendered now determine whether each child array should be included in the parent array. Thanks to that boolean in the second element of each child array, we can partition the parent array into two groups: those where the second element which represents that the module is ‘active’ are true, and those were that element is false. We use another helper method to partition and reassemble the array into groups.
in app/views/helpers/subjects_helper.rb:
def sidebar_modules_in_order
active_group, empty_group = sidebar_modules_to_sort.partition {|m| m[1]}
active_group.concat empty_group
end
Finally we return to our sidebar template to do the actual rendering.
in app/views/subjects/_sidebar.rhtml:
<%= sidebar_modules_in_order.map {|m| render sidebar_module_partial(m)}.join %>
This line in the template takes the sorted array of sidebar modules and replaces each element in the array with the rendered partial. Then the join
method converts each element to a string and concatenates them. sidebar_module_partial
is a call to one more helper. This helper assembles the arguments for render
out of the elements provided in the array. It looks like this:
in app/helpers/subjects_helper.rb:
def sidebar_module_partial(m)
m[2].merge({:partial => m[0]})
end
In the snippet above, sidebar_module_partial
takes the third element of each module array, which is either an empty hash or some special options for render
, and merges a key specifying the template path onto that hash.
We definitely could’ve hidden these rendering gymnastics behind a helper, perhaps called render_sidebar_modules
or something similar. However we’ve decided for style reasons to avoid calling render
from within our helpers. Therefore we decided to use a helper to merely fill in the arguments to the call to render
within the template itself.
In the end, we have a new sidebar design and some clean and intention-revealing code. This was a fun chance for me to expand my Ruby knowledge by dipping into the nuts and bolts of arrays and hashes. Thanks to Jamis for reviews and advice when I knew there had to be “a better way.” We hope you enjoy the new sidebar modules in Highrise.
Related: What belongs in a helper method?
Eugene
on 28 Aug 08Nice job! Any plans for integration with Basecamp?
Rian
on 28 Aug 08So much easier to scan. thank you, thank you.
Richard Allum
on 28 Aug 08Nice work.
“as we add more features to Highrise the mess will only multiply”
tease ;-) I’m hoping you’ve got a pipeline/opportunity feature in mind.
MiSc
on 28 Aug 08another nice detail insight.
Andy
on 28 Aug 08I’m interested you are happy to have the edit links in grey. I assumed the original logic was to have one colour for inline/ajax links and one for traditional page links.
Richard Allum
on 28 Aug 08Having used HR today I really like the changes – do you think the same approach would enhance the tasks on the dashboard? maybe more emphasis on overdue ones which currently sit under a banner ‘upcoming tasks’ – doesn’t really read right.
Jay Owen
on 28 Aug 08We have used Basecamp for some time now and just started seriously with Highrise this week. I had gone back and forth about using it or a desktop app like Contactizer on the Mac. The main problem with the desktop app was that it had TOO MANY features and TOO MANY options. I am sure this is great for some, but so far the fresh, clean interface of Highrise has made the difference for us.
I know you hear it all the time – but it would be GREAT if this information synced with Basecamp and would save a lot of re-entry of contact information, companies, and people. Much easier said than done – but that would be the clincher to keep me with Highrise for life. Right now we are really in trial mode with it.
Tomas Jogin
on 28 Aug 08Great stuff. Your design iteration posts are informative as always. I wouldn’t mind more code examples from you guys, too.
Andrew Francis
on 28 Aug 08Excellent enhancement! Keep up the great work.
Duff OMelia
on 28 Aug 08I love posts like this. Thanks for sharing your thoughts about how each iterative design could be improved. Well done.
MikeInAZ
on 28 Aug 08The code examples are great. Love it.
Ryan
on 28 Aug 08golf clap very clever use of Arrays.
Justin Britten
on 28 Aug 08This is an excellent, technical post for your fellow Rails developers. You guys are frequently pushing the Rails envelope in your products—doing innovative things that make me wonder “how did they do that.” I’d love to see more posts like this from you guys. Perhaps this could be a theme for another blog in your portfolio.
Bob
on 28 Aug 08Surprised no one’s asked yet about the “Deals” tab on the screenshot.
CGregg
on 28 Aug 08Really appreciate all of the improvements since we started to use Highrise. I’m not that computer savvy but wonder if it will ever be possible to create permissions for multiple contacts. The ability to tag a group of people all at once has been GREAT.
Orhan Toy
on 28 Aug 08Nice post. I specially liked the part about ‘view polymorphism’. In many situations I’ve considered if I should make 2-3 similar models or 1 abstract model.
I would guess that your database structure reflects your models, so you probably have some kind of ‘type’ field in your ‘Subject’-model? Is it like if ‘type’ equals 1, then it’s a ‘Person’, if ‘type’ equals 2, then it’s a ‘Company’ and so on…?
Or do you have a second table/model ‘SubjectType’ that contains an id and name for every type but then you would have to have a foreign key ‘subject_type_id’ in your ‘Subject’-model.
What was your solution?
RS
on 28 Aug 08Orhan:
I generally stay out of the model so I wouldn’t be able to tell you how things work in the database. We don’t have a problem with polymorphism on the model side of things. Views can’t really be polymorphic in the strictly defined sense (thus the scare quotes). I meant “view polymorphism” to refer to situations where you try to maintain the abstractions of a polymorphism up into the layout of your view templates, such that the same template is being rendered for all the classes under the polymorphism.
What we have discovered is that following through with the same level of abstraction on the view produces templates that are hard to locate and — once you’ve found the right template — hard to understand.
For example, we use the same header partial for all the subjects in Highrise. But since each kind of subject has its own particularities, the partial has lots of conditions like `if @subject.is_a?(Party)′ and so on. Another trouble spot is the headlines of notes/emails/files in Highrise, which are abstracted as ‘recordings.’ The possible conditions are so gnarly for those headers that we ended up using a method object to build them. It seems to be the best solution so far, but still we feel uneasy with so much plumbing and indirection.
Recently we’ve experimented with some different conventions to clarify our templates when these abstractions are at play, but however you roll em, “polymorphic views” are going to present challenges that you don’t see on the model side.
Josh A.
on 28 Aug 08You guys never fail to disappoint. Like everyone else, I love the fact that you always pay so much attention to detail.
There are a hell of a lot of sites that don’t. And you’re always the exception. :) Not only does lack of attention to detail leave you with something that looks like a couple of drunks with hangovers knocked up in their sleep 20 years ago, but it’s also bad for business: I mean who the hell would want to visit “A place for friends”, when it’s so poorly coded, badly designed, and, well, rubbish (although to their credit, the redesign was marginally better).
Ok, MySpace rant over—and sadly it’s not just them. Anyway, awesome post!
Richard Allum
on 28 Aug 08Just spotted that Deals tab too – now I am excited. Salesforce renewal 3 weeks away – holding my breath!
Angela Booth
on 28 Aug 08Thanks for that – makes my favorite app even better.
Highrise is the first thing I look at in the morning, and the last app I check in the evening. I’m an addict.
I spent years searching for the “perfect PIM” and in Highrise, I found it.
Keep up the great work. :-)
Robb Smith
on 28 Aug 08I love the little touches you add to your products!
I too am wondering about integration with Basecamp. Our team finds itself bouncing back and forth, trying to decide when to promote/demote i.e. transfer a contact from one app to another. Combining the two products would rock my world.
Richard Allum
on 28 Aug 08Hi Ryan – sent a note to support about this but have checked it our further and …the empty blocks don’t drop to the bottom when viewing an email or note attached to a client. Work fine elsewhere.
Steven Hambleton
on 29 Aug 08I’d like to see a thread style of viewing companies and contacts -
Company —Contact —Contact
It makes it much easier to remove people or whole organisations in one go.
Also the ability to define how many contacts/companies are shown on one page (20/50/100 etc).
Steve Muir
on 29 Aug 08I haven’t got a clue about pretty much everything I read that you folk have talked about in here. But I do know good when I see it. I stumbled across this web app because I bought an iPhone and was looking for a cross platform management tool for my growing business … and I have been completely blown away by what this tool can do for me and my colleagues. It will grow with us!
I’m so new to the net that this is my first blog posting ever … but I figure if I am going to put myself ‘out there’ then Highrise gets the gurnsey for it. Well done team!
Orhan Toy
on 30 Aug 08RS: Could David or someone else answer my question about abstraction of models? It’s a topic I’ve been thinking about for quite a while and I would like to hear a proffesional’s idea…
Guntram
on 31 Aug 08Guys, don’t take it personally, but pretty much all “improvements” of this kind are little less than avoidance of the real issue: Integrate Basecamp & Highrise!!!!!
This discussion is closed.