my $hold_handle_result;
$hold_handle_result = $self->handle_hold_update($action) if $action;
- my $holds_object = $self->fetch_user_holds($hold_id ? [$hold_id] : undef, 0, 1, $available, $limit, $offset);
+ my $holds_object;
+ if ($self->cgi->param('sort') ne "") {
+ $holds_object = $self->fetch_user_holds($hold_id ? [$hold_id] : undef, 0, 1, $available);
+ }
+ else {
+ $holds_object = $self->fetch_user_holds($hold_id ? [$hold_id] : undef, 0, 1, $available, $limit, $offset);
+ }
+
if($holds_object->{holds}) {
$ctx->{holds} = $holds_object->{holds};
}
$ctx->{circ_history_limit} = $limit;
$ctx->{circ_history_offset} = $offset;
- my $circ_ids = $e->json_query({
+ my $circ_ids;
+ if ($self->cgi->param('sort') ne "") { # Defer limitation to circ_history.tt2
+ $circ_ids = $e->json_query({
+ select => {
+ au => [{
+ column => 'id',
+ transform => 'action.usr_visible_circs',
+ result_field => 'id'
+ }]
+ },
+ from => 'au',
+ where => {id => $e->requestor->id}
+ });
+
+ } else {
+ $circ_ids = $e->json_query({
select => {
au => [{
column => 'id',
where => {id => $e->requestor->id},
limit => $limit,
offset => $offset
- });
+ });
+ }
$ctx->{circs} = $self->fetch_user_circs(1, [map { $_->{id} } @$circ_ids]);
return Apache2::Const::OK;
{
font-weight: bold;
}
+}
+
+.sort_deemphasize {
+ font-weight: lighter;
+ font-size: 70%;
+}
[% PROCESS "opac/parts/header.tt2";
PROCESS "opac/parts/misc_util.tt2";
+ PROCESS "opac/parts/myopac/column_sort_support.tt2";
WRAPPER "opac/parts/myopac/base.tt2";
myopac_page = "circs"
limit = ctx.circ_history_limit;
<div id="acct_checked_tabs">
<div class="align">
- <a href='[% mkurl('circs') %]'>[% l("Current Items Checked Out") %]</a>
+ <a href='[% mkurl('circs',{},1) %]'>[% l("Current Items Checked Out") %]</a>
</div>
<div class="align selected">
<a href="#">[% l("Check Out History") %]</a>
</div>
</div>
+ [%
+ # In the sorting case, the size is the size of ALL the circ items. In the non-sorting case,
+ # the size is simply the size of the chunk passed in. See the TODO below for the still-lingering
+ # bug.
+ sort_field = CGI.param('sort');
+ IF (sort_field);
+ no_next = ctx.circs.size - offset <= limit;
+ ELSE;
+ no_next = ctx.circs.size < limit;
+ END;
+ %]
+
<div class="header_middle">
<span class="float-left">[% l('Previously Checked Out Items') %]</span>
<span class='float-left' style='padding-left: 10px;'>
[% IF offset == 0 %] class='invisible' [% END %]><span class="nav_arrow_fix">◄</span>[% l('Previous') %]</a>
[%# TODO: get total to prevent paging off then end of the list.. %]
<a href='[% mkurl('circ_history', {limit => limit, offset => (offset + limit)}) %]'
- [% IF ctx.circs.size < limit %] class='invisible' [% END %] >[% l('Next') %]<span class="nav_arrow_fix">►</span></a>
+ [% IF no_next %] class='invisible' [% END %] >[% l('Next') %]<span class="nav_arrow_fix">►</span></a>
</span>
<div class="float-left">
<form action="[% mkurl(ctx.opac_root _ '/myopac/circ_history/export') %]" method="post">
title="[% l('History of Items Checked Out') %]">
<thead>
<tr>
- <th>[% l('Title / Author') %]</th>
- <th>[% l('Checkout Date') %]</th>
- <th>[% l('Due Date') %]</th>
- <th>[% l('Date Returned') %]</th>
- <th>[% l('Barcode') %]</th>
- <th>[% l('Call Number') %]</th>
+ <th>[% sort_head("sort_title", l("Title")) %]</th>
+ <th>[% sort_head("author", l("Author")) %]</th>
+ <th>[% sort_head("checkout", l("Checkout Date")) %]</th>
+ <th>[% sort_head("due", l("Due Date")) %]</th>
+ <th>[% sort_head("returned", l("Date Returned")) %]</th>
+ <th>[% sort_head("barcode", l("Barcode")) %]</th>
+ <th>[% sort_head("callnum", l("Call Number")) %]</th>
</tr>
</thead>
<tbody>
- [% FOR circ IN ctx.circs;
- attrs = {marc_xml => circ.marc_xml};
- PROCESS get_marc_attrs args=attrs; %]
+ [%# Copy the ctx.circs into a local array, then add a SORT field
+ that contains the value to sort on. Since we need the item attrs,
+ invoke it and save the result in ATTRS.
+ %]
+ [%
+ circ_items = ctx.circs; # Array assignment
+
+ FOR circ IN circ_items;
+ circ.ATTRS = {marc_xml => circ.marc_xml};
+ PROCESS get_marc_attrs args=circ.ATTRS;
+
+ SWITCH sort_field;
+
+ CASE "sort_title";
+ circ.SORTING = circ.ATTRS.sort_title;
+
+ CASE "author";
+ circ.SORTING = circ.ATTRS.author;
+
+ CASE "checkout";
+ circ.SORTING = circ.circ.xact_start;
+
+ CASE "due";
+ circ.SORTING = circ.circ.due_date;
+
+ CASE "returned";
+ circ.SORTING = circ.circ.checkin_time;
+
+ CASE "barcode";
+ circ.SORTING = circ.circ.target_copy.barcode;
+
+ CASE "callnum";
+ circ.SORTING = circ.circ.target_copy.call_number.label;
+
+ CASE;
+ sort_field = "";
+ END; # SWITCH
+ END; #FOR circ
+
+ IF (sort_field != "sort_title");
+ deemphasize_class = "";
+ ELSE;
+ deemphasize_class = " class=\"sort_deemphasize\"";
+ END;
+
+ # Apply sorting to circ_items
+ IF (sort_field);
+ circ_items = circ_items.sort("SORTING");
+ IF (CGI.param("sort_type") == "desc");
+ circ_items = circ_items.reverse;
+ END;
+
+ # Shorten the circ_items list per offset/limit/cout
+ hi = offset + limit - 1;
+ hi = hi > circ_items.max ? circ_items.max : hi;
+
+ circ_items = circ_items.slice(offset, hi);
+ END;
+
+ # circ_items list is now sorted. Traverse and dump the information.
+
+ FOR circ IN circ_items; %]
<tr>
<td>
- <a href="[% mkurl(ctx.opac_root _ '/record/' _ circ.circ.target_copy.call_number.record.id) %]"
- [% html_text_attr('title', l('Catalog record [_1]', attrs.title)) %]>
- [% attrs.title | html %]
- </a>
- [% IF attrs.author %] /
+ <a href="[% mkurl(ctx.opac_root _ '/record/' _
+ circ.circ.target_copy.call_number.record.id, {}, 1) %]"
+ name="[% l('Catalog record') %]"><span[%- deemphasize_class -%]>
+ [%- circ.ATTRS.title.substr(0,circ.ATTRS.nonfiling_characters) | html %]</span>
+ [%- circ.ATTRS.title.substr(circ.ATTRS.nonfiling_characters) | html %]</a>
+ </td>
+ <td>
<a href="[% mkurl(ctx.opac_root _ '/results',
- {qtype => 'author', query => attrs.author.replace('[,\.:;]', '')}
- )%]">[% attrs.author | html %]</a>
- [% END %]
+ {qtype => 'author', query => circ.ATTRS.author.replace('[,\.:;]', '')},
+ 1
+ ) %]">[% circ.ATTRS.author | html %]</a>
</td>
<td>
[% date.format(ctx.parse_datetime(circ.circ.xact_start),DATE_FORMAT); %]
[% date.format(ctx.parse_datetime(circ.circ.due_date),DATE_FORMAT); %]
</td>
<td>
- [%
- IF circ.circ.checkin_time;
+ [% IF circ.circ.checkin_time;
date.format(ctx.parse_datetime(circ.circ.checkin_time),DATE_FORMAT);
ELSE; %]
<span style='color:blue;'>*</span><!-- meh -->
[% PROCESS "opac/parts/header.tt2";
PROCESS "opac/parts/misc_util.tt2";
+ PROCESS "opac/parts/myopac/column_sort_support.tt2";
WRAPPER "opac/parts/myopac/base.tt2";
myopac_page = "circs" %]
<h3 class="sr-only">[% l('Current Items Checked Out') %]</h3>
<a href="#">[% l("Current Items Checked Out") %]</a>
</div>
<div class="align">
- <a href="[% mkurl('circ_history') %]">[% l("Check Out History") %]</a>
+ <a href="[% mkurl('circ_history',{},1) %]">[% l("Check Out History") %]</a>
</div>
</div>
onclick="var inputs=document.getElementsByTagName('input'); for (i = 0; i < inputs.length; i++) { if (inputs[i].name == 'circ' && !inputs[i].disabled) inputs[i].checked = this.checked;}"
/>
</th>
- <th>[% l('Title / Author') %]</th>
- <th>[% l('Renewals Left') %]</th>
- <th>[% l('Due Date') %]</th>
- <th>[% l('Barcode') %]</th>
- <th>[% l('Call number') %]</th>
+ <th>[% sort_head("sort_title", l("Title")) %]</th>
+ <th>[% sort_head("author", l("Author")) %]</th>
+ <th>[% sort_head("renews", l("Renewals Left")) %]</th>
+ <th>[% sort_head("due", l("Due Date")) %]</th>
+ <th>[% sort_head("barcode", l("Barcode")) %]</th>
+ <th>[% sort_head("callnum", l("Call number")) %]</th>
</tr>
</thead>
- <tbody>
- [% FOR circ IN ctx.circs;
- attrs = {marc_xml => circ.marc_xml};
- PROCESS get_marc_attrs args=attrs; %]
+ <tbody>
+ [%# Copy the ctx.circs into a local array, then add a SORT field
+ that contains the value to sort on. Since we need the item attrs,
+ invoke it and save the result in ATTRS.
+ %]
+ [%
+ circ_items = ctx.circs; # Array assignment
+
+ sort_field = CGI.param('sort'); # unless changed below...
+
+ FOR circ IN circ_items;
+ circ.ATTRS = {marc_xml => circ.marc_xml};
+ PROCESS get_marc_attrs args=circ.ATTRS;
+
+ SWITCH sort_field;
+
+ CASE "sort_title";
+ circ.SORTING = circ.ATTRS.sort_title;
+
+ CASE "author";
+ circ.SORTING = circ.ATTRS.author;
+
+ CASE "renews";
+ circ.SORTING = circ.circ.renewal_remaining;
+
+ CASE "due";
+ circ.SORTING = circ.circ.due_date;
+
+ CASE "barcode";
+ circ.SORTING = circ.circ.target_copy.barcode;
+
+ CASE "callnum";
+ circ.SORTING = circ.circ.target_copy.call_number.label;
+
+ CASE;
+ sort_field = "";
+ END; # SWITCH
+ END; #FOR circ
+
+ IF (sort_field != "sort_title");
+ deemphasize_class = "";
+ ELSE;
+ deemphasize_class = " class=\"sort_deemphasize\"";
+ END;
+
+ # Apply sorting to circ_items
+ IF (sort_field);
+ circ_items = circ_items.sort("SORTING");
+ IF (CGI.param("sort_type") == "desc");
+ circ_items = circ_items.reverse;
+ END;
+ END;
+
+ # circ_items list is now sorted. Traverse and dump the information.
+
+ FOR circ IN circ_items; %]
<tr>
<td class="checkbox_column" valign="top">
<input type="checkbox" name="circ"
[% IF circ.circ.renewal_remaining < 1 %] disabled="disabled" [% END %]
value="[% circ.circ.id %]" />
</td>
- <td name="author">
+ <td name="title">
[% IF circ.circ.target_copy.call_number.id == -1 %]
[% circ.circ.target_copy.dummy_title | html %]
- [% ELSIF attrs.title %]
- <a href="[% mkurl(ctx.opac_root _ '/record/' _
- circ.circ.target_copy.call_number.record.id) %]"
- name="[% l('Catalog record') %]">[% attrs.title | html %]</a>
+ [% ELSIF circ.ATTRS.title %]
+ <a href="[% mkurl(ctx.opac_root _ '/record/' _
+ circ.circ.target_copy.call_number.record.id, {}, 1) %]"
+ name="[% l('Catalog record') %]"><span[%- deemphasize_class -%]>
+ [%- circ.ATTRS.title.substr(0,circ.ATTRS.nonfiling_characters) | html %]</span>
+ [%- circ.ATTRS.title.substr(circ.ATTRS.nonfiling_characters) | html %]</a>
[% END %]
- [% IF circ.circ.target_copy.call_number.id == -1 %] /
+ </td>
+ <td name="author">
+ [% IF circ.circ.target_copy.call_number.id == -1 %]
[% circ.circ.target_copy.dummy_author | html %]
- [% ELSIF attrs.author %] /
- <a href="[% mkurl(ctx.opac_root _ '/results',
- {qtype => 'author', query => attrs.author.replace('[,\.:;]', '')}
- ) %]">[% attrs.author | html %]</a>
+ [% ELSIF circ.ATTRS.author %]
+ <a href="[% mkurl(ctx.opac_root _ '/results',
+ {qtype => 'author', query => circ.ATTRS.author.replace('[,\.:;]', '')},
+ 1
+ ) %]">[% circ.ATTRS.author | html %]</a>
[% END %]
</td>
<td name="renewals">
</span>
</td>
</tr>
- [% END;
+ [% END; # FOR
+
END %]
</tbody>
</table>
[% PROCESS "opac/parts/header.tt2";
PROCESS "opac/parts/misc_util.tt2";
PROCESS "opac/parts/hold_status.tt2";
+ PROCESS "opac/parts/myopac/column_sort_support.tt2";
WRAPPER "opac/parts/myopac/base.tt2";
myopac_page = "holds";
limit = (ctx.holds_limit.defined) ? ctx.holds_limit : 0;
<a href='#'>[% l("Items on Hold") %]</a>
</div>
<div class="align">
- <a href='[% mkurl('hold_history', {}, ['limit','offset','available']) %]'>[% l("Holds History") %]</a>
+ <a href='[% mkurl('hold_history', {}, ['limit','offset','available','sort','sort_type']) %]'>[% l("Holds History") %]</a>
</div>
</div>
[% IF count <= limit + offset %] class='invisible' [% END %] >[% l('Next') %]<span class="nav_arrow_fix">►</span></a>
</span>
- <span style="float:right;">
- <a class="hide_me" href="#">[% l('Export List') %]</a>
- </span>
</div>
<div class="clear-both"></div>
<div id='holds_main'>
<input type="checkbox" title="[% l('Select All Holds') %]"
onclick="var inputs=document.getElementsByTagName('input'); for (i = 0; i < inputs.length; i++) { if (inputs[i].name == 'hold_id' && !inputs[i].disabled) inputs[i].checked = this.checked;}"/>
</th>
- <th>[% l('Title') %]</th>
- <th>[% l('Author') %]</th>
- <th>[% l('Format') %]</th>
+ <th>[% sort_head("sort_title", l('Title')) %]</th>
+ <th>[% sort_head("author", l('Author')) %]</th>
+ <th>[% sort_head("format", l('Format')) %]</th>
<th>[% l('Pickup Location') %]</th>
<th>[% l('Activate') %]</th>
<th>[% l('Cancel if not filled by') %]</th>
</tr>
</thead>
<tbody id="holds_temp_parent">
- [% FOR hold IN ctx.holds;
- attrs = {marc_xml => hold.marc_xml};
- PROCESS get_marc_attrs args=attrs;
+
+ [%# Copy the ctx.holds into a local array, then add a SORT field
+ that contains the value to sort on. Since we need the item attrs,
+ invoke it and save the result in ATTRS.
+ %]
+ [%
+ hold_items = ctx.holds;
+
+ sort_field = CGI.param('sort');
+
+ FOR hold IN hold_items;
+ hold.ATTRS = {marc_xml => hold.marc_xml};
+ PROCESS get_marc_attrs args=hold.ATTRS;
+
+ SWITCH sort_field;
+
+ CASE "sort_title";
+ hold.SORTING = hold.ATTRS.sort_title;
+
+ CASE "author";
+ hold.SORTING = hold.ATTRS.author;
+
+ CASE "format";
+ hold.SORTING = hold.ATTRS.format_label;
+
+ CASE;
+ sort_field = "";
+ END; # SWITCH
+ END; #FOR hold
+
+ IF (sort_field != "sort_title");
+ deemphasize_class = "";
+ ELSE;
+ deemphasize_class = " class=\"sort_deemphasize\"";
+ END;
+
+ # Apply sorting to hold_items
+ IF (sort_field != "");
+ hold_items = hold_items.sort("SORTING");
+ IF (CGI.param("sort_type") == "desc");
+ hold_items = hold_items.reverse;
+ END;
+
+ # Shorten the hold_items list per offset/limit/count
+ hi = offset + limit - 1;
+ hi = hi > hold_items.max ? hold_items.max : hi;
+
+ hold_items = hold_items.slice(offset, hi);
+ END;
+
+ # hold_items list is now sorted. Traverse and dump the information.
+
+ FOR hold IN hold_items;
ahr = hold.hold.hold %]
<tr name="acct_holds_temp"
class="acct_holds_temp[% ahr.frozen == 't' ? ' inactive-hold' : '' %]">
</td>
<td>
<div>
- [%
- title = attrs.title;
- IF ahr.hold_type == 'P';
- title = l('[_1] ([_2])', title, hold.hold.part.label);
- END;
- %]
- <a href="[% mkurl(ctx.opac_root _ '/record/' _ hold.hold.bre_id) %]">[% title | html %]</a>
+ [% title = hold.ATTRS.title;
+ IF ahr.hold_type == 'P';
+ title = l('[_1] ([_2])', title, hold.hold.part.label);
+ END; %]
+
+ <a href="[% mkurl(ctx.opac_root _ '/record/' _
+ hold.hold.bre_id, {}, 1) %]"
+ name="[% l('Catalog record') %]"><span[%- deemphasize_class -%]>
+ [%- title.substr(0,hold.ATTRS.nonfiling_characters) | html %]</span>
+ [%- title.substr(hold.ATTRS.nonfiling_characters) | html %]</a>
</div>
</td>
<td>
<div>
<a href="[% mkurl(ctx.opac_root _ '/results',
- {qtype => 'author', query => attrs.author.replace('[,\.:;]', '')}
- ) %]">[% attrs.author | html %]</a>
+ {qtype => 'author', query => hold.ATTRS.author.replace('[,\.:;]', '')},
+ 1
+ ) %]">[% hold.ATTRS.author | html %]</a>
</div>
</td>
<td>
<div class="format_icon">
[%
- formats = attrs.all_formats;
+ formats = hold.ATTRS.all_formats;
IF ahr.hold_type == 'M';
# only show selected formats for metarecords
formats = [];
titresults = xml.findnodes('//*[@tag="245"]/*[@code="a" or @code="b" or @code="n" or @code="p"]');
titresults_content = [];
FOR sub IN titresults; titresults_content.push(sub.textContent); END;
+
args.title = titresults_content.join(" ");
# Avoid ugly trailing syntax on brief titles
args.title = args.title | replace('[:;/]$', '');
END;
args.title_extended = (args.titles.size) ? args.titles.0 : '';
+ # Create a version of the title designed for sorted displays.
+ args.sort_title = args.title | upper;
+
+ # If the title has a "non-filing chaaracters"
+ # (to logically remove leading "The " for example)
+ # chop the title. Otherwise, chop until the first alphanumeric.
+ # BTW: Template Toolkit folds 1-element arrays to scalars!
+ title_node = xml.findnodes('//*[@tag="245"]');
+
+ args.nonfiling_characters = title_node.findvalue('@ind2');
+
+ IF (args.nonfiling_characters > 0);
+ args.sort_title = args.sort_title.substr(args.nonfiling_characters);
+ ELSE;
+ args.sort_title = args.sort_title.replace('^[^A-Z0-9]*','');
+ END;
+
args.pubplaces = [];
pubplace_hunt = xml.findnodes('//*[@tag="260"]/*[@code="a"]') ||
xml.findnodes('//*[@tag="264" and @ind2="1"]/*[@code="a"]');
ELSE;
cls_which = "acct-tab-off";
END -%]
- <a href="[% mkurl(ctx.opac_root _ '/myopac/' _ page.url, {}, ['bbid', 'offset', 'limit']) %]"
+ <a href="[% mkurl(ctx.opac_root _ '/myopac/' _ page.url, {}, ['bbid', 'offset', 'limit','sort','sort_type']) %]"
class="[% cls_which %]">[% page.name; %]</a>
[% END %]
</div>
--- /dev/null
+[%# Produce a URL for a given field that cycles for sorting from
+ "nothing" to "ascending" to "descending" then back to "nothing".
+%]
+[% MACRO sort_url(field)
+ IF (CGI.param('sort') == field);
+ SWITCH CGI.param('sort_type');
+ CASE "asc";
+ mkurl('',{sort=>field, sort_type=>'desc'},1);
+ CASE "desc";
+ mkurl('',{},1);
+ CASE;
+ mkurl('',{sort=>field, sort_type=>'asc'}, 1);
+ END;
+ ELSE;
+ mkurl('',{sort=>field, sort_type=>'asc'}, 1);
+ END;
+%]
+[%# SET click_sort = l("click to sort") %]
+[%# SET click_sort = "title=\"$click_sort\"" %]
+
+[%# Produce arrows to indicate the sorting status of the column %]
+[% MACRO sort_indicator(field)
+ IF (CGI.param('sort') == field);
+ SWITCH CGI.param('sort_type');
+ CASE "asc";
+" <span class=\"column_sort_arrow\">▲</span>";
+ CASE "desc";
+" <span class=\"column_sort_arrow\">▼</span>";
+ END;
+ END;
+%]
+
+[%# Column headers for sortable columns %]
+[% MACRO sort_head(field, field_label)
+ BLOCK %]
+<a href="[% sort_url(field) %]" [% click_sort %]>[% l(field_label) %]</a>[%- sort_indicator(field) %]
+[% END
+%]
</div>
[% END %]
</div>
- <a href="[% mkurl(ctx.opac_root _ '/myopac/main', {}, ['single', 'message_id']) %]"
+ <a href="[% mkurl(ctx.opac_root _ '/myopac/main', {}, ['single', 'message_id', 'sort','sort_type']) %]"
class="opac-button">[% l('My Account') %]</a>
<a href="[% mkurl(ctx.opac_root _ '/myopac/lists', {}, ['single', 'message_id']) %]"
class="opac-button">[% l('My Lists') %]</a>
</div>
<div id="dashboard">
<span class="dash-align">
- <a class="dash-link" href="[% mkurl(ctx.opac_root _ '/myopac/circs', {}, ['limit','offset', 'single', 'message_id'])
+ <a class="dash-link" href="[% mkurl(ctx.opac_root _ '/myopac/circs', {},
+ ['limit','offset', 'single', 'message_id', 'sort','sort_type'])
%]"><span id="dash_checked">[% ctx.user_stats.checkouts.total_out
%]</span> [% l("Checked Out") %]</a>
</span>
<span class="dash_divider">|</span>
<span class="dash-align">
- <a class="dash-link" href="[% mkurl(ctx.opac_root _ '/myopac/holds', {}, ['available', 'single', 'message_id'])
+ <a class="dash-link" href="[% mkurl(ctx.opac_root _ '/myopac/holds', {},
+ ['available', 'single', 'message_id', 'sort','sort_type'])
%]"><span id="dash_holds">[% ctx.user_stats.holds.total
%]</span> [% l("On Hold") %]</a>
</span>
<span class="dash_divider">|</span>
<span class="dash-align">
<a class="dash-link" href="[% mkurl(ctx.opac_root _ '/myopac/holds',
- {available => 1}, ['single', 'message_id']) %]"><span id="dash_pickup">[%
+ {available => 1}, ['single', 'message_id', 'sort','sort_type']) %]"><span id="dash_pickup">[%
ctx.user_stats.holds.ready %]</span> [% l("Ready for Pickup") %]</a>
</span>
<span class="dash_divider">|</span>
<span class="dash-align">
- <a class="dash-link" href="[% mkurl(ctx.opac_root _ '/myopac/main', {}, ['single', 'message_id'])
+ <a class="dash-link" href="[% mkurl(ctx.opac_root _ '/myopac/main', {}, ['single', 'message_id', 'sort','sort_type'])
%]"><span id="dash_fines">[% money(ctx.user_stats.fines.balance_owed)
%]</span> [% l("Fines") %]</a>
</span>
--- /dev/null
+Column sorting in circulation screens
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Sorting of selected columns is now available in the *Items Checked Out*, *Check Out History*,
+and *Holds* screen.
+
+* Clicking on the appropriate column heads now sorts the contents from
+``ascending'' to ``descending'' to ``no sort''. (The ``no sort'' restores the
+original list as presented in the screen.)
+
+* The sort indicator (an up or down arrow) is placed to the right
+of the column head, as appropriate.
+
+* The combined *Title/Author* column in the *Items Checked Out* screen is now separated into two
+independently sortable columns (Title and Author).
+
+* Title sorting is done with the so-called `filing' characters (leading ``the'', ``a'',
+``an'', and other langugage equivalents) removed. The leading articles are rendered in
+a smaller font, so as to keep the main entry prominent. In
+addition to the filing characters removed for the sort, leading
+non-alphanumeric characters are ignored in the sort.