summaryrefslogtreecommitdiff
path: root/rt
diff options
context:
space:
mode:
Diffstat (limited to 'rt')
-rw-r--r--rt/etc/initialdata19
-rw-r--r--rt/lib/RT/Search/UnrepliedTickets.pm69
-rw-r--r--rt/lib/RT/Ticket_Vendor.pm23
-rw-r--r--rt/share/html/Elements/CalendarSlotSchedule137
-rw-r--r--rt/share/html/Elements/CollectionAsTable/Row5
-rw-r--r--rt/share/html/Elements/QueueSummaryByLifecycle20
-rw-r--r--rt/share/html/Search/Schedule.html353
-rwxr-xr-xrt/share/html/Search/UnrepliedTickets.html156
-rwxr-xr-xrt/share/html/Ticket/ModifyAll.html6
-rw-r--r--rt/share/static/css/freeside3/ticket-lists.css14
-rw-r--r--rt/share/static/css/freeside4/ticket-lists.css13
11 files changed, 612 insertions, 203 deletions
diff --git a/rt/etc/initialdata b/rt/etc/initialdata
index 96255b5ed..825e6506f 100644
--- a/rt/etc/initialdata
+++ b/rt/etc/initialdata
@@ -106,9 +106,6 @@
{ Name => 'Open Tickets', # loc
Description => 'Open tickets on correspondence', # loc
ExecModule => 'AutoOpen' },
- { Name => 'Open Inactive Tickets', # loc
- Description => 'Open inactive tickets', # loc
- ExecModule => 'AutoOpenInactive' },
{ Name => 'Extract Subject Tag', # loc
Description => 'Extract tags from a Transaction\'s subject and add them to the Ticket\'s subject.', # loc
ExecModule => 'ExtractSubjectTag' },
@@ -801,9 +798,9 @@ Hour: { $SubscriptionObj->SubValue('Hour') }
# ScripCondition => 'On Correspond',
# ScripAction => 'Notify Requestors And Ccs',
# Template => 'Correspondence in HTML' },
- { Description => 'On Correspond Open Inactive Tickets',
+ { Description => 'On Correspond Open Tickets',
ScripCondition => 'On Correspond',
- ScripAction => 'Open Inactive Tickets',
+ ScripAction => 'Open Tickets',
Template => 'Blank' },
{ Description => 'On Create Autoreply To Requestors',
ScripCondition => 'On Create',
@@ -947,7 +944,17 @@ Hour: { $SubscriptionObj->SubValue('Hour') }
'on correspond' => {
'notify requestors and ccs' => { 'correspondence' => 1 },
'notify other recipients' => { 'correspondence' => 1 },
- }
+ # RT 4.2
+ # superseded by "notify owner and adminccs"
+ 'notify adminccs' => { 'admin correspondence' => 1 },
+ # the new way, but doesn't work right vs. "open tickets"
+ 'open inactive tickets' => { 'blank' => 1 },
+ },
+ 'on create' => {
+ # RT 4.2
+ # superseded by "notify owner and adminccs"
+ 'notify adminccs' => { 'transaction' => 1 },
+ },
);
# -*- perl -*-
diff --git a/rt/lib/RT/Search/UnrepliedTickets.pm b/rt/lib/RT/Search/UnrepliedTickets.pm
new file mode 100644
index 000000000..032898391
--- /dev/null
+++ b/rt/lib/RT/Search/UnrepliedTickets.pm
@@ -0,0 +1,69 @@
+=head1 NAME
+
+ RT::Search::UnrepliedTickets
+
+=head1 SYNOPSIS
+
+=head1 DESCRIPTION
+
+Find all unresolved tickets owned by the current user where the last
+correspondence from a requestor (or ticket creation) is more recent than the
+last correspondence from a non-requestor (if there is any).
+
+=head1 METHODS
+
+=cut
+
+package RT::Search::UnrepliedTickets;
+
+use strict;
+use warnings;
+use base qw(RT::Search);
+
+
+sub Describe {
+ my $self = shift;
+ return ($self->loc("Tickets awaiting a reply"));
+}
+
+sub Prepare {
+ my $self = shift;
+
+ my $TicketsObj = $self->TicketsObj;
+ # if SystemUser does this search (as in QueueSummaryByLifecycle), they
+ # should get all tickets regardless of ownership
+ if ($TicketsObj->CurrentUser->id != RT->SystemUser->id) {
+ $TicketsObj->Limit(
+ FIELD => 'Owner',
+ VALUE => $TicketsObj->CurrentUser->id
+ );
+ }
+ foreach my $status (qw(resolved rejected deleted)) {
+ $TicketsObj->Limit(
+ FIELD => 'Status',
+ OPERATOR => '!=',
+ ENTRYAGGREGATOR => 'AND',
+ VALUE => $status,
+ );
+ }
+ my $txn_alias = $TicketsObj->JoinTransactions;
+ $TicketsObj->Limit(
+ ALIAS => $txn_alias,
+ FIELD => 'Created',
+ OPERATOR => '>',
+ VALUE => 'COALESCE(main.Told,\'1970-01-01\')',
+ QUOTEVALUE => 0,
+ );
+ $TicketsObj->Limit(
+ ALIAS => $txn_alias,
+ FIELD => 'Type',
+ OPERATOR => 'IN',
+ VALUE => [ 'Correspond', 'Create' ],
+ );
+
+ return(1);
+}
+
+RT::Base->_ImportOverlays();
+
+1;
diff --git a/rt/lib/RT/Ticket_Vendor.pm b/rt/lib/RT/Ticket_Vendor.pm
index a55bb7b0d..4a7883888 100644
--- a/rt/lib/RT/Ticket_Vendor.pm
+++ b/rt/lib/RT/Ticket_Vendor.pm
@@ -92,5 +92,28 @@ sub WillResolveAsString {
return $self->WillResolveObj->AsString();
}
+=head2 IsUnreplied
+
+Returns true if there's a Correspond or Create transaction more recent than
+the Told date of this ticket (or the ticket has no Told date) and the ticket
+is not rejected or resolved.
+
+=cut
+
+sub IsUnreplied {
+ my $self = shift;
+ return 0 if $self->Status eq 'resolved'
+ or $self->Status eq 'rejected';
+
+ my $Told = $self->Told || '1970-01-01';
+ my $Txns = $self->Transactions;
+ $Txns->Limit(FIELD => 'Type',
+ OPERATOR => 'IN',
+ VALUE => [ 'Correspond', 'Create' ]);
+ $Txns->Limit(FIELD => 'Created',
+ OPERATOR => '>',
+ VALUE => $Told);
+ $Txns->Count ? 1 : 0;
+}
1;
diff --git a/rt/share/html/Elements/CalendarSlotSchedule b/rt/share/html/Elements/CalendarSlotSchedule
index b82997be8..f12b4a6be 100644
--- a/rt/share/html/Elements/CalendarSlotSchedule
+++ b/rt/share/html/Elements/CalendarSlotSchedule
@@ -11,6 +11,7 @@
$pkgnum => undef
$RedirectToBasics => 0
</%ARGS>
+% my $scheduling = ($custnum && $LengthMin) ? 1 : 0;
% foreach my $username ( @username ) {
%
% my %schedule = UserDaySchedule( username => $username,
@@ -18,20 +19,23 @@
% Tickets => \@Tickets,
% );
%
-% my $bgcolor = '666666;border-color:#555555';
-% my $content = '';
-% my $link = '';
-% my $selectable = 0;
-% my $draggable_ticketid = 0;
+% my $bgcolor = '#666666';
+% my $border = '1px solid #555555';
+% my $label_time = '';
+% my $label_title = '';
+% my $selectable = 0; # can we schedule a new appointment
+% my $ticketid = 0;
% my $draggable_length = 0;
-% my $droppable = 0;
-% my $cells = 0;
+% my $droppable = 0; # can we reschedule an appointment here
+% my $cells = 0; # total cell count for appointment
+% my $offset = 0; # position of cell in appointment
%
% #white out available times
% foreach my $avail ( @{ $schedule{'avail'} } ) {
% my( $start, $end ) = @$avail;
% next if $start >= ($tod_row+$timestep) || $end <= $tod_row;
-% $bgcolor = 'FFFFFF';
+% $bgcolor = '#FFFFFF';
+% $border = '1px solid #D7D7D7';
% $selectable = 1
% if $LengthMin <= $end - $tod_row #the slot is long enough
% && ! grep { $_ > $tod_row && $LengthMin > $_ - $tod_row }
@@ -39,51 +43,50 @@
% }
%
% #block out / show / color code existing appointments
-% #my %line = ();
+% my $maxstarts = 0;
% foreach my $id ( keys %{ $schedule{'scheduled'} } ) {
%
% my( $starts, $due, $col, $t ) = @{ $schedule{'scheduled'}->{$id} };
%
+% # misleading loop--at most one id should pass this test
% next if $starts >= ($tod_row+$timestep) || $due <= $tod_row;
%
-% $bgcolor = $col;
-% $selectable = 0;
+% # but, if for any reason a scheduling conflict occurs,
+% # use the later starting one to minimize UI conflicts--
+% # not to imply that this scenario has been tested or should ever happen!!!
+% next if $starts < $maxstarts;
+% $maxstarts = $starts;
%
-% if ( $starts >= $tod_row ) { #first row
-%
-% #false laziness w/misc/xmlhttp-ticket-update.html & CalendarDaySchedule
-% my %hash = $m->comp('/Ticket/Elements/Customers', Ticket => $t);
-% my @cust_main = values( %{$hash{cust_main}} );
-%
-% $content .= ($content?', ':''). #$id. ': '.
-% #false laziness w/xmlhttp-ticket-update.html
-% FS::sched_avail::pretty_time($starts). '-'.
-% FS::sched_avail::pretty_time($due).
-% ': '. $cust_main[0]->_FreesideURILabel;
-% #'install for custname XX miles away'; #XXX placeholder/more
-% $link = qq( <A HREF="$RT::WebPath/Ticket/Display.html?id=$id" target="_blank">view</A> ).
-% include('/elements/popup_link.html',
-% action=>$RT::WebPath.'/Ticket/ModifyCustomFieldsPopup.html?id='.$id,
-% label =>'edit',
-% actionlabel => 'Edit appointment',
-% height => 436, # better: A + B * (num_custom_fields)
-% );
-% $draggable_ticketid = $id;
-% $draggable_length = $due - $starts;
-%
-% $cells = int( ($due-$starts) / $timestep );
-% $cells++ if ($due-$starts) % $timestep;
-%
-% #} else {
-% # $content .= ($content?', ':''). $id;
-% }
+% $ticketid = $id;
+% $bgcolor = '#'.$col;
+% $border = '1px solid #D7D7D7';
+% # can't schedule a new appointment
+% $selectable = 0;
+% # but can reschedule a ticket overlapping its old slot (filtered by can_drop)
+% $droppable = 1 unless $scheduling;
+% $draggable_length = $due - $starts;
+% $cells = int( ($due-$starts) / $timestep );
+% $cells++ if ($due-$starts) % $timestep;
+%
+% #false laziness w/misc/xmlhttp-ticket-update.html & CalendarDaySchedule
+% my %hash = $m->comp('/Ticket/Elements/Customers', Ticket => $t);
+% my @cust_main = values( %{$hash{cust_main}} );
+%
+% #false laziness w/xmlhttp-ticket-update.html
+% $label_time = FS::sched_avail::pretty_time($starts). '-'.
+% FS::sched_avail::pretty_time($due);
+% $label_title = $cust_main[0]->_FreesideURILabel;
+% #'install for custname XX miles away'; #XXX placeholder/more
+%
+% $offset = int( ($tod_row - $starts) / $timestep );
+% $offset++ if ($tod_row - $starts) % $timestep;
% }
%
% my $td_id = 'td_'. $Date->epoch. '_'. $tod_row. '_'. $username;
- <td style = "background-color:#<%$bgcolor%>"
+ <td style = "background-color: <% $bgcolor %>; border: <% $border %>"
ID="<% $td_id %>"
- class = "<% ($selectable && $custnum && $LengthMin) ? 'weeklyselectable' : 'weekly' %>"
+ class = "<% ($selectable && $scheduling) ? 'weeklyselectable' : 'weekly' %>"
%# <% $is_today ? 'today'
%# : $is_yesterday ? 'yesterday'
%# : $is_aweekago ? 'aweekago'
@@ -91,7 +94,8 @@
%# %>"
% if ( $selectable ) {
%
-% if ( $custnum && $LengthMin ) {
+% # Scheduling a new appointment
+% if ( $scheduling ) {
%
% #XXX for now, construct a ticket creation URL
% # eventually, do much the same, but say "appointment made", show time
@@ -131,39 +135,42 @@
%>"
onclick = "window.location.href = '<% $url %>'"
%
+% # If not scheduling, allow drag-and-drop rescheduling
% } else {
% $droppable = 1;
% }
%
% }
- ><% $content |h %><% $link |n %></td>
+ ></td>
<SCRIPT TYPE="text/javascript">
- $('#<% $td_id %>').data('username', "<% $username %>");
- $('#<% $td_id %>').data('starts', <% $Date->epoch + $tod_row*60 %>);
- $('#<% $td_id %>').data('epoch', <% $Date->epoch %>);
- $('#<% $td_id %>').data('tod_row', <% $tod_row %>);
+ var $cell_<% $td_id %> = $('#<% $td_id %>');
+ $cell_<% $td_id %>.data('username', "<% $username %>");
+ $cell_<% $td_id %>.data('starts', <% $Date->epoch + $tod_row*60 %>);
+ $cell_<% $td_id %>.data('epoch', <% $Date->epoch %>);
+ $cell_<% $td_id %>.data('tod_row', <% $tod_row %>);
-% if ( $droppable ) {
- $('#<% $td_id %>').droppable({
- over: boxon_drop,
- drop: reschedule_appointment,
- tolerance: 'pointer'
- });
+% if ($selectable) {
+ set_schedulable_cell($cell_<% $td_id %>);
% }
-% if ( $draggable_ticketid ) {
- $('#<% $td_id %>').draggable({
- containment: '.titlebox-content',
-%# revert: 'invalid',
- revert: true,
- revertDuration: 0,
- stop: clear_drag_hi,
- });
- $('#<% $td_id %>').data('ticketid', <% $draggable_ticketid %>);
- $('#<% $td_id %>').data('length', <% $draggable_length * 60 %>);
- $('#<% $td_id %>').data('cells', <% $cells %>);
- $('#<% $td_id %>').data('bgcolor', "#<% $bgcolor %>");
+% if ($ticketid) {
+ set_appointment_cell(
+ $cell_<% $td_id %>,
+ <% $ticketid |js_string %>,
+ <% $bgcolor |n,js_string %>,
+ <% $label_time |n,js_string %>,
+ <% $label_title |n,js_string %>,
+ <% $draggable_length * 60 %>,
+ <% $cells %>,
+ <% $offset %>
+ );
+% }
+% if ( $droppable ) {
+% if ( $draggable_length ) {
+ set_draggable_cell($cell_<% $td_id %>);
+% }
+ set_droppable_cell($cell_<% $td_id %>);
% }
</SCRIPT>
diff --git a/rt/share/html/Elements/CollectionAsTable/Row b/rt/share/html/Elements/CollectionAsTable/Row
index deaa312ba..4b2cfae43 100644
--- a/rt/share/html/Elements/CollectionAsTable/Row
+++ b/rt/share/html/Elements/CollectionAsTable/Row
@@ -57,6 +57,11 @@ $Class => 'RT__Ticket'
$Classes => ''
</%ARGS>
<%init>
+# it's a hack, but it has to be applied in every ticket search regardless
+# of format, so...
+if ( $record and $record->isa('RT::Ticket') and $record->IsUnreplied ) {
+ $Classes .= ' unreplied-ticket';
+}
$m->out( '<tr class="' . $Classes . ' '
. ( $Warning ? 'warnline' : $i % 2 ? 'oddline' : 'evenline' ) . '" >'
. "\n" );
diff --git a/rt/share/html/Elements/QueueSummaryByLifecycle b/rt/share/html/Elements/QueueSummaryByLifecycle
index f21cb20c3..54e6e4239 100644
--- a/rt/share/html/Elements/QueueSummaryByLifecycle
+++ b/rt/share/html/Elements/QueueSummaryByLifecycle
@@ -66,8 +66,11 @@ for my $queue (@queues) {
next if lc($queue->{Lifecycle} || '') ne lc $lifecycle->Name;
$i++;
+
+ my $classes = $i%2 ? 'oddline' : 'evenline';
+ $classes .= ' unreplied-ticket' if $queue->{Unreplied} > 0;
</%PERL>
-<tr class="<% $i%2 ? 'oddline' : 'evenline'%>" >
+<tr class="<% $classes %>">
<td>
<a href="<% $link_all->($queue, \@cur_statuses) %>" title="<% $queue->{Description} %>"><% $queue->{Name} %></a>
@@ -134,6 +137,21 @@ for my $queue (@queues) {
$lifecycle{ lc $cycle->Name } = $cycle;
}
+use RT::Search::UnrepliedTickets;
+my $Tickets = RT::Tickets->new( RT->SystemUser );
+my $Search = RT::Search::UnrepliedTickets->new( TicketsObj => $Tickets );
+$Search->Prepare;
+
+for my $queue (@queues) {
+ # show whether there are unreplied tickets
+ # somewhat inefficient but we only use the count query
+ my $tix = $Tickets->Clone;
+ $tix->Limit(FIELD => 'Queue',
+ OPERATOR => '=',
+ VALUE => $queue->{id});
+ $queue->{Unreplied} = $tix->Count;
+}
+
unless (@statuses) {
my %seen;
foreach my $set ( 'initial', 'active' ) {
diff --git a/rt/share/html/Search/Schedule.html b/rt/share/html/Search/Schedule.html
index 0dbe8c30b..df7b53d81 100644
--- a/rt/share/html/Search/Schedule.html
+++ b/rt/share/html/Search/Schedule.html
@@ -2,25 +2,86 @@
<SCRIPT TYPE="text/javascript">
+ // sets cell content and bgcolor in a div, for use as a draggable
+ // (draggable tds have border problems on FF/IE)
+ function set_cell_div ($cell,content,bgcolor) {
+ var $div = $cell.data('div');
+ if (!$div) {
+ $div = $(document.createElement('div'));
+ $div.data('cell',$cell);
+ $cell.data('div',$div);
+ $cell.append($div);
+ }
+ $div.css('white-space','nowrap');
+ $div.css('width','100%');
+ $div.css('background-color', bgcolor);
+ $div.html(content || '&nbsp;<br>&nbsp;<br>&nbsp;');
+ }
+
+ // gives cell the appearance dictated by its data
+ function set_data_cell ($cell) {
+ $cell.css('border', '1px solid #D7D7D7' );
+ $cell.css('background-color', $cell.data('bgcolor'));
+ set_cell_div($cell,$cell.data('content'),$cell.data('bgcolor'));
+ }
+
+ // sets cell data and appearance to schedulable
+ function set_schedulable_cell ($cell) {
+ $cell.data('bgcolor', '#FFFFFF' );
+ $cell.data('ticketid', 0 );
+ $cell.data('length', 0 );
+ $cell.data('cells', 0 );
+ $cell.data('offset', 0 );
+ $cell.data('label', '' );
+ $cell.data('content', '' );
+ set_data_cell($cell);
+ }
+
+ // sets cell data and appearance as an appointment
+ function set_appointment_cell ($cell,ticketid,bgcolor,labeltime,labeltitle,length,cells,offset) {
+ $cell.data('bgcolor', bgcolor );
+ $cell.data('ticketid', ticketid );
+ $cell.data('length', length );
+ $cell.data('cells', cells );
+ $cell.data('offset', offset );
+ var label = labeltime + ' <br>' + labeltitle + ' <br>';
+ $cell.data('label', label );
+ $cell.data('content', '');
+ if ( offset == 0 ) { // first row
+ var title =
+ label +
+ ' <A HREF="<%$RT::WebPath%>/Ticket/Display.html?id=' + ticketid + '" target="_blank">view</A> ' +
+ <% include('/elements/popup_link.html',
+ action=>$RT::WebPath.'/Ticket/ModifyCustomFieldsPopup.html?id=__MAGIC_TICKET_ID__',
+ label =>'edit',
+ actionlabel => 'Edit appointment',
+ height => 436, # better: A + B * (num_custom_fields)
+ ) |n,js_string
+ %>;
+ title = title.replace( /__MAGIC_TICKET_ID__/, ticketid );
+ $cell.data('content', title);
+ }
+ set_data_cell($cell);
+ }
+
% if ( $cells ) {
+ // hover effects for scheduling new appointment
+
function boxon(what) {
var $this = $(what);
for ( var c=0; c < <%$cells%>; c++) {
$this.css('background-color', '#ffffdd');
+ set_cell_div($this,'','#ffffdd');
if ( c == 0 ) {
$this.css('border-top', '1px double black');
- $this.css('border-left', '1px double black');
- $this.css('border-right', '1px solid black');
- } else if ( c == <%$cells-1%> ) {
- $this.css('border-left', '1px double black');
- $this.css('border-right', '1px solid black');
+ }
+ if ( c == <%$cells-1%> ) {
$this.css('border-bottom', '1px solid black');
- } else {
- $this.css('border-left', '1px double black');
- $this.css('border-right', '1px solid black');
}
+ $this.css('border-left', '1px double black');
+ $this.css('border-right', '1px solid black');
var rownum = $this.parent().prevAll('tr').length;
var colnum = $this.prevAll('td').length;
@@ -31,12 +92,9 @@
function boxoff(what) {
var $this = $(what);
for ( var c=0; c < <%$cells%>; c++) {
-
- //$this.css('background-color', '');
- //$this.css('border', ''); //IE8 woes, removes cell borders
- $this.removeAttr('style'); //slightly "flashy" on cell changes under IE8
- //but at least it doesn't remove cell borders
-
+ $this.css('background-color', '#ffffff');
+ set_cell_div($this,'','#ffffff');
+ $this.css('border', '1px solid #D7D7D7'); //watch out in IE8 woes, empty string removes cell borders
var rownum = $this.parent().prevAll('tr').length;
var colnum = $this.prevAll('td').length;
$this = $this.parent().parent().children('tr').eq(rownum+1).children('td').eq(colnum);
@@ -44,14 +102,27 @@
}
-% }
+% } else {
-% # it would be better if we had draggable-specific droppables, but this will prevent overlap for now...
- function can_drop ($where, cells) {
+ // functions for drag-and-drop rescheduling
+
+ // ticket-dependant test if we can drop here
+ // prevent overlap with other appointments, while allowing appointment to overlap itself
+ function can_drop ($where, ui) {
+ var cells = ui.draggable.data('cell').data('cells');
+ var ticketid = ui.draggable.data('cell').data('ticketid');
for (var c=0; c < cells; c++) {
if (!$where.is('.ui-droppable')) {
return false;
}
+ if ($where.data('ticketid')) {
+ if ($where.data('ticketid') != ticketid) {
+ return false;
+ }
+ if ($where.data('offset') == c) { // don't reschedule in the same slot
+ return false;
+ }
+ }
var rownum = $where.parent().prevAll('tr').length;
var colnum = $where.prevAll('td').length;
$where = $where.parent().parent().children('tr').eq(rownum+1).children('td').eq(colnum);
@@ -59,71 +130,129 @@
return true;
}
- var drag_cells = 0;
+ // makes cell droppable (can reschedule here, subject to can_drop)
+ function set_droppable_cell ($cell) {
+ $cell.droppable({
+ over: appointment_drag_over,
+ drop: reschedule_appointment,
+ tolerance: 'pointer'
+ });
+ }
+
+ // makes cell draggable (able to be rescheduled)
+ function set_draggable_cell ($cell) {
+ var $div = $cell.data('div');
+ $div.draggable({
+ containment: '.titlebox-content',
+ revert: true,
+ revertDuration: 0,
+ start: appointment_drag_start,
+ stop: appointment_drag_stop,
+ zIndex: 10,
+ });
+ }
+
+ // gives cell a white (schedulable) appearance, without changing cell data
+ function set_white_cell ($cell) {
+ $cell.css('border', '1px solid #D7D7D7' );
+ $cell.css('background-color', '#FFFFFF');
+ set_cell_div($cell,'','#FFFFFF');
+ }
+
+ // track drag highlighting
var drag_hi;
- // on drag stop (regardless of if it was dropped)
- function clear_drag_hi () {
+ // clear drag highlighting
+ function clear_drag_hi (cells) {
if ( drag_hi ) {
- boxoff_do(drag_hi);
+ for ( var c=0; c < cells; c++) {
+ if (drag_hi.data('isdragging')) {
+ drag_hi.css('border', '1px solid #D7D7D7' );
+ drag_hi.css('background-color', '#FFFFFF' );
+ } else {
+ set_white_cell(drag_hi);
+ }
+ var rownum = drag_hi.parent().prevAll('tr').length;
+ var colnum = drag_hi.prevAll('td').length;
+ drag_hi = drag_hi.parent().parent().children('tr').eq(rownum+1).children('td').eq(colnum);
+ }
drag_hi = undefined;
}
}
- // on drag over
- function boxon_drop(event, ui) {
- //var $this = $(what);
+ // drag start event
+ function appointment_drag_start(event, ui) {
var $this = $(this);
-
- drag_cells = ui.draggable.data('cells');
-
- clear_drag_hi();
-
- if (!can_drop($this, drag_cells)) return;
-
- drag_hi = $this;
-
- for ( var c=0; c < drag_cells; c++) {
-
- /* well, its not exactly what i want, would prefer if it could properly
- mouse in-out, but this sorta helps for now?
- revisit when everthing else is working */
-/* $this.effect("highlight", {}, 1500); */
-
- $this.css('background-color', '#ffffdd');
- if ( c == 0 ) {
- $this.css('border-top', '1px double black');
- $this.css('border-left', '1px double black');
- $this.css('border-right', '1px solid black');
- } else if ( c == (drag_cells-1) ) {
- $this.css('border-left', '1px double black');
- $this.css('border-right', '1px solid black');
- $this.css('border-bottom', '1px solid black');
+ // cell that's dragging
+ $this = $this.data('cell');
+ set_cell_div($this,$this.data('label'),$this.data('bgcolor'));
+ $this.data('isdragging',true);
+ var offset = $this.data('offset');
+ var cells = $this.data('cells');
+ // jump to first cell in appointment
+ var rownum = $this.parent().prevAll('tr').length;
+ var colnum = $this.prevAll('td').length;
+ $this = $this.parent().parent().children('tr').eq(rownum-offset).children('td').eq(colnum);
+ // loop through all cells in appointment
+ for ( var c=0; c < cells; c++) {
+ if ($this.data('isdragging')) {
+ $this.css('background-color', '#FFFFFF');
} else {
- $this.css('border-left', '1px double black');
- $this.css('border-right', '1px solid black');
+ set_white_cell($this);
}
-
var rownum = $this.parent().prevAll('tr').length;
var colnum = $this.prevAll('td').length;
$this = $this.parent().parent().children('tr').eq(rownum+1).children('td').eq(colnum);
}
-
-
}
- // clears highlighted box, used by clear_hi_drag (drag stop event)
- function boxoff_do(what) {
-
- var $this = what;
-
- for ( var c=0; c < drag_cells; c++) {
-
- //$this.css('background-color', '');
- //$this.css('border', ''); //IE8 woes, removes cell borders
- $this.removeAttr('style'); //slightly "flashy" on cell changes under IE8
- //but at least it doesn't remove cell borders
+ // drag stop event
+ function appointment_drag_stop(event, ui) {
+ var $this = $(this);
+ // cell that's dragging
+ $this = $this.data('cell');
+ var cells = $this.data('cells');
+ clear_drag_hi(cells);
+ $this.data('isdragging',false);
+ var offset = $this.data('offset');
+ // jump to first cell in appointment
+ var rownum = $this.parent().prevAll('tr').length;
+ var colnum = $this.prevAll('td').length;
+ $this = $this.parent().parent().children('tr').eq(rownum-offset).children('td').eq(colnum);
+ // loop through all cells in appointment
+ for ( var c=0; c < cells; c++) {
+ set_data_cell($this);
+ var rownum = $this.parent().prevAll('tr').length;
+ var colnum = $this.prevAll('td').length;
+ $this = $this.parent().parent().children('tr').eq(rownum+1).children('td').eq(colnum);
+ }
+ }
+ // drag over event
+ function appointment_drag_over(event, ui) {
+ // the cell that's dragging
+ var cells = ui.draggable.data('cell').data('cells');
+ // the droppable cell that you're over
+ var $this = $(this);
+ clear_drag_hi(cells);
+ if (!can_drop($this, ui)) return;
+ drag_hi = $this;
+ // loop through potential appointment cells
+ for ( var c=0; c < cells; c++) {
+ $this.css('background-color', '#ffffdd');
+ if ( !$this.data('isdragging')) {
+ set_cell_div($this,'','#ffffdd');
+ } else {
+ $this.css('background-color','#ffffdd');
+ }
+ if ( c == 0 ) {
+ $this.css('border-top', '1px double black');
+ }
+ if ( c == (cells-1) ) {
+ $this.css('border-bottom', '1px solid black');
+ }
+ $this.css('border-left', '1px double black');
+ $this.css('border-right', '1px solid black');
var rownum = $this.parent().prevAll('tr').length;
var colnum = $this.prevAll('td').length;
$this = $this.parent().parent().children('tr').eq(rownum+1).children('td').eq(colnum);
@@ -133,27 +262,27 @@
// drop event
function reschedule_appointment( event, ui ) {
+ // the droppable cell that you're over
var $this = $(this);
- if (!can_drop($this, ui.draggable.data('cells'))) return;
+ if (!can_drop($this, ui)) return;
% #get the ticket number and appointment length (from the draggable object)
- var ticketid = ui.draggable.data('ticketid');
- var length = ui.draggable.data('length');
- var bgcolor = ui.draggable.data('bgcolor');
+ var dragcell = ui.draggable.data('cell');
+ var ticketid = dragcell.data('ticketid');
+ var length = dragcell.data('length');
+ var bgcolor = dragcell.data('bgcolor');
+ var offset = dragcell.data('offset');
% #and.. the new date and time, and username (from the droppable object)
var starts = $this.data('starts');
var username = $this.data('username');
-
var due = parseInt(starts) + parseInt(length);
-
var n_epoch = $this.data('epoch');
var n_st_tod_row = $this.data('tod_row');
- var draggable = ui.draggable;
var droppable = $this;
- draggable.effect( "transfer", { to: droppable }, 420 );
+ ui.draggable.effect( "transfer", { to: droppable }, 420 );
% #tell the backend to reschedule it
var url = "<% popurl(3) %>misc/xmlhttp-ticket-update.html?" +
@@ -164,78 +293,52 @@
if ( data.error && data.error.length ) {
% #error? "that shouldn't happen" but should display
alert(data.error);
-% #XX and should revert the dragable...
- } else {
- //draggable.effect( "transfer", { to: droppable }, 1000 );
+ } else {
var label = data.sched_label;
-
-% #remove the old appointment entirely
- var epoch = ui.draggable.data('epoch');
- var st_tod_row = ui.draggable.data('tod_row');
- var old_username = ui.draggable.data('username');
- var cells = ui.draggable.data('cells');
+ var labeltime = data.sched_label_time;
+ var labeltitle = data.sched_label_title;
+
+ // jump to first cell in appointment
+ var rownum = dragcell.parent().prevAll('tr').length;
+ var colnum = dragcell.prevAll('td').length;
+ dragcell = dragcell.parent().parent().children('tr').eq(rownum-offset).children('td').eq(colnum);
+
+ // remove old appointment entirely
+ var epoch = dragcell.data('epoch');
+ var st_tod_row = dragcell.data('tod_row');
+ var old_username = dragcell.data('username');
+ var cells = dragcell.data('cells');
for ( var c=0; c < cells; c++) {
var tod_row = parseInt(st_tod_row) + (c * <%$timestep%>);
var td_id = 'td_' + epoch +
'_' + String( tod_row ) +
'_' + old_username;
- $('#'+td_id).css('background-color', '#FFFFFF');
- $('#'+td_id).text('');
-% #(and make those boxes droppable)
- $('#'+td_id).droppable({
- over: boxon_drop,
- drop: reschedule_appointment,
- tolerance: 'pointer'
- });
+ var $cell = $('#'+td_id);
+ $cell.data('div').draggable('destroy');
+ set_schedulable_cell($cell);
+ set_droppable_cell($cell);
}
-% #maybe use that animation which shows the box from point A to B
-
- clear_drag_hi();
+ // set appointment in new position
+ clear_drag_hi(cells);
for ( var d=0; d < cells; d++) {
var n_tod_row = parseInt(n_st_tod_row) + (d * <%$timestep%>);
var n_td_id = 'td_' + n_epoch +
'_' + String( n_tod_row ) +
'_' + username;
- $('#'+n_td_id).css('background-color', bgcolor);
-% #remove their droppable
- $('#'+n_td_id).droppable('destroy');
- if ( d == 0 ) {
- var title =
- label +
- ' <A HREF="<%$RT::WebPath%>/Ticket/Display.html?id=' + ticketid + '" target="_blank">view</A> ' +
- <% include('/elements/popup_link.html',
- action=>$RT::WebPath.'/Ticket/ModifyCustomFieldsPopup.html?id=__MAGIC_TICKET_ID__',
- label =>'edit',
- actionlabel => 'Edit appointment',
- height => 436, # better: A + B * (num_custom_fields)
- ) |n,js_string
- %>;
- title = title.replace( /__MAGIC_TICKET_ID__/, ticketid );
- $('#'+n_td_id).html( title );
-% #(and make the top draggable, so we could do it all over again)
- $('#'+n_td_id).draggable({
- containment: '.titlebox-content',
-%# revert: 'invalid',
- revert: true,
- revertDuration: 0,
- stop: clear_drag_hi,
- });
- $('#'+n_td_id).data('ticketid', ticketid );
- $('#'+n_td_id).data('length', length );
- $('#'+n_td_id).data('cells', cells );
- $('#'+n_td_id).data('bgcolor', bgcolor );
- }
+ var $cell = $('#'+n_td_id);
+ set_appointment_cell($cell,ticketid,bgcolor,labeltime,labeltitle,length,cells,d);
+ set_draggable_cell($cell);
+ set_droppable_cell($cell);
}
-
}
-
});
-
}
+% } # end of rescheduling functions
+
</SCRIPT>
<& /Search/Calendar.html,
diff --git a/rt/share/html/Search/UnrepliedTickets.html b/rt/share/html/Search/UnrepliedTickets.html
new file mode 100755
index 000000000..37f94e0b2
--- /dev/null
+++ b/rt/share/html/Search/UnrepliedTickets.html
@@ -0,0 +1,156 @@
+%# false laziness with Results.html; basically this is the same thing but with
+%# a hardcoded RT::Tickets object instead of a Query param
+
+<& /Elements/Header, Title => $title,
+ Refresh => $refresh,
+ LinkRel => \%link_rel &>
+
+% $m->callback( ARGSRef => \%ARGS, Format => \$Format, CallbackName => 'BeforeResults' );
+
+<& /Elements/CollectionList,
+ Class => 'RT::Tickets',
+ Collection => $session{tickets},
+ TotalFound => $ticketcount,
+ AllowSorting => 1,
+ OrderBy => $OrderBy,
+ Order => $Order,
+ Rows => $Rows,
+ Page => $Page,
+ Format => $Format,
+ BaseURL => $BaseURL,
+ SavedSearchId => $ARGS{'SavedSearchId'},
+ SavedChartSearchId => $ARGS{'SavedChartSearchId'},
+ PassArguments => [qw(Format Rows Page Order OrderBy SavedSearchId SavedChartSearchId)],
+&>
+% $m->callback( ARGSRef => \%ARGS, CallbackName => 'AfterResults' );
+
+% my %hiddens = (Format => $Format, Rows => $Rows, OrderBy => $OrderBy, Order => $Order, HideResults => $HideResults, Page => $Page, SavedChartSearchId => $SavedChartSearchId );
+<div align="right" class="refresh">
+<form method="get" action="<%RT->Config->Get('WebPath')%>/Search/UnrepliedTickets.html">
+% foreach my $key (keys(%hiddens)) {
+<input type="hidden" class="hidden" name="<%$key%>" value="<% defined($hiddens{$key})?$hiddens{$key}:'' %>" />
+% }
+<& /Elements/Refresh, Name => 'TicketsRefreshInterval', Default => $session{'tickets_refresh_interval'}||RT->Config->Get('SearchResultsRefreshInterval', $session{'CurrentUser'}) &>
+<input type="submit" class="button" value="<&|/l&>Change</&>" />
+</form>
+</div>
+<%INIT>
+$m->callback( ARGSRef => \%ARGS, CallbackName => 'Initial' );
+
+# Read from user preferences
+my $prefs = $session{'CurrentUser'}->UserObj->Preferences("SearchDisplay") || {};
+
+# These variables are what define a search_hash; this is also
+# where we give sane defaults.
+$Format ||= $prefs->{'Format'} || RT->Config->Get('DefaultSearchResultFormat');
+$Order ||= $prefs->{'Order'} || RT->Config->Get('DefaultSearchResultOrder');
+$OrderBy ||= $prefs->{'OrderBy'} || RT->Config->Get('DefaultSearchResultOrderBy');
+
+# In this case the search UI isn't available, so trust the defaults.
+
+# Some forms pass in "RowsPerPage" rather than "Rows"
+# We call it RowsPerPage everywhere else.
+
+if ( defined $prefs->{'RowsPerPage'} ) {
+ $Rows = $prefs->{'RowsPerPage'};
+} else {
+ $Rows = 50;
+}
+$Page = 1 unless $Page && $Page > 0;
+
+use RT::Search::UnrepliedTickets;
+
+$session{'i'}++;
+$session{'tickets'} = RT::Tickets->new($session{'CurrentUser'}) ;
+my $search = RT::Search::UnrepliedTickets->new( TicketsObj => $session{'tickets'} );
+$search->Prepare;
+
+if ($OrderBy =~ /\|/) {
+ # Multiple Sorts
+ my @OrderBy = split /\|/,$OrderBy;
+ my @Order = split /\|/,$Order;
+ $session{'tickets'}->OrderByCols(
+ map { { FIELD => $OrderBy[$_], ORDER => $Order[$_] } } ( 0
+ .. $#OrderBy ) );;
+} else {
+ $session{'tickets'}->OrderBy(FIELD => $OrderBy, ORDER => $Order);
+}
+$session{'tickets'}->RowsPerPage( $Rows ) if $Rows;
+$session{'tickets'}->GotoPage( $Page - 1 );
+
+# use this to set a CSRF token applying to the search, so that the user can come
+# back to this page without triggering a referrer check
+$session{'CurrentSearchHash'} = {
+ Format => $Format,
+ Page => $Page,
+ Order => $Order,
+ OrderBy => $OrderBy,
+ RowsPerPage => $Rows
+};
+
+
+my $ticketcount = $session{tickets}->CountAll();
+my $title = loc('New activity on [quant,_1,ticket,tickets]', $ticketcount);
+
+# pass this through on pagination links
+my $QueryString = "?".$m->comp('/Elements/QueryString',
+ Format => $Format,
+ Rows => $Rows,
+ OrderBy => $OrderBy,
+ Order => $Order,
+ Page => $Page);
+
+if ($ARGS{'TicketsRefreshInterval'}) {
+ $session{'tickets_refresh_interval'} = $ARGS{'TicketsRefreshInterval'};
+}
+
+my $refresh = $session{'tickets_refresh_interval'}
+ || RT->Config->Get('SearchResultsRefreshInterval', $session{'CurrentUser'} );
+
+# Check $m->request_args, not $DECODED_ARGS, to avoid creating a new CSRF token on each refresh
+if (RT->Config->Get('RestrictReferrer') and $refresh and not $m->request_args->{CSRF_Token}) {
+ my $token = RT::Interface::Web::StoreRequestToken( $session{'CurrentSearchHash'} );
+ $m->notes->{RefreshURL} = RT->Config->Get('WebURL')
+ . "Search/UnrepliedTickets.html?CSRF_Token="
+ . $token;
+}
+
+my %link_rel;
+my $genpage = sub {
+ return $m->comp(
+ '/Elements/QueryString',
+ Format => $Format,
+ Rows => $Rows,
+ OrderBy => $OrderBy,
+ Order => $Order,
+ Page => shift(@_),
+ );
+};
+
+if ( RT->Config->Get('SearchResultsAutoRedirect') && $ticketcount == 1 &&
+ $session{tickets}->First ) {
+# $ticketcount is not always precise unless $UseSQLForACLChecks is set to true,
+# check $session{tickets}->First here is to make sure the ticket is there.
+ RT::Interface::Web::Redirect( RT->Config->Get('WebURL')
+ ."Ticket/Display.html?id=". $session{tickets}->First->id );
+}
+
+my $BaseURL = RT->Config->Get('WebPath')."/Search/UnrepliedTickets.html?";
+$link_rel{first} = $BaseURL . $genpage->(1) if $Page > 1;
+$link_rel{prev} = $BaseURL . $genpage->($Page - 1) if $Page > 1;
+$link_rel{next} = $BaseURL . $genpage->($Page + 1) if ($Page * $Rows) < $ticketcount;
+$link_rel{last} = $BaseURL . $genpage->(POSIX::ceil($ticketcount/$Rows)) if $Rows and ($Page * $Rows) < $ticketcount;
+</%INIT>
+<%CLEANUP>
+$session{'tickets'}->PrepForSerialization();
+</%CLEANUP>
+<%ARGS>
+$HideResults => 0
+$Rows => undef
+$Page => 1
+$OrderBy => undef
+$Order => undef
+$SavedSearchId => undef
+$SavedChartSearchId => undef
+$Format => undef
+</%ARGS>
diff --git a/rt/share/html/Ticket/ModifyAll.html b/rt/share/html/Ticket/ModifyAll.html
index f0b70b578..7d923872e 100755
--- a/rt/share/html/Ticket/ModifyAll.html
+++ b/rt/share/html/Ticket/ModifyAll.html
@@ -175,7 +175,7 @@ $m->callback( TicketObj => $Ticket, ARGSRef => \%ARGS, skip_update => \$skip_upd
$skip_update = 1;
}
}
-
+
# There might be two owners.
if ( ref ($ARGS{'Owner'} )) {
my @owners =@{$ARGS{'Owner'}};
@@ -187,7 +187,7 @@ if ( ref ($ARGS{'Owner'} )) {
elsif (length $owner) {
$ARGS{'Owner'} = $owner unless ($Ticket->OwnerObj->id == $owner);
}
- }
+ }
}
unless ($skip_update or $OnlySearchForPeople or $OnlySearchForGroup or $ARGS{'AddMoreAttach'} ) {
@@ -197,7 +197,6 @@ unless ($skip_update or $OnlySearchForPeople or $OnlySearchForGroup or $ARGS{'Ad
push @results, ProcessUpdateMessage( TicketObj => $Ticket, ARGSRef=>\%ARGS );
push @results, ProcessTicketBasics( TicketObj => $Ticket, ARGSRef => \%ARGS );
push @results, ProcessTicketLinks( TicketObj => $Ticket, ARGSRef => \%ARGS);
-}
push @results, ProcessTicketStatus( TicketObj => $Ticket, ARGSRef => \%ARGS );
$Ticket->ApplyTransactionBatch;
@@ -207,7 +206,6 @@ unless ($skip_update or $OnlySearchForPeople or $OnlySearchForGroup or $ARGS{'Ad
Path => "/Ticket/ModifyAll.html",
Arguments => { id => $Ticket->id },
);
-
}
# If they've gone and moved the ticket to somewhere they can't see, etc...
diff --git a/rt/share/static/css/freeside3/ticket-lists.css b/rt/share/static/css/freeside3/ticket-lists.css
index 84c9a92de..257cf3b07 100644
--- a/rt/share/static/css/freeside3/ticket-lists.css
+++ b/rt/share/static/css/freeside3/ticket-lists.css
@@ -99,8 +99,18 @@ tr.collection-as-table+tr.collection-as-table th {
}
-
-
+tr.unreplied-ticket > :first-child::before {
+ /* green dot */
+ border: 1px solid black;
+ border-radius: 50%;
+ display: inline-block;
+ height: 1ex;
+ width: 1ex;
+ float: left;
+ content: '';
+ margin-top: 1ex;
+ background-color: green;
+}
table.queue-summary td {
background: #efefef;
diff --git a/rt/share/static/css/freeside4/ticket-lists.css b/rt/share/static/css/freeside4/ticket-lists.css
index cdf10193a..3d4706fd2 100644
--- a/rt/share/static/css/freeside4/ticket-lists.css
+++ b/rt/share/static/css/freeside4/ticket-lists.css
@@ -81,6 +81,19 @@ table.collection-as-table.chart th {
border-bottom: 2px solid #ccc
}
+tr.unreplied-ticket > :first-child::before {
+ /* green dot */
+ border: 1px solid black;
+ border-radius: 50%;
+ display: inline-block;
+ height: 1ex;
+ width: 1ex;
+ float: left;
+ content: '';
+ margin-right: 1ex;
+ background-color: green;
+}
+
table.queue-summary td {
background: #efefef;
border-bottom: 1px solid #ccc;