summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authormark <mark>2011-04-27 08:31:03 +0000
committermark <mark>2011-04-27 08:31:03 +0000
commit68fcc90d8e95f1efe0efe07b2f59e5fab2d8c535 (patch)
tree77e67410169c23cfa4499d9bbf779b0005a045fd
parent9d5ada5763fdeca4f90a7357ab7c384b52c1c41c (diff)
RT mobile UI, #11630
-rw-r--r--FS/FS/UI/Web.pm14
-rw-r--r--httemplate/elements/header.html30
-rw-r--r--httemplate/elements/menu.html20
-rw-r--r--httemplate/elements/searchbar-combined.html51
-rw-r--r--httemplate/search/searchbar.cgi16
-rw-r--r--rt/FREESIDE_MODIFIED7
-rw-r--r--rt/etc/RT_Config.pm3
-rw-r--r--rt/etc/RT_Config.pm.in3
-rw-r--r--rt/lib/RT/Extension/MobileUI.pm57
-rw-r--r--rt/share/html/Callbacks/RT-Extension-MobileUI/Elements/Login/Header12
-rw-r--r--rt/share/html/Callbacks/RT-Extension-MobileUI/Ticket/Create.html/Init13
-rw-r--r--rt/share/html/Callbacks/RT-Extension-MobileUI/Ticket/Display.html/Initial14
-rw-r--r--rt/share/html/Callbacks/RT-Extension-MobileUI/index.html/Initial16
-rw-r--r--rt/share/html/m/_elements/footer11
-rw-r--r--rt/share/html/m/_elements/full_site_link1
-rw-r--r--rt/share/html/m/_elements/header38
-rw-r--r--rt/share/html/m/_elements/menu63
-rw-r--r--rt/share/html/m/_elements/raw_style417
-rw-r--r--rt/share/html/m/_elements/ticket_list64
-rw-r--r--rt/share/html/m/_elements/ticket_menu31
-rw-r--r--rt/share/html/m/_elements/wrapper15
-rw-r--r--rt/share/html/m/dhandler5
-rw-r--r--rt/share/html/m/index.html4
-rw-r--r--rt/share/html/m/login84
-rw-r--r--rt/share/html/m/logout7
-rw-r--r--rt/share/html/m/style.css5
-rw-r--r--rt/share/html/m/ticket/create400
-rw-r--r--rt/share/html/m/ticket/history31
-rw-r--r--rt/share/html/m/ticket/modify0
-rw-r--r--rt/share/html/m/ticket/reply171
-rw-r--r--rt/share/html/m/ticket/select_create_queue18
-rw-r--r--rt/share/html/m/ticket/show454
-rw-r--r--rt/share/html/m/tickets/requested4
-rw-r--r--rt/share/html/m/tickets/search64
34 files changed, 2133 insertions, 10 deletions
diff --git a/FS/FS/UI/Web.pm b/FS/FS/UI/Web.pm
index 029b02c..40abdc4 100644
--- a/FS/FS/UI/Web.pm
+++ b/FS/FS/UI/Web.pm
@@ -484,6 +484,20 @@ sub cust_aligns {
}
}
+=item is_mobile
+
+Utility function to determine if the client is a mobile browser.
+
+=cut
+
+sub is_mobile {
+ my $ua = $ENV{'HTTP_USER_AGENT'} || '';
+ if ( $ua =~ /(?:hiptop|Blazer|Novarra|Vagabond|SonyEricsson|Symbian|NetFront|UP.Browser|UP.Link|Windows CE|MIDP|J2ME|DoCoMo|J-PHONE|PalmOS|PalmSource|iPhone|iPod|AvantGo|Nokia|Android|WebOS|S60|Opera Mini|Opera Mobi)/io ) {
+ return 1;
+ }
+ return 0;
+}
+
###
# begin JSRPC code...
###
diff --git a/httemplate/elements/header.html b/httemplate/elements/header.html
index c83529e..432e9c6 100644
--- a/httemplate/elements/header.html
+++ b/httemplate/elements/header.html
@@ -28,10 +28,14 @@ Example:
<META HTTP-Equiv="Cache-Control" Content="no-cache">
<META HTTP-Equiv="Pragma" Content="no-cache">
<META HTTP-Equiv="Expires" Content="0">
+% if ( $mobile ) {
+ <META NAME="viewport" content="width=device-width height=device-height user-scalable=yes">
+% }
<% include('menu.html', 'freeside_baseurl' => $fsurl,
'position' => $menu_position,
'nocss' => $nocss,
+ 'mobile' => $mobile,
) |n
%>
@@ -67,6 +71,19 @@ Example:
<TR>
+% if ( $mobile ) {
+
+ <TD STYLE="padding:1px 0px 0px 0px;border-top: 1px solid #7e0079;width:auto" BGCOLOR="#cccccc">
+ <SCRIPT TYPE="text/javascript">
+ document.write(myBar.toString());
+ </SCRIPT>
+ </TD>
+ <TD STYLE="padding:1px 0px 0px 0px;border-top: 1px solid #7e0079;width:auto" BGCOLOR="#cccccc">
+ <% include('searchbar-combined.html') |n %>
+ </TD>
+
+% } else {
+
<TD COLSPAN="7" WIDTH="100%" STYLE="padding:1px 0px 0px 0px;border-top: 1px solid #7e0079" BGCOLOR="#cccccc">
<SCRIPT TYPE="text/javascript">
document.write(myBar);
@@ -100,6 +117,7 @@ Example:
<TD COLSPAN=1 BGCOLOR="#cccccc" ALIGN="right" STYLE="padding-left:2px;padding-right:2px">
<% include('searchbar-ticket.html') |n %>
</TD>
+% }
</TR>
</TABLE>
@@ -154,6 +172,9 @@ Example:
my( $title, $menubar, $etc, $head ) = ( '', '', '', '' );
my( $nobr, $nocss ) = ( 0, 0 );
+
+my $mobile;
+
if ( ref($_[0]) ) {
my $opt = shift;
$title = $opt->{title};
@@ -162,12 +183,20 @@ if ( ref($_[0]) ) {
$head = $opt->{head};
$nobr = $opt->{nobr};
$nocss = $opt->{nocss};
+ $mobile = $opt->{mobile};
} else {
($title, $menubar) = ( shift, shift );
$etc = @_ ? shift : ''; #$etc is for things like onLoad= etc.
$head = @_ ? shift : ''; #$head is for things that go in the <HEAD> section
}
+if ( !defined($mobile) ) {
+ $mobile = FS::UI::Web::is_mobile();
+}
+if ( $cgi->param('mobile') =~ /^(\d)$/ ) { # allow client to override
+ $mobile = $1;
+}
+
my $conf = new FS::Conf;
my $curuser = $FS::CurrentUser::CurrentUser;
@@ -182,5 +211,4 @@ if ( scalar(@agentnums) == 1 ) {
} else {
$company_name = $conf->config('company_name');
}
-
</%init>
diff --git a/httemplate/elements/menu.html b/httemplate/elements/menu.html
index f558777..2ca2dbd 100644
--- a/httemplate/elements/menu.html
+++ b/httemplate/elements/menu.html
@@ -41,7 +41,17 @@
var myBar = new WebFXMenuBar;
-% foreach my $item ( keys %menu ) {
+% if ( $mobile ) {
+%
+% my( $subhtml, $submenuname ) = submenu(\%menu, 'Freeside');
+ <% $subhtml |n %>
+ myBar.add(new WebFXMenuButton("Freeside",
+ null,
+ null,
+ <% $submenuname |n %>));
+% }
+% else {
+% foreach my $item ( keys %menu ) {
%
% my( $url_or_submenu, $tooltip ) = @{ $menu{$item} };
%
@@ -59,8 +69,8 @@
myBar.add(new WebFXMenuButton("<% $item %>", "<% $url_or_submenu %>", "<% $tooltip %>" ));
% }
-%
-% }
+% } #foreach $item
+% } #if $mobile
myBar.show( null, 'vertical' );
myBar.width = 154;
@@ -72,6 +82,8 @@ my( %opt ) = @_;
my $conf = new FS::Conf;
my $fsurl = $opt{'freeside_baseurl'};
+my $mobile = $opt{'mobile'} || 0;
+
my $curuser = $FS::CurrentUser::CurrentUser;
#XXX Active tickets not assigned to a customer
@@ -634,7 +646,7 @@ $menu{'Reports'} = [ \%report_menu, 'Lists, reporting and graphing' ]
if keys %report_menu;
$menu{'Tools'} = [ \%tools_menu, 'Tools' ]
if keys %tools_menu;
-$menu{'Configuration'} = [ \%config_menu, 'Configuraiton and setup' ]
+$menu{'Configuration'} = [ \%config_menu, 'Configuration and setup' ]
if $curuser->access_right('Configuration')
|| $curuser->access_right('Edit package definitions')
|| $curuser->access_right('Edit global package definitions')
diff --git a/httemplate/elements/searchbar-combined.html b/httemplate/elements/searchbar-combined.html
new file mode 100644
index 0000000..20425cd
--- /dev/null
+++ b/httemplate/elements/searchbar-combined.html
@@ -0,0 +1,51 @@
+<DIV STYLE="display:inline;
+ padding:3px 7px;
+ font-size:16px;
+ float:right;
+ border:1px solid #cccccc">
+Search
+<FORM ACTION="<%$fsurl%>search/searchbar.cgi"
+ METHOD="GET"
+ STYLE="margin:0; display:inline">
+ <SCRIPT TYPE="text/javascript">
+var hints=new Array(<% join(',', map {"'$hints{$_}'"} @searches) |n%>);
+function update_hint() {
+ var s = document.getElementById('search_for');
+ var q = document.getElementById('q');
+ q.value = hints[s.selectedIndex];
+}
+function clear_hint() {
+ var s = document.getElementById('search_for');
+ var q = document.getElementById('q');
+ if ( q.value == hints[s.selectedIndex] ) {
+ q.value = '';
+ }
+}
+ </SCRIPT>
+ <SELECT NAME="search_for" id="search_for" STYLE="width:auto" onchange="update_hint()">
+% foreach (@searches) {
+ <OPTION NAME="<% $_ %>"><% $_ %></OPTION>
+% }
+ </SELECT>
+ <INPUT NAME="q" ID="q" TYPE="text" STYLE="width:140px" VALUE="<% $hints{$searches[0]} %>" onmousedown="clear_hint()">
+ <INPUT TYPE="submit" VALUE="Search" STYLE="width:auto">
+</FORM>
+</DIV>
+<%init>
+my $curuser = $FS::CurrentUser::CurrentUser;
+my @searches = ();
+push @searches, 'customers' if $curuser->access_right('List customers');
+push @searches, 'prospects' if $curuser->access_right('List prospects');
+push @searches, 'invoices' if $curuser->access_right('View invoices');
+push @searches, 'services' if $curuser->access_right('View customer services');
+push @searches, 'tickets' if FS::Conf->new->exists('ticket_system');
+
+my %hints = (
+ 'customers' => '(cust #, name, company)',
+ 'prospects' => '(name, company, phone)',
+ 'invoices' => '(invoice #)',
+ 'services' => '(user, email, phone...)',
+ 'tickets' => '(ticket #, subject, email)',
+);
+
+</%init>
diff --git a/httemplate/search/searchbar.cgi b/httemplate/search/searchbar.cgi
new file mode 100644
index 0000000..c932871
--- /dev/null
+++ b/httemplate/search/searchbar.cgi
@@ -0,0 +1,16 @@
+<%init>
+my %searches = (
+ 'customers' => 'cust_main.cgi?search_cust=',
+ 'prospects' => 'prospect_main.html?search_prospect=',
+ 'invoices' => 'cust_bill.html?invnum=',
+ 'services' => 'cust_svc.html?search_svc=',
+);
+if ( FS::Conf->new->config('ticket_system') ) {
+ $searches{'tickets'} = FS::TicketSystem->baseurl . 'index.html?q=';
+}
+
+$cgi->param('search_for') =~ /^(\w+)$/;
+my $search = $searches{$1} or die "unknown search type: '$1'\n";
+my $q = $cgi->param('q'); # pass through unparsed
+</%init>
+<% $cgi->redirect($search . $q) %>
diff --git a/rt/FREESIDE_MODIFIED b/rt/FREESIDE_MODIFIED
index e42182b..e109174 100644
--- a/rt/FREESIDE_MODIFIED
+++ b/rt/FREESIDE_MODIFIED
@@ -128,9 +128,10 @@ share/html/Callbacks/SearchCustomerFields/*
share/html/Callbacks/RTx-Statistics/*
share/html/RTx/Statistics/*
-share/html/Callbacks/Results-XLS/*
share/html/Search/Results.xls
-lib/RT/Extension/SearchResults/XLS.pm
-
share/html/Search/Results.csv
share/html/Search/Elements/ResultViews
+
+lib/RT/Extension/MobileUI.pm
+share/html/Callbacks/RT-Extension-MobileUI/*
+share/html/m/*
diff --git a/rt/etc/RT_Config.pm b/rt/etc/RT_Config.pm
index 12044a4..996bc0b 100644
--- a/rt/etc/RT_Config.pm
+++ b/rt/etc/RT_Config.pm
@@ -1810,7 +1810,8 @@ C<Set(@Plugins, (qw(Extension::QuickDelete RT::FM)));>
=cut
-Set(@Plugins, (qw(RTx::Calendar))); #RTx::Checklist ));
+Set(@Plugins, (qw(RTx::Calendar
+ RT::Extension::MobileUI))); #RTx::Checklist ));
=back
diff --git a/rt/etc/RT_Config.pm.in b/rt/etc/RT_Config.pm.in
index 2018023..575a94f 100644
--- a/rt/etc/RT_Config.pm.in
+++ b/rt/etc/RT_Config.pm.in
@@ -1820,7 +1820,8 @@ C<Set(@Plugins, (qw(Extension::QuickDelete RT::FM)));>
=cut
-Set(@Plugins, (qw(RTx::Calendar))); #RTx::Checklist ));
+Set(@Plugins, (qw(RTx::Calendar
+ RT::Extension::MobileUI))); #RTx::Checklist ));
=back
diff --git a/rt/lib/RT/Extension/MobileUI.pm b/rt/lib/RT/Extension/MobileUI.pm
new file mode 100644
index 0000000..26873c7
--- /dev/null
+++ b/rt/lib/RT/Extension/MobileUI.pm
@@ -0,0 +1,57 @@
+use warnings;
+use strict;
+
+package RT::Extension::MobileUI;
+
+our $VERSION = "1.01";
+
+
+=head1 NAME
+
+RT::Extension::MobileUI - A phone friendly web interface for RT
+
+=head1 DESCRIPTION
+
+This RT extension adds a mobile interface for RT.
+
+=head1 INSTALLATION
+
+ # perl Makefile.PL
+ # make
+ # make install
+
+ Add RT::Extension::MobileUI to your /opt/rt3/etc/RT_SiteConfig.pm file
+ Set(@Plugins, qw(RT::Extension::MobileUI));
+
+ If you have more than one Plugin enabled, you must enable them as one
+ Set(@Plugins, qw(Foo Bar)); command
+
+ # restart apache
+=cut
+
+
+
+
+sub MobileClient {
+ my $self = shift;
+
+
+if (($ENV{'HTTP_USER_AGENT'} || '') =~ /(?:hiptop|Blazer|Novarra|Vagabond|SonyEricsson|Symbian|NetFront|UP.Browser|UP.Link|Windows CE|MIDP|J2ME|DoCoMo|J-PHONE|PalmOS|PalmSource|iPhone|iPod|AvantGo|Nokia|Android|WebOS|S60|Opera Mini|Opera Mobi)/io && !$HTML::Mason::Commands::session{'NotMobile'}) {
+ return 1;
+} else {
+ return undef;
+}
+
+}
+
+=head1 AUTHOR
+
+Jesse Vincent E<lt>jesse@bestpractical.comE<gt>
+
+=head1 LICENSE
+
+GPL version 2.
+
+=cut
+
+1;
diff --git a/rt/share/html/Callbacks/RT-Extension-MobileUI/Elements/Login/Header b/rt/share/html/Callbacks/RT-Extension-MobileUI/Elements/Login/Header
new file mode 100644
index 0000000..9e6ac0a
--- /dev/null
+++ b/rt/share/html/Callbacks/RT-Extension-MobileUI/Elements/Login/Header
@@ -0,0 +1,12 @@
+<%init>
+if ( defined($RT::Extension::MobileUI::VERSION)
+ and ( RT::Extension::MobileUI->MobileClient() ||
+ ($m->request_comp->path() =~ m{^/m(?:\/|$)})) {
+
+ $m->comp('/m/login',%ARGS);
+ $m->abort;
+} else {
+return;
+}
+
+</%init>
diff --git a/rt/share/html/Callbacks/RT-Extension-MobileUI/Ticket/Create.html/Init b/rt/share/html/Callbacks/RT-Extension-MobileUI/Ticket/Create.html/Init
new file mode 100644
index 0000000..f9c418f
--- /dev/null
+++ b/rt/share/html/Callbacks/RT-Extension-MobileUI/Ticket/Create.html/Init
@@ -0,0 +1,13 @@
+<%INIT>
+if ( defined($RT::Extension::MobileUI::VERSION)
+ and RT::Extension::MobileUI::MobileClient() ) {
+ RT::Interface::Web::Redirect(
+ RT->Config->Get('WebURL').'m/ticket/create?'.
+ $m->comp('/Elements/QueryString', %$ARGSRef),
+ );
+ $m->abort;
+}
+</%INIT>
+<%ARGS>
+$ARGSRef => {}
+</%ARGS>
diff --git a/rt/share/html/Callbacks/RT-Extension-MobileUI/Ticket/Display.html/Initial b/rt/share/html/Callbacks/RT-Extension-MobileUI/Ticket/Display.html/Initial
new file mode 100644
index 0000000..6b6edbe
--- /dev/null
+++ b/rt/share/html/Callbacks/RT-Extension-MobileUI/Ticket/Display.html/Initial
@@ -0,0 +1,14 @@
+<%INIT>
+return if $ARGSRef->{'NoRedirect'};
+if ( defined($RT::Extension::MobileUI::VERSION)
+ and RT::Extension::MobileUI::MobileClient()) {
+ my $id = $ARGSRef->{'id'} || ($TicketObj ? $TicketObj->id : undef);
+ RT::Interface::Web::Redirect(RT->Config->Get('WebURL').'m/ticket/show?id='.$id);
+ $m->abort;
+}
+</%INIT>
+
+<%ARGS>
+$TicketObj => undef
+$ARGSRef => {}
+</%ARGS>
diff --git a/rt/share/html/Callbacks/RT-Extension-MobileUI/index.html/Initial b/rt/share/html/Callbacks/RT-Extension-MobileUI/index.html/Initial
new file mode 100644
index 0000000..d634454
--- /dev/null
+++ b/rt/share/html/Callbacks/RT-Extension-MobileUI/index.html/Initial
@@ -0,0 +1,16 @@
+<%init>
+# avoid fatal errors if the extension isn't loaded
+if ( defined( $RT::Extension::MobileUI::VERSION )
+ and RT::Extension::MobileUI->MobileClient()) {
+ my $path = 'm';
+ if ( $ARGSRef->{'q'} ) {
+ $path = "m/tickets/search?q=". $m->interp->apply_escapes($ARGSRef->{'q'});
+ }
+ RT::Interface::Web::Redirect( RT->Config->Get('WebURL') . $path);
+} else {
+return
+}
+</%init>
+<%ARGS>
+$ARGSRef => {}
+</%ARGS>
diff --git a/rt/share/html/m/_elements/footer b/rt/share/html/m/_elements/footer
new file mode 100644
index 0000000..f3e0837
--- /dev/null
+++ b/rt/share/html/m/_elements/footer
@@ -0,0 +1,11 @@
+<& /elements/footer.html &>
+% if ( 0 ) {
+ <div id="bpscredits">
+ <& /Elements/Logo, ShowName => 0 &>
+ <div id="copyright">
+<&|/l, '', '', '2010', '<a href="http://www.bestpractical.com?rt='.$RT::VERSION.'">Best Practical Solutions, LLC</a>', &>[_1] RT [_2] Copyright 1996-[_3] [_4].</&>
+</div>
+</div>
+</body>
+</html>
+% }
diff --git a/rt/share/html/m/_elements/full_site_link b/rt/share/html/m/_elements/full_site_link
new file mode 100644
index 0000000..7f43968
--- /dev/null
+++ b/rt/share/html/m/_elements/full_site_link
@@ -0,0 +1 @@
+<a id="fullsite" href="<%RT->Config->Get('WebPath')%>/m/index.html?NotMobile=1"><&|/l&>Not using a mobile browser?</&></a>
diff --git a/rt/share/html/m/_elements/header b/rt/share/html/m/_elements/header
new file mode 100644
index 0000000..4af6299
--- /dev/null
+++ b/rt/share/html/m/_elements/header
@@ -0,0 +1,38 @@
+<%args>
+$title => undef
+$show_home_button => 1
+</%args>
+<%init>
+$r->headers_out->{'Pragma'} = 'no-cache';
+$r->headers_out->{'Cache-control'} = 'no-cache';
+
+my $head = '';
+
+my $etc = '';
+
+</%init>
+<& /elements/header.html, {
+ 'title' => $title,
+ 'head' => $head,
+ 'etc' => $etc,
+ 'nocss' => 1,
+ 'nobr' => 1,
+ 'mobile' => 1,
+} &>
+<link rel="stylesheet" type="text/css" href="<%RT->Config->Get('WebPath')|n%>/m/style.css"/>
+% if ( 0 ) { # Disabled in favor of Freeside header
+<html>
+<head>
+<title><%$title%></title>
+</head>
+<body>
+% if ($show_home_button) {
+% # The align is for older browsers, like the blackberry
+<div id="gohome" align="right">
+<a href="<%RT->Config->Get('WebPath')%>/m/"><&|/l&>Homepage</&></a>
+</div>
+% }
+% if ($title) {
+<h1><%$title%></h1>
+% }
+% } # disabled
diff --git a/rt/share/html/m/_elements/menu b/rt/share/html/m/_elements/menu
new file mode 100644
index 0000000..54e7fe9
--- /dev/null
+++ b/rt/share/html/m/_elements/menu
@@ -0,0 +1,63 @@
+<&| /Widgets/TitleBox, class => 'menu'&>
+<ul class="menu">
+% for my $item (@menu) {
+% if (exists $item->{html}) {
+<li><%$item->{html} |n%></li>
+% } else {
+<li><a href="<%RT->Config->Get('WebPath')%><%$item->{url}%>"><%$item->{label}%></a></li>
+% }
+% }
+</ul>
+</&>
+<%init>
+use RT::SavedSearches;
+my @menu = (
+ { html => '<form method="GET" id="search" action="'
+ . RT->Config->Get('WebPath')
+ . '/m/tickets/search">'
+ . loc("Search")
+ . ': <input type="text" name="q" id="q" value=""/>'
+ . '<input type="submit" value="'
+ . loc("Go")
+ . '"/></form>'
+ },
+ { label => loc("New ticket"),
+ url => '/m/ticket/select_create_queue',
+ },
+ { label => loc("Bookmarked tickets"),
+ url => '/m/tickets/search?name=Bookmarked%20Tickets',
+ },
+ { label => loc("Tickets I own"),
+ url => '/m/tickets/search?name=My%20Tickets',
+ },
+ { label => loc("Unowned tickets"),
+ url => '/m/tickets/search?name=Unowned%20Tickets',
+ },
+ { label => loc("All tickets"),
+ url => '/m/tickets/search?query=id!%3d0&order_by=id&order=DESC'
+ },
+);
+
+
+if ( $session{'CurrentUser'}->HasRight( Right => 'LoadSavedSearch', Object => $RT::System))
+ {
+
+ my @Objects = RT::SavedSearches->new( $session{CurrentUser} )->_PrivacyObjects;
+ push @Objects, RT::System->new( $session{'CurrentUser'} )
+ if $session{'CurrentUser'}->HasRight(
+ Object => $RT::System,
+ Right => 'SuperUser'
+ );
+
+ foreach my $object (@Objects) {
+ my @searches = $object->Attributes->Named('SavedSearch');
+ foreach my $search (@searches) {
+ next unless $search->SubValue("SearchType") eq 'Ticket';
+ push @menu, { label => $search->Description, url => '/m/tickets/search?query=' . $search->SubValue("Query").'&order='.$search->SubValue("Order").'&order_by='.$search->SubValue("OrderBy") };
+
+ }
+ }
+}
+push @menu, { label => loc("Logout"), url => '/m/logout', }
+ if !RT->Config->Get('WebExternalAuth');
+</%init>
diff --git a/rt/share/html/m/_elements/raw_style b/rt/share/html/m/_elements/raw_style
new file mode 100644
index 0000000..8c19977
--- /dev/null
+++ b/rt/share/html/m/_elements/raw_style
@@ -0,0 +1,417 @@
+body {
+ font-family: helvetica, arial, sans-serif;
+ /*background-color: #ccf;*/
+ background-color: #f8f8f8;
+ margin: 0;
+}
+
+h1 {
+ font-size: 1.2em;
+ padding-top: 0.5em;
+ padding-left: 0.2em;
+ display: block;
+ background-color: #f8f8f8;
+
+}
+
+div.buttons {
+ text-align: right;
+ padding-right: 0.5em;
+ padding-bottom: 0.5em;
+}
+
+.titlebox-title {
+ font-size: 1.1em;
+ margin-left: 0.5em;
+ margin-top: -1.2em;
+ top: -0.5em;
+ padding: 0.5em;
+ position: relative;
+ display: inline-block;
+ text-decoration: none;
+ /*background-color: #fff;*/
+ background-color: #ccc;
+ -moz-border-radius: 0.25em;
+ -webkit-border-radius: 0.25em;
+ -webkit-box-shadow: #333 0px 0px 5px;
+ -moz-box-shadow: #333 0px 0px 5px;
+ box-shadow: #333 0px 0px 5px;
+}
+
+ul.menu
+{
+ text-align: left;
+ list-style: none;
+ padding: 0;
+ margin: -0.6em;
+ left: 0;
+}
+
+ul.menu li
+{
+ display: block;
+ margin: 0;
+ padding: 0;
+ font-weight: bold;
+}
+
+ul.ticketlist li:active, ul.ticketlist li:hover,
+ul.menu li:active, ul.menu li:hover {
+ background-color: #eee;
+}
+
+
+ul.menu li
+{
+ display: block;
+ padding: 1em;
+ margin: 0;
+ border:0;
+ border-top-width: 1px;
+ border-top-color: #666;
+ border-style: solid;
+ text-decoration: none;
+}
+
+ul.menu li:first-child{
+ border: none;
+}
+
+ul.menu li#active a
+{
+ color: #800000;
+}
+
+div.titlebox, #bpscredits, .ticket_menu{
+ -moz-border-radius: 1em;
+ -webkit-border-radius: 1em;
+ margin: 0.5em;
+ background-color: #fff;
+ padding-top: 1em;
+ padding-bottom: 0.8em;
+ margin-top: 1.25em;
+ -webkit-box-shadow: #333 0px 0px 5px;
+ -moz-box-shadow: #333 0px 0px 5px;
+ box-shadow: #333 0px 0px 5px;
+ margin-bottom: 1em;
+}
+
+div .titlebox-content {
+ padding-left: 0.5em;
+ padding-right: 0.5em;
+}
+
+hr.clear {
+ display: none;
+}
+
+
+.label, .labeltop {
+ font-weight: normal;
+}
+.value {
+ font-weight: bold;
+ display:inline-block;
+}
+
+ul.ticketlist {
+ list-style: none;
+ padding-left: -0.5em;
+ padding-right: -0.5em; /* to counteract the titlebox and get shading to the end*/
+ margin-left: -0.5em;
+ margin-right: -0.5em;
+ padding: 0em;
+ padding-bottom: 1em;
+}
+
+ul.ticketlist li.ticket {
+ padding: 0.5em;
+ font-weight: bold;
+ border-bottom: 1px solid #999;
+
+}
+ul.ticketlist li.ticket:first-child {
+ border-top: 1px solid #999;
+}
+
+ul.ticketlist li.ticket a.ticket{
+ display: inline-block;
+ font-size: 1em;
+ width: 100%;
+ padding: 0.5em;
+ padding-bottom: 5em;
+ margin-bottom: -5em;
+}
+ul.ticketlist li.ticket div.metadata {
+}
+
+
+ul.ticketlist li.ticket div.metadata div {
+ padding: 0.2em;
+ font-size:0.8em;
+ display: block;
+}
+
+ul.ticketlist li.ticket div.metadata .label {
+ display: inline-block;
+ width: 6em;
+ font-size: 0.8em;
+ text-align: right;
+ color: #666;
+}
+
+div#paging {
+ text-align: center;
+}
+
+.ticket-reply .titlebox-title, .titlebox.search .titlebox-title, .titlebox.menu .titlebox-title, .ticket_menu .titlebox-title, .history .titlebox-title, #ticket-create-basics .titlebox-title{
+ display: none;
+}
+
+a {
+ color: #000;
+}
+
+.ticket_menu a, .menu a {
+ text-decoration: none;
+}
+
+ul.menu a {
+ padding: 0.5em;
+ margin-top: -0.5em;
+ margin-bottom: -0.5em;
+ display: inline-block;
+ width: 100%;
+}
+
+ul.menu a:after {
+ color: #666;
+ float: right;
+ content: ">";
+ font-size: 1.5em;
+ padding: 0;
+ margin: 0;
+ padding-right: 1em;
+
+}
+
+ul.menu form {
+ display: inline;
+}
+
+ul.menu form * {
+ display: inline;
+}
+
+
+ul.menu form input[type=text] {
+ width: 7em;
+}
+
+ul.menu form input{
+
+ width: auto;
+ padding: 0.5em;
+ margin: -0.5em;
+ margin-left: 1em;
+}
+
+.ticket_menu {
+ text-align: center;
+}
+
+.ticket_menu ul {
+ display: block;
+ margin: 0;
+ padding: 0;
+}
+
+.ticket_menu ul li {
+
+ display: inline-block;
+ text-align: center;
+ padding-bottom: 0.25em;
+ padding-top: 0.25em;
+ font-size: 1em;
+ width: 28%;
+ padding-right: 0.3em;
+ padding-left: 0.2em;
+ border-right: 1px solid #000;
+}
+.ticket_menu ul li:last-child {
+ padding-right: 0;
+ border-right: 0;
+}
+
+.ticket-info-reminders table {
+
+ width: 100%;
+}
+
+#ticket-create .label:after {
+ content: ": ";
+ padding-right: 0.25em;
+
+}
+
+#ticket-create .content-label {
+ width: auto;
+ display: block;
+ text-align: left;
+
+}
+
+#ticket-show .label, .login-body .label {
+ display: inline-block;
+ text-align: right;
+ width: 6em;
+ padding-right: 0.25em;
+ font-size: 0.8em;
+}
+
+.login-body .value {
+ width: auto;
+}
+
+.history ul.history-list {
+ padding: 0;
+ margin: 0;
+ padding-bottom: 2em;
+}
+
+
+.history ul.history-list li:first-child {
+ border-top: 1px solid #ccc;
+}
+
+.history ul.history-list li {
+ list-style: none;
+ border-bottom: 1px solid #ccc;
+ padding: 0.5em;
+}
+
+.history .age {
+ display: inline-block;
+ min-width: 8em;
+ text-align: right;
+
+}
+
+div#login-box div.titlebox {
+ width: 100%;
+ margin-left:auto;
+ margin-right: auto;
+}
+
+div#login-box input[type=text], div#login-box input[type=password] {
+ width: 100%;
+}
+
+#bpscredits img {
+ padding-bottom: 1em;
+}
+
+
+
+#bpscredits {
+ float: right;
+ text-align: right;
+ width: auto;
+ font-size: 0.8em;
+ padding: 1em;
+}
+
+
+:focus {
+ background-color: #ffc;
+ border-color: #000;
+ border-weight: 3px;
+}
+
+input[type=submit], input[type=button], button, #paging a {
+ border: 2px outset;
+ margin: 0.3em;
+ padding: 0.3em;
+ padding-left: 0.6em;
+ padding-right: 0.6em;
+ -moz-border-radius: 0.5em;
+ -webkit-border-radius: 0.5em;
+ background-color: #006699;
+ color: #fff;
+}
+
+form {
+
+ margin:0;
+}
+
+#gohome {
+ position: absolute;
+ top: 0;
+ right: 0;
+ border-left: 1px solid black;
+ border-bottom: 1px solid black;
+ -moz-border-radius-bottomleft: 1em;
+ -webkit-border-bottom-left-radius: 1em;
+ padding: 0.5em;
+ background-color: #fff;
+}
+
+#gohome a {
+ font-size: 1em;
+ padding: 0.25em;
+ color: #000;
+}
+
+div.txn-content {
+
+ font-size:0.8em;
+ padding-left:1em;
+ padding-top:0.5em;
+ margin-top: 0.5em;
+ margin-left: 2em;
+ padding-bottom: 0.5em;
+ border-left: 5px solid #00c;
+
+}
+
+.label {
+ text-align: left;
+ width: 10em;
+ color: #666;
+ display: block;
+ padding-bottom: 0.2em;
+ padding-right: 0.2em;
+
+}
+
+div.entry, tr.input-row {
+ margin-bottom: 0.25em;
+ padding-bottom: 0.25em;
+ border-bottom: 1px solid #ccc;
+ display: block;
+ width: 100%;
+ min-height: 1em;
+}
+
+
+input, input[type=text], input[type=password], select {
+ width: 100%;
+}
+
+.timefield input {
+ width: 5em;
+}
+
+.timefield select {
+ width: auto;
+}
+
+
+textarea {
+ width: 100%;
+}
+
+a#fullsite {
+ padding-left: 1em;
+}
diff --git a/rt/share/html/m/_elements/ticket_list b/rt/share/html/m/_elements/ticket_list
new file mode 100644
index 0000000..822efe8
--- /dev/null
+++ b/rt/share/html/m/_elements/ticket_list
@@ -0,0 +1,64 @@
+<%args>
+$order => undef
+$order_by => undef
+$query => ''
+$page => 1
+</%args>
+<%init>
+my $collection = RT::Tickets->new($session{'CurrentUser'});
+$collection->FromSQL($query);
+$collection->RowsPerPage(10);
+$collection->GotoPage($page-1);
+# XXX: ->{'order_by'} is hacky, but there is no way to check if
+# collection is ordered or not
+if ( $order_by) {
+ my @order_by = split /\|/, $order_by;
+ my @order = split /\|/,$order;
+ $collection->OrderByCols(
+ map { { FIELD => $order_by[$_], ORDER => $order[$_] } }
+ ( 0 .. $#order_by )
+ );
+}
+
+
+
+$collection->RedoSearch();
+
+if ($page > 1 && ! @{$collection->ItemsArrayRef||[]}) {
+ RT::Interface::Web::Redirect( RT->Config->Get('WebURL')."m/tickets/search?page=".($page-1)."&query=".$query."&order=$order&order_by=$order_by");
+}
+
+</%init>
+<&| /m/_elements/wrapper, title =>
+loc("Found [quant,_1,ticket]",$collection->Count) &>
+<&|/Widgets/TitleBox, class => 'search'
+&>
+<ul class="ticketlist">
+% while (my $ticket = $collection->Next()) {
+<li class="ticket">
+<a class="ticket" href="<%RT->Config->Get('WebPath')%>/m/ticket/show?id=<%$ticket->id%>"><%$ticket->id%>: <%$ticket->Subject%></a>
+<div class="metadata">
+<%perl>
+
+</%perl>
+<div class="requestors"><span class="label"><&|/l&>Requestors</&>:</span> <& /Ticket/Elements/ShowGroupMembers, Group => $ticket->Requestors, Ticket => $ticket &></div>
+<div class="status"><span class="label"><&|/l&>Status</&>:</span> <%$ticket->Status%></div>
+<div class="owner"><span class="label"><&|/l&>Owner</&>:</span> <& /Elements/ShowUser, User => $ticket->OwnerObj, Ticket => $ticket &></div>
+<div class="created"><span class="label"><&|/l&>Created</&>:</span> <%$ticket->CreatedObj->AgeAsString()%></div>
+% if ($ticket->Priority) {
+<div class="priority"><span class="label"><&|/l&>Priority</&>:</span> <%$ticket->Priority%></div>
+% }
+</div>
+</li>
+% }
+</ul>
+<div id="paging">
+% if ($page > 1) {
+<a href="<%RT->Config->Get('WebPath')%>/m/tickets/search?page=<%$page-1%>&query=<%$query%>&order=<%$order%>&order_by=<%$order_by%>">Back</a>
+% }
+Page <%$page%>
+
+<a href="<%RT->Config->Get('WebPath')%>/m/tickets/search?page=<%$page+1%>&query=<%$query%>&order=<%$order%>&order_by=<%$order_by%>">Next</a>
+</div>
+</&>
+</&>
diff --git a/rt/share/html/m/_elements/ticket_menu b/rt/share/html/m/_elements/ticket_menu
new file mode 100644
index 0000000..257b066
--- /dev/null
+++ b/rt/share/html/m/_elements/ticket_menu
@@ -0,0 +1,31 @@
+<%args>
+$ticket
+</%args>
+<div class="ticket_menu">
+<ul>
+% for my $item (@menu) {
+<li><a href="<%RT->Config->Get('WebPath')%><%$item->{url}%>"><%$item->{label}%></a></li>
+% }
+</ul>
+</div>
+<%init>
+my @menu = (
+{ label => loc("Basics"),
+ url => '/m/ticket/show?id='.$ticket->id
+},
+ {
+ label => loc("History"),
+ url => '/m/ticket/history?id='.$ticket->id
+ },
+ #{ label => loc("Modify"), url => '/m/ticket/modify?id='.$ticket->id },
+{
+ label => loc("Reply"),
+ url => '/m/ticket/reply?id='.$ticket->id
+}
+
+
+);
+
+my $width = int(100/ ($#menu +1))-5;
+
+</%init>
diff --git a/rt/share/html/m/_elements/wrapper b/rt/share/html/m/_elements/wrapper
new file mode 100644
index 0000000..794385d
--- /dev/null
+++ b/rt/share/html/m/_elements/wrapper
@@ -0,0 +1,15 @@
+<%args>
+$title => ''
+$show_home_button => 1
+</%args>
+<%init>
+if ($m->request_args->{'NotMobile'}) {
+ $session{'NotMobile'} = 1;
+ RT::Interface::Web::Redirect(RT->Config->Get('WebURL'));
+ $m->abort();
+}
+$m->comp('header', title => $title, show_home_button => $show_home_button);
+$m->out($m->content);
+$m->comp('footer');
+$m->abort();
+</%init>
diff --git a/rt/share/html/m/dhandler b/rt/share/html/m/dhandler
new file mode 100644
index 0000000..627ec22
--- /dev/null
+++ b/rt/share/html/m/dhandler
@@ -0,0 +1,5 @@
+<%init>
+# deal with users who don't have options indexes set right
+RT::Interface::Web::Redirect(RT->Config->Get('WebURL')."m/index.html");
+$m->abort();
+</%init>
diff --git a/rt/share/html/m/index.html b/rt/share/html/m/index.html
new file mode 100644
index 0000000..5b38125
--- /dev/null
+++ b/rt/share/html/m/index.html
@@ -0,0 +1,4 @@
+<&| _elements/wrapper, title => loc("RT for [_1]",RT->Config->Get('rtname'))&>
+<& _elements/menu &>
+<& _elements/full_site_link &>
+</&>
diff --git a/rt/share/html/m/login b/rt/share/html/m/login
new file mode 100644
index 0000000..af7f67a
--- /dev/null
+++ b/rt/share/html/m/login
@@ -0,0 +1,84 @@
+<%INIT>
+
+my $req_uri;
+
+if (UNIVERSAL::can($r, 'uri') and $r->uri =~ m{.*/m/(.*)}) {
+ $req_uri = '/m/'.$1;
+}
+
+my $default_path = RT->Config->Get('WebPath') ."/m/";
+
+my $form_action = defined $goto ? $goto
+ : defined $req_uri ? $req_uri
+ : $default_path
+ ;
+
+# sanitize $form_action
+my $uri = URI->new($form_action);
+
+# You get undef scheme with a relative uri like "/Search/Build.html"
+unless (!defined($uri->scheme) || $uri->scheme eq 'http' || $uri->scheme eq 'https') {
+ $form_action = $default_path;
+}
+
+# Make sure we're logging in to the same domain
+# You can get an undef authority with a relative uri like "index.html"
+my $uri_base_url = URI->new(RT->Config->Get('WebURL')."m/");
+unless (!defined($uri->authority) || $uri->authority eq $uri_base_url->authority) {
+ $form_action = $default_path;
+}
+</%INIT>
+<&| /m/_elements/wrapper, show_home_button => 0 &>
+<style>
+<& /m/_elements/raw_style &>
+</style>
+<h1><&|/l, RT->Config->Get('rtname') &>RT for [_1]</&></h1>
+<div id="body" class="login-body">
+% if ($Error) {
+<&| "/Widgets/TitleBox", title => loc('Error'), hideable => 0, class => 'error' &>
+<% $Error %>
+</&>
+% }
+
+
+<div id="login-box">
+<&| /Widgets/TitleBox, title => loc('Login'), hideable => 0 &>
+
+% unless (RT->Config->Get('WebExternalAuth') and !RT->Config->Get('WebFallbackToInternalAuth')) {
+<form id="login" name="login" method="post" action="<% $form_action %>">
+
+<div class="entry">
+ <span class="label"><&|/l&>Username</&>:</span><span class="value"><input name="user" value="<%$user%>" id="user" /></span>
+</div>
+
+<div class="entry">
+ <span class="label"><&|/l&>Password</&>:</span><span class="value"><input type="password" name="pass"/></span>
+</div>
+
+<& /Elements/Submit, Label => loc('Login')&>
+
+% foreach my $key (keys %ARGS) {
+% if (($key ne 'user') and ($key ne 'pass')) {
+% if (ref($ARGS{$key}) =~ /ARRAY/) {
+% foreach my $val (@{$ARGS{$key}}) {
+<input type="hidden" class="hidden" name="<%$key %>" value="<% $val %>" />
+% }
+% }
+% else {
+<input type="hidden" class="hidden" name="<% $key %>" value="<% $ARGS{$key} %>" />
+% }
+% }
+% }
+</form>
+% }
+</&>
+</div><!-- #login-box -->
+</div><!-- #login-body -->
+<& _elements/full_site_link &>
+</&>
+<%ARGS>
+$user => ""
+$pass => undef
+$goto => undef
+$Error => undef
+</%ARGS>
diff --git a/rt/share/html/m/logout b/rt/share/html/m/logout
new file mode 100644
index 0000000..3006ea8
--- /dev/null
+++ b/rt/share/html/m/logout
@@ -0,0 +1,7 @@
+<%init>
+if (keys %session) {
+ tied(%session)->delete;
+ $session{'CurrentUser'} = RT::CurrentUser->new;
+}
+RT::Interface::Web::Redirect(RT->Config->Get('WebURL')."m/");
+</%init>
diff --git a/rt/share/html/m/style.css b/rt/share/html/m/style.css
new file mode 100644
index 0000000..22be0a9
--- /dev/null
+++ b/rt/share/html/m/style.css
@@ -0,0 +1,5 @@
+<%init>
+ $HTML::Mason::Commands::r->content_type('text/css');
+ $m->comp('/m/_elements/raw_style');
+ $m->abort();
+</%init>
diff --git a/rt/share/html/m/ticket/create b/rt/share/html/m/ticket/create
new file mode 100644
index 0000000..7c23194
--- /dev/null
+++ b/rt/share/html/m/ticket/create
@@ -0,0 +1,400 @@
+<%ARGS>
+$QuoteTransaction => undef
+$CloneTicket => undef
+</%ARGS>
+<%init>
+$m->callback( CallbackName => "Init", ARGSRef => \%ARGS );
+my $Queue = $ARGS{Queue};
+
+
+my $showrows = sub {
+ my @pairs = @_;
+
+ while (@pairs) {
+ my $key = shift @pairs;
+ my $val = shift @pairs;
+
+ $m->out("<div class=\"entry\"><span class=\"label\">$key</span><div class=\"value\">$val</div></div>");
+
+ }
+
+};
+
+
+my $CloneTicketObj;
+if ($CloneTicket) {
+ $CloneTicketObj = RT::Ticket->new( $session{CurrentUser} );
+ $CloneTicketObj->Load($CloneTicket)
+ or Abort( loc("Ticket could not be loaded") );
+
+ my $clone = {
+ Requestors => join( ',', $CloneTicketObj->RequestorAddresses ),
+ Cc => join( ',', $CloneTicketObj->CcAddresses ),
+ AdminCc => join( ',', $CloneTicketObj->AdminCcAddresses ),
+ InitialPriority => $CloneTicketObj->Priority,
+ };
+
+ $clone->{$_} = $CloneTicketObj->$_()
+ for qw/Owner Subject FinalPriority TimeEstimated TimeWorked
+ Status TimeLeft/;
+
+ $clone->{$_} = $CloneTicketObj->$_->AsString
+ for grep { $CloneTicketObj->$_->Unix }
+ map { $_ . "Obj" } qw/Starts Started Due Resolved/;
+
+ my $members = $CloneTicketObj->Members;
+ my ( @members, @members_of, @refers, @refers_by, @depends, @depends_by );
+ my $refers = $CloneTicketObj->RefersTo;
+ while ( my $refer = $refers->Next ) {
+ push @refers, $refer->LocalTarget;
+ }
+ $clone->{'new-RefersTo'} = join ' ', @refers;
+
+ my $refers_by = $CloneTicketObj->ReferredToBy;
+ while ( my $refer_by = $refers_by->Next ) {
+ push @refers_by, $refer_by->LocalBase;
+ }
+ $clone->{'RefersTo-new'} = join ' ', @refers_by;
+ if (0) { # Temporarily disabled
+ my $depends = $CloneTicketObj->DependsOn;
+ while ( my $depend = $depends->Next ) {
+ push @depends, $depend->LocalTarget;
+ }
+ $clone->{'new-DependsOn'} = join ' ', @depends;
+
+ my $depends_by = $CloneTicketObj->DependedOnBy;
+ while ( my $depend_by = $depends_by->Next ) {
+ push @depends_by, $depend_by->LocalBase;
+ }
+ $clone->{'DependsOn-new'} = join ' ', @depends_by;
+
+ while ( my $member = $members->Next ) {
+ push @members, $member->LocalBase;
+ }
+ $clone->{'MemberOf-new'} = join ' ', @members;
+
+ my $members_of = $CloneTicketObj->MemberOf;
+ while ( my $member_of = $members_of->Next ) {
+ push @members_of, $member_of->LocalTarget;
+ }
+ $clone->{'new-MemberOf'} = join ' ', @members_of;
+
+ }
+
+ my $cfs = $CloneTicketObj->QueueObj->TicketCustomFields();
+ while ( my $cf = $cfs->Next ) {
+ my $cf_id = $cf->id;
+ my $cf_values = $CloneTicketObj->CustomFieldValues( $cf->id );
+ my @cf_values;
+ while ( my $cf_value = $cf_values->Next ) {
+ push @cf_values, $cf_value->Content;
+ }
+ $clone->{"Object-RT::Ticket--CustomField-$cf_id-Value"} = join "\n",
+ @cf_values;
+ }
+
+ for ( keys %$clone ) {
+ $ARGS{$_} = $clone->{$_} if not defined $ARGS{$_};
+ }
+
+}
+
+my @results;
+
+my $title = loc("Create a ticket");
+
+my $QueueObj = new RT::Queue($session{'CurrentUser'});
+$QueueObj->Load($Queue) || Abort(loc("Queue could not be loaded."));
+
+$m->callback( QueueObj => $QueueObj, title => \$title, results => \@results, ARGSRef => \%ARGS );
+
+$QueueObj->Disabled && Abort(loc("Cannot create tickets in a disabled queue."));
+
+my $CFs = $QueueObj->TicketCustomFields();
+
+my $ValidCFs = $m->comp(
+ '/Elements/ValidateCustomFields',
+ CustomFields => $CFs,
+ ARGSRef => \%ARGS
+);
+
+# {{{ deal with deleting uploaded attachments
+foreach my $key (keys %ARGS) {
+ if ($key =~ m/^DeleteAttach-(.+)$/) {
+ delete $session{'Attachments'}{$1};
+ }
+ $session{'Attachments'} = { %{$session{'Attachments'} || {}} };
+}
+# }}}
+
+# {{{ store the uploaded attachment in session
+if ($ARGS{'Attach'}) { # attachment?
+ my $attachment = MakeMIMEEntity(
+ AttachmentFieldName => 'Attach'
+ );
+
+ my $file_path = Encode::decode_utf8("$ARGS{'Attach'}");
+ $session{'Attachments'} = {
+ %{$session{'Attachments'} || {}},
+ $file_path => $attachment,
+ };
+}
+# }}}
+
+# delete temporary storage entry to make WebUI clean
+unless (keys %{$session{'Attachments'}} and $ARGS{'id'} eq 'new') {
+ delete $session{'Attachments'};
+}
+
+my $checks_failure = 0;
+
+my $gnupg_widget = $m->comp('/Elements/GnuPG/SignEncryptWidget:new', Arguments => \%ARGS );
+$m->comp( '/Elements/GnuPG/SignEncryptWidget:Process',
+ self => $gnupg_widget,
+ QueueObj => $QueueObj,
+);
+
+
+if ( !exists $ARGS{'AddMoreAttach'} && ($ARGS{'id'}||'') eq 'new' ) {
+ my $status = $m->comp('/Elements/GnuPG/SignEncryptWidget:Check',
+ self => $gnupg_widget,
+ Operation => 'Create',
+ QueueObj => $QueueObj,
+ );
+ $checks_failure = 1 unless $status;
+}
+
+# check email addresses for RT's
+{
+ foreach my $field ( qw(Requestors Cc AdminCc) ) {
+ my $value = $ARGS{ $field };
+ next unless defined $value && length $value;
+
+ my @emails = Email::Address->parse( $value );
+ foreach my $email ( grep RT::EmailParser->IsRTAddress($_->address), @emails ) {
+ push @results, loc("[_1] is an address RT receives mail at. Adding it as a '[_2]' would create a mail loop", $email->format, loc($field =~ /^(.*?)s?$/) );
+ $checks_failure = 1;
+ $email = undef;
+ }
+ $ARGS{ $field } = join ', ', map $_->format, grep defined, @emails;
+ }
+}
+
+my $skip_create = 0;
+$m->callback( CallbackName => 'BeforeCreate', ARGSRef => \%ARGS, skip_create => \$skip_create,
+ checks_failure => $checks_failure, results => \@results );
+
+if ((!exists $ARGS{'AddMoreAttach'}) and (defined($ARGS{'id'}) and $ARGS{'id'} eq 'new')) { # new ticket?
+ if ( $ValidCFs && !$checks_failure && !$skip_create ) {
+ $m->comp('show', %ARGS);
+ $RT::Logger->crit("After display call; error is $@");
+ $m->abort();
+ }
+ elsif ( !$ValidCFs ) {
+ # Invalid CFs
+ while (my $CF = $CFs->Next) {
+ my $msg = $m->notes('InvalidField-' . $CF->Id) or next;
+ push @results, $CF->Name . ': ' . $msg;
+ }
+ }
+}
+
+
+
+
+</%init>
+<&| /m/_elements/wrapper, title => $title &>
+<& /Elements/ListActions, actions => \@results &>
+<form action="<% RT->Config->Get('WebPath') %>/m/ticket/create" method="post" enctype="multipart/form-data" name="TicketCreate" id="ticket-create">
+<input type="hidden" class="hidden" name="id" value="new" />
+% $m->callback( CallbackName => 'FormStart', QueueObj => $QueueObj, ARGSRef => \%ARGS );
+% if ($gnupg_widget) {
+<& /Elements/GnuPG/SignEncryptWidget:ShowIssues, self => $gnupg_widget &>
+% }
+
+
+<div id="ticket-create-simple">
+<&| /Widgets/TitleBox, title => $QueueObj->Name &>
+
+<%perl>
+$showrows->(
+ loc("Subject") => '<input name="Subject" size="30" maxsize="200" value="'.($ARGS{Subject} || '').'" />');
+</%perl>
+ <span class="content-label label"><%loc("Describe the issue below")%></span>
+ <& /Elements/MessageBox, exists $ARGS{Content} ? (Default => $ARGS{Content}, IncludeSignature => 0 ) : ( QuoteTransaction => $QuoteTransaction ), Height => 5 &>
+
+
+<&/Elements/Submit, Label => loc("Create") &>
+
+
+</&>
+</div>
+
+<div id="ticket-create-basics">
+<&| /Widgets/TitleBox &>
+ <input type="hidden" class="hidden" name="Queue" value="<%$QueueObj->id %>" />
+<%perl>
+
+$showrows->(
+
+ # loc('Queue') => $m->scomp( '/Ticket/Elements/ShowQueue', QueueObj => $QueueObj ) ,
+
+ loc('Status') =>
+
+ $m->scomp(
+ "/Elements/SelectStatus",
+ Name => "Status",
+ Default => $ARGS{Status} || 'new',
+ DefaultValue => 0,
+ SkipDeleted => 1
+ ),
+
+ loc("Owner") =>
+
+ $m->scomp(
+ "/Elements/SelectOwner",
+ Name => "Owner",
+ QueueObj => $QueueObj,
+ Default => $ARGS{Owner} || $RT::Nobody->Id,
+ DefaultValue => 0
+ ),
+
+ loc("Requestors") => $m->scomp(
+ "/Elements/EmailInput",
+ Name => 'Requestors',
+ Size => '40',
+ Default => $ARGS{Requestors} || $session{CurrentUser}->EmailAddress
+ ),
+
+ loc("Cc") =>
+
+ $m->scomp( "/Elements/EmailInput", Name => 'Cc', Size => '40', Default => $ARGS{Cc} )
+ . '<span class="comment"><i><font size="-2">'
+ . loc(
+ "(Sends a carbon-copy of this update to a comma-delimited list of email addresses. These people <strong>will</strong> receive future updates.)"
+ )
+ . '</font></i></span>',
+
+ loc("Admin Cc") =>
+
+ $m->scomp( "/Elements/EmailInput", Name => 'AdminCc', Size => '40', Default => $ARGS{AdminCc} )
+ . '<span class="comment" colspan="2"><i><font size="-2">'
+ . loc(
+ "(Sends a carbon-copy of this update to a comma-delimited list of administrative email addresses. These people <strong>will</strong> receive future updates.)"
+ )
+ . '</font></i></span>',
+
+
+);
+
+
+$m->scomp("/Ticket/Elements/EditCustomFields", %ARGS, QueueObj => $QueueObj );
+
+
+$m->scomp("/Ticket/Elements/EditTransactionCustomFields", %ARGS, QueueObj => $QueueObj );
+
+</%perl>
+% if (exists $session{'Attachments'}) {
+
+<%loc("Attached file") %>
+
+<%loc("Check box to delete")%><br />
+% foreach my $attach_name (keys %{$session{'Attachments'}}) {
+<input type="checkbox" class="checkbox" name="DeleteAttach-<%$attach_name%>" value="1" /><%$attach_name%><br />
+% } # end of foreach
+
+
+% } # end of if
+
+<%perl>
+$showrows->(
+ loc("Attach file") =>
+
+ '<div class="value" colspan="5">
+<input type="file" name="Attach" />
+<input type="submit" class="button" name="AddMoreAttach" value="' . loc("Add More Files") . '" />'
+);
+</%perl>
+
+
+% if ( $gnupg_widget ) {
+%$m->scomp("/Elements/GnuPG/SignEncryptWidget", self => $gnupg_widget, QueueObj => $QueueObj )
+% }
+
+
+ <div class="ticket-info-basics">
+ <&| /Widgets/TitleBox, title => loc('The Basics'),
+ title_class=> 'inverse',
+ color => "#993333" &>
+<%perl>
+$showrows->(
+ loc("Priority") => $m->scomp(
+ "/Elements/SelectPriority",
+ Name => "InitialPriority",
+ Default => $ARGS{InitialPriority} ? $ARGS{InitialPriority} : $QueueObj->InitialPriority,
+ ),
+ loc("Final Priority") => $m->scomp(
+ "/Elements/SelectPriority",
+ Name => "FinalPriority",
+ Default => $ARGS{FinalPriority} ? $ARGS{FinalPriority} : $QueueObj->FinalPriority,
+ ),
+
+ loc("Time Estimated") => '<span class="timefield">'.$m->scomp(
+ "/Elements/EditTimeValue",
+ Name => 'TimeEstimated',
+ Default => $ARGS{TimeEstimated} || '',
+ InUnits => $ARGS{'TimeEstimated-TimeUnits'}
+ ).'</span>',
+
+ loc("Time Worked") => '<span class="timefield">'.$m->scomp(
+ "/Elements/EditTimeValue",
+ Name => 'TimeWorked',
+ Default => $ARGS{TimeWorked} || '',
+ InUnits => $ARGS{'TimeWorked-TimeUnits'}
+ ). '</span>',
+
+ loc("Time Left") => '<span class="timefield">'.$m->scomp(
+ "/Elements/EditTimeValue",
+ Name => 'TimeLeft',
+ Default => $ARGS{TimeLeft} || '',
+ InUnits => $ARGS{'TimeLeft-TimeUnits'}
+ ).'</span>',
+);
+
+</%perl>
+</&>
+<&|/Widgets/TitleBox, title => loc("Dates"),
+ title_class=> 'inverse',
+ color => "#663366" &>
+
+<%perl>
+$showrows->(
+ loc("Starts") => $m->scomp( "/Elements/SelectDate", Name => "Starts", Default => ( $ARGS{Starts} || '' )),
+ loc("Due") => $m->scomp( "/Elements/SelectDate", Name => "Due", Default => ($ARGS{Due} || '' ))
+);
+
+</%perl>
+</&>
+
+<&|/Widgets/TitleBox, title => loc('Links'), title_class=> 'inverse' &>
+
+<em><%loc("(Enter ticket ids or URLs, separated with spaces)")%></em>
+
+<%perl>
+$showrows->(
+ loc("Depends on") => '<input size="10" name="new-DependsOn" value="' . ($ARGS{'new-DependsOn'} || '' ). '" />',
+ loc("Depended on by") => '<input size="10" name="DependsOn-new" value="' . ($ARGS{'DependsOn-new'} || '' ) . '" />',
+ loc("Parents") => '<input size="10" name="new-MemberOf" value="' . ($ARGS{'new-MemberOf'} || '') . '" />',
+ loc("Children") => '<input size="10" name="MemberOf-new" value="' . ($ARGS{'MemberOf-new'} || '') . '" />',
+ loc("Refers to") => '<input size="10" name="new-RefersTo" value="' . ($ARGS{'new-RefersTo'} || '') . '" />',
+ loc("Referred to by") => '<input size="10" name="RefersTo-new" value="' . ($ARGS{'RefersTo-new'} || ''). '" />'
+);
+</%perl>
+
+</&>
+
+
+<& /Elements/Submit, Label => loc("Create") &>
+</form>
+</&>
+</&>
diff --git a/rt/share/html/m/ticket/history b/rt/share/html/m/ticket/history
new file mode 100644
index 0000000..a49945d
--- /dev/null
+++ b/rt/share/html/m/ticket/history
@@ -0,0 +1,31 @@
+<%args>
+$id => undef
+</%args>
+<%init>
+my $t = RT::Ticket->new($session{CurrentUser});
+$t->Load($id);
+my $history = $t->Transactions()->ItemsArrayRef;
+</%init>
+<&| /m/_elements/wrapper, title => $t->Subject &>
+<div class="history">
+<& /m/_elements/ticket_menu, ticket => $t &>
+<&|/Widgets/TitleBox &>
+<ul class="history-list">
+% for my $entry (reverse @$history) {
+<li>
+<span class="age"><% $entry->CreatedObj->AgeAsString() %></span> -
+<& /Elements/ShowUser, User => $entry->CreatorObj &> -
+<%$entry->BriefDescription%>
+% if ($entry->Type !~ /EmailRecord/) {
+% if ($entry->ContentObj) {
+<div class="txn-content">
+<%$entry->Content%>
+</div>
+%}
+% }
+</li>
+% }
+</ul>
+</&>
+</div>
+</&>
diff --git a/rt/share/html/m/ticket/modify b/rt/share/html/m/ticket/modify
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/rt/share/html/m/ticket/modify
diff --git a/rt/share/html/m/ticket/reply b/rt/share/html/m/ticket/reply
new file mode 100644
index 0000000..ea2a6ca
--- /dev/null
+++ b/rt/share/html/m/ticket/reply
@@ -0,0 +1,171 @@
+<&|/m/_elements/wrapper, title => loc('Update ticket #[_1]', $t->id) &>
+<& /m/_elements/ticket_menu, ticket => $t &>
+<& /Elements/ListActions, actions => \@results &>
+<div class="ticket-reply">
+<&|/Widgets/TitleBox &>
+<form action="reply" id="update"
+ method="post" enctype="multipart/form-data">
+<input type="hidden" class="hidden" name="DefaultStatus" value="<% $DefaultStatus ||''%>" />
+<input type="hidden" class="hidden" name="Action" value="<% $ARGS{Action}||'' %>" />
+
+<div class="entry"><span class="label"><&|/l&>Status</&>:</span>
+<div class="value">
+<& /Elements/SelectStatus, Name=>"Status", DefaultLabel => loc("[_1] (Unchanged)", loc($t->Status)), Default => $ARGS{'Status'} || ($t->Status eq $DefaultStatus ? undef : $DefaultStatus)&>
+</div></div>
+
+<div class="entry"><span class="label"><&|/l&>Owner</&>:</span>
+<div class="value">
+<& /Elements/SelectOwner,
+ Name => "Owner",
+ TicketObj => $t,
+ QueueObj => $t->QueueObj,
+ DefaultLabel => loc("[_1] (Unchanged)", $t->OwnerObj->Name),
+ Default => $ARGS{'Owner'}
+&>
+</div></div>
+<div class="entry timefield"><span class="label"><&|/l&>Worked</&>:</span><span class="value">
+<& /Elements/EditTimeValue,
+ Name => 'UpdateTimeWorked',
+ Default => $ARGS{UpdateTimeWorked}||'',
+ InUnits => $ARGS{'UpdateTimeWorked-TimeUnits'}||'minutes',
+&>
+</span></div>
+<input type="hidden" class="hidden" name="id" value="<%$t->Id%>" /><br />
+<div class="entry"><span class="label"><&|/l&>Update Type</&>:</span>
+<div class="value"><select name="UpdateType">
+% if ($CanComment) {
+<option value="private" <% ($ARGS{'UpdateType'} && $ARGS{'UpdateType'} eq "private") ? qq[ selected="selected"] : !$ARGS{'UpdateType'}&&$CommentDefault |n %>><&|/l&>Comments (Not sent to requestors)</&></option>
+% }
+% if ($CanRespond) {
+<option value="response" <% ($ARGS{'UpdateType'} && $ARGS{'UpdateType'} eq "response") ? qq[ selected="selected"] : !$ARGS{'UpdateType'}&&$ResponseDefault |n %>><&|/l&>Reply to requestors</&></option>
+% }
+</select>
+</div></div>
+<div class="entry"><span class="label"><&|/l&>Subject</&>:</span><div class="value"> <input name="UpdateSubject" size="60" value="<% $ARGS{UpdateSubject} || $t->Subject()%>" />
+% $m->callback( %ARGS, CallbackName => 'AfterSubject' );
+</div></div>
+
+<div class="entry"><span class="label"><&|/l&>One-time Cc</&>:</span><span class="value"><& /Elements/EmailInput, Name => 'UpdateCc', Size => '60', Default => $ARGS{UpdateCc} &></span></div>
+
+<div class="entry"><span class="label"><&|/l&>One-time Bcc</&>:</span><span class="value"><& /Elements/EmailInput, Name => 'UpdateBcc', Size => '60', Default => $ARGS{UpdateBcc} &></span></div>
+
+<div class="entry"><span class="label" ><&|/l&>Message</&>:</span><div class="value">
+% if (exists $ARGS{UpdateContent}) {
+% # preserve QuoteTransaction so we can use it to set up sane references/in/reply to
+% my $temp = $ARGS{'QuoteTransaction'};
+% delete $ARGS{'QuoteTransaction'};
+<& /Elements/MessageBox, Name=>"UpdateContent", Default=>$ARGS{UpdateContent}, IncludeSignature => 0, %ARGS&>
+% $ARGS{'QuoteTransaction'} = $temp;
+% } else {
+% my $IncludeSignature = 1;
+% $IncludeSignature = 0 if $Action ne 'Respond' && !RT->Config->Get('MessageBoxIncludeSignatureOnComment');
+<& /Elements/MessageBox, Name=>"UpdateContent", IncludeSignature => $IncludeSignature, %ARGS &>
+% }
+</div></div>
+<& /Elements/Submit, Label => loc('Update Ticket'), Name => 'SubmitTicket' &>
+</form>
+</&>
+</div>
+</&>
+<%INIT>
+my $CanRespond = 0;
+my $CanComment = 0;
+my $checks_failure = 0;
+my $title;
+
+my $t = LoadTicket($id);
+
+my @results;
+
+$m->callback( Ticket => $t, ARGSRef => \%ARGS, results => \@results, CallbackName => 'Initial' );
+
+unless($DefaultStatus){
+ $DefaultStatus=($ARGS{'Status'} ||$t->Status());
+}
+
+if ($DefaultStatus eq 'new'){
+ $DefaultStatus='open';
+}
+
+if ($DefaultStatus eq 'resolved') {
+ $title = loc("Resolve ticket #[_1] ([_2])", $t->id, $t->Subject);
+} else {
+ $title = loc("Update ticket #[_1] ([_2])", $t->id, $t->Subject);
+}
+
+# Things needed in the template - we'll do the processing here, just
+# for the convenience:
+
+my ($CommentDefault, $ResponseDefault);
+if ($Action ne 'Respond') {
+ $CommentDefault = qq[ selected="selected"];
+ $ResponseDefault = "";
+} else {
+ $CommentDefault = "";
+ $ResponseDefault = qq[ selected="selected"];
+}
+
+
+
+$CanRespond = 1 if ( $t->CurrentUserHasRight('ReplyToTicket') or
+ $t->CurrentUserHasRight('ModifyTicket') );
+
+$CanComment = 1 if ( $t->CurrentUserHasRight('CommentOnTicket') or
+ $t->CurrentUserHasRight('ModifyTicket') );
+
+
+# {{{ deal with deleting uploaded attachments
+foreach my $key (keys %ARGS) {
+ if ($key =~ m/^DeleteAttach-(.+)$/) {
+ delete $session{'Attachments'}{$1};
+ }
+ $session{'Attachments'} = { %{$session{'Attachments'} || {}} };
+}
+# }}}
+
+# {{{ store the uploaded attachment in session
+if ($ARGS{'Attach'}) { # attachment?
+ my $attachment = MakeMIMEEntity(
+ AttachmentFieldName => 'Attach'
+ );
+
+ my $file_path = Encode::decode_utf8("$ARGS{'Attach'}");
+ $session{'Attachments'} = {
+ %{$session{'Attachments'} || {}},
+ $file_path => $attachment,
+ };
+}
+# }}}
+
+# delete temporary storage entry to make WebUI clean
+unless (keys %{$session{'Attachments'}} and $ARGS{'UpdateAttach'}) {
+ delete $session{'Attachments'};
+}
+# }}}
+
+# check email addresses for RT's
+{
+ foreach my $field ( qw(UpdateCc UpdateBcc) ) {
+ my $value = $ARGS{ $field };
+ next unless defined $value && length $value;
+
+ my @emails = Email::Address->parse( $value );
+ foreach my $email ( grep RT::EmailParser->IsRTAddress($_->address), @emails ) {
+ push @results, loc("[_1] is an address RT receives mail at. Adding it as a '[_2]' would create a mail loop", $email->format, loc(substr($field, 6)) );
+ $checks_failure = 1;
+ $email = undef;
+ }
+ $ARGS{ $field } = join ', ', map $_->format, grep defined, @emails;
+ }
+}
+
+if ( !$checks_failure && exists $ARGS{SubmitTicket} ) {
+ return $m->comp('/m/ticket/show', TicketObj => $t, %ARGS);
+}
+</%INIT>
+
+<%ARGS>
+$id => undef
+$Action => 'reply'
+$DefaultStatus => undef
+</%ARGS>
diff --git a/rt/share/html/m/ticket/select_create_queue b/rt/share/html/m/ticket/select_create_queue
new file mode 100644
index 0000000..88cf203
--- /dev/null
+++ b/rt/share/html/m/ticket/select_create_queue
@@ -0,0 +1,18 @@
+<%init>
+my $queues = RT::Queues->new($session{'CurrentUser'});
+$queues->UnLimit();
+
+</%init>
+<&| /m/_elements/wrapper, title => loc("Create a ticket") &>
+<div class="select_queue">
+<&|/Widgets/TitleBox, title => loc("Select a queue") &>
+<ul class="menu">
+% while (my $q = $queues->Next()) {
+% next if (! $q->CurrentUserHasRight('CreateTicket'));
+<li><a href="<%RT->Config->Get('WebPath')%>/m/ticket/create?Queue=<%$q->id%>"><%$q->Name%></a></li>
+% }
+</ul>
+</&>
+</div>
+</&>
+
diff --git a/rt/share/html/m/ticket/show b/rt/share/html/m/ticket/show
new file mode 100644
index 0000000..e979da3
--- /dev/null
+++ b/rt/share/html/m/ticket/show
@@ -0,0 +1,454 @@
+<%args>
+$id => undef
+</%args>
+<%init>
+my $Ticket;
+my @Actions;
+
+unless ($id) {
+ Abort('No ticket specified');
+}
+
+if ($ARGS{'id'} eq 'new') {
+ # {{{ Create a new ticket
+
+ my $Queue = new RT::Queue( $session{'CurrentUser'} );
+ $Queue->Load($ARGS{'Queue'});
+ unless ( $Queue->id ) {
+ Abort('Queue not found');
+ }
+
+ unless ( $Queue->CurrentUserHasRight('CreateTicket') ) {
+ Abort('You have no permission to create tickets in that queue.');
+ }
+
+ ($Ticket, @Actions) = CreateTicket(
+ Attachments => delete $session{'Attachments'},
+ %ARGS,
+ );
+ unless ( $Ticket->CurrentUserHasRight('ShowTicket') ) {
+ Abort("No permission to view newly created ticket #".$Ticket->id.".");
+ }
+ # }}}
+} else {
+ $Ticket ||= LoadTicket($ARGS{'id'});
+
+ $m->callback( CallbackName => 'BeforeProcessArguments',
+ TicketObj => $Ticket,
+ ActionsRef => \@Actions, ARGSRef => \%ARGS );
+ if ( defined $ARGS{'Action'} ) {
+ if ($ARGS{'Action'} =~ /^(Steal|Kill|Take|SetTold)$/) {
+ my $action = $1;
+ my ($res, $msg) = $Ticket->$action();
+ push(@Actions, $msg);
+ }
+ }
+
+ $m->callback(CallbackName => 'ProcessArguments',
+ Ticket => $Ticket,
+ ARGSRef => \%ARGS,
+ Actions => \@Actions);
+
+ $ARGS{UpdateAttachments} = $session{'Attachments'};
+ push @Actions,
+ ProcessUpdateMessage(
+ ARGSRef => \%ARGS,
+ Actions => \@Actions,
+ TicketObj => $Ticket,
+ );
+ delete $session{'Attachments'};
+
+ #Process status updates
+ push @Actions, ProcessTicketWatchers(ARGSRef => \%ARGS, TicketObj => $Ticket );
+ push @Actions, ProcessTicketBasics( ARGSRef => \%ARGS, TicketObj => $Ticket );
+ push @Actions, ProcessTicketLinks( ARGSRef => \%ARGS, TicketObj => $Ticket );
+ push @Actions, ProcessTicketDates( ARGSRef => \%ARGS, TicketObj => $Ticket );
+ push @Actions, ProcessObjectCustomFieldUpdates(ARGSRef => \%ARGS, TicketObj => $Ticket );
+
+ # XXX: we shouldn't block actions here if user has no right to see the ticket,
+ # but we should allow him to see actions he has done
+ unless ($Ticket->CurrentUserHasRight('ShowTicket')) {
+ Abort("No permission to view ticket");
+ }
+ if ( $ARGS{'MarkAsSeen'} ) {
+ $Ticket->SetAttribute(
+ Name => 'User-'. $Ticket->CurrentUser->id .'-SeenUpTo',
+ Content => $Ticket->LastUpdated,
+ );
+ push @Actions, loc('Marked all messages as seen');
+ }
+}
+
+$m->callback(
+ CallbackName => 'BeforeDisplay',
+ TicketObj => \$Ticket,
+ Actions => \@Actions,
+ ARGSRef => \%ARGS,
+);
+
+# This code does automatic redirection if any updates happen.
+
+if (@Actions) {
+
+ # We've done something, so we need to clear the decks to avoid
+ # resubmission on refresh.
+ # But we need to store Actions somewhere too, so we don't lose them.
+ my $key = Digest::MD5::md5_hex( rand(1024) );
+ push @{ $session{"Actions"}->{$key} ||= [] }, @Actions;
+ $session{'i'}++;
+ my $url = RT->Config->Get('WebURL') . "m/ticket/show?id=" . $Ticket->id . "&results=" . $key;
+ $url .= '#' . $ARGS{Anchor} if $ARGS{Anchor};
+ RT::Interface::Web::Redirect($url);
+}
+
+# If we haven't been passed in an Attachments object (through the precaching mechanism)
+# then we need to find one
+my $Attachments = $m->comp('/Ticket/Elements/FindAttachments', Ticket => $Ticket);
+
+my %documents;
+while ( my $attach = $Attachments->Next() ) {
+ next unless ($attach->Filename());
+ unshift( @{ $documents{ $attach->Filename } }, $attach );
+}
+
+my $Customers = $Ticket->Customers;
+my @customers;
+while ( my $customer = $Customers->Next() ) {
+ push @customers, $customer;
+}
+
+my $CustomFields = $Ticket->CustomFields;
+$m->callback(
+ CallbackName => 'MassageCustomFields',
+ Object => $Ticket,
+ CustomFields => $CustomFields,
+);
+
+my $print_value = sub {
+ my ($cf, $value) = @_;
+ my $linked = $value->LinkValueTo;
+ if ( defined $linked && length $linked ) {
+ my $linked = $m->interp->apply_escapes( $linked, 'h' );
+ $m->out('<a href="'. $linked .'" target="_new">');
+ }
+ my $comp = "ShowCustomField". $cf->Type;
+ $m->callback(
+ CallbackName => 'ShowComponentName',
+ Name => \$comp,
+ CustomField => $cf,
+ Object => $Ticket,
+ );
+ if ( $m->comp_exists( $comp ) ) {
+ $m->comp( $comp, Object => $value );
+ } else {
+ $m->out( $m->interp->apply_escapes( $value->Content, 'h' ) );
+ }
+ $m->out('</a>') if defined $linked && length $linked;
+
+ # This section automatically populates a<div with the "IncludeContentForValue" for this custom
+ # field if it's been defined
+ if ( $cf->IncludeContentForValue ) {
+ my $vid = $value->id;
+ $m->out( '<div class="object_cf_value_include" id="object_cf_value_'. $vid .'">' );
+ $m->print( loc("See also:") );
+ $m->out( '<a href="'. $value->IncludeContentForValue .'">' );
+ $m->print( $value->IncludeContentForValue );
+ $m->out( qq{</a></div>\n} );
+ $m->out( qq{<script><!--\nahah('} );
+ $m->print( $value->IncludeContentForValue );
+ $m->out( qq{', 'object_cf_value_$vid');\n--></script>\n} );
+ }
+};
+
+</%init>
+<&| /m/_elements/wrapper, title => $Ticket->Subject &>
+<div id="ticket-show">
+<& /m/_elements/ticket_menu, ticket => $Ticket &>
+
+ <&| /Widgets/TitleBox, title => loc('The Basics'),
+ class => 'ticket-info-basics',
+ &>
+
+
+ <div class="entry">
+ <div class="label id"><&|/l&>Id</&>:</div>
+ <div class="value id"><%$Ticket->Id %></div>
+ </div>
+ <div class="entry">
+ <div class="label status"><&|/l&>Status</&>:</div>
+ <div class="value status"><% loc($Ticket->Status) %></div>
+ </div>
+% if ($Ticket->TimeEstimated) {
+ <div class="entry">
+ <div class="label time estimated"><&|/l&>Estimated</&>:</div>
+ <div class="value time estimated"><& /Ticket/Elements/ShowTime, minutes => $Ticket->TimeEstimated &></div>
+ </div>
+% }
+% if ($Ticket->TimeWorked) {
+ <div class="entry">
+ <div class="label time worked"><&|/l&>Worked</&>:</div>
+ <div class="value time worked"><& /Ticket/Elements/ShowTime, minutes => $Ticket->TimeWorked &></div>
+ </div>
+% }
+% if ($Ticket->TimeLeft) {
+ <div class="entry">
+ <div class="label time left"><&|/l&>Left</&>:</div>
+ <div class="value time left"><& /Ticket/Elements/ShowTime, minutes => $Ticket->TimeLeft &></div>
+ </div>
+% }
+ <div class="entry">
+ <div class="label priority"><&|/l&>Priority</&>:</div>
+ <div class="value priority"><& /Ticket/Elements/ShowPriority, Ticket => $Ticket &></div>
+ </div>
+ <div class="entry">
+ <div class="label queue"><&|/l&>Queue</&>:</div>
+ <div class="value queue"><& /Ticket/Elements/ShowQueue, QueueObj => $Ticket->QueueObj &></div>
+ </div>
+ </&>
+
+% if ($Ticket->CustomFields->First) {
+ <&| /Widgets/TitleBox, title => loc('Custom Fields'),
+ class => 'ticket-info-cfs',
+ &>
+
+% while ( my $CustomField = $CustomFields->Next ) {
+% my $Values = $Ticket->CustomFieldValues( $CustomField->Id );
+% my $count = $Values->Count;
+ <div class="entry" id="CF-<%$CustomField->id%>-ShowRow">
+ <div class="label"><% $CustomField->Name %>:</div>
+ <div class="value">
+% unless ( $count ) {
+<i><&|/l&>(no value)</&></i>
+% } elsif ( $count == 1 ) {
+% $print_value->( $CustomField, $Values->First );
+% } else {
+<ul>
+% while ( my $Value = $Values->Next ) {
+<li>
+% $print_value->( $CustomField, $Value );
+</li>
+% }
+</ul>
+% }
+ </div>
+ </div>
+% }
+
+</&>
+% }
+
+ <&| /Widgets/TitleBox, title => loc('People'), class => 'ticket-info-people' &>
+
+
+ <div class="entry">
+ <div class="label"><&|/l&>Owner</&>:</div>
+ <div class="value"><& /Elements/ShowUser, User => $Ticket->OwnerObj, Ticket => $Ticket &>
+ </div>
+ </div>
+ <div class="entry">
+ <div class="label"><&|/l&>Requestors</&>:</div>
+ <div class="value"><& /Ticket/Elements/ShowGroupMembers, Group => $Ticket->Requestors, Ticket => $Ticket &></div>
+ </div>
+ <div class="entry">
+ <div class="label"><&|/l&>Cc</&>:</div>
+ <div class="value"><& /Ticket/Elements/ShowGroupMembers, Group => $Ticket->Cc, Ticket => $Ticket &></div>
+ </div>
+ <div class="entry">
+ <div class="label"><&|/l&>AdminCc</&>:</div>
+ <div class="value"><& /Ticket/Elements/ShowGroupMembers, Group => $Ticket->AdminCc, Ticket => $Ticket &></div>
+ </div>
+
+ </&>
+
+% if (keys %documents) {
+<&| /Widgets/TitleBox, title => loc('Attachments'),
+ title_class=> 'inverse',
+ class => 'ticket-info-attachments',
+ color => "#336699" &>
+
+% foreach my $key (keys %documents) {
+
+<%$key%><br />
+<ul>
+% foreach my $rev (@{$documents{$key}}) {
+
+<%PERL>
+my $size = $rev->ContentLength;
+
+if ($size) {
+ my $kb = int($size/102.4) / 10;
+ my $units = RT->Config->Get('AttachmentUnits');
+
+ if (!defined($units)) {
+ if ($size > 1024) {
+ $size = $kb . "k";
+ }
+ else {
+ $size = $size . "b";
+ }
+ }
+ elsif ($units eq 'k') {
+ $size = $kb . "k";
+ }
+ else {
+ $size = $size . "b";
+ }
+
+</%PERL>
+
+<li><font size="-2">
+<a href="<%RT->Config->Get('WebPath')%>/Ticket/Attachment/<%$rev->TransactionId%>/<%$rev->Id%>/<%$rev->Filename | u%>">
+<&|/l, $rev->CreatedAsString, $size, $rev->CreatorObj->Name &>[_1] ([_2]) by [_3]</&>
+</a>
+</font></li>
+% }
+% }
+</ul>
+
+% }
+</&>
+
+% }
+% # too painful to deal with reminders
+% if ( 0 && RT->Config->Get('EnableReminders') ) {
+ <&|/Widgets/TitleBox, title => loc("Reminders"),
+ class => 'ticket-info-reminders',
+ &>
+ <div class="entry"><div
+ <form action="<%RT->Config->Get('WebPath')%>/Ticket/Display.html" method="post">
+ <& /Ticket/Elements/Reminders, Ticket => $Ticket, ShowCompleted => 0 &>
+ <div align="right"><input type="submit" class="button" value="<&|/l&>Save</&>" /></div>
+ </form>
+ </div></div>
+ </&>
+% }
+
+% if ( @customers ) {
+ <&| /Widgets/TitleBox, title => loc("Customers"),
+ class => 'ticket-info-customers',
+ &>
+% foreach my $customer ( @customers ) {
+% my $resolver = $customer->TargetURI->Resolver or next;
+<div class="entry"><a href="<% $resolver->HREF %>"><% $resolver->AsString |n%></A>
+</div>
+% } #foreach
+ </&>
+% } # if @customers
+
+
+ <&| /Widgets/TitleBox, title => loc("Dates"),
+ class => 'ticket-info-dates',
+ &>
+
+
+ <div class="entry">
+ <div class="label date created"><&|/l&>Created</&>:</div>
+ <div class="value date created"><% $Ticket->CreatedObj->AsString %></div>
+ </div>
+ <div class="entry">
+ <div class="label date starts"><&|/l&>Starts</&>:</div>
+ <div class="value date starts"><% $Ticket->StartsObj->AsString %></div>
+ </div>
+ <div class="entry">
+ <div class="label date started"><&|/l&>Started</&>:</div>
+ <div class="value date started"><% $Ticket->StartedObj->AsString %></div>
+ </div>
+ <div class="entry">
+ <div class="label date told"><&|/l&>Last Contact</&>:</div>
+ <div class="value date told"><% $Ticket->ToldObj->AsString %></div>
+ </div>
+ <div class="entry">
+ <div class="label date due"><&|/l&>Due</&>:</div>
+% my $due = $Ticket->DueObj;
+% if ( $due && $due->Unix > 0 && $due->Diff < 0 ) {
+ <div class="value date due"><span class="overdue"><% $due->AsString %></span></div>
+% } else {
+ <div class="value date due"><% $due->AsString %></div>
+% }
+ </div>
+ <div class="entry">
+ <div class="label date resolved"><&|/l&>Closed</&>:</div>
+ <div class="value date resolved"><% $Ticket->ResolvedObj->AsString %></div>
+ </div>
+ <div class="entry">
+ <div class="label date updated"><&|/l&>Updated</&>:</div>
+% my $UpdatedString = $Ticket->LastUpdated ? loc("[_1] by [_2]", $Ticket->LastUpdatedAsString, $Ticket->LastUpdatedByObj->Name) : loc("Never");
+ <div class="value date updated"><% $UpdatedString | h %></div>
+ </div>
+
+ </&>
+
+ <&| /Widgets/TitleBox, title => loc('Links'), class => 'ticket-info-links' &>
+
+ <div class="entry">
+ <div class="label"><% loc('Depends on')%>:</div>
+ <div class="value">
+
+<%PERL>
+my ( @active, @inactive, @not_tickets );
+for my $link ( @{ $Ticket->DependsOn->ItemsArrayRef } ) {
+ my $target = $link->TargetObj;
+ if ( $target && $target->isa('RT::Ticket') ) {
+ if ( $target->QueueObj->IsInactiveStatus( $target->Status ) ) {
+ push( @inactive, $link->TargetURI );
+ }
+ else {
+ push( @active, $link->TargetURI );
+ }
+ }
+ else {
+ push( @not_tickets, $link->TargetURI );
+ }
+}
+</%PERL>
+
+
+<ul>
+% for my $Link (@not_tickets, @active, @inactive) {
+<li><& /Elements/ShowLink, URI => $Link &></li>
+% }
+</ul>
+ </div>
+ </div>
+ <div class="entry">
+ <div class="label"><% loc('Depended on by')%>:</div>
+ <div class="value">
+<ul>
+% while (my $Link = $Ticket->DependedOnBy->Next) {
+<li><& /Elements/ShowLink, URI => $Link->BaseURI &></li>
+% }
+</ul>
+ </div>
+ </div>
+ <div class="entry">
+ <div class="label"><% loc('Parents') %>:</div>
+ <div class="value"><& /Ticket/Elements/ShowParents, Ticket => $Ticket &></div>
+ </div>
+ <div class="entry">
+ <div class="label"><% loc('Children')%>:</div>
+ <div class="value"><& /Ticket/Elements/ShowMembers, Ticket => $Ticket &></div>
+ </div>
+ <div class="entry">
+ <div class="label"><% loc('Refers to')%>:</div>
+ <div class="value">
+<ul>
+% while (my $Link = $Ticket->RefersTo->Next) {
+<li><& /Elements/ShowLink, URI => $Link->TargetURI &></li>
+% }
+</ul>
+ </div>
+ </div>
+ <div class="entry">
+ <div class="label"><% loc('Referred to by')%>:</div>
+ <div class="value">
+ <ul>
+% while (my $Link = $Ticket->ReferredToBy->Next) {
+% next if (UNIVERSAL::isa($Link->BaseObj, 'RT::Ticket') && $Link->BaseObj->Type eq 'reminder');
+<li><& /Elements/ShowLink, URI => $Link->BaseURI &></li>
+% }
+</ul>
+ </div>
+ </div>
+ </&>
+</div>
+</&>
diff --git a/rt/share/html/m/tickets/requested b/rt/share/html/m/tickets/requested
new file mode 100644
index 0000000..3043e05
--- /dev/null
+++ b/rt/share/html/m/tickets/requested
@@ -0,0 +1,4 @@
+<%init>
+ $m->comp('../_elements/ticket_list', %ARGS, query => 'Requestors.EmailAddress = "'.$session{CurrentUser}->EmailAddress.'" AND (Status != "resolved" AND Status != "rejected" AND Status != "stalled")');
+$m->abort();
+</%init>
diff --git a/rt/share/html/m/tickets/search b/rt/share/html/m/tickets/search
new file mode 100644
index 0000000..16864b4
--- /dev/null
+++ b/rt/share/html/m/tickets/search
@@ -0,0 +1,64 @@
+<%args>
+$page => 1
+$order_by => 'id'
+$order => 'desc'
+$name => undef
+</%args>
+<%init>
+use RT::Search::Googleish;
+my $query = $ARGS{'query'};
+if ($ARGS{'q'}) {
+ my $tickets = RT::Tickets->new( $session{'CurrentUser'} );
+ my %args = (
+ Argument => $ARGS{q},
+ TicketsObj => $tickets,
+ );
+ my $search = RT::Search::Googleish->new(%args);
+ $query = $search->QueryToSQL();
+
+}
+
+elsif ($ARGS{'name'}) {
+my $search_arg;
+
+my $search;
+
+ if ($name) {
+ ($search) = RT::System->new( $session{'CurrentUser'} )->Attributes->Named( 'Search - ' . $name );
+ unless ( $search && $search->Id ) {
+ my (@custom_searches) = RT::System->new( $session{'CurrentUser'} )->Attributes->Named('SavedSearch');
+ foreach my $custom (@custom_searches) {
+ if ( $custom->Description eq $name ) { $search = $custom; last }
+ }
+ unless ( $search && $search->id ) {
+ $m->out("Predefined search $name not found");
+ return;
+ }
+ }
+
+ $search_arg = $session{'CurrentUser'}->UserObj->Preferences( $search, $search->Content );
+ }
+
+ foreach ($search_arg) {
+ if ( $_->{'Query'} =~ /__Bookmarks__/ ) {
+ $_->{'Rows'} = 999;
+
+ # DEPRECATED: will be here for a while up to 3.10/4.0
+ my $bookmarks = $session{'CurrentUser'}->UserObj->FirstAttribute('Bookmarks');
+ $bookmarks = $bookmarks->Content if $bookmarks;
+ $bookmarks ||= {};
+ my $query = join( " OR ", map " id = '$_' ", grep $bookmarks->{$_}, keys %$bookmarks ) || 'id=0';
+ $_->{'Query'} =~ s/__Bookmarks__/( $query )/g;
+ }
+ }
+
+ $query = $search_arg->{Query};
+ $order_by = $search_arg->{OrderBy};
+ $order = $search_arg->{Order};
+
+}
+
+
+$m->comp('../_elements/ticket_list', query => $query, page => $page, order_by => $order_by, order => $order);
+$m->abort();
+</%init>