[Rt-commit] rt branch, 4.6/search-selection-next, created. rt-4.4.1-3-gdaf15f7e5
Jim Brandt
jbrandt at bestpractical.com
Thu Jan 11 16:07:36 EST 2018
The branch, 4.6/search-selection-next has been created
at daf15f7e5177c9f321b89715749ce8f1cd6d803b (commit)
- Log -----------------------------------------------------------------
commit 7782e58a156b3cf5a0ae15f49b1afb397c9b8e2e
Author: Shawn M Moore <shawn at bestpractical.com>
Date: Thu Nov 3 21:40:19 2016 +0000
New drag and drop UI for selecting searches for homepage and dashboards
This is a rough draft :)
diff --git a/lib/RT/Interface/Web.pm b/lib/RT/Interface/Web.pm
index e3cf905c7..7158638ce 100644
--- a/lib/RT/Interface/Web.pm
+++ b/lib/RT/Interface/Web.pm
@@ -112,6 +112,7 @@ sub JSFiles {
jquery-ui.min.js
jquery-ui-timepicker-addon.js
jquery-ui-patch-datepicker.js
+ jquery.ui.sortable.js
jquery.modal.min.js
jquery.modal-defaults.js
jquery.cookie.js
diff --git a/share/html/Dashboards/Elements/DashboardsForObject b/share/html/Dashboards/Elements/DashboardsForObject
index 149da29c4..7da2980ad 100644
--- a/share/html/Dashboards/Elements/DashboardsForObject
+++ b/share/html/Dashboards/Elements/DashboardsForObject
@@ -47,12 +47,16 @@
%# END BPS TAGGED BLOCK }}}
<%args>
$Object => undef
+$User => $session{'CurrentUser'}
+$Flat => 0
</%args>
<%init>
# Returns a hash of dashboards associated on $Object
+# or, if $Flat, a simple list
use RT::Dashboard;
my %dashboards;
+my @flat_dashboards;
my $privacy = RT::Dashboard->_build_privacy($Object);
while (my $attr = $Object->Attributes->Next) {
@@ -65,21 +69,23 @@ while (my $attr = $Object->Attributes->Next) {
next;
}
- if ($Object->isa('RT::System')) {
- push @{ $dashboards{system} }, $dashboard;
- }
- elsif ($Object->isa('RT::User')) {
- push @{ $dashboards{personal} }, $dashboard;
+ if ($Flat) {
+ push @flat_dashboards, $dashboard;
}
- elsif ($Object->isa('RT::Group')) {
- push @{ $dashboards{group}{$Object->Name} }, $dashboard;
+ else {
+ if ($Object->isa('RT::System')) {
+ push @{ $dashboards{system} }, $dashboard;
+ }
+ elsif ($Object->isa('RT::User')) {
+ push @{ $dashboards{personal} }, $dashboard;
+ }
+ elsif ($Object->isa('RT::Group')) {
+ push @{ $dashboards{group}{$Object->Name} }, $dashboard;
+ }
}
}
}
-return \%dashboards;
-</%init>
-<%args>
-$User => $session{'CurrentUser'}
-</%args>
+return $Flat ? @flat_dashboards : \%dashboards;
+</%init>
diff --git a/share/html/Dashboards/Queries.html b/share/html/Dashboards/Queries.html
index 55eabf9ca..4efe3d6b1 100644
--- a/share/html/Dashboards/Queries.html
+++ b/share/html/Dashboards/Queries.html
@@ -47,32 +47,18 @@
%# END BPS TAGGED BLOCK }}}
<& /Elements/Header, Title => $title &>
<& /Elements/Tabs &>
-
<& /Elements/ListActions, actions => \@results &>
-<& Elements/Deleted, searches => \@deleted, Dashboard => $Dashboard &>
-
-<& Elements/HiddenSearches, searches => \@hidden_searches, Dashboard => $Dashboard &>
-
-<table width="100%" border="0">
-% for my $pane (@panes) {
-<tr><td valign="top" class="boxcontainer">
-<form action="Queries.html" name="Dashboard-<%$pane->{Name}%>" method="post" enctype="multipart/form-data">
-<input type="hidden" class="hidden" name="id" value="<%$Dashboard->Id%>" />
-<input type="hidden" class="hidden" name="Privacy" value="<%$Dashboard->Privacy%>" />
-
-<&| /Widgets/TitleBox, title => $pane->{DisplayName} &>
-% my ( $pane_name ) = $pane->{Name} =~ /Searches-(.+)/;
- <& /Widgets/SelectionBox:show, self => $pane, grep( {
- $_->{pane} eq $pane_name} @deleted ) ? ( ShowUpdate => 1 ) : () &>
-</&>
+<form method="post" action="<% RT->Config->Get('WebPath') %>/Helpers/UpdateDashboard" data-dashboard_id='<%$Dashboard->Id%>'>
+ <& /Widgets/SearchSelection,
+ pane_name => \%pane_name,
+ sections => \@sections,
+ selected => \%selected,
+ filters => \@filters,
+ &>
+ <input type="submit" class="button" name="UpdateSearches" value="<% loc('Save') %>" />
</form>
-</td></tr>
-% }
-</table>
-
<%INIT>
-
my @results;
use RT::Dashboard;
@@ -81,197 +67,148 @@ my ($ok, $msg) = $Dashboard->LoadById($id);
$ok || Abort(loc("Couldn't load dashboard [_1]: [_2]", $id, $msg));
my $title = loc("Modify the content of dashboard [_1]", $Dashboard->Name);
-my %desc_of;
-my @items;
-my %selected;
-my %still_exists;
-
-# add portlets (homepage componenets)
-my @components = @{ RT->Config->Get('HomepageComponents') };
+my @sections;
+my %item_for;
-for my $desc (@components) {
- my $name = "component-$desc";
- push @items, [$name, loc($desc)];
- $desc_of{$name} = loc($desc);
- $still_exists{$name} = 1;
-}
+my @components = map { type => "component", name => $_, label => loc($_) }, @{RT->Config->Get('HomepageComponents')};
-# add dashboards
-my @dashboards = $m->comp("/Dashboards/Elements/ListOfDashboards");
-for my $dashboard (@dashboards) {
- # Users *can* set up mutually recursive dashboards, but don't make it THIS
- # easy for them to shoot themselves in the foot.
- next if $dashboard->Id == $Dashboard->Id;
+$item_for{ $_->{type} }{ $_->{name} } = $_ for @components;
- my $name = 'dashboard-' . $dashboard->Id . '-' . $dashboard->Privacy;
- my $type = loc('Dashboard'); # loc
- my $desc = "$type: " . $dashboard->Name;
- push @items, [$name, $desc];
- $desc_of{$name} = $desc;
- $still_exists{$name} = 1;
-}
+push @sections, {
+ id => 'components',
+ label => loc("Components"),
+ items => \@components,
+};
-# add saved searches
-my @objs = RT::System->new($session{'CurrentUser'});
+my $sys = RT::System->new($session{'CurrentUser'});
+my @objs = ($sys);
push @objs, RT::SavedSearch->new( $session{CurrentUser} )->ObjectsForLoading
if $session{'CurrentUser'}->HasRight( Right => 'LoadSavedSearch',
Object => $RT::System );
for my $object (@objs) {
+ my @items;
+ my $object_id = ref($object) . '-' . $object->Id;
+ $object_id = 'system' if $object eq $sys;
+
+ # saved searches and charts
for ($m->comp("/Search/Elements/SearchesForObject", Object => $object)) {
my ($desc, $loc_desc, $search) = @$_;
- my $SearchType = $search->Content->{'SearchType'} || 'Ticket';
- my $type = ( $SearchType eq 'Ticket' )
- ? 'Saved Search' # loc
- : $SearchType;
- $desc = loc($type) . ": $loc_desc";
- my $privacy = $Dashboard->_build_privacy($object);
- my $name = 'search-' . $search->Id . '-' . $privacy;
- push @items, [$name, $desc];
- $desc_of{$name} = $desc;
- $still_exists{$name} = 1;
- }
-}
-# Get the list of portlets already in use
-my @deleted;
-do {
- my $panes = $Dashboard->Panes;
- for my $pane (keys %$panes) {
- for my $portlet (@{ $panes->{$pane} }) {
- my $name;
- my $type = $portlet->{portlet_type};
+ my $SearchType = 'Ticket';
+ if ((ref($search->Content)||'') eq 'HASH') {
+ $SearchType = $search->Content->{'SearchType'}
+ if $search->Content->{'SearchType'};
+ }
+ else {
+ $RT::Logger->debug("Search ".$search->id." ($desc) appears to have no Content");
+ }
- if ($type eq 'search' || $type eq 'dashboard') {
- $name = join '-', $type, $portlet->{id}, $portlet->{privacy};
- }
- elsif ($type eq 'component') {
- $name = join '-', 'component', $portlet->{component};
- }
+ my $item;
+ if ($object eq $sys && $SearchType eq 'Ticket') {
+ $item = { type => 'system', name => $desc, label => $loc_desc, search_id => $search->id };
- if (!$still_exists{$name}) {
- push @deleted, {
- pane => $pane,
- name => $name,
- description => $portlet->{description},
- };
- next;
- }
+ # make system searches more easily accessible since dashboards
+ # historically used a human-readable description rather than
+ # something we can comfortably use as a hash key
+ $item_for{ $item->{type} }{ $search->id } = $item;
+ }
+ else {
+ my $oid = $object_id.'-SavedSearch-'.$search->Id;
+ $item = { type => 'saved', name => $oid, search_type => $SearchType, label => $loc_desc };
+
+ my $setting = RT::SavedSearch->new($session{CurrentUser});
+ $setting->Load($object_id, $search->Id);
- push @{ $selected{$pane} }, $name;
- $desc_of{$name} = $portlet->{description};
+ $item->{possibly_hidden} = !$setting->IsVisibleTo($Dashboard->Privacy);
}
+
+ $item_for{ $item->{type} }{ $item->{name} } = $item;
+ push @items, $item;
}
-};
-$m->callback(
- CallbackName => 'PopulatePossibilities',
- Dashboard => $Dashboard,
- items => \@items,
- desc_of => \%desc_of,
- still_exists => \%still_exists,
- selected => \%selected,
-);
+ for my $dashboard ($m->comp("/Dashboards/Elements/DashboardsForObject", Object => $object, Flat => 1)) {
+ # Users *can* set up mutually recursive dashboards, but don't make it
+ # THIS easy for them to shoot themselves in the foot.
+ next if $dashboard->Id == $Dashboard->id;
-# Create selectionbox widgets for those portlets
+ my $name = 'dashboard-' . $dashboard->Id . '-' . $dashboard->Privacy;
-my %pane_name = (
- 'body' => loc('Body'),
- 'sidebar' => loc('Sidebar'),
-);
+ my $item = { type => 'dashboard', name => $name, label => $dashboard->Name };
+ $item->{possibly_hidden} = !$dashboard->IsVisibleTo($Dashboard->Privacy);
-$m->callback(
- CallbackName => 'Panes',
- Dashboard => $Dashboard,
- panes => \%pane_name,
-);
+ $item_for{ $item->{type} }{ $item->{name} } = $item;
+ push @items, $item;
+ }
-my @panes;
-for my $pane (sort keys %pane_name) {
- my $sel = $m->comp(
- '/Widgets/SelectionBox:new',
- Action => 'Queries.html',
- Name => "Searches-$pane",
- DisplayName => $pane_name{$pane},
- Available => \@items,
- Selected => $selected{$pane},
- AutoSave => 1,
- OnSubmit => sub {
- my $self = shift;
+ my $label = $object eq $sys ? loc('System')
+ : $object->isa('RT::Group') ? $object->Label
+ : $object->Name;
- $m->callback(
- CallbackName => 'Submit',
- Dashboard => $Dashboard,
- Selected => $self->{Current},
- pane => $pane,
- );
+ push @sections, {
+ id => $object_id,
+ label => $label,
+ items => [ sort { lc($a->{label}) cmp lc($b->{label}) } @items ],
+ };
+}
- my @portlets;
- for (@{ $self->{Current} }) {
- my $item = $_;
- my $desc = $desc_of{$item};
- my $portlet_type = $1 if $item =~ s/^(\w+)-//;
+my %pane_name = (
+ 'body' => loc('Body'),
+ 'sidebar' => loc('Sidebar'),
+);
- if ($portlet_type eq 'search' || $portlet_type eq 'dashboard') {
- my ($id, $privacy) = split '-', $item, 2;
- push @portlets, {
- portlet_type => $portlet_type,
- privacy => $privacy,
- id => $id,
- description => $desc,
- pane => $pane,
- };
+my %selected;
+do {
+ my $panes = $Dashboard->Panes;
+ for my $pane (keys %$panes) {
+ my @items;
+ for my $saved (@{ $panes->{$pane} }) {
+ my $item;
+ if ($saved->{portlet_type} eq 'component') {
+ $item = $item_for{ $saved->{portlet_type} }{ $saved->{component} };
+ }
+ elsif ($saved->{portlet_type} eq 'search') {
+ if ($saved->{privacy} =~ /^RT::System-/) {
+ $item = $item_for{system}{$saved->{id}};
}
- elsif ($portlet_type eq 'component') {
- # Absolute paths stay absolute, relative paths go into
- # /Elements. This way, extensions that add portlets work.
- my $path = $item;
- $path = "/Elements/$path" if substr($path, 0, 1) ne '/';
-
- push @portlets, {
- portlet_type => $portlet_type,
- component => $item,
- path => $path,
- description => $item,
- pane => $pane,
- };
+ else {
+ my $name = join '-', $saved->{privacy}, 'SavedSearch', $saved->{id};
+ $item = $item_for{saved}{$name};
}
}
+ else {
+ my $type = $saved->{portlet_type};
+ my $name = join '-', $type, $saved->{id}, $saved->{privacy};
+ $item = $item_for{$type}{$name};
+ }
- # we want to keep all the other panes the same
- my $panes = $Dashboard->Panes;
- $panes->{$pane} = \@portlets;
-
- # remove "deleted" warnings about this pane
- @deleted = grep { $_->{pane} ne $pane } @deleted;
-
- $m->callback(
- CallbackName => 'BeforeUpdate',
- Dashboard => $Dashboard,
- panes => $panes,
- );
-
- my ($ok, $msg) = $Dashboard->Update(Panes => $panes);
-
- if ($ok) {
- push @results, loc("Dashboard updated");
+ if ($item) {
+ push @items, $item;
}
else {
- push @results, loc("Dashboard could not be updated: [_1]", $msg);
+ push @results, loc('Unable to find [_1] [_2]', $saved->{portlet_type}, $saved->{description});
}
- },
- );
-
- push @panes, $sel;
-}
+ }
+ $selected{$pane} = \@items;
+ }
+};
-$m->comp('/Widgets/SelectionBox:process', %ARGS, self => $_ )
- for @panes;
+my @filters = (
+ [ 'component' => loc('Components') ],
+ [ 'dashboard' => loc('Dashboards') ],
+ [ 'ticket' => loc('Tickets') ],
+ [ 'chart' => loc('Charts') ],
+);
-my @hidden_searches = $Dashboard->PossibleHiddenSearches;
+$m->callback(
+ CallbackName => 'Default',
+ pane_name => \%pane_name,
+ sections => \@sections,
+ selected => \%selected,
+ filters => \@filters,
+);
</%INIT>
<%ARGS>
$id => '' unless defined $id
</%ARGS>
-
diff --git a/share/html/Dashboards/Elements/DashboardsForObject b/share/html/Elements/ShowSelectSearch
similarity index 68%
copy from share/html/Dashboards/Elements/DashboardsForObject
copy to share/html/Elements/ShowSelectSearch
index 149da29c4..6b2ea4da9 100644
--- a/share/html/Dashboards/Elements/DashboardsForObject
+++ b/share/html/Elements/ShowSelectSearch
@@ -45,41 +45,27 @@
%# those contributions and any derivatives thereof.
%#
%# END BPS TAGGED BLOCK }}}
-<%args>
-$Object => undef
-</%args>
-<%init>
-# Returns a hash of dashboards associated on $Object
+<%ARGS>
+$type
+$name
+$label
+$search_id => undef
+$search_type => undef
+$possibly_hidden => 0
+</%ARGS>
+<li data-type="<% $type %>" data-name="<% $name %>" data-search-type="<% $search_type || '' %>" data-search-id="<% $search_id || '' %>" data-description="<% $label %>"><a href="#" class="remove"><img src="<% RT->Config->Get('WebPath') %>/static/images/close.png" alt="<% loc('Remove') %>" title="<% loc('Remove') %>" /></a> <% $prefix ? $prefix . ': ' : '' %>
+<% $label %>
+% if ($possibly_hidden) {
+<span class="warning"><&|/l&>Warning: may not be visible to all viewers</&></span>
+% }
+</li>
+<%INIT>
+my $prefix;
-use RT::Dashboard;
-my %dashboards;
-my $privacy = RT::Dashboard->_build_privacy($Object);
-
-while (my $attr = $Object->Attributes->Next) {
- if ($attr->Name =~ /^Dashboard\b/) {
- my $dashboard = RT::Dashboard->new($User);
- my ($ok, $msg) = $dashboard->Load($privacy, $attr->id);
-
- if (!$ok) {
- $RT::Logger->debug("Unable to load dashboard $ok (privacy $privacy): $msg");
- next;
- }
-
- if ($Object->isa('RT::System')) {
- push @{ $dashboards{system} }, $dashboard;
- }
- elsif ($Object->isa('RT::User')) {
- push @{ $dashboards{personal} }, $dashboard;
- }
- elsif ($Object->isa('RT::Group')) {
- push @{ $dashboards{group}{$Object->Name} }, $dashboard;
- }
- }
+if ($type eq 'saved' || $type eq 'search') {
+ $prefix = loc(ucfirst($search_type || 'ticket'));
}
-return \%dashboards;
-</%init>
-
-<%args>
-$User => $session{'CurrentUser'}
-</%args>
-
+elsif ($type eq 'dashboard') {
+ $prefix = loc('Dashboard');
+}
+</%INIT>
diff --git a/share/html/Helpers/UpdateDashboard b/share/html/Helpers/UpdateDashboard
new file mode 100644
index 000000000..3ede557b6
--- /dev/null
+++ b/share/html/Helpers/UpdateDashboard
@@ -0,0 +1,124 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2016 Best Practical Solutions, LLC
+%# <sales at bestpractical.com>
+%#
+%# (Except where explicitly superseded by other copyright notices)
+%#
+%#
+%# LICENSE:
+%#
+%# This work is made available to you under the terms of Version 2 of
+%# the GNU General Public License. A copy of that license should have
+%# been provided with this software, but in any event can be snarfed
+%# from www.gnu.org.
+%#
+%# This work is distributed in the hope that it will be useful, but
+%# WITHOUT ANY WARRANTY; without even the implied warranty of
+%# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+%# General Public License for more details.
+%#
+%# You should have received a copy of the GNU General Public License
+%# along with this program; if not, write to the Free Software
+%# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+%# 02110-1301 or visit their web page on the internet at
+%# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+%#
+%#
+%# CONTRIBUTION SUBMISSION POLICY:
+%#
+%# (The following paragraph is not intended to limit the rights granted
+%# to you to modify and distribute this software under the terms of
+%# the GNU General Public License and is only of importance to you if
+%# you choose to contribute your changes and enhancements to the
+%# community by submitting them to Best Practical Solutions, LLC.)
+%#
+%# By intentionally submitting any modifications, corrections or
+%# derivatives to this work, or any other work intended for use with
+%# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+%# you are the copyright holder for those contributions and you grant
+%# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
+%# royalty-free, perpetual, license to use, copy, create derivative
+%# works based on those contributions, and sublicense and distribute
+%# those contributions and any derivatives thereof.
+%#
+%# END BPS TAGGED BLOCK }}}
+<%ARGS>
+$content
+</%ARGS>
+<%INIT>
+my $args = JSON::from_json($content);
+my $id = $args->{dashboard_id};
+
+my ($ok, $msg);
+if ($id eq 'MyRT') {
+ my $user = $session{CurrentUser}->UserObj;
+ ($ok, $msg) = $user->SetPreferences('HomepageSettings', $args->{panes});
+}
+else {
+ use RT::Dashboard;
+ my $Dashboard = RT::Dashboard->new($session{'CurrentUser'});
+ ($ok, $msg) = $Dashboard->LoadById($id);
+
+ # report error at the bottom
+ goto DONE unless $ok && $Dashboard->Id;
+
+ my $content;
+ for my $pane_name (keys %{ $args->{panes} }) {
+ my @pane;
+
+ for my $item (@{ $args->{panes}{$pane_name} }) {
+ my %saved;
+ $saved{pane} = $pane_name;
+ $saved{portlet_type} = $item->{type};
+ $saved{description} = $item->{description};
+
+ if ($item->{type} eq 'component') {
+ $saved{component} = $item->{name};
+
+ # Absolute paths stay absolute, relative paths go into
+ # /Elements. This way, extensions that add portlets work.
+ my $path = $item->{name};
+ $path = "/Elements/$path" if substr($path, 0, 1) ne '/';
+
+ $saved{path} = $path;
+ }
+ elsif ($item->{type} eq 'system' || $item->{type} eq 'saved') {
+ $saved{portlet_type} = 'search';
+ my $type = $item->{searchType};
+ $type = 'Saved Search' if !$type || $type eq 'Ticket';
+ $saved{description} = loc($type) . ': ' . $saved{description};
+
+ if ($item->{type} eq 'system') {
+ $saved{privacy} = 'RT::System-1';
+ $saved{id} = $item->{searchId};
+ }
+ else {
+ my ($obj_type, $obj_id, undef, $search_id) = split '-', $item->{name};
+ $saved{privacy} = "$obj_type-$obj_id";
+ $saved{id} = $search_id;
+ }
+ }
+ elsif ($item->{type} eq 'dashboard') {
+ my (undef, $dashboard_id, $obj_type, $obj_id) = split '-', $item->{name};
+ $saved{privacy} = "$obj_type-$obj_id";
+ $saved{id} = $dashboard_id;
+ $saved{description} = loc('Dashboard') . ': ' . $saved{description};
+ }
+
+ push @pane, \%saved;
+ }
+
+ $content->{$pane_name} = \@pane;
+ }
+
+ ($ok, $msg) = $Dashboard->Update(Panes => $content);
+}
+
+DONE:
+$r->content_type('application/json; charset=utf-8');
+$m->print(JSON({ ok => $ok, msg => $msg}));
+$m->abort;
+</%INIT>
diff --git a/share/html/Prefs/MyRT.html b/share/html/Prefs/MyRT.html
index 183f036e6..17b1faf05 100644
--- a/share/html/Prefs/MyRT.html
+++ b/share/html/Prefs/MyRT.html
@@ -46,20 +46,18 @@
%#
%# END BPS TAGGED BLOCK }}}
<& /Elements/Header, Title => $title &>
-<& /Elements/Tabs
-&>
+<& /Elements/Tabs &>
<& /Elements/ListActions, actions => \@results &>
-<br />
-
-% for my $pane ( @panes ) {
-<&|/Widgets/TitleBox,
- title => loc('RT at a glance') .': '. loc( $pane->{Name} ),
- bodyclass => ""
-&>
-<& /Widgets/SelectionBox:show, self => $pane &>
-</&>
-% }
+<form method="post" action="<% RT->Config->Get('WebPath') %>/Helpers/UpdateDashboard" data-dashboard_id='MyRT'>
+ <& /Widgets/SearchSelection,
+ pane_name => \%pane_name,
+ sections => \@sections,
+ selected => \%selected,
+ filters => \@filters,
+ &>
+ <input type="submit" class="button" name="UpdateSearches" value="<% loc('Save') %>" />
+</form>
<&|/Widgets/TitleBox, title => loc('Options'), bodyclass => "" &>
<form method="post" action="MyRT.html">
@@ -108,8 +106,18 @@ unless ($portlets) {
$portlets = $defaults ? $defaults->Content : {};
}
-my %seen;
-my @items = map ["component-$_", loc($_)], grep !$seen{$_}++, @{RT->Config->Get('HomepageComponents')};
+my @sections;
+my %item_for;
+
+my @components = map { type => "component", name => $_, label => loc($_) }, @{RT->Config->Get('HomepageComponents')};
+
+$item_for{ $_->{type} }{ $_->{name} } = $_ for @components;
+
+push @sections, {
+ id => 'components',
+ label => loc("Components"),
+ items => \@components,
+};
my $sys = RT::System->new($session{'CurrentUser'});
my @objs = ($sys);
@@ -118,8 +126,11 @@ push @objs, RT::SavedSearch->new( $session{CurrentUser} )->ObjectsForLoading
if $session{'CurrentUser'}->HasRight( Right => 'LoadSavedSearch',
Object => $RT::System );
-my @sys_searches;
for my $object (@objs) {
+ my @items;
+ my $object_id = ref($object) . '-' . $object->Id;
+ $object_id = 'system' if $object eq $sys;
+
for ($m->comp("/Search/Elements/SearchesForObject", Object => $object)) {
my ($desc, $loc_desc, $search) = @$_;
@@ -132,34 +143,64 @@ for my $object (@objs) {
$RT::Logger->debug("Search ".$search->id." ($desc) appears to have no Content");
}
+ my $item;
if ($object eq $sys && $SearchType eq 'Ticket') {
- push @items, ["system-$desc", $loc_desc];
- push @sys_searches, [$desc, $search];
+ $item = { type => 'system', name => $desc, label => $loc_desc };
}
else {
- my $oid = ref($object).'-'.$object->Id.'-SavedSearch-'.$search->Id;
- my $type = ($SearchType eq 'Ticket')
- ? 'Saved Search' # loc
- : $SearchType;
- push @items, ["saved-$oid", loc($type).": $loc_desc"];
+ my $oid = $object_id.'-SavedSearch-'.$search->Id;
+ $item = { type => 'saved', name => $oid, search_type => $SearchType, label => $loc_desc };
}
+
+ $item_for{ $item->{type} }{ $item->{name} } = $item;
+ push @items, $item;
}
+
+ my $label = $object eq $sys ? loc('System')
+ : $object->isa('RT::Group') ? $object->Label
+ : $object->Name;
+
+ push @sections, {
+ id => $object_id,
+ label => $label,
+ items => [ sort { lc($a->{label}) cmp lc($b->{label}) } @items ],
+ };
}
-my @panes = $m->comp(
- '/Admin/Elements/ConfigureMyRT',
- panes => ['body', 'sidebar'],
- Action => 'MyRT.html',
- items => \@items,
- current_portlets => $portlets,
- OnSave => sub {
- my ( $conf, $pane ) = @_;
- my ($ok, $msg) = $user->SetPreferences( 'HomepageSettings', $conf );
- push @results, $ok ? loc('Preferences saved for [_1].', $pane) : $msg;
+my %selected;
+for my $pane (keys %$portlets) {
+ my @items;
+
+ for my $saved (@{ $portlets->{$pane} }) {
+ my $item = $item_for{ $saved->{type} }{ $saved->{name} };
+ if ($item) {
+ push @items, $item;
+ }
+ else {
+ push @results, loc('Unable to find [_1] [_2]', $saved->{type}, $saved->{name});
+ }
}
+
+ $selected{$pane} = \@items;
+}
+
+my %pane_name = (
+ 'body' => loc('Body'),
+ 'sidebar' => loc('Sidebar'),
);
-$m->comp( '/Widgets/SelectionBox:process', %ARGS, self => $_ )
- for @panes;
+my @filters = (
+ [ 'component' => loc('Components') ],
+ [ 'ticket' => loc('Tickets') ],
+ [ 'chart' => loc('Charts') ],
+);
+
+$m->callback(
+ CallbackName => 'Default',
+ pane_name => \%pane_name,
+ sections => \@sections,
+ selected => \%selected,
+ filters => \@filters,
+);
</%INIT>
diff --git a/share/html/Dashboards/Elements/DashboardsForObject b/share/html/Widgets/SearchSelection
similarity index 57%
copy from share/html/Dashboards/Elements/DashboardsForObject
copy to share/html/Widgets/SearchSelection
index 149da29c4..f3bcef543 100644
--- a/share/html/Dashboards/Elements/DashboardsForObject
+++ b/share/html/Widgets/SearchSelection
@@ -45,41 +45,66 @@
%# those contributions and any derivatives thereof.
%#
%# END BPS TAGGED BLOCK }}}
-<%args>
-$Object => undef
-</%args>
-<%init>
-# Returns a hash of dashboards associated on $Object
+<div class="selectionbox-js">
+ <div class="destinations">
+% for my $pane (sort keys %pane_name) {
+ <div class="destination" data-pane="<% $pane %>">
+ <h2><% $pane_name{$pane} %></h2>
+ <div class="contents">
+ <ul>
+% for my $item (@{ $selected{$pane} }) {
+ <& /Elements/ShowSelectSearch, %$item &>
+% }
+ </ul>
+ </div>
+ </div>
+% }
+ </div>
+ <div class="source">
+ <h2><&|/l&>Saved Searches</&></h2>
-use RT::Dashboard;
-my %dashboards;
-my $privacy = RT::Dashboard->_build_privacy($Object);
+ <div class="filters">
+ <input type="search" name="search" placeholder="<&|/l&>Search…</&>" autocomplete="off">
+ <select name="filter">
+ <option value=""><&|/l&>All Types</&></option>
+% for (@filters) {
+% my ($value, $label) = @$_;
+ <option value="<% $value %>"><% $label %></option>
+% }
+ </select>
+ </div>
-while (my $attr = $Object->Attributes->Next) {
- if ($attr->Name =~ /^Dashboard\b/) {
- my $dashboard = RT::Dashboard->new($User);
- my ($ok, $msg) = $dashboard->Load($privacy, $attr->id);
+ <div class="contents">
+% for my $section (@sections) {
+% my $label = $section->{label};
+% my $items = $section->{items};
- if (!$ok) {
- $RT::Logger->debug("Unable to load dashboard $ok (privacy $privacy): $msg");
- next;
- }
+ <div class="section">
+ <h3><% $label | n %></h3>
+ <ul>
+% for my $item (@$items) {
+ <& /Elements/ShowSelectSearch, %$item &>
+% }
+ </ul>
+ </div>
+% }
+ </div>
+ </div>
+</div>
- if ($Object->isa('RT::System')) {
- push @{ $dashboards{system} }, $dashboard;
- }
- elsif ($Object->isa('RT::User')) {
- push @{ $dashboards{personal} }, $dashboard;
- }
- elsif ($Object->isa('RT::Group')) {
- push @{ $dashboards{group}{$Object->Name} }, $dashboard;
- }
- }
-}
-return \%dashboards;
-</%init>
-
-<%args>
-$User => $session{'CurrentUser'}
-</%args>
+<div class="clear"></div>
+<%INIT>
+$m->callback(
+ CallbackName => 'Default',
+ sections => \@sections,
+ selected => \%selected,
+ filters => \@filters,
+);
+</%INIT>
+<%ARGS>
+%pane_name
+ at filters
+ at sections
+%selected
+</%ARGS>
diff --git a/share/static/css/base/forms.css b/share/static/css/base/forms.css
index 2584ee02b..f84781f7c 100644
--- a/share/static/css/base/forms.css
+++ b/share/static/css/base/forms.css
@@ -280,3 +280,126 @@ ul.selectable a {
font-weight: bold;
text-decoration: underline;
}
+
+/* javascript selection box */
+
+.selectionbox-js {
+ width: 750px;
+}
+
+.selectionbox-js .source,
+.selectionbox-js .destinations {
+ width: 350px;
+}
+
+.selectionbox-js .destinations {
+ float: right;
+}
+
+.selectionbox-js .source li .remove {
+ display: none;
+}
+
+/* make it look more like we're cloning */
+.selectionbox-js .source .placeholder {
+ display: none;
+}
+
+.selectionbox-js .destination {
+ margin-bottom: 2em;
+}
+
+.selectionbox-js .source .contents {
+ height: 450px;
+}
+
+/* include ul rule specifically to make the drop target work when there are
+ no selected searches */
+.selectionbox-js .destination .contents,
+.selectionbox-js .destination .contents ul {
+ height: 200px;
+}
+
+.selectionbox-js h2 {
+ margin: 0 0 .5em 0;
+}
+
+.selectionbox-js .contents {
+ border: 2px solid #aaa;
+ overflow-y: scroll;
+ padding: 0 .5em;
+}
+
+.selectionbox-js .contents ul {
+ list-style-type: none;
+ padding: 0;
+ margin: 0;
+}
+
+.selectionbox-js .contents li,
+.selectionbox-js .contents .placeholder {
+ border: 2px solid #ddd;
+ padding: .5em;
+ margin: .5em 0;
+ background-color: #eee;
+ cursor: default;
+}
+
+.selectionbox-js .contents li {
+ position: relative;
+
+ /* wrap very long search names */
+ word-wrap: break-word;
+ padding-right: 2em;
+}
+
+.selectionbox-js .contents li .warning {
+ display: block;
+ font-size: .9em;
+ font-style: italic;
+ color: #a00;
+}
+
+.selectionbox-js .contents li a.remove {
+ position: absolute;
+ right: .5em;
+}
+
+.selectionbox-js .contents li a.remove img {
+ height: 1em;
+ border-style: none;
+}
+
+.selectionbox-js .contents .placeholder {
+ box-sizing: border-box;
+ border-style: dashed;
+ opacity: 0.5;
+ padding-left: 0;
+ padding-right: 0;
+ width: 100%;
+}
+
+.selectionbox-js .contents h3 {
+ margin: .5em;
+}
+
+.selectionbox-js .contents .section {
+ margin-bottom: 1em;
+}
+
+.selectionbox-js select[name=filter] {
+ float: right;
+}
+
+/* always show scrollbars */
+.selectionbox-js .contents::-webkit-scrollbar {
+ -webkit-appearance: none;
+ width: 7px;
+}
+
+.selectionbox-js .contents::-webkit-scrollbar-thumb {
+ border-radius: 4px;
+ background-color: rgba(0, 0, 0, .5);
+ -webkit-box-shadow: 0 0 1px rgba(255, 255, 255, .5);
+}
+
diff --git a/share/static/js/forms.js b/share/static/js/forms.js
index 3c6f9b93d..5b3f724a7 100644
--- a/share/static/js/forms.js
+++ b/share/static/js/forms.js
@@ -15,4 +15,164 @@ jQuery(function() {
form.data('submitted', true);
}
});
+
+ jQuery('.selectionbox-js').each(function () {
+ var container = jQuery(this);
+ var source = container.find('.source');
+ var form = container.closest('form');
+ var submit = form.find('input[name=UpdateSearches]');
+
+ var copyHelper;
+ var draggedIntoDestination;
+
+ container.find('.destination ul').sortable({
+ connectWith: '.destination ul',
+ containment: container.find('.destinations'),
+ placeholder: 'placeholder',
+ forcePlaceholderSize: true,
+ axis: 'y',
+ cancel: '.remove',
+
+ // drag a clone of the source item
+ receive: function (e, ui) {
+ draggedIntoDestination = true;
+ copyHelper = null;
+ }
+ }).disableSelection().on('click', '.remove', function (e) {
+ e.preventDefault();
+ jQuery(e.target).closest('li').remove();
+ return false;
+ });
+
+ source.find('ul').sortable({
+ connectWith: '.destination ul',
+ containment: container,
+ placeholder: 'placeholder',
+ forcePlaceholderSize: true,
+
+ // drag a clone of the source item
+ helper: function (e, li) {
+ copyHelper = li.clone().insertAfter(li);
+ return li.clone();
+ },
+
+ start: function (e, ui) {
+ draggedIntoDestination = false;
+ },
+
+ stop: function (e, ui) {
+ if (copyHelper) {
+ copyHelper.remove();
+ }
+
+ if (!draggedIntoDestination) {
+ jQuery(this).sortable('cancel');
+ }
+ }
+ }).disableSelection();
+
+ var searchField = source.find('input[name=search]');
+ var filterField = source.find('select[name=filter]');
+
+ var refreshSource = function () {
+ var searchTerm = searchField.val().toLowerCase();
+ var filterType = filterField.val();
+
+ source.find('.section').each(function () {
+ var section = jQuery(this);
+ var sectionLabel = section.find('h3').text();
+ var sectionMatches = sectionLabel.toLowerCase().indexOf(searchTerm) > -1;
+
+ var visibleItem = false;
+ section.find('li').each(function () {
+ var item = jQuery(this);
+ var itemType = item.data('type');
+
+ if (filterType) {
+ // component and dashboard matches on data-type
+ if (filterType == 'component' || itemType == 'component' || filterType == 'dashboard' || itemType == 'dashboard') {
+ if (itemType != filterType) {
+ item.hide();
+ return;
+ }
+ }
+ // everything else matches on data-search-type
+ else {
+ var searchType = item.data('search-type');
+ if (searchType === '') { searchType = 'ticket' }
+
+ if (searchType.toLowerCase() != filterType) {
+ item.hide();
+ return;
+ }
+ }
+ }
+
+ if (sectionMatches || item.text().toLowerCase().indexOf(searchTerm) > -1) {
+ visibleItem = true;
+ item.show();
+ }
+ else {
+ item.hide();
+ }
+ });
+
+ if (visibleItem) {
+ section.show();
+ }
+ else {
+ section.hide();
+ }
+ });
+
+ source.find('.contents').scrollTop(0);
+ };
+
+ searchField.on('propertychange change keyup paste input', function () {
+ refreshSource();
+ });
+ filterField.on('change keyup', function () {
+ refreshSource();
+ });
+ refreshSource();
+
+ submit.click(function (e) {
+ e.preventDefault();
+
+ var url = form.attr('action');
+ var method = form.attr('method');
+ var params = form.data();
+
+ params.panes = {}
+ container.find('.destination').each(function () {
+ var pane = jQuery(this);
+ var name = pane.data('pane');
+ var items = [];
+ pane.find('li').each(function () {
+ var item = jQuery(this).data();
+ delete item.sortableItem;
+ items.push(item);
+ });
+ params.panes[name] = items;
+ });
+
+ jQuery.ajax({
+ url: url,
+ method: method,
+ data: { content: JSON.stringify(params) },
+ timeout: 30000, /* 30 seconds */
+ success: function (response) {
+ if (response.ok) {
+ window.location.reload();
+ }
+ else {
+ alert(response.msg);
+ }
+ },
+ error: function (xhr, reason) {
+ alert(reason);
+ }
+ });
+ });
+ });
});
diff --git a/share/static/js/jquery.ui.sortable.js b/share/static/js/jquery.ui.sortable.js
new file mode 100644
index 000000000..1ade1e31d
--- /dev/null
+++ b/share/static/js/jquery.ui.sortable.js
@@ -0,0 +1,1250 @@
+/*!
+ * jQuery UI Sortable 1.10.0
+ * http://jqueryui.com
+ *
+ * Copyright 2013 jQuery Foundation and other contributors
+ * Released under the MIT license.
+ * http://jquery.org/license
+ *
+ * http://api.jqueryui.com/sortable/
+ *
+ * Depends:
+ * jquery.ui.core.js
+ * jquery.ui.mouse.js
+ * jquery.ui.widget.js
+ */
+(function( $, undefined ) {
+
+/*jshint loopfunc: true */
+
+function isOverAxis( x, reference, size ) {
+ return ( x > reference ) && ( x < ( reference + size ) );
+}
+
+$.widget("ui.sortable", $.ui.mouse, {
+ version: "1.10.0",
+ widgetEventPrefix: "sort",
+ ready: false,
+ options: {
+ appendTo: "parent",
+ axis: false,
+ connectWith: false,
+ containment: false,
+ cursor: "auto",
+ cursorAt: false,
+ dropOnEmpty: true,
+ forcePlaceholderSize: false,
+ forceHelperSize: false,
+ grid: false,
+ handle: false,
+ helper: "original",
+ items: "> *",
+ opacity: false,
+ placeholder: false,
+ revert: false,
+ scroll: true,
+ scrollSensitivity: 20,
+ scrollSpeed: 20,
+ scope: "default",
+ tolerance: "intersect",
+ zIndex: 1000,
+
+ // callbacks
+ activate: null,
+ beforeStop: null,
+ change: null,
+ deactivate: null,
+ out: null,
+ over: null,
+ receive: null,
+ remove: null,
+ sort: null,
+ start: null,
+ stop: null,
+ update: null
+ },
+ _create: function() {
+
+ var o = this.options;
+ this.containerCache = {};
+ this.element.addClass("ui-sortable");
+
+ //Get the items
+ this.refresh();
+
+ //Let's determine if the items are being displayed horizontally
+ this.floating = this.items.length ? o.axis === "x" || (/left|right/).test(this.items[0].item.css("float")) || (/inline|table-cell/).test(this.items[0].item.css("display")) : false;
+
+ //Let's determine the parent's offset
+ this.offset = this.element.offset();
+
+ //Initialize mouse events for interaction
+ this._mouseInit();
+
+ //We're ready to go
+ this.ready = true;
+
+ },
+
+ _destroy: function() {
+ this.element
+ .removeClass("ui-sortable ui-sortable-disabled");
+ this._mouseDestroy();
+
+ for ( var i = this.items.length - 1; i >= 0; i-- ) {
+ this.items[i].item.removeData(this.widgetName + "-item");
+ }
+
+ return this;
+ },
+
+ _setOption: function(key, value){
+ if ( key === "disabled" ) {
+ this.options[ key ] = value;
+
+ this.widget().toggleClass( "ui-sortable-disabled", !!value );
+ } else {
+ // Don't call widget base _setOption for disable as it adds ui-state-disabled class
+ $.Widget.prototype._setOption.apply(this, arguments);
+ }
+ },
+
+ _mouseCapture: function(event, overrideHandle) {
+ var currentItem = null,
+ validHandle = false,
+ that = this;
+
+ if (this.reverting) {
+ return false;
+ }
+
+ if(this.options.disabled || this.options.type === "static") {
+ return false;
+ }
+
+ //We have to refresh the items data once first
+ this._refreshItems(event);
+
+ //Find out if the clicked node (or one of its parents) is a actual item in this.items
+ $(event.target).parents().each(function() {
+ if($.data(this, that.widgetName + "-item") === that) {
+ currentItem = $(this);
+ return false;
+ }
+ });
+ if($.data(event.target, that.widgetName + "-item") === that) {
+ currentItem = $(event.target);
+ }
+
+ if(!currentItem) {
+ return false;
+ }
+ if(this.options.handle && !overrideHandle) {
+ $(this.options.handle, currentItem).find("*").addBack().each(function() {
+ if(this === event.target) {
+ validHandle = true;
+ }
+ });
+ if(!validHandle) {
+ return false;
+ }
+ }
+
+ this.currentItem = currentItem;
+ this._removeCurrentsFromItems();
+ return true;
+
+ },
+
+ _mouseStart: function(event, overrideHandle, noActivation) {
+
+ var i,
+ o = this.options;
+
+ this.currentContainer = this;
+
+ //We only need to call refreshPositions, because the refreshItems call has been moved to mouseCapture
+ this.refreshPositions();
+
+ //Create and append the visible helper
+ this.helper = this._createHelper(event);
+
+ //Cache the helper size
+ this._cacheHelperProportions();
+
+ /*
+ * - Position generation -
+ * This block generates everything position related - it's the core of draggables.
+ */
+
+ //Cache the margins of the original element
+ this._cacheMargins();
+
+ //Get the next scrolling parent
+ this.scrollParent = this.helper.scrollParent();
+
+ //The element's absolute position on the page minus margins
+ this.offset = this.currentItem.offset();
+ this.offset = {
+ top: this.offset.top - this.margins.top,
+ left: this.offset.left - this.margins.left
+ };
+
+ $.extend(this.offset, {
+ click: { //Where the click happened, relative to the element
+ left: event.pageX - this.offset.left,
+ top: event.pageY - this.offset.top
+ },
+ parent: this._getParentOffset(),
+ relative: this._getRelativeOffset() //This is a relative to absolute position minus the actual position calculation - only used for relative positioned helper
+ });
+
+ // Only after we got the offset, we can change the helper's position to absolute
+ // TODO: Still need to figure out a way to make relative sorting possible
+ this.helper.css("position", "absolute");
+ this.cssPosition = this.helper.css("position");
+
+ //Generate the original position
+ this.originalPosition = this._generatePosition(event);
+ this.originalPageX = event.pageX;
+ this.originalPageY = event.pageY;
+
+ //Adjust the mouse offset relative to the helper if "cursorAt" is supplied
+ (o.cursorAt && this._adjustOffsetFromHelper(o.cursorAt));
+
+ //Cache the former DOM position
+ this.domPosition = { prev: this.currentItem.prev()[0], parent: this.currentItem.parent()[0] };
+
+ //If the helper is not the original, hide the original so it's not playing any role during the drag, won't cause anything bad this way
+ if(this.helper[0] !== this.currentItem[0]) {
+ this.currentItem.hide();
+ }
+
+ //Create the placeholder
+ this._createPlaceholder();
+
+ //Set a containment if given in the options
+ if(o.containment) {
+ this._setContainment();
+ }
+
+ if(o.cursor) { // cursor option
+ if ($("body").css("cursor")) {
+ this._storedCursor = $("body").css("cursor");
+ }
+ $("body").css("cursor", o.cursor);
+ }
+
+ if(o.opacity) { // opacity option
+ if (this.helper.css("opacity")) {
+ this._storedOpacity = this.helper.css("opacity");
+ }
+ this.helper.css("opacity", o.opacity);
+ }
+
+ if(o.zIndex) { // zIndex option
+ if (this.helper.css("zIndex")) {
+ this._storedZIndex = this.helper.css("zIndex");
+ }
+ this.helper.css("zIndex", o.zIndex);
+ }
+
+ //Prepare scrolling
+ if(this.scrollParent[0] !== document && this.scrollParent[0].tagName !== "HTML") {
+ this.overflowOffset = this.scrollParent.offset();
+ }
+
+ //Call callbacks
+ this._trigger("start", event, this._uiHash());
+
+ //Recache the helper size
+ if(!this._preserveHelperProportions) {
+ this._cacheHelperProportions();
+ }
+
+
+ //Post "activate" events to possible containers
+ if( !noActivation ) {
+ for ( i = this.containers.length - 1; i >= 0; i-- ) {
+ this.containers[ i ]._trigger( "activate", event, this._uiHash( this ) );
+ }
+ }
+
+ //Prepare possible droppables
+ if($.ui.ddmanager) {
+ $.ui.ddmanager.current = this;
+ }
+
+ if ($.ui.ddmanager && !o.dropBehaviour) {
+ $.ui.ddmanager.prepareOffsets(this, event);
+ }
+
+ this.dragging = true;
+
+ this.helper.addClass("ui-sortable-helper");
+ this._mouseDrag(event); //Execute the drag once - this causes the helper not to be visible before getting its correct position
+ return true;
+
+ },
+
+ _mouseDrag: function(event) {
+ var i, item, itemElement, intersection,
+ o = this.options,
+ scrolled = false;
+
+ //Compute the helpers position
+ this.position = this._generatePosition(event);
+ this.positionAbs = this._convertPositionTo("absolute");
+
+ if (!this.lastPositionAbs) {
+ this.lastPositionAbs = this.positionAbs;
+ }
+
+ //Do scrolling
+ if(this.options.scroll) {
+ if(this.scrollParent[0] !== document && this.scrollParent[0].tagName !== "HTML") {
+
+ if((this.overflowOffset.top + this.scrollParent[0].offsetHeight) - event.pageY < o.scrollSensitivity) {
+ this.scrollParent[0].scrollTop = scrolled = this.scrollParent[0].scrollTop + o.scrollSpeed;
+ } else if(event.pageY - this.overflowOffset.top < o.scrollSensitivity) {
+ this.scrollParent[0].scrollTop = scrolled = this.scrollParent[0].scrollTop - o.scrollSpeed;
+ }
+
+ if((this.overflowOffset.left + this.scrollParent[0].offsetWidth) - event.pageX < o.scrollSensitivity) {
+ this.scrollParent[0].scrollLeft = scrolled = this.scrollParent[0].scrollLeft + o.scrollSpeed;
+ } else if(event.pageX - this.overflowOffset.left < o.scrollSensitivity) {
+ this.scrollParent[0].scrollLeft = scrolled = this.scrollParent[0].scrollLeft - o.scrollSpeed;
+ }
+
+ } else {
+
+ if(event.pageY - $(document).scrollTop() < o.scrollSensitivity) {
+ scrolled = $(document).scrollTop($(document).scrollTop() - o.scrollSpeed);
+ } else if($(window).height() - (event.pageY - $(document).scrollTop()) < o.scrollSensitivity) {
+ scrolled = $(document).scrollTop($(document).scrollTop() + o.scrollSpeed);
+ }
+
+ if(event.pageX - $(document).scrollLeft() < o.scrollSensitivity) {
+ scrolled = $(document).scrollLeft($(document).scrollLeft() - o.scrollSpeed);
+ } else if($(window).width() - (event.pageX - $(document).scrollLeft()) < o.scrollSensitivity) {
+ scrolled = $(document).scrollLeft($(document).scrollLeft() + o.scrollSpeed);
+ }
+
+ }
+
+ if(scrolled !== false && $.ui.ddmanager && !o.dropBehaviour) {
+ $.ui.ddmanager.prepareOffsets(this, event);
+ }
+ }
+
+ //Regenerate the absolute position used for position checks
+ this.positionAbs = this._convertPositionTo("absolute");
+
+ //Set the helper position
+ if(!this.options.axis || this.options.axis !== "y") {
+ this.helper[0].style.left = this.position.left+"px";
+ }
+ if(!this.options.axis || this.options.axis !== "x") {
+ this.helper[0].style.top = this.position.top+"px";
+ }
+
+ //Rearrange
+ for (i = this.items.length - 1; i >= 0; i--) {
+
+ //Cache variables and intersection, continue if no intersection
+ item = this.items[i];
+ itemElement = item.item[0];
+ intersection = this._intersectsWithPointer(item);
+ if (!intersection) {
+ continue;
+ }
+
+ // Only put the placeholder inside the current Container, skip all
+ // items form other containers. This works because when moving
+ // an item from one container to another the
+ // currentContainer is switched before the placeholder is moved.
+ //
+ // Without this moving items in "sub-sortables" can cause the placeholder to jitter
+ // beetween the outer and inner container.
+ if (item.instance !== this.currentContainer) {
+ continue;
+ }
+
+ // cannot intersect with itself
+ // no useless actions that have been done before
+ // no action if the item moved is the parent of the item checked
+ if (itemElement !== this.currentItem[0] &&
+ this.placeholder[intersection === 1 ? "next" : "prev"]()[0] !== itemElement &&
+ !$.contains(this.placeholder[0], itemElement) &&
+ (this.options.type === "semi-dynamic" ? !$.contains(this.element[0], itemElement) : true)
+ ) {
+
+ this.direction = intersection === 1 ? "down" : "up";
+
+ if (this.options.tolerance === "pointer" || this._intersectsWithSides(item)) {
+ this._rearrange(event, item);
+ } else {
+ break;
+ }
+
+ this._trigger("change", event, this._uiHash());
+ break;
+ }
+ }
+
+ //Post events to containers
+ this._contactContainers(event);
+
+ //Interconnect with droppables
+ if($.ui.ddmanager) {
+ $.ui.ddmanager.drag(this, event);
+ }
+
+ //Call callbacks
+ this._trigger("sort", event, this._uiHash());
+
+ this.lastPositionAbs = this.positionAbs;
+ return false;
+
+ },
+
+ _mouseStop: function(event, noPropagation) {
+
+ if(!event) {
+ return;
+ }
+
+ //If we are using droppables, inform the manager about the drop
+ if ($.ui.ddmanager && !this.options.dropBehaviour) {
+ $.ui.ddmanager.drop(this, event);
+ }
+
+ if(this.options.revert) {
+ var that = this,
+ cur = this.placeholder.offset();
+
+ this.reverting = true;
+
+ $(this.helper).animate({
+ left: cur.left - this.offset.parent.left - this.margins.left + (this.offsetParent[0] === document.body ? 0 : this.offsetParent[0].scrollLeft),
+ top: cur.top - this.offset.parent.top - this.margins.top + (this.offsetParent[0] === document.body ? 0 : this.offsetParent[0].scrollTop)
+ }, parseInt(this.options.revert, 10) || 500, function() {
+ that._clear(event);
+ });
+ } else {
+ this._clear(event, noPropagation);
+ }
+
+ return false;
+
+ },
+
+ cancel: function() {
+
+ if(this.dragging) {
+
+ this._mouseUp({ target: null });
+
+ if(this.options.helper === "original") {
+ this.currentItem.css(this._storedCSS).removeClass("ui-sortable-helper");
+ } else {
+ this.currentItem.show();
+ }
+
+ //Post deactivating events to containers
+ for (var i = this.containers.length - 1; i >= 0; i--){
+ this.containers[i]._trigger("deactivate", null, this._uiHash(this));
+ if(this.containers[i].containerCache.over) {
+ this.containers[i]._trigger("out", null, this._uiHash(this));
+ this.containers[i].containerCache.over = 0;
+ }
+ }
+
+ }
+
+ if (this.placeholder) {
+ //$(this.placeholder[0]).remove(); would have been the jQuery way - unfortunately, it unbinds ALL events from the original node!
+ if(this.placeholder[0].parentNode) {
+ this.placeholder[0].parentNode.removeChild(this.placeholder[0]);
+ }
+ if(this.options.helper !== "original" && this.helper && this.helper[0].parentNode) {
+ this.helper.remove();
+ }
+
+ $.extend(this, {
+ helper: null,
+ dragging: false,
+ reverting: false,
+ _noFinalSort: null
+ });
+
+ if(this.domPosition.prev) {
+ $(this.domPosition.prev).after(this.currentItem);
+ } else {
+ $(this.domPosition.parent).prepend(this.currentItem);
+ }
+ }
+
+ return this;
+
+ },
+
+ serialize: function(o) {
+
+ var items = this._getItemsAsjQuery(o && o.connected),
+ str = [];
+ o = o || {};
+
+ $(items).each(function() {
+ var res = ($(o.item || this).attr(o.attribute || "id") || "").match(o.expression || (/(.+)[\-=_](.+)/));
+ if (res) {
+ str.push((o.key || res[1]+"[]")+"="+(o.key && o.expression ? res[1] : res[2]));
+ }
+ });
+
+ if(!str.length && o.key) {
+ str.push(o.key + "=");
+ }
+
+ return str.join("&");
+
+ },
+
+ toArray: function(o) {
+
+ var items = this._getItemsAsjQuery(o && o.connected),
+ ret = [];
+
+ o = o || {};
+
+ items.each(function() { ret.push($(o.item || this).attr(o.attribute || "id") || ""); });
+ return ret;
+
+ },
+
+ /* Be careful with the following core functions */
+ _intersectsWith: function(item) {
+
+ var x1 = this.positionAbs.left,
+ x2 = x1 + this.helperProportions.width,
+ y1 = this.positionAbs.top,
+ y2 = y1 + this.helperProportions.height,
+ l = item.left,
+ r = l + item.width,
+ t = item.top,
+ b = t + item.height,
+ dyClick = this.offset.click.top,
+ dxClick = this.offset.click.left,
+ isOverElement = (y1 + dyClick) > t && (y1 + dyClick) < b && (x1 + dxClick) > l && (x1 + dxClick) < r;
+
+ if ( this.options.tolerance === "pointer" ||
+ this.options.forcePointerForContainers ||
+ (this.options.tolerance !== "pointer" && this.helperProportions[this.floating ? "width" : "height"] > item[this.floating ? "width" : "height"])
+ ) {
+ return isOverElement;
+ } else {
+
+ return (l < x1 + (this.helperProportions.width / 2) && // Right Half
+ x2 - (this.helperProportions.width / 2) < r && // Left Half
+ t < y1 + (this.helperProportions.height / 2) && // Bottom Half
+ y2 - (this.helperProportions.height / 2) < b ); // Top Half
+
+ }
+ },
+
+ _intersectsWithPointer: function(item) {
+
+ var isOverElementHeight = (this.options.axis === "x") || isOverAxis(this.positionAbs.top + this.offset.click.top, item.top, item.height),
+ isOverElementWidth = (this.options.axis === "y") || isOverAxis(this.positionAbs.left + this.offset.click.left, item.left, item.width),
+ isOverElement = isOverElementHeight && isOverElementWidth,
+ verticalDirection = this._getDragVerticalDirection(),
+ horizontalDirection = this._getDragHorizontalDirection();
+
+ if (!isOverElement) {
+ return false;
+ }
+
+ return this.floating ?
+ ( ((horizontalDirection && horizontalDirection === "right") || verticalDirection === "down") ? 2 : 1 )
+ : ( verticalDirection && (verticalDirection === "down" ? 2 : 1) );
+
+ },
+
+ _intersectsWithSides: function(item) {
+
+ var isOverBottomHalf = isOverAxis(this.positionAbs.top + this.offset.click.top, item.top + (item.height/2), item.height),
+ isOverRightHalf = isOverAxis(this.positionAbs.left + this.offset.click.left, item.left + (item.width/2), item.width),
+ verticalDirection = this._getDragVerticalDirection(),
+ horizontalDirection = this._getDragHorizontalDirection();
+
+ if (this.floating && horizontalDirection) {
+ return ((horizontalDirection === "right" && isOverRightHalf) || (horizontalDirection === "left" && !isOverRightHalf));
+ } else {
+ return verticalDirection && ((verticalDirection === "down" && isOverBottomHalf) || (verticalDirection === "up" && !isOverBottomHalf));
+ }
+
+ },
+
+ _getDragVerticalDirection: function() {
+ var delta = this.positionAbs.top - this.lastPositionAbs.top;
+ return delta !== 0 && (delta > 0 ? "down" : "up");
+ },
+
+ _getDragHorizontalDirection: function() {
+ var delta = this.positionAbs.left - this.lastPositionAbs.left;
+ return delta !== 0 && (delta > 0 ? "right" : "left");
+ },
+
+ refresh: function(event) {
+ this._refreshItems(event);
+ this.refreshPositions();
+ return this;
+ },
+
+ _connectWith: function() {
+ var options = this.options;
+ return options.connectWith.constructor === String ? [options.connectWith] : options.connectWith;
+ },
+
+ _getItemsAsjQuery: function(connected) {
+
+ var i, j, cur, inst,
+ items = [],
+ queries = [],
+ connectWith = this._connectWith();
+
+ if(connectWith && connected) {
+ for (i = connectWith.length - 1; i >= 0; i--){
+ cur = $(connectWith[i]);
+ for ( j = cur.length - 1; j >= 0; j--){
+ inst = $.data(cur[j], this.widgetFullName);
+ if(inst && inst !== this && !inst.options.disabled) {
+ queries.push([$.isFunction(inst.options.items) ? inst.options.items.call(inst.element) : $(inst.options.items, inst.element).not(".ui-sortable-helper").not(".ui-sortable-placeholder"), inst]);
+ }
+ }
+ }
+ }
+
+ queries.push([$.isFunction(this.options.items) ? this.options.items.call(this.element, null, { options: this.options, item: this.currentItem }) : $(this.options.items, this.element).not(".ui-sortable-helper").not(".ui-sortable-placeholder"), this]);
+
+ for (i = queries.length - 1; i >= 0; i--){
+ queries[i][0].each(function() {
+ items.push(this);
+ });
+ }
+
+ return $(items);
+
+ },
+
+ _removeCurrentsFromItems: function() {
+
+ var list = this.currentItem.find(":data(" + this.widgetName + "-item)");
+
+ this.items = $.grep(this.items, function (item) {
+ for (var j=0; j < list.length; j++) {
+ if(list[j] === item.item[0]) {
+ return false;
+ }
+ }
+ return true;
+ });
+
+ },
+
+ _refreshItems: function(event) {
+
+ this.items = [];
+ this.containers = [this];
+
+ var i, j, cur, inst, targetData, _queries, item, queriesLength,
+ items = this.items,
+ queries = [[$.isFunction(this.options.items) ? this.options.items.call(this.element[0], event, { item: this.currentItem }) : $(this.options.items, this.element), this]],
+ connectWith = this._connectWith();
+
+ if(connectWith && this.ready) { //Shouldn't be run the first time through due to massive slow-down
+ for (i = connectWith.length - 1; i >= 0; i--){
+ cur = $(connectWith[i]);
+ for (j = cur.length - 1; j >= 0; j--){
+ inst = $.data(cur[j], this.widgetFullName);
+ if(inst && inst !== this && !inst.options.disabled) {
+ queries.push([$.isFunction(inst.options.items) ? inst.options.items.call(inst.element[0], event, { item: this.currentItem }) : $(inst.options.items, inst.element), inst]);
+ this.containers.push(inst);
+ }
+ }
+ }
+ }
+
+ for (i = queries.length - 1; i >= 0; i--) {
+ targetData = queries[i][1];
+ _queries = queries[i][0];
+
+ for (j=0, queriesLength = _queries.length; j < queriesLength; j++) {
+ item = $(_queries[j]);
+
+ item.data(this.widgetName + "-item", targetData); // Data for target checking (mouse manager)
+
+ items.push({
+ item: item,
+ instance: targetData,
+ width: 0, height: 0,
+ left: 0, top: 0
+ });
+ }
+ }
+
+ },
+
+ refreshPositions: function(fast) {
+
+ //This has to be redone because due to the item being moved out/into the offsetParent, the offsetParent's position will change
+ if(this.offsetParent && this.helper) {
+ this.offset.parent = this._getParentOffset();
+ }
+
+ var i, item, t, p;
+
+ for (i = this.items.length - 1; i >= 0; i--){
+ item = this.items[i];
+
+ //We ignore calculating positions of all connected containers when we're not over them
+ if(item.instance !== this.currentContainer && this.currentContainer && item.item[0] !== this.currentItem[0]) {
+ continue;
+ }
+
+ t = this.options.toleranceElement ? $(this.options.toleranceElement, item.item) : item.item;
+
+ if (!fast) {
+ item.width = t.outerWidth();
+ item.height = t.outerHeight();
+ }
+
+ p = t.offset();
+ item.left = p.left;
+ item.top = p.top;
+ }
+
+ if(this.options.custom && this.options.custom.refreshContainers) {
+ this.options.custom.refreshContainers.call(this);
+ } else {
+ for (i = this.containers.length - 1; i >= 0; i--){
+ p = this.containers[i].element.offset();
+ this.containers[i].containerCache.left = p.left;
+ this.containers[i].containerCache.top = p.top;
+ this.containers[i].containerCache.width = this.containers[i].element.outerWidth();
+ this.containers[i].containerCache.height = this.containers[i].element.outerHeight();
+ }
+ }
+
+ return this;
+ },
+
+ _createPlaceholder: function(that) {
+ that = that || this;
+ var className,
+ o = that.options;
+
+ if(!o.placeholder || o.placeholder.constructor === String) {
+ className = o.placeholder;
+ o.placeholder = {
+ element: function() {
+
+ var el = $(document.createElement(that.currentItem[0].nodeName))
+ .addClass(className || that.currentItem[0].className+" ui-sortable-placeholder")
+ .removeClass("ui-sortable-helper")[0];
+
+ if(!className) {
+ el.style.visibility = "hidden";
+ }
+
+ return el;
+ },
+ update: function(container, p) {
+
+ // 1. If a className is set as 'placeholder option, we don't force sizes - the class is responsible for that
+ // 2. The option 'forcePlaceholderSize can be enabled to force it even if a class name is specified
+ if(className && !o.forcePlaceholderSize) {
+ return;
+ }
+
+ //If the element doesn't have a actual height by itself (without styles coming from a stylesheet), it receives the inline height from the dragged item
+ if(!p.height()) { p.height(that.currentItem.innerHeight() - parseInt(that.currentItem.css("paddingTop")||0, 10) - parseInt(that.currentItem.css("paddingBottom")||0, 10)); }
+ if(!p.width()) { p.width(that.currentItem.innerWidth() - parseInt(that.currentItem.css("paddingLeft")||0, 10) - parseInt(that.currentItem.css("paddingRight")||0, 10)); }
+ }
+ };
+ }
+
+ //Create the placeholder
+ that.placeholder = $(o.placeholder.element.call(that.element, that.currentItem));
+
+ //Append it after the actual current item
+ that.currentItem.after(that.placeholder);
+
+ //Update the size of the placeholder (TODO: Logic to fuzzy, see line 316/317)
+ o.placeholder.update(that, that.placeholder);
+
+ },
+
+ _contactContainers: function(event) {
+ var i, j, dist, itemWithLeastDistance, posProperty, sizeProperty, base, cur, nearBottom,
+ innermostContainer = null,
+ innermostIndex = null;
+
+ // get innermost container that intersects with item
+ for (i = this.containers.length - 1; i >= 0; i--) {
+
+ // never consider a container that's located within the item itself
+ if($.contains(this.currentItem[0], this.containers[i].element[0])) {
+ continue;
+ }
+
+ if(this._intersectsWith(this.containers[i].containerCache)) {
+
+ // if we've already found a container and it's more "inner" than this, then continue
+ if(innermostContainer && $.contains(this.containers[i].element[0], innermostContainer.element[0])) {
+ continue;
+ }
+
+ innermostContainer = this.containers[i];
+ innermostIndex = i;
+
+ } else {
+ // container doesn't intersect. trigger "out" event if necessary
+ if(this.containers[i].containerCache.over) {
+ this.containers[i]._trigger("out", event, this._uiHash(this));
+ this.containers[i].containerCache.over = 0;
+ }
+ }
+
+ }
+
+ // if no intersecting containers found, return
+ if(!innermostContainer) {
+ return;
+ }
+
+ // move the item into the container if it's not there already
+ if(this.containers.length === 1) {
+ this.containers[innermostIndex]._trigger("over", event, this._uiHash(this));
+ this.containers[innermostIndex].containerCache.over = 1;
+ } else {
+
+ //When entering a new container, we will find the item with the least distance and append our item near it
+ dist = 10000;
+ itemWithLeastDistance = null;
+ posProperty = this.containers[innermostIndex].floating ? "left" : "top";
+ sizeProperty = this.containers[innermostIndex].floating ? "width" : "height";
+ base = this.positionAbs[posProperty] + this.offset.click[posProperty];
+ for (j = this.items.length - 1; j >= 0; j--) {
+ if(!$.contains(this.containers[innermostIndex].element[0], this.items[j].item[0])) {
+ continue;
+ }
+ if(this.items[j].item[0] === this.currentItem[0]) {
+ continue;
+ }
+ cur = this.items[j].item.offset()[posProperty];
+ nearBottom = false;
+ if(Math.abs(cur - base) > Math.abs(cur + this.items[j][sizeProperty] - base)){
+ nearBottom = true;
+ cur += this.items[j][sizeProperty];
+ }
+
+ if(Math.abs(cur - base) < dist) {
+ dist = Math.abs(cur - base); itemWithLeastDistance = this.items[j];
+ this.direction = nearBottom ? "up": "down";
+ }
+ }
+
+ //Check if dropOnEmpty is enabled
+ if(!itemWithLeastDistance && !this.options.dropOnEmpty) {
+ return;
+ }
+
+ this.currentContainer = this.containers[innermostIndex];
+ itemWithLeastDistance ? this._rearrange(event, itemWithLeastDistance, null, true) : this._rearrange(event, null, this.containers[innermostIndex].element, true);
+ this._trigger("change", event, this._uiHash());
+ this.containers[innermostIndex]._trigger("change", event, this._uiHash(this));
+
+ //Update the placeholder
+ this.options.placeholder.update(this.currentContainer, this.placeholder);
+
+ this.containers[innermostIndex]._trigger("over", event, this._uiHash(this));
+ this.containers[innermostIndex].containerCache.over = 1;
+ }
+
+
+ },
+
+ _createHelper: function(event) {
+
+ var o = this.options,
+ helper = $.isFunction(o.helper) ? $(o.helper.apply(this.element[0], [event, this.currentItem])) : (o.helper === "clone" ? this.currentItem.clone() : this.currentItem);
+
+ //Add the helper to the DOM if that didn't happen already
+ if(!helper.parents("body").length) {
+ $(o.appendTo !== "parent" ? o.appendTo : this.currentItem[0].parentNode)[0].appendChild(helper[0]);
+ }
+
+ if(helper[0] === this.currentItem[0]) {
+ this._storedCSS = { width: this.currentItem[0].style.width, height: this.currentItem[0].style.height, position: this.currentItem.css("position"), top: this.currentItem.css("top"), left: this.currentItem.css("left") };
+ }
+
+ if(!helper[0].style.width || o.forceHelperSize) {
+ helper.width(this.currentItem.width());
+ }
+ if(!helper[0].style.height || o.forceHelperSize) {
+ helper.height(this.currentItem.height());
+ }
+
+ return helper;
+
+ },
+
+ _adjustOffsetFromHelper: function(obj) {
+ if (typeof obj === "string") {
+ obj = obj.split(" ");
+ }
+ if ($.isArray(obj)) {
+ obj = {left: +obj[0], top: +obj[1] || 0};
+ }
+ if ("left" in obj) {
+ this.offset.click.left = obj.left + this.margins.left;
+ }
+ if ("right" in obj) {
+ this.offset.click.left = this.helperProportions.width - obj.right + this.margins.left;
+ }
+ if ("top" in obj) {
+ this.offset.click.top = obj.top + this.margins.top;
+ }
+ if ("bottom" in obj) {
+ this.offset.click.top = this.helperProportions.height - obj.bottom + this.margins.top;
+ }
+ },
+
+ _getParentOffset: function() {
+
+
+ //Get the offsetParent and cache its position
+ this.offsetParent = this.helper.offsetParent();
+ var po = this.offsetParent.offset();
+
+ // This is a special case where we need to modify a offset calculated on start, since the following happened:
+ // 1. The position of the helper is absolute, so it's position is calculated based on the next positioned parent
+ // 2. The actual offset parent is a child of the scroll parent, and the scroll parent isn't the document, which means that
+ // the scroll is included in the initial calculation of the offset of the parent, and never recalculated upon drag
+ if(this.cssPosition === "absolute" && this.scrollParent[0] !== document && $.contains(this.scrollParent[0], this.offsetParent[0])) {
+ po.left += this.scrollParent.scrollLeft();
+ po.top += this.scrollParent.scrollTop();
+ }
+
+ // This needs to be actually done for all browsers, since pageX/pageY includes this information
+ // with an ugly IE fix
+ if( this.offsetParent[0] === document.body || (this.offsetParent[0].tagName && this.offsetParent[0].tagName.toLowerCase() === "html" && $.ui.ie)) {
+ po = { top: 0, left: 0 };
+ }
+
+ return {
+ top: po.top + (parseInt(this.offsetParent.css("borderTopWidth"),10) || 0),
+ left: po.left + (parseInt(this.offsetParent.css("borderLeftWidth"),10) || 0)
+ };
+
+ },
+
+ _getRelativeOffset: function() {
+
+ if(this.cssPosition === "relative") {
+ var p = this.currentItem.position();
+ return {
+ top: p.top - (parseInt(this.helper.css("top"),10) || 0) + this.scrollParent.scrollTop(),
+ left: p.left - (parseInt(this.helper.css("left"),10) || 0) + this.scrollParent.scrollLeft()
+ };
+ } else {
+ return { top: 0, left: 0 };
+ }
+
+ },
+
+ _cacheMargins: function() {
+ this.margins = {
+ left: (parseInt(this.currentItem.css("marginLeft"),10) || 0),
+ top: (parseInt(this.currentItem.css("marginTop"),10) || 0)
+ };
+ },
+
+ _cacheHelperProportions: function() {
+ this.helperProportions = {
+ width: this.helper.outerWidth(),
+ height: this.helper.outerHeight()
+ };
+ },
+
+ _setContainment: function() {
+
+ var ce, co, over,
+ o = this.options;
+ if(o.containment === "parent") {
+ o.containment = this.helper[0].parentNode;
+ }
+ if(o.containment === "document" || o.containment === "window") {
+ this.containment = [
+ 0 - this.offset.relative.left - this.offset.parent.left,
+ 0 - this.offset.relative.top - this.offset.parent.top,
+ $(o.containment === "document" ? document : window).width() - this.helperProportions.width - this.margins.left,
+ ($(o.containment === "document" ? document : window).height() || document.body.parentNode.scrollHeight) - this.helperProportions.height - this.margins.top
+ ];
+ }
+
+ if(!(/^(document|window|parent)$/).test(o.containment)) {
+ ce = $(o.containment)[0];
+ co = $(o.containment).offset();
+ over = ($(ce).css("overflow") !== "hidden");
+
+ this.containment = [
+ co.left + (parseInt($(ce).css("borderLeftWidth"),10) || 0) + (parseInt($(ce).css("paddingLeft"),10) || 0) - this.margins.left,
+ co.top + (parseInt($(ce).css("borderTopWidth"),10) || 0) + (parseInt($(ce).css("paddingTop"),10) || 0) - this.margins.top,
+ co.left+(over ? Math.max(ce.scrollWidth,ce.offsetWidth) : ce.offsetWidth) - (parseInt($(ce).css("borderLeftWidth"),10) || 0) - (parseInt($(ce).css("paddingRight"),10) || 0) - this.helperProportions.width - this.margins.left,
+ co.top+(over ? Math.max(ce.scrollHeight,ce.offsetHeight) : ce.offsetHeight) - (parseInt($(ce).css("borderTopWidth"),10) || 0) - (parseInt($(ce).css("paddingBottom"),10) || 0) - this.helperProportions.height - this.margins.top
+ ];
+ }
+
+ },
+
+ _convertPositionTo: function(d, pos) {
+
+ if(!pos) {
+ pos = this.position;
+ }
+ var mod = d === "absolute" ? 1 : -1,
+ scroll = this.cssPosition === "absolute" && !(this.scrollParent[0] !== document && $.contains(this.scrollParent[0], this.offsetParent[0])) ? this.offsetParent : this.scrollParent,
+ scrollIsRootNode = (/(html|body)/i).test(scroll[0].tagName);
+
+ return {
+ top: (
+ pos.top + // The absolute mouse position
+ this.offset.relative.top * mod + // Only for relative positioned nodes: Relative offset from element to offset parent
+ this.offset.parent.top * mod - // The offsetParent's offset without borders (offset + border)
+ ( ( this.cssPosition === "fixed" ? -this.scrollParent.scrollTop() : ( scrollIsRootNode ? 0 : scroll.scrollTop() ) ) * mod)
+ ),
+ left: (
+ pos.left + // The absolute mouse position
+ this.offset.relative.left * mod + // Only for relative positioned nodes: Relative offset from element to offset parent
+ this.offset.parent.left * mod - // The offsetParent's offset without borders (offset + border)
+ ( ( this.cssPosition === "fixed" ? -this.scrollParent.scrollLeft() : scrollIsRootNode ? 0 : scroll.scrollLeft() ) * mod)
+ )
+ };
+
+ },
+
+ _generatePosition: function(event) {
+
+ var top, left,
+ o = this.options,
+ pageX = event.pageX,
+ pageY = event.pageY,
+ scroll = this.cssPosition === "absolute" && !(this.scrollParent[0] !== document && $.contains(this.scrollParent[0], this.offsetParent[0])) ? this.offsetParent : this.scrollParent, scrollIsRootNode = (/(html|body)/i).test(scroll[0].tagName);
+
+ // This is another very weird special case that only happens for relative elements:
+ // 1. If the css position is relative
+ // 2. and the scroll parent is the document or similar to the offset parent
+ // we have to refresh the relative offset during the scroll so there are no jumps
+ if(this.cssPosition === "relative" && !(this.scrollParent[0] !== document && this.scrollParent[0] !== this.offsetParent[0])) {
+ this.offset.relative = this._getRelativeOffset();
+ }
+
+ /*
+ * - Position constraining -
+ * Constrain the position to a mix of grid, containment.
+ */
+
+ if(this.originalPosition) { //If we are not dragging yet, we won't check for options
+
+ if(this.containment) {
+ if(event.pageX - this.offset.click.left < this.containment[0]) {
+ pageX = this.containment[0] + this.offset.click.left;
+ }
+ if(event.pageY - this.offset.click.top < this.containment[1]) {
+ pageY = this.containment[1] + this.offset.click.top;
+ }
+ if(event.pageX - this.offset.click.left > this.containment[2]) {
+ pageX = this.containment[2] + this.offset.click.left;
+ }
+ if(event.pageY - this.offset.click.top > this.containment[3]) {
+ pageY = this.containment[3] + this.offset.click.top;
+ }
+ }
+
+ if(o.grid) {
+ top = this.originalPageY + Math.round((pageY - this.originalPageY) / o.grid[1]) * o.grid[1];
+ pageY = this.containment ? ( (top - this.offset.click.top >= this.containment[1] && top - this.offset.click.top <= this.containment[3]) ? top : ((top - this.offset.click.top >= this.containment[1]) ? top - o.grid[1] : top + o.grid[1])) : top;
+
+ left = this.originalPageX + Math.round((pageX - this.originalPageX) / o.grid[0]) * o.grid[0];
+ pageX = this.containment ? ( (left - this.offset.click.left >= this.containment[0] && left - this.offset.click.left <= this.containment[2]) ? left : ((left - this.offset.click.left >= this.containment[0]) ? left - o.grid[0] : left + o.grid[0])) : left;
+ }
+
+ }
+
+ return {
+ top: (
+ pageY - // The absolute mouse position
+ this.offset.click.top - // Click offset (relative to the element)
+ this.offset.relative.top - // Only for relative positioned nodes: Relative offset from element to offset parent
+ this.offset.parent.top + // The offsetParent's offset without borders (offset + border)
+ ( ( this.cssPosition === "fixed" ? -this.scrollParent.scrollTop() : ( scrollIsRootNode ? 0 : scroll.scrollTop() ) ))
+ ),
+ left: (
+ pageX - // The absolute mouse position
+ this.offset.click.left - // Click offset (relative to the element)
+ this.offset.relative.left - // Only for relative positioned nodes: Relative offset from element to offset parent
+ this.offset.parent.left + // The offsetParent's offset without borders (offset + border)
+ ( ( this.cssPosition === "fixed" ? -this.scrollParent.scrollLeft() : scrollIsRootNode ? 0 : scroll.scrollLeft() ))
+ )
+ };
+
+ },
+
+ _rearrange: function(event, i, a, hardRefresh) {
+
+ a ? a[0].appendChild(this.placeholder[0]) : i.item[0].parentNode.insertBefore(this.placeholder[0], (this.direction === "down" ? i.item[0] : i.item[0].nextSibling));
+
+ //Various things done here to improve the performance:
+ // 1. we create a setTimeout, that calls refreshPositions
+ // 2. on the instance, we have a counter variable, that get's higher after every append
+ // 3. on the local scope, we copy the counter variable, and check in the timeout, if it's still the same
+ // 4. this lets only the last addition to the timeout stack through
+ this.counter = this.counter ? ++this.counter : 1;
+ var counter = this.counter;
+
+ this._delay(function() {
+ if(counter === this.counter) {
+ this.refreshPositions(!hardRefresh); //Precompute after each DOM insertion, NOT on mousemove
+ }
+ });
+
+ },
+
+ _clear: function(event, noPropagation) {
+
+ this.reverting = false;
+ // We delay all events that have to be triggered to after the point where the placeholder has been removed and
+ // everything else normalized again
+ var i,
+ delayedTriggers = [];
+
+ // We first have to update the dom position of the actual currentItem
+ // Note: don't do it if the current item is already removed (by a user), or it gets reappended (see #4088)
+ if(!this._noFinalSort && this.currentItem.parent().length) {
+ this.placeholder.before(this.currentItem);
+ }
+ this._noFinalSort = null;
+
+ if(this.helper[0] === this.currentItem[0]) {
+ for(i in this._storedCSS) {
+ if(this._storedCSS[i] === "auto" || this._storedCSS[i] === "static") {
+ this._storedCSS[i] = "";
+ }
+ }
+ this.currentItem.css(this._storedCSS).removeClass("ui-sortable-helper");
+ } else {
+ this.currentItem.show();
+ }
+
+ if(this.fromOutside && !noPropagation) {
+ delayedTriggers.push(function(event) { this._trigger("receive", event, this._uiHash(this.fromOutside)); });
+ }
+ if((this.fromOutside || this.domPosition.prev !== this.currentItem.prev().not(".ui-sortable-helper")[0] || this.domPosition.parent !== this.currentItem.parent()[0]) && !noPropagation) {
+ delayedTriggers.push(function(event) { this._trigger("update", event, this._uiHash()); }); //Trigger update callback if the DOM position has changed
+ }
+
+ // Check if the items Container has Changed and trigger appropriate
+ // events.
+ if (this !== this.currentContainer) {
+ if(!noPropagation) {
+ delayedTriggers.push(function(event) { this._trigger("remove", event, this._uiHash()); });
+ delayedTriggers.push((function(c) { return function(event) { c._trigger("receive", event, this._uiHash(this)); }; }).call(this, this.currentContainer));
+ delayedTriggers.push((function(c) { return function(event) { c._trigger("update", event, this._uiHash(this)); }; }).call(this, this.currentContainer));
+ }
+ }
+
+
+ //Post events to containers
+ for (i = this.containers.length - 1; i >= 0; i--){
+ if(!noPropagation) {
+ delayedTriggers.push((function(c) { return function(event) { c._trigger("deactivate", event, this._uiHash(this)); }; }).call(this, this.containers[i]));
+ }
+ if(this.containers[i].containerCache.over) {
+ delayedTriggers.push((function(c) { return function(event) { c._trigger("out", event, this._uiHash(this)); }; }).call(this, this.containers[i]));
+ this.containers[i].containerCache.over = 0;
+ }
+ }
+
+ //Do what was originally in plugins
+ if(this._storedCursor) {
+ $("body").css("cursor", this._storedCursor);
+ }
+ if(this._storedOpacity) {
+ this.helper.css("opacity", this._storedOpacity);
+ }
+ if(this._storedZIndex) {
+ this.helper.css("zIndex", this._storedZIndex === "auto" ? "" : this._storedZIndex);
+ }
+
+ this.dragging = false;
+ if(this.cancelHelperRemoval) {
+ if(!noPropagation) {
+ this._trigger("beforeStop", event, this._uiHash());
+ for (i=0; i < delayedTriggers.length; i++) {
+ delayedTriggers[i].call(this, event);
+ } //Trigger all delayed events
+ this._trigger("stop", event, this._uiHash());
+ }
+
+ this.fromOutside = false;
+ return false;
+ }
+
+ if(!noPropagation) {
+ this._trigger("beforeStop", event, this._uiHash());
+ }
+
+ //$(this.placeholder[0]).remove(); would have been the jQuery way - unfortunately, it unbinds ALL events from the original node!
+ this.placeholder[0].parentNode.removeChild(this.placeholder[0]);
+
+ if(this.helper[0] !== this.currentItem[0]) {
+ this.helper.remove();
+ }
+ this.helper = null;
+
+ if(!noPropagation) {
+ for (i=0; i < delayedTriggers.length; i++) {
+ delayedTriggers[i].call(this, event);
+ } //Trigger all delayed events
+ this._trigger("stop", event, this._uiHash());
+ }
+
+ this.fromOutside = false;
+ return true;
+
+ },
+
+ _trigger: function() {
+ if ($.Widget.prototype._trigger.apply(this, arguments) === false) {
+ this.cancel();
+ }
+ },
+
+ _uiHash: function(_inst) {
+ var inst = _inst || this;
+ return {
+ helper: inst.helper,
+ placeholder: inst.placeholder || $([]),
+ position: inst.position,
+ originalPosition: inst.originalPosition,
+ offset: inst.positionAbs,
+ item: inst.currentItem,
+ sender: _inst ? _inst.element : null
+ };
+ }
+
+});
+
+})(jQuery);
commit c81c7f422ed1be51edef0dfde1c2ef282e985f60
Author: craig <craig at bestpractical.com>
Date: Wed Oct 18 14:39:49 2017 -0400
Add support for Drag and Drop UI to Admin->Global and Admin->Users
diff --git a/share/html/Admin/Global/MyRT.html b/share/html/Admin/Global/MyRT.html
index 2f3374a50..dbd9cca6b 100644
--- a/share/html/Admin/Global/MyRT.html
+++ b/share/html/Admin/Global/MyRT.html
@@ -45,66 +45,124 @@
%# those contributions and any derivatives thereof.
%#
%# END BPS TAGGED BLOCK }}}
-<& /Admin/Elements/Header, Title => loc("RT at a glance") &>
+<& /Elements/Header, Title => $title &>
<& /Elements/Tabs &>
+<& /Elements/ListActions, actions => \@results &>
-<& /Elements/ListActions, actions => \@actions &>
-<br />
-% for my $pane (@panes) {
-<&|/Widgets/TitleBox, title => loc('RT at a glance').': '.loc($pane->{Name}), bodyclass => "" &>
-<& /Widgets/SelectionBox:show, self => $pane &></&>
-<br />
-% }
-<%init>
-my @actions;
-
-my @items = map { [ "component-$_", loc($_) ] } sort @{ RT->Config->Get('HomepageComponents') };
-my $sys = RT::System->new( $session{'CurrentUser'} );
-# XXX: put this in savedsearches_to_portlet_items
-for ( $m->comp( "/Search/Elements/SearchesForObject",
- Object => $sys )) {
- my ( $desc, $loc_desc, $search ) = @$_;
- my $SearchType = $search->Content->{'SearchType'} || 'Ticket';
- if ( $SearchType eq 'Ticket' ) {
- push @items, [ "system-$desc", $loc_desc ];
- } else {
- my $oid = ref($sys) . '-' . $sys->Id . '-SavedSearch-' . $search->Id;
- my $type = ( $SearchType eq 'Ticket' )
- ? 'Saved Search' # loc
- : $SearchType;
- push @items, [ "saved-$oid", loc($type) . ": $loc_desc" ];
+<form method="post" action="<% RT->Config->Get('WebPath') %>/Helpers/UpdateDashboard" data-dashboard_id='MyRT' data-is_global=True>
+ <& /Widgets/SearchSelection,
+ pane_name => \%pane_name,
+ sections => \@sections,
+ selected => \%selected,
+ filters => \@filters,
+ &>
+ <input type="submit" class="button" name="UpdateSearches" value="<% loc('Save') %>" />
+</form>
+
+<%INIT>
+my @results;
+my $title = loc("Customize").' '.loc("Global RT at a glance");
+my $user = $session{'CurrentUser'}->UserObj;
+
+my $portlets;
+my ($defaults) = RT::System->new($session{'CurrentUser'})->Attributes->Named('HomepageSettings');
+$portlets = $defaults ? $defaults->Content : {};
+
+my @sections;
+my %item_for;
+
+my @components = map { type => "component", name => $_, label => loc($_) }, @{RT->Config->Get('HomepageComponents')};
+
+$item_for{ $_->{type} }{ $_->{name} } = $_ for @components;
+
+push @sections, {
+ id => 'components',
+ label => loc("Components"),
+ items => \@components,
+};
+
+my $sys = RT::System->new($session{'CurrentUser'});
+my @objs = ($sys);
+
+push @objs, RT::SavedSearch->new( $session{CurrentUser} )->ObjectsForLoading
+ if $session{'CurrentUser'}->HasRight( Right => 'LoadSavedSearch',
+ Object => $RT::System );
+
+for my $object (@objs) {
+ my @items;
+ my $object_id = ref($object) . '-' . $object->Id;
+ $object_id = 'system' if $object eq $sys;
+
+ for ($m->comp("/Search/Elements/SearchesForObject", Object => $object)) {
+ my ($desc, $loc_desc, $search) = @$_;
+
+ my $SearchType = 'Ticket';
+ if ((ref($search->Content)||'') eq 'HASH') {
+ $SearchType = $search->Content->{'SearchType'}
+ if $search->Content->{'SearchType'};
+ }
+ else {
+ $RT::Logger->debug("Search ".$search->id." ($desc) appears to have no Content");
+ }
+
+ my $item;
+ if ($object eq $sys && $SearchType eq 'Ticket') {
+ $item = { type => 'system', name => $desc, label => $loc_desc };
+ }
+ else {
+ my $oid = $object_id.'-SavedSearch-'.$search->Id;
+ $item = { type => 'saved', name => $oid, search_type => $SearchType, label => $loc_desc };
+ }
+
+ $item_for{ $item->{type} }{ $item->{name} } = $item;
+ push @items, $item;
}
+
+ my $label = $object eq $sys ? loc('System')
+ : $object->isa('RT::Group') ? $object->Label
+ : $object->Name;
+
+ push @sections, {
+ id => $object_id,
+ label => $label,
+ items => [ sort { lc($a->{label}) cmp lc($b->{label}) } @items ],
+ };
}
-my ($default_portlets) = $sys->Attributes->Named('HomepageSettings');
-
-my $has_right = $session{'CurrentUser'}->HasRight( Object=> $RT::System, Right => 'SuperUser');
-
-my @panes = $m->comp(
- '/Admin/Elements/ConfigureMyRT',
- panes => [
- 'body', #loc
- 'sidebar', #loc
- ],
- Action => 'MyRT.html',
- items => \@items,
- ReadOnly => !$has_right,
- current_portlets => $default_portlets->Content,
- OnSave => sub {
- my ( $conf, $pane ) = @_;
- if (!$has_right) {
- push @actions, loc( 'Permission Denied' );
+my %selected;
+for my $pane (keys %$portlets) {
+ my @items;
+
+ for my $saved (@{ $portlets->{$pane} }) {
+ my $item = $item_for{ $saved->{type} }{ $saved->{name} };
+ if ($item) {
+ push @items, $item;
}
else {
- $default_portlets->SetContent( $conf );
- push @actions, loc( 'Global portlet [_1] saved.', $pane );
+ push @results, loc('Unable to find [_1] [_2]', $saved->{type}, $saved->{name});
}
}
-);
-$m->comp( '/Widgets/SelectionBox:process', %ARGS, self => $_ )
- for @panes;
+ $selected{$pane} = \@items;
+}
+my %pane_name = (
+ 'body' => loc('Body'),
+ 'sidebar' => loc('Sidebar'),
+);
-</%init>
+my @filters = (
+ [ 'component' => loc('Components') ],
+ [ 'ticket' => loc('Tickets') ],
+ [ 'chart' => loc('Charts') ],
+);
+
+$m->callback(
+ CallbackName => 'Default',
+ pane_name => \%pane_name,
+ sections => \@sections,
+ selected => \%selected,
+ filters => \@filters,
+);
+</%INIT>
diff --git a/share/html/Admin/Users/MyRT.html b/share/html/Admin/Users/MyRT.html
index 0bd24738e..5eac261f8 100644
--- a/share/html/Admin/Users/MyRT.html
+++ b/share/html/Admin/Users/MyRT.html
@@ -47,81 +47,123 @@
%# END BPS TAGGED BLOCK }}}
<& /Admin/Elements/Header, Title => $title &>
<& /Elements/Tabs &>
-
<& /Elements/ListActions, actions => \@actions &>
-<form method="post" action="MyRT.html">
-<input type="hidden" name="id" value="<% $id %>" />
-<input type="hidden" name="Reset" value="1" />
-<input type="submit" class="button" value="<%loc('Reset to default')%>">
+<form method="post" action="<% RT->Config->Get('WebPath') %> /Helpers/UpdateDashboard" data-dashboard_id = "MyRT" data-user_id = '<% $id %>'>
+ <& /Widgets/SearchSelection,
+ pane_name => \%pane_name,
+ sections => \@sections,
+ selected => \%selected,
+ filters => \@filters,
+ &>
+ <input type="hidden" name="id" value="<% $id %>"/>
+ <input type"submit" class="button" name="UpdateSearches" value= "<% loc('Save') %>" >
</form>
-<br />
-
-% for my $pane (@panes) {
-<&|/Widgets/TitleBox, title => loc('RT at a glance').': '.loc($pane->{Name}), bodyclass => "" &>
-<& /Widgets/SelectionBox:show, self => $pane &></&>
-<br />
-% }
-
<%init>
my @actions;
my $UserObj = RT::User->new($session{'CurrentUser'});
$UserObj->Load($id) || Abort("Couldn't load user '" . ($id || '') . "'");
+my $user = RT::User->new($session{'CurrentUser'});
my $title = loc("RT at a glance for the user [_1]", $UserObj->Name);
-if ($ARGS{Reset}) {
- my ($ok, $msg) = $UserObj->SetPreferences('HomepageSettings', {});
- push @actions, $ok ? loc('Preferences saved for user [_1].', $UserObj->Name) : $msg;
+my $portlets = $UserObj->Preferences('HomepageSettings');
+unless ($portlets) {
+ my ($defaults) = RT::System->new($session{'CurrentUser'})->Attributes->Named('HomepageSettings');
+ $portlets = $defaults ? $defaults->Content : {};
}
-my ($default_portlets) = RT::System->new($session{'CurrentUser'})->Attributes->Named('HomepageSettings');
-my $portlets = $UserObj->Preferences('HomepageSettings', $default_portlets ? $default_portlets->Content : {});
-
-my %allowed_components = map {$_ => 1} @{ RT->Config->Get('HomepageComponents') };
+my @sections;
+my %item_for;
-my @items;
-push @items, map {["component-$_", loc($_)]} sort keys %allowed_components;
+my @components = map { type => "component", name => $_, label => loc($_) }, @{RT->Config->Get('HomepageComponents')};
+$item_for{ $_->{type} }{ $_->{name} } = $_ for @components;
-my $sys = RT::System->new( RT::CurrentUser->new($UserObj) );
+push @sections, {
+ id => 'components',
+ label => loc("Components"),
+ items => \@components,
+ };
+my $sys = RT::System->new($session{'CurrentUser'});
+$sys->Load($id) || Abort("Couldn't load user '" . ($id || '') . "'");
my @objs = ($sys);
+push @objs, RT::SavedSearch->new ($session{'CurrentUser'})->ObjectsForLoading
+ if $session{'CurrentUser'}->HasRight( Right => 'LoadSavedSearch',
+ Object => $RT::System );
+for my $object (@objs) {
+ my @items;
+ my $object_id = ref($object) . '-' . $object->Id;
+ $object_id = 'system' if $object eq $sys;
-push @objs, RT::SavedSearch->new( RT::CurrentUser->new( $UserObj ) )->ObjectsForLoading;
+ for ($m->comp("/Search/Elements/SearchesForObject", Object => $object)) {
+ my ($desc, $loc_desc, $search) = @$_;
-for my $object (@objs) {
- for ($m->comp("/Search/Elements/SearchesForObject", Object => $object)) {
- my ($desc, $loc_desc, $search) = @$_;
- my $SearchType = $search->Content->{'SearchType'} || 'Ticket';
- if ($object eq $sys && $SearchType eq 'Ticket') {
- push @items, ["system-$desc", $loc_desc];
+ my $SearchType = 'Ticket';
+ if ((ref($search->Content)||'') eq 'HASH') {
+ $SearchType = $search->Content->{'SearchType'}
+ if $search->Content->{'SearchType'};
+ }
+ else {
+ $RT::Logger->debug("Search ".$search->id." ($desc) appears to have no Content");
+ }
+
+ my $item;
+ if ($object eq $sys && $SearchType eq 'Ticket') {
+ $item = { type => 'system', name => $desc, label => $loc_desc };
+ }
+ else {
+ my $oid = $object_id.'-SavedSearch-'.$search->Id;
+ $item = { type => 'saved', name => $oid, search_type => $SearchType, label => $loc_desc };
+ }
+ $item_for{ $item->{type} }{ $item->{name} } = $item;
+ push @items, $item;
+ }
+
+ my $label = $object eq $sys ? loc('System')
+ : $object->isa('RT::Group') ? $object->Label
+ : $object->Name;
+ push @sections, {
+ id => $object_id,
+ label => $label,
+ items => [ sort { lc($a->{label}) cmp lc($b->{label}) } @items ],
+ };
+ }
+
+my %selected;
+for my $pane (keys %$portlets) {
+ my @items;
+
+ for my $saved (@{ $portlets->{$pane} }) {
+ my $item = $item_for{ $saved->{type} }{ $saved->{name} };
+ if ($item) {
+ push @items, $item;
}
else {
- my $oid = ref($object).'-'.$object->Id.'-SavedSearch-'.$search->Id;
- my $type = ($SearchType eq 'Ticket')
- ? 'Saved Search' # loc
- : $SearchType;
- push @items, ["saved-$oid", loc($type).": $loc_desc"];
+ push @actions, loc('Unable to find [_1] [_2]', $saved->{type}, $saved->{name});
}
}
-}
-
-my @panes = $m->comp(
- '/Admin/Elements/ConfigureMyRT',
- panes => ['body', 'sidebar'],
- Action => "MyRT.html?id=$id",
- items => \@items,
- current_portlets => $portlets,
- OnSave => sub {
- my ( $conf, $pane ) = @_;
- my ($ok, $msg) = $UserObj->SetPreferences( 'HomepageSettings', $conf );
- push @actions, $ok ? loc('Preferences [_1] for user [_2].', $pane, $UserObj->Name) : $msg;
+ $selected{$pane} = \@items;
}
+
+my %pane_name = (
+ 'body' => loc('Body'),
+ 'sidebar' => loc('Sidebar'),
);
-$m->comp( '/Widgets/SelectionBox:process', %ARGS, self => $_ )
- for @panes;
+my @filters = (
+ [ 'component' => loc('Components') ],
+ [ 'ticket' => loc('Tickets') ],
+ [ 'chart' => loc('Charts') ],
+);
+$m->callback(
+ CallbackName => 'Default',
+ pane_name => \%pane_name,
+ sections => \@sections,
+ selected => \%selected,
+ filters => \@filters,
+);
-</%init>
+</%INIT>
<%ARGS>
-$id => undef
+ $id => undef
</%ARGS>
diff --git a/share/html/Helpers/UpdateDashboard b/share/html/Helpers/UpdateDashboard
index 3ede557b6..fb686ac61 100644
--- a/share/html/Helpers/UpdateDashboard
+++ b/share/html/Helpers/UpdateDashboard
@@ -51,11 +51,24 @@ $content
<%INIT>
my $args = JSON::from_json($content);
my $id = $args->{dashboard_id};
+my $user_id = $args->{user_id};
+my $is_global = $args->{is_global};
my ($ok, $msg);
if ($id eq 'MyRT') {
- my $user = $session{CurrentUser}->UserObj;
- ($ok, $msg) = $user->SetPreferences('HomepageSettings', $args->{panes});
+ my $user = $session{CurrentUser};
+ if($user_id){
+ $user->Load($user_id);
+ ($ok, $msg) = $user->SetPreferences('HomepageSettings', $args->{panes});
+ }
+ elsif($is_global){
+ my $sys = RT::System->new($session{'CurrentUser'});
+ my ($default_portlets) = $sys->Attributes->Named('HomepageSettings');
+ ($ok, $msg) = $default_portlets->SetContent( $args->{panes} );
+ }
+ else{
+ ($ok, $msg) = $user->SetPreferences('HomepageSettings', $args->{panes});
+ }
}
else {
use RT::Dashboard;
commit daf15f7e5177c9f321b89715749ce8f1cd6d803b
Author: Jim Brandt <jbrandt at bestpractical.com>
Date: Thu Jan 11 16:05:46 2018 -0500
Alpha sort items in the select from list
diff --git a/share/html/Widgets/SearchSelection b/share/html/Widgets/SearchSelection
index f3bcef543..39f898038 100644
--- a/share/html/Widgets/SearchSelection
+++ b/share/html/Widgets/SearchSelection
@@ -82,7 +82,7 @@
<div class="section">
<h3><% $label | n %></h3>
<ul>
-% for my $item (@$items) {
+% for my $item (sort {$a->{'label'} cmp $b->{'label'}} @$items) {
<& /Elements/ShowSelectSearch, %$item &>
% }
</ul>
-----------------------------------------------------------------------
More information about the rt-commit
mailing list