diff options
Diffstat (limited to 'rt')
-rw-r--r-- | rt/etc/initialdata | 19 | ||||
-rw-r--r-- | rt/lib/RT/Search/UnrepliedTickets.pm | 69 | ||||
-rw-r--r-- | rt/lib/RT/Ticket_Vendor.pm | 23 | ||||
-rw-r--r-- | rt/share/html/Elements/CalendarSlotSchedule | 137 | ||||
-rw-r--r-- | rt/share/html/Elements/CollectionAsTable/Row | 5 | ||||
-rw-r--r-- | rt/share/html/Elements/QueueSummaryByLifecycle | 20 | ||||
-rw-r--r-- | rt/share/html/Search/Schedule.html | 353 | ||||
-rwxr-xr-x | rt/share/html/Search/UnrepliedTickets.html | 156 | ||||
-rwxr-xr-x | rt/share/html/Ticket/ModifyAll.html | 6 | ||||
-rw-r--r-- | rt/share/static/css/freeside3/ticket-lists.css | 14 | ||||
-rw-r--r-- | rt/share/static/css/freeside4/ticket-lists.css | 13 |
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 || ' <br> <br> '); + } + + // 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; |