[Rt-commit] rt branch, 4.6/search-selection-next, created. rt-4.4.4-774-g3b4a80fbb

Blaine Motsinger blaine at bestpractical.com
Tue Mar 10 19:54:14 EDT 2020


The branch, 4.6/search-selection-next has been created
        at  3b4a80fbb35a7019a74ff171d0c5809e47589f70 (commit)

- Log -----------------------------------------------------------------
commit 2aab9ecde32e219b8544df47f9f8fd0877ba4304
Author: Blaine Motsinger <blaine at bestpractical.com>
Date:   Wed Jan 22 11:12:07 2020 -0600

    Add RT-Extension-DashboardSelectionUI to core

diff --git a/lib/RT/Interface/Web.pm b/lib/RT/Interface/Web.pm
index ea5666c0c..b61f48824 100644
--- a/lib/RT/Interface/Web.pm
+++ b/lib/RT/Interface/Web.pm
@@ -115,6 +115,7 @@ sub JSFiles {
         jquery-ui.min.js
         jquery-ui-timepicker-addon.js
         jquery-ui-patch-datepicker.js
+        jquery.ui.sortable.js
         jquery.cookie.js
         popper.min.js
         bootstrap.min.js
diff --git a/share/html/Admin/Global/MyRT.html b/share/html/Admin/Global/MyRT.html
index f582af6e5..2ec3a597c 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 99d069cec..706fd3e76 100644
--- a/share/html/Admin/Users/MyRT.html
+++ b/share/html/Admin/Users/MyRT.html
@@ -47,12 +47,17 @@
 %# 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" />
+<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') %>"  >
 <div class="form-row">
   <div class="col-md-12">
     <input type="submit" class="button form-control btn btn-primary" value="<%loc('Reset to default')%>">
@@ -60,72 +65,110 @@
 </div>
 </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/Dashboards/Elements/DashboardsForObject b/share/html/Dashboards/Elements/DashboardsForObject
index ddc5649ed..a4e855a80 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 785ea588d..dfbb8522c 100644
--- a/share/html/Dashboards/Queries.html
+++ b/share/html/Dashboards/Queries.html
@@ -47,28 +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 &>
-
-% for my $pane (@panes) {
-<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>
-% }
-
 <%INIT>
-
 my @results;
 
 use RT::Dashboard;
@@ -80,197 +70,148 @@ unless ($ok) {
 }
 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 65%
copy from share/html/Dashboards/Elements/DashboardsForObject
copy to share/html/Elements/ShowSelectSearch
index ddc5649ed..6b2ea4da9 100644
--- a/share/html/Dashboards/Elements/DashboardsForObject
+++ b/share/html/Elements/ShowSelectSearch
@@ -2,7 +2,7 @@
 %#
 %# COPYRIGHT:
 %#
-%# This software is Copyright (c) 1996-2019 Best Practical Solutions, LLC
+%# This software is Copyright (c) 1996-2016 Best Practical Solutions, LLC
 %#                                          <sales at bestpractical.com>
 %#
 %# (Except where explicitly superseded by other copyright notices)
@@ -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..fb686ac61
--- /dev/null
+++ b/share/html/Helpers/UpdateDashboard
@@ -0,0 +1,137 @@
+%# 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 $user_id = $args->{user_id};
+my $is_global = $args->{is_global};
+
+my ($ok, $msg);
+if ($id eq 'MyRT') {
+        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;
+    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 1bc841f80..15c6bccb4 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">
@@ -116,8 +114,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);
@@ -126,8 +134,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) = @$_;
 
@@ -140,34 +151,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 55%
copy from share/html/Dashboards/Elements/DashboardsForObject
copy to share/html/Widgets/SearchSelection
index ddc5649ed..7416e4a4b 100644
--- a/share/html/Dashboards/Elements/DashboardsForObject
+++ b/share/html/Widgets/SearchSelection
@@ -2,7 +2,7 @@
 %#
 %# COPYRIGHT:
 %#
-%# This software is Copyright (c) 1996-2019 Best Practical Solutions, LLC
+%# This software is Copyright (c) 1996-2016 Best Practical Solutions, LLC
 %#                                          <sales at bestpractical.com>
 %#
 %# (Except where explicitly superseded by other copyright notices)
@@ -45,41 +45,67 @@
 %# 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 (sort {$a->{'label'} cmp $b->{'label'}} @$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>
+use utf8;
+$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/elevator-light/forms.css b/share/static/css/elevator-light/forms.css
index e422e40ba..83516af15 100644
--- a/share/static/css/elevator-light/forms.css
+++ b/share/static/css/elevator-light/forms.css
@@ -393,3 +393,125 @@ textarea.code {
 #EditConfig ul.plugins {
     margin: 0;
 }
+
+/* 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..a8d5392b8 100644
--- a/share/static/js/forms.js
+++ b/share/static/js/forms.js
@@ -15,4 +15,173 @@ 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',
+            placeholder: 'placeholder',
+            forcePlaceholderSize: true,
+            cancel: '.remove',
+
+            // drag a clone of the source item
+            receive: function (e, ui) {
+                draggedIntoDestination = true;
+                copyHelper = null;
+            },
+           over: function () {
+               removeIntent = false;
+           },
+           out: function () {
+               removeIntent = true;
+           },
+           beforeStop: function (event, ui) {
+               if(removeIntent == true){
+                   ui.item.remove();
+               }
+           },
+        }).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 6deca7f572ad3cb135a4c7e184b86ed8c5721f0a
Author: Blaine Motsinger <blaine at bestpractical.com>
Date:   Fri Jan 24 15:18:40 2020 -0600

    Upgrade jQuery ui sortable and add disableSelection
    
    Our jQuery UI build doesn't contain disableSelection which is
    used within the DashboardSelectionUI extension code.  Rather than
    upgrade our core jQuery UI build, this commit adds a minified
    disableSelection to be loaded through Web.pm.
    
    jQuery UI sortable also needed additional includes, namely
    data.js, scroll-parent.js, widgets/sortable.js, and
    widgets/mouse.js.  Those were added within the sortable js file
    and minified to save disk space.

diff --git a/lib/RT/Interface/Web.pm b/lib/RT/Interface/Web.pm
index b61f48824..2940813d9 100644
--- a/lib/RT/Interface/Web.pm
+++ b/lib/RT/Interface/Web.pm
@@ -115,7 +115,8 @@ sub JSFiles {
         jquery-ui.min.js
         jquery-ui-timepicker-addon.js
         jquery-ui-patch-datepicker.js
-        jquery.ui.sortable.js
+        jquery-ui-sortable.min.js
+        jquery-ui-disableSelection.min.js
         jquery.cookie.js
         popper.min.js
         bootstrap.min.js
diff --git a/share/static/js/jquery-ui-disableSelection.min.js b/share/static/js/jquery-ui-disableSelection.min.js
new file mode 100644
index 000000000..1231680f6
--- /dev/null
+++ b/share/static/js/jquery-ui-disableSelection.min.js
@@ -0,0 +1,6 @@
+/*! jQuery UI - v1.12.1 - 2020-01-24
+* http://jqueryui.com
+* Includes: disable-selection.js
+* Copyright jQuery Foundation and other contributors; Licensed MIT */
+
+(function(t){"function"==typeof define&&define.amd?define(["jquery"],t):t(jQuery)})(function(t){t.ui=t.ui||{},t.ui.version="1.12.1",t.fn.extend({disableSelection:function(){var t="onselectstart"in document.createElement("div")?"selectstart":"mousedown";return function(){return this.on(t+".ui-disableSelection",function(t){t.preventDefault()})}}(),enableSelection:function(){return this.off(".ui-disableSelection")}})});
\ No newline at end of file
diff --git a/share/static/js/jquery-ui-sortable.min.js b/share/static/js/jquery-ui-sortable.min.js
new file mode 100644
index 000000000..fe3c2cfc4
--- /dev/null
+++ b/share/static/js/jquery-ui-sortable.min.js
@@ -0,0 +1,7 @@
+/*! jQuery UI - v1.12.1 - 2020-01-24
+* http://jqueryui.com
+* Includes: widget.js, data.js, scroll-parent.js, widgets/sortable.js, widgets/mouse.js
+* Copyright jQuery Foundation and other contributors; Licensed MIT */
+
+(function(t){"function"==typeof define&&define.amd?define(["jquery"],t):t(jQuery)})(function(t){t.ui=t.ui||{},t.ui.version="1.12.1";var e=0,i=Array.prototype.slice;t.cleanData=function(e){return function(i){var s,n,a;for(a=0;null!=(n=i[a]);a++)try{s=t._data(n,"events"),s&&s.remove&&t(n).triggerHandler("remove")}catch(o){}e(i)}}(t.cleanData),t.widget=function(e,i,s){var n,a,o,r={},h=e.split(".")[0];e=e.split(".")[1];var l=h+"-"+e;return s||(s=i,i=t.Widget),t.isArray(s)&&(s=t.extend.apply(null,[{}].concat(s))),t.expr[":"][l.toLowerCase()]=function(e){return!!t.data(e,l)},t[h]=t[h]||{},n=t[h][e],a=t[h][e]=function(t,e){return this._createWidget?(arguments.length&&this._createWidget(t,e),void 0):new a(t,e)},t.extend(a,n,{version:s.version,_proto:t.extend({},s),_childConstructors:[]}),o=new i,o.options=t.widget.extend({},o.options),t.each(s,function(e,s){return t.isFunction(s)?(r[e]=function(){function t(){return i.prototype[e].apply(this,arguments)}function n(t){return i.prototype[e].ap
 ply(this,t)}return function(){var e,i=this._super,a=this._superApply;return this._super=t,this._superApply=n,e=s.apply(this,arguments),this._super=i,this._superApply=a,e}}(),void 0):(r[e]=s,void 0)}),a.prototype=t.widget.extend(o,{widgetEventPrefix:n?o.widgetEventPrefix||e:e},r,{constructor:a,namespace:h,widgetName:e,widgetFullName:l}),n?(t.each(n._childConstructors,function(e,i){var s=i.prototype;t.widget(s.namespace+"."+s.widgetName,a,i._proto)}),delete n._childConstructors):i._childConstructors.push(a),t.widget.bridge(e,a),a},t.widget.extend=function(e){for(var s,n,a=i.call(arguments,1),o=0,r=a.length;r>o;o++)for(s in a[o])n=a[o][s],a[o].hasOwnProperty(s)&&void 0!==n&&(e[s]=t.isPlainObject(n)?t.isPlainObject(e[s])?t.widget.extend({},e[s],n):t.widget.extend({},n):n);return e},t.widget.bridge=function(e,s){var n=s.prototype.widgetFullName||e;t.fn[e]=function(a){var o="string"==typeof a,r=i.call(arguments,1),h=this;return o?this.length||"instance"!==a?this.each(function(){var i,s=t.
 data(this,n);return"instance"===a?(h=s,!1):s?t.isFunction(s[a])&&"_"!==a.charAt(0)?(i=s[a].apply(s,r),i!==s&&void 0!==i?(h=i&&i.jquery?h.pushStack(i.get()):i,!1):void 0):t.error("no such method '"+a+"' for "+e+" widget instance"):t.error("cannot call methods on "+e+" prior to initialization; "+"attempted to call method '"+a+"'")}):h=void 0:(r.length&&(a=t.widget.extend.apply(null,[a].concat(r))),this.each(function(){var e=t.data(this,n);e?(e.option(a||{}),e._init&&e._init()):t.data(this,n,new s(a,this))})),h}},t.Widget=function(){},t.Widget._childConstructors=[],t.Widget.prototype={widgetName:"widget",widgetEventPrefix:"",defaultElement:"<div>",options:{classes:{},disabled:!1,create:null},_createWidget:function(i,s){s=t(s||this.defaultElement||this)[0],this.element=t(s),this.uuid=e++,this.eventNamespace="."+this.widgetName+this.uuid,this.bindings=t(),this.hoverable=t(),this.focusable=t(),this.classesElementLookup={},s!==this&&(t.data(s,this.widgetFullName,this),this._on(!0,this.elem
 ent,{remove:function(t){t.target===s&&this.destroy()}}),this.document=t(s.style?s.ownerDocument:s.document||s),this.window=t(this.document[0].defaultView||this.document[0].parentWindow)),this.options=t.widget.extend({},this.options,this._getCreateOptions(),i),this._create(),this.options.disabled&&this._setOptionDisabled(this.options.disabled),this._trigger("create",null,this._getCreateEventData()),this._init()},_getCreateOptions:function(){return{}},_getCreateEventData:t.noop,_create:t.noop,_init:t.noop,destroy:function(){var e=this;this._destroy(),t.each(this.classesElementLookup,function(t,i){e._removeClass(i,t)}),this.element.off(this.eventNamespace).removeData(this.widgetFullName),this.widget().off(this.eventNamespace).removeAttr("aria-disabled"),this.bindings.off(this.eventNamespace)},_destroy:t.noop,widget:function(){return this.element},option:function(e,i){var s,n,a,o=e;if(0===arguments.length)return t.widget.extend({},this.options);if("string"==typeof e)if(o={},s=e.split(".
 "),e=s.shift(),s.length){for(n=o[e]=t.widget.extend({},this.options[e]),a=0;s.length-1>a;a++)n[s[a]]=n[s[a]]||{},n=n[s[a]];if(e=s.pop(),1===arguments.length)return void 0===n[e]?null:n[e];n[e]=i}else{if(1===arguments.length)return void 0===this.options[e]?null:this.options[e];o[e]=i}return this._setOptions(o),this},_setOptions:function(t){var e;for(e in t)this._setOption(e,t[e]);return this},_setOption:function(t,e){return"classes"===t&&this._setOptionClasses(e),this.options[t]=e,"disabled"===t&&this._setOptionDisabled(e),this},_setOptionClasses:function(e){var i,s,n;for(i in e)n=this.classesElementLookup[i],e[i]!==this.options.classes[i]&&n&&n.length&&(s=t(n.get()),this._removeClass(n,i),s.addClass(this._classes({element:s,keys:i,classes:e,add:!0})))},_setOptionDisabled:function(t){this._toggleClass(this.widget(),this.widgetFullName+"-disabled",null,!!t),t&&(this._removeClass(this.hoverable,null,"ui-state-hover"),this._removeClass(this.focusable,null,"ui-state-focus"))},enable:func
 tion(){return this._setOptions({disabled:!1})},disable:function(){return this._setOptions({disabled:!0})},_classes:function(e){function i(i,a){var o,r;for(r=0;i.length>r;r++)o=n.classesElementLookup[i[r]]||t(),o=e.add?t(t.unique(o.get().concat(e.element.get()))):t(o.not(e.element).get()),n.classesElementLookup[i[r]]=o,s.push(i[r]),a&&e.classes[i[r]]&&s.push(e.classes[i[r]])}var s=[],n=this;return e=t.extend({element:this.element,classes:this.options.classes||{}},e),this._on(e.element,{remove:"_untrackClassesElement"}),e.keys&&i(e.keys.match(/\S+/g)||[],!0),e.extra&&i(e.extra.match(/\S+/g)||[]),s.join(" ")},_untrackClassesElement:function(e){var i=this;t.each(i.classesElementLookup,function(s,n){-1!==t.inArray(e.target,n)&&(i.classesElementLookup[s]=t(n.not(e.target).get()))})},_removeClass:function(t,e,i){return this._toggleClass(t,e,i,!1)},_addClass:function(t,e,i){return this._toggleClass(t,e,i,!0)},_toggleClass:function(t,e,i,s){s="boolean"==typeof s?s:i;var n="string"==typeof t|
 |null===t,a={extra:n?e:i,keys:n?t:e,element:n?this.element:t,add:s};return a.element.toggleClass(this._classes(a),s),this},_on:function(e,i,s){var n,a=this;"boolean"!=typeof e&&(s=i,i=e,e=!1),s?(i=n=t(i),this.bindings=this.bindings.add(i)):(s=i,i=this.element,n=this.widget()),t.each(s,function(s,o){function r(){return e||a.options.disabled!==!0&&!t(this).hasClass("ui-state-disabled")?("string"==typeof o?a[o]:o).apply(a,arguments):void 0}"string"!=typeof o&&(r.guid=o.guid=o.guid||r.guid||t.guid++);var h=s.match(/^([\w:-]*)\s*(.*)$/),l=h[1]+a.eventNamespace,u=h[2];u?n.on(l,u,r):i.on(l,r)})},_off:function(e,i){i=(i||"").split(" ").join(this.eventNamespace+" ")+this.eventNamespace,e.off(i).off(i),this.bindings=t(this.bindings.not(e).get()),this.focusable=t(this.focusable.not(e).get()),this.hoverable=t(this.hoverable.not(e).get())},_delay:function(t,e){function i(){return("string"==typeof t?s[t]:t).apply(s,arguments)}var s=this;return setTimeout(i,e||0)},_hoverable:function(e){this.hover
 able=this.hoverable.add(e),this._on(e,{mouseenter:function(e){this._addClass(t(e.currentTarget),null,"ui-state-hover")},mouseleave:function(e){this._removeClass(t(e.currentTarget),null,"ui-state-hover")}})},_focusable:function(e){this.focusable=this.focusable.add(e),this._on(e,{focusin:function(e){this._addClass(t(e.currentTarget),null,"ui-state-focus")},focusout:function(e){this._removeClass(t(e.currentTarget),null,"ui-state-focus")}})},_trigger:function(e,i,s){var n,a,o=this.options[e];if(s=s||{},i=t.Event(i),i.type=(e===this.widgetEventPrefix?e:this.widgetEventPrefix+e).toLowerCase(),i.target=this.element[0],a=i.originalEvent)for(n in a)n in i||(i[n]=a[n]);return this.element.trigger(i,s),!(t.isFunction(o)&&o.apply(this.element[0],[i].concat(s))===!1||i.isDefaultPrevented())}},t.each({show:"fadeIn",hide:"fadeOut"},function(e,i){t.Widget.prototype["_"+e]=function(s,n,a){"string"==typeof n&&(n={effect:n});var o,r=n?n===!0||"number"==typeof n?i:n.effect||i:e;n=n||{},"number"==typeof
  n&&(n={duration:n}),o=!t.isEmptyObject(n),n.complete=a,n.delay&&s.delay(n.delay),o&&t.effects&&t.effects.effect[r]?s[e](n):r!==e&&s[r]?s[r](n.duration,n.easing,a):s.queue(function(i){t(this)[e](),a&&a.call(s[0]),i()})}}),t.widget,t.extend(t.expr[":"],{data:t.expr.createPseudo?t.expr.createPseudo(function(e){return function(i){return!!t.data(i,e)}}):function(e,i,s){return!!t.data(e,s[3])}}),t.fn.scrollParent=function(e){var i=this.css("position"),s="absolute"===i,n=e?/(auto|scroll|hidden)/:/(auto|scroll)/,a=this.parents().filter(function(){var e=t(this);return s&&"static"===e.css("position")?!1:n.test(e.css("overflow")+e.css("overflow-y")+e.css("overflow-x"))}).eq(0);return"fixed"!==i&&a.length?a:t(this[0].ownerDocument||document)},t.ui.ie=!!/msie [\w.]+/.exec(navigator.userAgent.toLowerCase());var s=!1;t(document).on("mouseup",function(){s=!1}),t.widget("ui.mouse",{version:"1.12.1",options:{cancel:"input, textarea, button, select, option",distance:1,delay:0},_mouseInit:function(){v
 ar e=this;this.element.on("mousedown."+this.widgetName,function(t){return e._mouseDown(t)}).on("click."+this.widgetName,function(i){return!0===t.data(i.target,e.widgetName+".preventClickEvent")?(t.removeData(i.target,e.widgetName+".preventClickEvent"),i.stopImmediatePropagation(),!1):void 0}),this.started=!1},_mouseDestroy:function(){this.element.off("."+this.widgetName),this._mouseMoveDelegate&&this.document.off("mousemove."+this.widgetName,this._mouseMoveDelegate).off("mouseup."+this.widgetName,this._mouseUpDelegate)},_mouseDown:function(e){if(!s){this._mouseMoved=!1,this._mouseStarted&&this._mouseUp(e),this._mouseDownEvent=e;var i=this,n=1===e.which,a="string"==typeof this.options.cancel&&e.target.nodeName?t(e.target).closest(this.options.cancel).length:!1;return n&&!a&&this._mouseCapture(e)?(this.mouseDelayMet=!this.options.delay,this.mouseDelayMet||(this._mouseDelayTimer=setTimeout(function(){i.mouseDelayMet=!0},this.options.delay)),this._mouseDistanceMet(e)&&this._mouseDelayMe
 t(e)&&(this._mouseStarted=this._mouseStart(e)!==!1,!this._mouseStarted)?(e.preventDefault(),!0):(!0===t.data(e.target,this.widgetName+".preventClickEvent")&&t.removeData(e.target,this.widgetName+".preventClickEvent"),this._mouseMoveDelegate=function(t){return i._mouseMove(t)},this._mouseUpDelegate=function(t){return i._mouseUp(t)},this.document.on("mousemove."+this.widgetName,this._mouseMoveDelegate).on("mouseup."+this.widgetName,this._mouseUpDelegate),e.preventDefault(),s=!0,!0)):!0}},_mouseMove:function(e){if(this._mouseMoved){if(t.ui.ie&&(!document.documentMode||9>document.documentMode)&&!e.button)return this._mouseUp(e);if(!e.which)if(e.originalEvent.altKey||e.originalEvent.ctrlKey||e.originalEvent.metaKey||e.originalEvent.shiftKey)this.ignoreMissingWhich=!0;else if(!this.ignoreMissingWhich)return this._mouseUp(e)}return(e.which||e.button)&&(this._mouseMoved=!0),this._mouseStarted?(this._mouseDrag(e),e.preventDefault()):(this._mouseDistanceMet(e)&&this._mouseDelayMet(e)&&(this._
 mouseStarted=this._mouseStart(this._mouseDownEvent,e)!==!1,this._mouseStarted?this._mouseDrag(e):this._mouseUp(e)),!this._mouseStarted)},_mouseUp:function(e){this.document.off("mousemove."+this.widgetName,this._mouseMoveDelegate).off("mouseup."+this.widgetName,this._mouseUpDelegate),this._mouseStarted&&(this._mouseStarted=!1,e.target===this._mouseDownEvent.target&&t.data(e.target,this.widgetName+".preventClickEvent",!0),this._mouseStop(e)),this._mouseDelayTimer&&(clearTimeout(this._mouseDelayTimer),delete this._mouseDelayTimer),this.ignoreMissingWhich=!1,s=!1,e.preventDefault()},_mouseDistanceMet:function(t){return Math.max(Math.abs(this._mouseDownEvent.pageX-t.pageX),Math.abs(this._mouseDownEvent.pageY-t.pageY))>=this.options.distance},_mouseDelayMet:function(){return this.mouseDelayMet},_mouseStart:function(){},_mouseDrag:function(){},_mouseStop:function(){},_mouseCapture:function(){return!0}}),t.widget("ui.sortable",t.ui.mouse,{version:"1.12.1",widgetEventPrefix:"sort",ready:!1,o
 ptions:{appendTo:"parent",axis:!1,connectWith:!1,containment:!1,cursor:"auto",cursorAt:!1,dropOnEmpty:!0,forcePlaceholderSize:!1,forceHelperSize:!1,grid:!1,handle:!1,helper:"original",items:"> *",opacity:!1,placeholder:!1,revert:!1,scroll:!0,scrollSensitivity:20,scrollSpeed:20,scope:"default",tolerance:"intersect",zIndex:1e3,activate:null,beforeStop:null,change:null,deactivate:null,out:null,over:null,receive:null,remove:null,sort:null,start:null,stop:null,update:null},_isOverAxis:function(t,e,i){return t>=e&&e+i>t},_isFloating:function(t){return/left|right/.test(t.css("float"))||/inline|table-cell/.test(t.css("display"))},_create:function(){this.containerCache={},this._addClass("ui-sortable"),this.refresh(),this.offset=this.element.offset(),this._mouseInit(),this._setHandleClassName(),this.ready=!0},_setOption:function(t,e){this._super(t,e),"handle"===t&&this._setHandleClassName()},_setHandleClassName:function(){var e=this;this._removeClass(this.element.find(".ui-sortable-handle"),"
 ui-sortable-handle"),t.each(this.items,function(){e._addClass(this.instance.options.handle?this.item.find(this.instance.options.handle):this.item,"ui-sortable-handle")})},_destroy:function(){this._mouseDestroy();for(var t=this.items.length-1;t>=0;t--)this.items[t].item.removeData(this.widgetName+"-item");return this},_mouseCapture:function(e,i){var s=null,n=!1,a=this;return this.reverting?!1:this.options.disabled||"static"===this.options.type?!1:(this._refreshItems(e),t(e.target).parents().each(function(){return t.data(this,a.widgetName+"-item")===a?(s=t(this),!1):void 0}),t.data(e.target,a.widgetName+"-item")===a&&(s=t(e.target)),s?!this.options.handle||i||(t(this.options.handle,s).find("*").addBack().each(function(){this===e.target&&(n=!0)}),n)?(this.currentItem=s,this._removeCurrentsFromItems(),!0):!1:!1)},_mouseStart:function(e,i,s){var n,a,o=this.options;if(this.currentContainer=this,this.refreshPositions(),this.helper=this._createHelper(e),this._cacheHelperProportions(),this._
 cacheMargins(),this.scrollParent=this.helper.scrollParent(),this.offset=this.currentItem.offset(),this.offset={top:this.offset.top-this.margins.top,left:this.offset.left-this.margins.left},t.extend(this.offset,{click:{left:e.pageX-this.offset.left,top:e.pageY-this.offset.top},parent:this._getParentOffset(),relative:this._getRelativeOffset()}),this.helper.css("position","absolute"),this.cssPosition=this.helper.css("position"),this.originalPosition=this._generatePosition(e),this.originalPageX=e.pageX,this.originalPageY=e.pageY,o.cursorAt&&this._adjustOffsetFromHelper(o.cursorAt),this.domPosition={prev:this.currentItem.prev()[0],parent:this.currentItem.parent()[0]},this.helper[0]!==this.currentItem[0]&&this.currentItem.hide(),this._createPlaceholder(),o.containment&&this._setContainment(),o.cursor&&"auto"!==o.cursor&&(a=this.document.find("body"),this.storedCursor=a.css("cursor"),a.css("cursor",o.cursor),this.storedStylesheet=t("<style>*{ cursor: "+o.cursor+" !important; }</style>").ap
 pendTo(a)),o.opacity&&(this.helper.css("opacity")&&(this._storedOpacity=this.helper.css("opacity")),this.helper.css("opacity",o.opacity)),o.zIndex&&(this.helper.css("zIndex")&&(this._storedZIndex=this.helper.css("zIndex")),this.helper.css("zIndex",o.zIndex)),this.scrollParent[0]!==this.document[0]&&"HTML"!==this.scrollParent[0].tagName&&(this.overflowOffset=this.scrollParent.offset()),this._trigger("start",e,this._uiHash()),this._preserveHelperProportions||this._cacheHelperProportions(),!s)for(n=this.containers.length-1;n>=0;n--)this.containers[n]._trigger("activate",e,this._uiHash(this));return t.ui.ddmanager&&(t.ui.ddmanager.current=this),t.ui.ddmanager&&!o.dropBehaviour&&t.ui.ddmanager.prepareOffsets(this,e),this.dragging=!0,this._addClass(this.helper,"ui-sortable-helper"),this._mouseDrag(e),!0},_mouseDrag:function(e){var i,s,n,a,o=this.options,r=!1;for(this.position=this._generatePosition(e),this.positionAbs=this._convertPositionTo("absolute"),this.lastPositionAbs||(this.lastPos
 itionAbs=this.positionAbs),this.options.scroll&&(this.scrollParent[0]!==this.document[0]&&"HTML"!==this.scrollParent[0].tagName?(this.overflowOffset.top+this.scrollParent[0].offsetHeight-e.pageY<o.scrollSensitivity?this.scrollParent[0].scrollTop=r=this.scrollParent[0].scrollTop+o.scrollSpeed:e.pageY-this.overflowOffset.top<o.scrollSensitivity&&(this.scrollParent[0].scrollTop=r=this.scrollParent[0].scrollTop-o.scrollSpeed),this.overflowOffset.left+this.scrollParent[0].offsetWidth-e.pageX<o.scrollSensitivity?this.scrollParent[0].scrollLeft=r=this.scrollParent[0].scrollLeft+o.scrollSpeed:e.pageX-this.overflowOffset.left<o.scrollSensitivity&&(this.scrollParent[0].scrollLeft=r=this.scrollParent[0].scrollLeft-o.scrollSpeed)):(e.pageY-this.document.scrollTop()<o.scrollSensitivity?r=this.document.scrollTop(this.document.scrollTop()-o.scrollSpeed):this.window.height()-(e.pageY-this.document.scrollTop())<o.scrollSensitivity&&(r=this.document.scrollTop(this.document.scrollTop()+o.scrollSpeed))
 ,e.pageX-this.document.scrollLeft()<o.scrollSensitivity?r=this.document.scrollLeft(this.document.scrollLeft()-o.scrollSpeed):this.window.width()-(e.pageX-this.document.scrollLeft())<o.scrollSensitivity&&(r=this.document.scrollLeft(this.document.scrollLeft()+o.scrollSpeed))),r!==!1&&t.ui.ddmanager&&!o.dropBehaviour&&t.ui.ddmanager.prepareOffsets(this,e)),this.positionAbs=this._convertPositionTo("absolute"),this.options.axis&&"y"===this.options.axis||(this.helper[0].style.left=this.position.left+"px"),this.options.axis&&"x"===this.options.axis||(this.helper[0].style.top=this.position.top+"px"),i=this.items.length-1;i>=0;i--)if(s=this.items[i],n=s.item[0],a=this._intersectsWithPointer(s),a&&s.instance===this.currentContainer&&n!==this.currentItem[0]&&this.placeholder[1===a?"next":"prev"]()[0]!==n&&!t.contains(this.placeholder[0],n)&&("semi-dynamic"===this.options.type?!t.contains(this.element[0],n):!0)){if(this.direction=1===a?"down":"up","pointer"!==this.options.tolerance&&!this._inte
 rsectsWithSides(s))break;this._rearrange(e,s),this._trigger("change",e,this._uiHash());break}return this._contactContainers(e),t.ui.ddmanager&&t.ui.ddmanager.drag(this,e),this._trigger("sort",e,this._uiHash()),this.lastPositionAbs=this.positionAbs,!1},_mouseStop:function(e,i){if(e){if(t.ui.ddmanager&&!this.options.dropBehaviour&&t.ui.ddmanager.drop(this,e),this.options.revert){var s=this,n=this.placeholder.offset(),a=this.options.axis,o={};a&&"x"!==a||(o.left=n.left-this.offset.parent.left-this.margins.left+(this.offsetParent[0]===this.document[0].body?0:this.offsetParent[0].scrollLeft)),a&&"y"!==a||(o.top=n.top-this.offset.parent.top-this.margins.top+(this.offsetParent[0]===this.document[0].body?0:this.offsetParent[0].scrollTop)),this.reverting=!0,t(this.helper).animate(o,parseInt(this.options.revert,10)||500,function(){s._clear(e)})}else this._clear(e,i);return!1}},cancel:function(){if(this.dragging){this._mouseUp(new t.Event("mouseup",{target:null})),"original"===this.options.hel
 per?(this.currentItem.css(this._storedCSS),this._removeClass(this.currentItem,"ui-sortable-helper")):this.currentItem.show();for(var e=this.containers.length-1;e>=0;e--)this.containers[e]._trigger("deactivate",null,this._uiHash(this)),this.containers[e].containerCache.over&&(this.containers[e]._trigger("out",null,this._uiHash(this)),this.containers[e].containerCache.over=0)}return this.placeholder&&(this.placeholder[0].parentNode&&this.placeholder[0].parentNode.removeChild(this.placeholder[0]),"original"!==this.options.helper&&this.helper&&this.helper[0].parentNode&&this.helper.remove(),t.extend(this,{helper:null,dragging:!1,reverting:!1,_noFinalSort:null}),this.domPosition.prev?t(this.domPosition.prev).after(this.currentItem):t(this.domPosition.parent).prepend(this.currentItem)),this},serialize:function(e){var i=this._getItemsAsjQuery(e&&e.connected),s=[];return e=e||{},t(i).each(function(){var i=(t(e.item||this).attr(e.attribute||"id")||"").match(e.expression||/(.+)[\-=_](.+)/);i&
 &s.push((e.key||i[1]+"[]")+"="+(e.key&&e.expression?i[1]:i[2]))}),!s.length&&e.key&&s.push(e.key+"="),s.join("&")},toArray:function(e){var i=this._getItemsAsjQuery(e&&e.connected),s=[];return e=e||{},i.each(function(){s.push(t(e.item||this).attr(e.attribute||"id")||"")}),s},_intersectsWith:function(t){var e=this.positionAbs.left,i=e+this.helperProportions.width,s=this.positionAbs.top,n=s+this.helperProportions.height,a=t.left,o=a+t.width,r=t.top,h=r+t.height,l=this.offset.click.top,u=this.offset.click.left,c="x"===this.options.axis||s+l>r&&h>s+l,d="y"===this.options.axis||e+u>a&&o>e+u,p=c&&d;return"pointer"===this.options.tolerance||this.options.forcePointerForContainers||"pointer"!==this.options.tolerance&&this.helperProportions[this.floating?"width":"height"]>t[this.floating?"width":"height"]?p:e+this.helperProportions.width/2>a&&o>i-this.helperProportions.width/2&&s+this.helperProportions.height/2>r&&h>n-this.helperProportions.height/2},_intersectsWithPointer:function(t){var e,i,
 s="x"===this.options.axis||this._isOverAxis(this.positionAbs.top+this.offset.click.top,t.top,t.height),n="y"===this.options.axis||this._isOverAxis(this.positionAbs.left+this.offset.click.left,t.left,t.width),a=s&&n;return a?(e=this._getDragVerticalDirection(),i=this._getDragHorizontalDirection(),this.floating?"right"===i||"down"===e?2:1:e&&("down"===e?2:1)):!1},_intersectsWithSides:function(t){var e=this._isOverAxis(this.positionAbs.top+this.offset.click.top,t.top+t.height/2,t.height),i=this._isOverAxis(this.positionAbs.left+this.offset.click.left,t.left+t.width/2,t.width),s=this._getDragVerticalDirection(),n=this._getDragHorizontalDirection();return this.floating&&n?"right"===n&&i||"left"===n&&!i:s&&("down"===s&&e||"up"===s&&!e)},_getDragVerticalDirection:function(){var t=this.positionAbs.top-this.lastPositionAbs.top;return 0!==t&&(t>0?"down":"up")},_getDragHorizontalDirection:function(){var t=this.positionAbs.left-this.lastPositionAbs.left;return 0!==t&&(t>0?"right":"left")},refre
 sh:function(t){return this._refreshItems(t),this._setHandleClassName(),this.refreshPositions(),this},_connectWith:function(){var t=this.options;return t.connectWith.constructor===String?[t.connectWith]:t.connectWith},_getItemsAsjQuery:function(e){function i(){r.push(this)}var s,n,a,o,r=[],h=[],l=this._connectWith();if(l&&e)for(s=l.length-1;s>=0;s--)for(a=t(l[s],this.document[0]),n=a.length-1;n>=0;n--)o=t.data(a[n],this.widgetFullName),o&&o!==this&&!o.options.disabled&&h.push([t.isFunction(o.options.items)?o.options.items.call(o.element):t(o.options.items,o.element).not(".ui-sortable-helper").not(".ui-sortable-placeholder"),o]);for(h.push([t.isFunction(this.options.items)?this.options.items.call(this.element,null,{options:this.options,item:this.currentItem}):t(this.options.items,this.element).not(".ui-sortable-helper").not(".ui-sortable-placeholder"),this]),s=h.length-1;s>=0;s--)h[s][0].each(i);return t(r)},_removeCurrentsFromItems:function(){var e=this.currentItem.find(":data("+this
 .widgetName+"-item)");this.items=t.grep(this.items,function(t){for(var i=0;e.length>i;i++)if(e[i]===t.item[0])return!1;return!0})},_refreshItems:function(e){this.items=[],this.containers=[this];var i,s,n,a,o,r,h,l,u=this.items,c=[[t.isFunction(this.options.items)?this.options.items.call(this.element[0],e,{item:this.currentItem}):t(this.options.items,this.element),this]],d=this._connectWith();if(d&&this.ready)for(i=d.length-1;i>=0;i--)for(n=t(d[i],this.document[0]),s=n.length-1;s>=0;s--)a=t.data(n[s],this.widgetFullName),a&&a!==this&&!a.options.disabled&&(c.push([t.isFunction(a.options.items)?a.options.items.call(a.element[0],e,{item:this.currentItem}):t(a.options.items,a.element),a]),this.containers.push(a));for(i=c.length-1;i>=0;i--)for(o=c[i][1],r=c[i][0],s=0,l=r.length;l>s;s++)h=t(r[s]),h.data(this.widgetName+"-item",o),u.push({item:h,instance:o,width:0,height:0,left:0,top:0})},refreshPositions:function(e){this.floating=this.items.length?"x"===this.options.axis||this._isFloating(
 this.items[0].item):!1,this.offsetParent&&this.helper&&(this.offset.parent=this._getParentOffset());var i,s,n,a;for(i=this.items.length-1;i>=0;i--)s=this.items[i],s.instance!==this.currentContainer&&this.currentContainer&&s.item[0]!==this.currentItem[0]||(n=this.options.toleranceElement?t(this.options.toleranceElement,s.item):s.item,e||(s.width=n.outerWidth(),s.height=n.outerHeight()),a=n.offset(),s.left=a.left,s.top=a.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--)a=this.containers[i].element.offset(),this.containers[i].containerCache.left=a.left,this.containers[i].containerCache.top=a.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(e){e=e||this;var i,s=e.options;s.placeholder&&s.placeholder.constructor!==String||
 (i=s.placeholder,s.placeholder={element:function(){var s=e.currentItem[0].nodeName.toLowerCase(),n=t("<"+s+">",e.document[0]);return e._addClass(n,"ui-sortable-placeholder",i||e.currentItem[0].className)._removeClass(n,"ui-sortable-helper"),"tbody"===s?e._createTrPlaceholder(e.currentItem.find("tr").eq(0),t("<tr>",e.document[0]).appendTo(n)):"tr"===s?e._createTrPlaceholder(e.currentItem,n):"img"===s&&n.attr("src",e.currentItem.attr("src")),i||n.css("visibility","hidden"),n},update:function(t,n){(!i||s.forcePlaceholderSize)&&(n.height()||n.height(e.currentItem.innerHeight()-parseInt(e.currentItem.css("paddingTop")||0,10)-parseInt(e.currentItem.css("paddingBottom")||0,10)),n.width()||n.width(e.currentItem.innerWidth()-parseInt(e.currentItem.css("paddingLeft")||0,10)-parseInt(e.currentItem.css("paddingRight")||0,10)))}}),e.placeholder=t(s.placeholder.element.call(e.element,e.currentItem)),e.currentItem.after(e.placeholder),s.placeholder.update(e,e.placeholder)},_createTrPlaceholder:fun
 ction(e,i){var s=this;e.children().each(function(){t("<td> </td>",s.document[0]).attr("colspan",t(this).attr("colspan")||1).appendTo(i)})},_contactContainers:function(e){var i,s,n,a,o,r,h,l,u,c,d=null,p=null;for(i=this.containers.length-1;i>=0;i--)if(!t.contains(this.currentItem[0],this.containers[i].element[0]))if(this._intersectsWith(this.containers[i].containerCache)){if(d&&t.contains(this.containers[i].element[0],d.element[0]))continue;d=this.containers[i],p=i}else this.containers[i].containerCache.over&&(this.containers[i]._trigger("out",e,this._uiHash(this)),this.containers[i].containerCache.over=0);if(d)if(1===this.containers.length)this.containers[p].containerCache.over||(this.containers[p]._trigger("over",e,this._uiHash(this)),this.containers[p].containerCache.over=1);else{for(n=1e4,a=null,u=d.floating||this._isFloating(this.currentItem),o=u?"left":"top",r=u?"width":"height",c=u?"pageX":"pageY",s=this.items.length-1;s>=0;s--)t.contains(this.containers[p].element[0],thi
 s.items[s].item[0])&&this.items[s].item[0]!==this.currentItem[0]&&(h=this.items[s].item.offset()[o],l=!1,e[c]-h>this.items[s][r]/2&&(l=!0),n>Math.abs(e[c]-h)&&(n=Math.abs(e[c]-h),a=this.items[s],this.direction=l?"up":"down"));if(!a&&!this.options.dropOnEmpty)return;if(this.currentContainer===this.containers[p])return this.currentContainer.containerCache.over||(this.containers[p]._trigger("over",e,this._uiHash()),this.currentContainer.containerCache.over=1),void 0;a?this._rearrange(e,a,null,!0):this._rearrange(e,null,this.containers[p].element,!0),this._trigger("change",e,this._uiHash()),this.containers[p]._trigger("change",e,this._uiHash(this)),this.currentContainer=this.containers[p],this.options.placeholder.update(this.currentContainer,this.placeholder),this.containers[p]._trigger("over",e,this._uiHash(this)),this.containers[p].containerCache.over=1}},_createHelper:function(e){var i=this.options,s=t.isFunction(i.helper)?t(i.helper.apply(this.element[0],[e,this.currentItem])):"clon
 e"===i.helper?this.currentItem.clone():this.currentItem;return s.parents("body").length||t("parent"!==i.appendTo?i.appendTo:this.currentItem[0].parentNode)[0].appendChild(s[0]),s[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")}),(!s[0].style.width||i.forceHelperSize)&&s.width(this.currentItem.width()),(!s[0].style.height||i.forceHelperSize)&&s.height(this.currentItem.height()),s},_adjustOffsetFromHelper:function(e){"string"==typeof e&&(e=e.split(" ")),t.isArray(e)&&(e={left:+e[0],top:+e[1]||0}),"left"in e&&(this.offset.click.left=e.left+this.margins.left),"right"in e&&(this.offset.click.left=this.helperProportions.width-e.right+this.margins.left),"top"in e&&(this.offset.click.top=e.top+this.margins.top),"bottom"in e&&(this.offset.click.top=this.helperProportions.height-e.bottom+this.margins.top)},_getPar
 entOffset:function(){this.offsetParent=this.helper.offsetParent();var e=this.offsetParent.offset();return"absolute"===this.cssPosition&&this.scrollParent[0]!==this.document[0]&&t.contains(this.scrollParent[0],this.offsetParent[0])&&(e.left+=this.scrollParent.scrollLeft(),e.top+=this.scrollParent.scrollTop()),(this.offsetParent[0]===this.document[0].body||this.offsetParent[0].tagName&&"html"===this.offsetParent[0].tagName.toLowerCase()&&t.ui.ie)&&(e={top:0,left:0}),{top:e.top+(parseInt(this.offsetParent.css("borderTopWidth"),10)||0),left:e.left+(parseInt(this.offsetParent.css("borderLeftWidth"),10)||0)}},_getRelativeOffset:function(){if("relative"===this.cssPosition){var t=this.currentItem.position();return{top:t.top-(parseInt(this.helper.css("top"),10)||0)+this.scrollParent.scrollTop(),left:t.left-(parseInt(this.helper.css("left"),10)||0)+this.scrollParent.scrollLeft()}}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 e,i,s,n=this.options;"parent"===n.containment&&(n.containment=this.helper[0].parentNode),("document"===n.containment||"window"===n.containment)&&(this.containment=[0-this.offset.relative.left-this.offset.parent.left,0-this.offset.relative.top-this.offset.parent.top,"document"===n.containment?this.document.width():this.window.width()-this.helperProportions.width-this.margins.left,("document"===n.containment?this.document.height()||document.body.parentNode.scrollHeight:this.window.height()||this.document[0].body.parentNode.scrollHeight)-this.helperProportions.height-this.margins.top]),/^(document|window|parent)$/.test(n.containment)||(e=t(n.containment)[0],i=t(n.containment).offset(),s="hidden"!==t(e).css("overflow"),this.containment=[i.left+(parseInt(t(e).css("borderLeftWi
 dth"),10)||0)+(parseInt(t(e).css("paddingLeft"),10)||0)-this.margins.left,i.top+(parseInt(t(e).css("borderTopWidth"),10)||0)+(parseInt(t(e).css("paddingTop"),10)||0)-this.margins.top,i.left+(s?Math.max(e.scrollWidth,e.offsetWidth):e.offsetWidth)-(parseInt(t(e).css("borderLeftWidth"),10)||0)-(parseInt(t(e).css("paddingRight"),10)||0)-this.helperProportions.width-this.margins.left,i.top+(s?Math.max(e.scrollHeight,e.offsetHeight):e.offsetHeight)-(parseInt(t(e).css("borderTopWidth"),10)||0)-(parseInt(t(e).css("paddingBottom"),10)||0)-this.helperProportions.height-this.margins.top])},_convertPositionTo:function(e,i){i||(i=this.position);var s="absolute"===e?1:-1,n="absolute"!==this.cssPosition||this.scrollParent[0]!==this.document[0]&&t.contains(this.scrollParent[0],this.offsetParent[0])?this.scrollParent:this.offsetParent,a=/(html|body)/i.test(n[0].tagName);return{top:i.top+this.offset.relative.top*s+this.offset.parent.top*s-("fixed"===this.cssPosition?-this.scrollParent.scrollTop():a?0
 :n.scrollTop())*s,left:i.left+this.offset.relative.left*s+this.offset.parent.left*s-("fixed"===this.cssPosition?-this.scrollParent.scrollLeft():a?0:n.scrollLeft())*s}
+},_generatePosition:function(e){var i,s,n=this.options,a=e.pageX,o=e.pageY,r="absolute"!==this.cssPosition||this.scrollParent[0]!==this.document[0]&&t.contains(this.scrollParent[0],this.offsetParent[0])?this.scrollParent:this.offsetParent,h=/(html|body)/i.test(r[0].tagName);return"relative"!==this.cssPosition||this.scrollParent[0]!==this.document[0]&&this.scrollParent[0]!==this.offsetParent[0]||(this.offset.relative=this._getRelativeOffset()),this.originalPosition&&(this.containment&&(e.pageX-this.offset.click.left<this.containment[0]&&(a=this.containment[0]+this.offset.click.left),e.pageY-this.offset.click.top<this.containment[1]&&(o=this.containment[1]+this.offset.click.top),e.pageX-this.offset.click.left>this.containment[2]&&(a=this.containment[2]+this.offset.click.left),e.pageY-this.offset.click.top>this.containment[3]&&(o=this.containment[3]+this.offset.click.top)),n.grid&&(i=this.originalPageY+Math.round((o-this.originalPageY)/n.grid[1])*n.grid[1],o=this.containment?i-this.off
 set.click.top>=this.containment[1]&&i-this.offset.click.top<=this.containment[3]?i:i-this.offset.click.top>=this.containment[1]?i-n.grid[1]:i+n.grid[1]:i,s=this.originalPageX+Math.round((a-this.originalPageX)/n.grid[0])*n.grid[0],a=this.containment?s-this.offset.click.left>=this.containment[0]&&s-this.offset.click.left<=this.containment[2]?s:s-this.offset.click.left>=this.containment[0]?s-n.grid[0]:s+n.grid[0]:s)),{top:o-this.offset.click.top-this.offset.relative.top-this.offset.parent.top+("fixed"===this.cssPosition?-this.scrollParent.scrollTop():h?0:r.scrollTop()),left:a-this.offset.click.left-this.offset.relative.left-this.offset.parent.left+("fixed"===this.cssPosition?-this.scrollParent.scrollLeft():h?0:r.scrollLeft())}},_rearrange:function(t,e,i,s){i?i[0].appendChild(this.placeholder[0]):e.item[0].parentNode.insertBefore(this.placeholder[0],"down"===this.direction?e.item[0]:e.item[0].nextSibling),this.counter=this.counter?++this.counter:1;var n=this.counter;this._delay(function
 (){n===this.counter&&this.refreshPositions(!s)})},_clear:function(t,e){function i(t,e,i){return function(s){i._trigger(t,s,e._uiHash(e))}}this.reverting=!1;var s,n=[];if(!this._noFinalSort&&this.currentItem.parent().length&&this.placeholder.before(this.currentItem),this._noFinalSort=null,this.helper[0]===this.currentItem[0]){for(s in this._storedCSS)("auto"===this._storedCSS[s]||"static"===this._storedCSS[s])&&(this._storedCSS[s]="");this.currentItem.css(this._storedCSS),this._removeClass(this.currentItem,"ui-sortable-helper")}else this.currentItem.show();for(this.fromOutside&&!e&&n.push(function(t){this._trigger("receive",t,this._uiHash(this.fromOutside))}),!this.fromOutside&&this.domPosition.prev===this.currentItem.prev().not(".ui-sortable-helper")[0]&&this.domPosition.parent===this.currentItem.parent()[0]||e||n.push(function(t){this._trigger("update",t,this._uiHash())}),this!==this.currentContainer&&(e||(n.push(function(t){this._trigger("remove",t,this._uiHash())}),n.push(functio
 n(t){return function(e){t._trigger("receive",e,this._uiHash(this))}}.call(this,this.currentContainer)),n.push(function(t){return function(e){t._trigger("update",e,this._uiHash(this))}}.call(this,this.currentContainer)))),s=this.containers.length-1;s>=0;s--)e||n.push(i("deactivate",this,this.containers[s])),this.containers[s].containerCache.over&&(n.push(i("out",this,this.containers[s])),this.containers[s].containerCache.over=0);if(this.storedCursor&&(this.document.find("body").css("cursor",this.storedCursor),this.storedStylesheet.remove()),this._storedOpacity&&this.helper.css("opacity",this._storedOpacity),this._storedZIndex&&this.helper.css("zIndex","auto"===this._storedZIndex?"":this._storedZIndex),this.dragging=!1,e||this._trigger("beforeStop",t,this._uiHash()),this.placeholder[0].parentNode.removeChild(this.placeholder[0]),this.cancelHelperRemoval||(this.helper[0]!==this.currentItem[0]&&this.helper.remove(),this.helper=null),!e){for(s=0;n.length>s;s++)n[s].call(this,t);this._tri
 gger("stop",t,this._uiHash())}return this.fromOutside=!1,!this.cancelHelperRemoval},_trigger:function(){t.Widget.prototype._trigger.apply(this,arguments)===!1&&this.cancel()},_uiHash:function(e){var i=e||this;return{helper:i.helper,placeholder:i.placeholder||t([]),position:i.position,originalPosition:i.originalPosition,offset:i.positionAbs,item:i.currentItem,sender:e?e.element:null}}})});
\ No newline at end of file
diff --git a/share/static/js/jquery.ui.sortable.js b/share/static/js/jquery.ui.sortable.js
deleted file mode 100644
index 1ade1e31d..000000000
--- a/share/static/js/jquery.ui.sortable.js
+++ /dev/null
@@ -1,1250 +0,0 @@
-/*!
- * 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 dd7e3e5e819c002413c2286f2297b10faa71c3df
Author: Blaine Motsinger <blaine at bestpractical.com>
Date:   Tue Jan 28 14:34:19 2020 -0600

    Update DashboardSelectionUI to elevator-light
    
    General design updates were also made from the original import to
    make it look better with our elevator-light theme.

diff --git a/share/html/Admin/Global/MyRT.html b/share/html/Admin/Global/MyRT.html
index 2ec3a597c..2d503d416 100644
--- a/share/html/Admin/Global/MyRT.html
+++ b/share/html/Admin/Global/MyRT.html
@@ -56,7 +56,7 @@
     selected  => \%selected,
     filters   => \@filters,
   &>
-  <input type="submit" class="button" name="UpdateSearches" value="<% loc('Save') %>" />
+  <input type="submit" class="button form-control btn btn-primary" name="UpdateSearches" value="<% loc('Save') %>" />
 </form>
 
 <%INIT>
diff --git a/share/html/Admin/Users/MyRT.html b/share/html/Admin/Users/MyRT.html
index 706fd3e76..ad4e2861f 100644
--- a/share/html/Admin/Users/MyRT.html
+++ b/share/html/Admin/Users/MyRT.html
@@ -57,7 +57,7 @@
                     filters   => \@filters,
     &>
 <input type="hidden" name="id" value="<% $id %>"/>
-<input type"submit" class="button" name="UpdateSearches" value= "<% loc('Save') %>"  >
+<input type="submit" class="button form-control btn btn-primary" name="UpdateSearches" value= "<% loc('Save') %>"  >
 <div class="form-row">
   <div class="col-md-12">
     <input type="submit" class="button form-control btn btn-primary" value="<%loc('Reset to default')%>">
diff --git a/share/html/Dashboards/Queries.html b/share/html/Dashboards/Queries.html
index dfbb8522c..2bd687ca8 100644
--- a/share/html/Dashboards/Queries.html
+++ b/share/html/Dashboards/Queries.html
@@ -56,7 +56,7 @@
     selected  => \%selected,
     filters   => \@filters,
   &>
-  <input type="submit" class="button" name="UpdateSearches" value="<% loc('Save') %>" />
+  <input type="submit" class="button form-control btn btn-primary" name="UpdateSearches" value="<% loc('Save') %>" />
 </form>
 <%INIT>
 my @results;
diff --git a/share/html/Prefs/MyRT.html b/share/html/Prefs/MyRT.html
index 15c6bccb4..5f4c2c6b0 100644
--- a/share/html/Prefs/MyRT.html
+++ b/share/html/Prefs/MyRT.html
@@ -56,7 +56,7 @@
     selected  => \%selected,
     filters   => \@filters,
   &>
-  <input type="submit" class="button" name="UpdateSearches" value="<% loc('Save') %>" />
+  <input type="submit" class="button form-control btn btn-primary" name="UpdateSearches" value="<% loc('Save') %>" />
 </form>
 
 <&|/Widgets/TitleBox, title => loc('Options'), bodyclass => "" &>
@@ -69,7 +69,7 @@
     <input class="form-control" type="text" name="SummaryRows" value="<% $ARGS{SummaryRows} %>" />
   </div>
   <div class="col-md-auto">
-    <input type="submit" class="button form-control btn btn-primary"" name="UpdateSummaryRows" value="<% loc('Save') %>" />
+    <input type="submit" class="button form-control btn btn-primary" name="UpdateSummaryRows" value="<% loc('Save') %>" />
   </div>
 </div>
 </form>
@@ -77,7 +77,7 @@
 <&|/Widgets/TitleBox, title => loc("Reset RT at a glance") &>
 <form method="post" action="MyRT.html">
 <input type="hidden" name="Reset" value="1" />
-<input type="submit" class="button form-control btn btn-primary"" value="<% loc('Reset to default') %>">
+<input type="submit" class="button form-control btn btn-primary" value="<% loc('Reset to default') %>">
 </form>
 </&>
 
diff --git a/share/html/Widgets/SearchSelection b/share/html/Widgets/SearchSelection
index 7416e4a4b..54c19b7a5 100644
--- a/share/html/Widgets/SearchSelection
+++ b/share/html/Widgets/SearchSelection
@@ -46,32 +46,25 @@
 %#
 %# END BPS TAGGED BLOCK }}}
 <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>
+  <div class="form-row">
 
+  <div class="col-md-4">
+  <div class="source">
     <div class="filters">
-      <input type="search" name="search" placeholder="<&|/l&>Search…</&>" autocomplete="off">
-      <select name="filter">
-          <option value=""><&|/l&>All Types</&></option>
+      <div class="form-row">
+        <div class="col-md-auto">
+          <input type="search" class="field form-control" name="search" placeholder="<&|/l&>Search…</&>" autocomplete="off">
+        </div>
+        <div class="col-md-auto">
+          <select class="form-control selectpicker" name="filter">
+              <option value=""><&|/l&>All Types</&></option>
 %         for (@filters) {
 %           my ($value, $label) = @$_;
-            <option value="<% $value %>"><% $label %></option>
+              <option value="<% $value %>"><% $label %></option>
 % }
-      </select>
+          </select>
+        </div>
+      </div>
     </div>
 
     <div class="contents">
@@ -90,9 +83,27 @@
 %     }
     </div>
   </div>
-</div>
+  </div>
+
+  <div class="col-md-4 ml-2">
+  <div class="destinations">
+%   for my $pane (sort keys %pane_name) {
+      <div class="destination" data-pane="<% $pane %>">
+        <div class="contents">
+          <h3><% $pane_name{$pane} %></h3>
+          <ul>
+%           for my $item (@{ $selected{$pane} }) {
+              <& /Elements/ShowSelectSearch, %$item &>
+%           }
+          </ul>
+        </div>
+      </div>
+%   }
+  </div>
+  </div>
 
-<div class="clear"></div>
+  </div>
+</div>
 
 <%INIT>
 use utf8;
diff --git a/share/static/css/elevator-light/forms.css b/share/static/css/elevator-light/forms.css
index 83516af15..957e6b243 100644
--- a/share/static/css/elevator-light/forms.css
+++ b/share/static/css/elevator-light/forms.css
@@ -396,19 +396,6 @@ textarea.code {
 
 /* 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;
 }
@@ -423,22 +410,31 @@ textarea.code {
 }
 
 .selectionbox-js .source .contents {
-    height: 450px;
+    height: 526px;
 }
 
 /* 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;
+    height: 250px;
 }
 
 .selectionbox-js h2 {
     margin: 0 0 .5em 0;
 }
 
+.selectionbox-js .source .filters {
+    margin-bottom: 1em;
+}
+
+.selectionbox-js .destinations {
+    margin-top: 3.75rem;
+}
+
 .selectionbox-js .contents {
-    border: 2px solid #aaa;
+    border: 1px solid #ccc;
+    border-radius: 0.25rem;
     overflow-y: scroll;
     padding: 0 .5em;
 }
@@ -451,7 +447,8 @@ textarea.code {
 
 .selectionbox-js .contents li,
 .selectionbox-js .contents .placeholder {
-    border: 2px solid #ddd;
+    border: 1px solid #aaa;
+    border-radius: 0.25rem;
     padding: .5em;
     margin: .5em 0;
     background-color: #eee;
@@ -493,7 +490,7 @@ textarea.code {
 }
 
 .selectionbox-js .contents h3 {
-    margin: .5em;
+    margin: .5em 0;
 }
 
 .selectionbox-js .contents .section {

commit 5d2fb31b1d7cc4cbd73359f515839a9ac6810d56
Author: Blaine Motsinger <blaine at bestpractical.com>
Date:   Wed Jan 29 16:35:31 2020 -0600

    Switch remove icon to fontawesome
    
    This commit also updates the js for the search select to clear the
    bootstrap tooltip on remove.  The tooltip will otherwise linger
    after clicking remove.

diff --git a/share/html/Elements/ShowSelectSearch b/share/html/Elements/ShowSelectSearch
index 6b2ea4da9..91b73af10 100644
--- a/share/html/Elements/ShowSelectSearch
+++ b/share/html/Elements/ShowSelectSearch
@@ -53,7 +53,11 @@ $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 . ': ' : '' %>
+% my $alt = loc('Remove');
+<li data-type="<% $type %>" data-name="<% $name %>" data-search-type="<% $search_type || ''  %>" data-search-id="<% $search_id || '' %>" data-description="<% $label %>">
+  <a href="#" class="remove">
+    <span class="far fa-times-circle" alt="<% $alt %>" data-toggle="tooltip" data-placement="top" data-original-title="<% $alt %>"></span>
+  </a> <% $prefix ? $prefix . ': ' : '' %>
 <% $label %>
 % if ($possibly_hidden) {
 <span class="warning"><&|/l&>Warning: may not be visible to all viewers</&></span>
diff --git a/share/static/css/elevator-light/forms.css b/share/static/css/elevator-light/forms.css
index 957e6b243..dc6836d25 100644
--- a/share/static/css/elevator-light/forms.css
+++ b/share/static/css/elevator-light/forms.css
@@ -473,6 +473,7 @@ textarea.code {
 .selectionbox-js .contents li a.remove {
     position: absolute;
     right: .5em;
+    color: #5C6273;
 }
 
 .selectionbox-js .contents li a.remove img {
diff --git a/share/static/js/forms.js b/share/static/js/forms.js
index a8d5392b8..95848550a 100644
--- a/share/static/js/forms.js
+++ b/share/static/js/forms.js
@@ -50,6 +50,13 @@ jQuery(function() {
         }).disableSelection().on('click', '.remove', function (e) {
             e.preventDefault();
             jQuery(e.target).closest('li').remove();
+
+            // dispose of the bootstrap tooltip.
+            // without manually clearing here, the tooltip lingers after clicking remove.
+            var bs_tooltip = jQuery('div[id^="tooltip"]');
+            bs_tooltip.tooltip('hide');
+            bs_tooltip.tooltip('dispose');
+
             return false;
         });
 

commit f554b70cb718268992b5c5c7c76db6f20b5c2071
Author: Blaine Motsinger <blaine at bestpractical.com>
Date:   Thu Jan 30 13:36:35 2020 -0600

    Fix reset to defaults
    
    This commit fixes the reset to defaults for the page by using the
    reset logic from the Prefs/MyRT.html.  Additionally, it changes
    the post to use id as a query param instead of form variable, so
    the url is correctly including id after post.

diff --git a/share/html/Admin/Users/MyRT.html b/share/html/Admin/Users/MyRT.html
index ad4e2861f..707f96946 100644
--- a/share/html/Admin/Users/MyRT.html
+++ b/share/html/Admin/Users/MyRT.html
@@ -58,11 +58,14 @@
     &>
 <input type="hidden" name="id" value="<% $id %>"/>
 <input type="submit" class="button form-control btn btn-primary" name="UpdateSearches" value= "<% loc('Save') %>"  >
-<div class="form-row">
-  <div class="col-md-12">
-    <input type="submit" class="button form-control btn btn-primary" value="<%loc('Reset to default')%>">
+</form>
+<form method="post" action="MyRT.html?id=<% $id %>">
+  <input type="hidden" name="Reset" value="1" />
+  <div class="form-row">
+    <div class="col-md-12">
+      <input type="submit" class="button form-control btn btn-primary" value="<%loc('Reset to default')%>">
+    </div>
   </div>
-</div>
 </form>
 
 <%init>
@@ -72,10 +75,19 @@ $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}) {
+    for my $pref_name ('HomepageSettings', 'SummaryRows') {
+        next unless $UserObj->Preferences($pref_name);
+        my ($ok, $msg) = $UserObj->DeletePreferences($pref_name);
+        push @actions, $msg unless $ok;
+    }
+    push @actions, loc('Preferences saved for user [_1].', $UserObj->Name) unless @actions;
+}
+
 my $portlets = $UserObj->Preferences('HomepageSettings');
 unless ($portlets) {
-   my ($defaults) = RT::System->new($session{'CurrentUser'})->Attributes->Named('HomepageSettings');
-   $portlets = $defaults ? $defaults->Content : {};
+    my ($defaults) = RT::System->new($session{'CurrentUser'})->Attributes->Named('HomepageSettings');
+    $portlets = $defaults ? $defaults->Content : {};
 }
 
 my @sections;

commit 0cc961e47165f67067ab1163d7a4738e94e30c05
Author: Blaine Motsinger <blaine at bestpractical.com>
Date:   Tue Feb 4 17:34:50 2020 -0600

    Force redirect back to the current page on submit
    
    If the user submits a post to "reset to default", then immediately
    a post to save new changes, the JS was instructing the browser to
    reload which then submitted the reset post again.
    
    Instead of reload, the JS now redirects back to the current page
    to clear out the previous post "state" so not to immediately reset
    the new changes.

diff --git a/share/static/js/forms.js b/share/static/js/forms.js
index 95848550a..f95c9e7ae 100644
--- a/share/static/js/forms.js
+++ b/share/static/js/forms.js
@@ -179,7 +179,16 @@ jQuery(function() {
                 timeout: 30000, /* 30 seconds */
                 success: function (response) {
                     if (response.ok) {
-                        window.location.reload();
+                        // Force redirect back to the current page on submit.
+                        // If the user submits a post to "reset to default", then immediately
+                        // a post to save new changes, the JS was instructing the browser to
+                        // reload which then submitted the reset post again.
+
+                        // Instead of reload, the JS now redirects back to the current page
+                        // to clear out the previous post "state" so not to immediately reset
+                        // the new changes.
+                        var current_location = window.location;
+                        window.location.replace( current_location );
                     }
                     else {
                         alert(response.msg);

commit e5ff16c4d370c74089431c0cece90d3348073897
Author: Blaine Motsinger <blaine at bestpractical.com>
Date:   Mon Feb 10 15:28:22 2020 -0600

    Add name to forms for UpdateDashboard
    
    In preparation for fixing the automated tests, having a named form
    is much easier to submit to using our test methods.

diff --git a/share/html/Admin/Global/MyRT.html b/share/html/Admin/Global/MyRT.html
index 2d503d416..cc3661293 100644
--- a/share/html/Admin/Global/MyRT.html
+++ b/share/html/Admin/Global/MyRT.html
@@ -49,7 +49,7 @@
 <& /Elements/Tabs &>
 <& /Elements/ListActions, actions => \@results &>
 
-<form method="post" action="<% RT->Config->Get('WebPath') %>/Helpers/UpdateDashboard" data-dashboard_id='MyRT' data-is_global=True>
+<form method="post" name="UpdateDashboard" action="<% RT->Config->Get('WebPath') %>/Helpers/UpdateDashboard" data-dashboard_id='MyRT' data-is_global=True>
   <& /Widgets/SearchSelection,
     pane_name => \%pane_name,
     sections  => \@sections,
diff --git a/share/html/Admin/Users/MyRT.html b/share/html/Admin/Users/MyRT.html
index 707f96946..d5d1d76a6 100644
--- a/share/html/Admin/Users/MyRT.html
+++ b/share/html/Admin/Users/MyRT.html
@@ -49,7 +49,7 @@
 <& /Elements/Tabs &>
 <& /Elements/ListActions, actions => \@actions &>
 
-<form method="post" action="<% RT->Config->Get('WebPath')  %> /Helpers/UpdateDashboard" data-dashboard_id = "MyRT" data-user_id = '<% $id %>'>
+<form method="post" name="UpdateDashboard" action="<% RT->Config->Get('WebPath')  %> /Helpers/UpdateDashboard" data-dashboard_id = "MyRT" data-user_id = '<% $id %>'>
   <& /Widgets/SearchSelection,
                     pane_name => \%pane_name,
                     sections  => \@sections,
diff --git a/share/html/Dashboards/Queries.html b/share/html/Dashboards/Queries.html
index 2bd687ca8..33981fd44 100644
--- a/share/html/Dashboards/Queries.html
+++ b/share/html/Dashboards/Queries.html
@@ -49,7 +49,7 @@
 <& /Elements/Tabs &>
 <& /Elements/ListActions, actions => \@results &>
 
-<form method="post" action="<% RT->Config->Get('WebPath') %>/Helpers/UpdateDashboard" data-dashboard_id='<%$Dashboard->Id%>'>
+<form method="post" name="UpdateDashboard" action="<% RT->Config->Get('WebPath') %>/Helpers/UpdateDashboard" data-dashboard_id='<%$Dashboard->Id%>'>
   <& /Widgets/SearchSelection,
     pane_name => \%pane_name,
     sections  => \@sections,
diff --git a/share/html/Prefs/MyRT.html b/share/html/Prefs/MyRT.html
index 5f4c2c6b0..f174dc6a0 100644
--- a/share/html/Prefs/MyRT.html
+++ b/share/html/Prefs/MyRT.html
@@ -49,7 +49,7 @@
 <& /Elements/Tabs &>
 <& /Elements/ListActions, actions => \@results &>
 
-<form method="post" action="<% RT->Config->Get('WebPath') %>/Helpers/UpdateDashboard" data-dashboard_id='MyRT'>
+<form method="post" name="UpdateDashboard" action="<% RT->Config->Get('WebPath') %>/Helpers/UpdateDashboard" data-dashboard_id='MyRT'>
   <& /Widgets/SearchSelection,
     pane_name => \%pane_name,
     sections  => \@sections,

commit 3b4a80fbb35a7019a74ff171d0c5809e47589f70
Author: Blaine Motsinger <blaine at bestpractical.com>
Date:   Wed Feb 12 20:30:22 2020 -0600

    Update tests for MyRT Helper changes
    
    Adding DashboardSelectionUI to core changed the way dashboards are
    updated to now post JSON to a Helper, instead of form values to
    the page itself.  This commit updates the tests to also post JSON
    to the Helper.

diff --git a/t/web/custom_frontpage.t b/t/web/custom_frontpage.t
index ee5e9f58a..ac3263a21 100644
--- a/t/web/custom_frontpage.t
+++ b/t/web/custom_frontpage.t
@@ -1,7 +1,7 @@
 use strict;
 use warnings;
 
-use RT::Test tests => 19;
+use RT::Test tests => undef;
 my ($baseurl, $m) = RT::Test->started_ok;
 
 my $url = $m->rt_base_url;
@@ -17,7 +17,7 @@ $user_obj->PrincipalObj->GrantRight(Right => 'EditSavedSearches');
 $user_obj->PrincipalObj->GrantRight(Right => 'CreateSavedSearch');
 $user_obj->PrincipalObj->GrantRight(Right => 'ModifySelf');
 
-ok $m->login( customer => 'customer' ), "logged in";
+ok $m->login( customer => 'customer' ), "logged in as non-root user";
 
 $m->get ( $url."Search/Build.html");
 
@@ -31,35 +31,105 @@ $m->click_button (name => 'SavedSearchSave');
 $m->get ( $url.'Prefs/MyRT.html' );
 $m->content_contains('stupid tickets', 'saved search listed in rt at a glance items');
 
-ok $m->login('root', 'password', logout => 1), 'we did log in as root';
-
-$m->get ( $url.'Prefs/MyRT.html' );
-$m->form_name ('SelectionBox-body');
-# can't use submit form for mutli-valued select as it uses set_fields
-$m->field ('body-Selected' => ['component-QuickCreate', 'system-Unowned Tickets', 'system-My Tickets']);
-$m->click_button (name => 'remove');
-$m->form_name ('SelectionBox-body');
-#$m->click_button (name => 'body-Save');
-$m->get ( $url );
-$m->content_lacks ('highest priority tickets', 'remove everything from body pane');
-
-$m->get ( $url.'Prefs/MyRT.html' );
-$m->form_name ('SelectionBox-body');
-$m->field ('body-Available' => ['component-QuickCreate', 'system-Unowned Tickets', 'system-My Tickets']);
-$m->click_button (name => 'add');
-
-$m->form_name ('SelectionBox-body');
-$m->field ('body-Selected' => ['component-QuickCreate']);
-$m->click_button (name => 'movedown');
-
-$m->form_name ('SelectionBox-body');
-$m->click_button (name => 'movedown');
+ok $m->login('root', 'password', logout => 1), 'logged in as root';
+
+# remove all portlets from the body pane except 'newest unowned tickets'
+my $payload = {
+    "dashboard_id" => "MyRT",
+    "panes"        => {
+        "body"    => [
+            {
+              "description" => "Unowned Tickets",
+              "name" => "Unowned Tickets",
+              "searchId" => "",
+              "searchType" => "",
+              "type" => "system"
+            },
+        ],
+        "sidebar" => [
+            {
+              "description" => "MyReminders",
+              "name" => "MyReminders",
+              "searchId" => "",
+              "searchType" => "",
+              "type" => "component"
+            },
+            {
+              "description" => "QueueList",
+              "name" => "QueueList",
+              "searchId" => "",
+              "searchType" => "",
+              "type" => "component"
+            },
+            {
+              "description" => "Dashboards",
+              "name" => "Dashboards",
+              "searchId" => "",
+              "searchType" => "",
+              "type" => "component"
+            },
+            {
+              "description" => "RefreshHomepage",
+              "name" => "RefreshHomepage",
+              "searchId" => "",
+              "searchType" => "",
+              "type" => "component"
+            },
+        ]
+    }
+};
+
+my $json = JSON::to_json( $payload );
+my $res  = $m->post(
+    $url . 'Helpers/UpdateDashboard',
+    [ content => $json ],
+);
+is( $res->code, 200, "remove all portlets from body except 'newest unowned tickets'" );
+
+$m->get( $url );
+$m->content_contains( 'newest unowned tickets', "'newest unowned tickets' is present" );
+$m->content_lacks( 'highest priority tickets', "'highest priority tickets' is not present" );
+$m->content_lacks( 'Bookmarked Tickets<span class="results-count">', "'Bookmarked Tickets' is not present" );  # 'Bookmarked Tickets' also shows up in the nav, so we need to be more specific
+$m->content_lacks( 'Quick ticket creation', "'Quick ticket creation' is not present" );
+
+# add back the previously removed portlets
+push(
+    @{$payload->{panes}->{body}},
+    {
+      "description" => "My Tickets",
+      "name" => "My Tickets",
+      "searchId" => "",
+      "searchType" => "",
+      "type" => "system"
+    },
+    {
+      "description" => "Bookmarked Tickets",
+      "name" => "Bookmarked Tickets",
+      "searchId" => "",
+      "searchType" => "",
+      "type" => "system"
+    },
+    {
+      "description" => "QuickCreate",
+      "name" => "QuickCreate",
+      "searchId" => "",
+      "searchType" => "",
+      "type" => "component"
+    },
+);
 
-$m->form_name ('SelectionBox-body');
-#$m->click_button (name => 'body-Save');
-$m->get ( $url );
-$m->content_contains('highest priority tickets', 'adds them back');
+$json = JSON::to_json( $payload );
+$res  = $m->post(
+    $url . 'Helpers/UpdateDashboard',
+    [ content => $json ],
+);
+is( $res->code, 200, 'add back previously removed portlets' );
 
+$m->get( $url );
+$m->content_contains( 'newest unowned tickets', "'newest unowned tickets' is present" );
+$m->content_contains( 'highest priority tickets', "'highest priority tickets' is present" );
+$m->content_contains( 'Bookmarked Tickets<span class="results-count">', "'Bookmarked Tickets' is present" );
+$m->content_contains( 'Quick ticket creation', "'Quick ticket creation' is present" );
 
 #create a saved search with special chars
 $m->get( $url . "Search/Build.html" );
@@ -73,22 +143,28 @@ $m->get( $url . 'Prefs/MyRT.html' );
 $m->content_contains( 'special chars [test] [_1] ~[_1~]',
     'saved search listed in rt at a glance items' );
 
-$m->get( $url . 'Prefs/MyRT.html' );
-$m->form_name('SelectionBox-body');
-$m->field(
-    'body-Available' => [
-        'component-QuickCreate',
-        'system-Unowned Tickets',
-        'system-My Tickets',
-        'saved-' . $name,
-    ]
+# add saved search to body
+push(
+    @{$payload->{panes}->{body}},
+    {
+      "description" => "special chars [test] [_1] ~[_1~]",
+      "name" => $name,
+      "searchId" => "",
+      "searchType" => "Ticket",
+      "type" => "saved"
+    },
 );
-$m->click_button( name => 'add' );
+
+$json = JSON::to_json( $payload );
+$res  = $m->post(
+    $url . 'Helpers/UpdateDashboard',
+    [ content => $json ],
+);
+is( $res->code, 200, 'add saved search to body' );
 
 $m->get($url);
 $m->content_like( qr/special chars \[test\] \d+ \[_1\]/,
-    'special chars in titlebox' );
-
+    "'special chars' is present" );
 
 # Edit a system saved search to contain "[more]"
 {
@@ -107,3 +183,4 @@ $m->content_like( qr/special chars \[test\] \d+ \[_1\]/,
     $m->content_contains($desc . " [more]", "found description: $desc");
 }
 
+done_testing();
diff --git a/t/web/dashboards-basics.t b/t/web/dashboards-basics.t
index fd93c6a79..9d957b502 100644
--- a/t/web/dashboards-basics.t
+++ b/t/web/dashboards-basics.t
@@ -90,22 +90,40 @@ $m->content_lacks("Subscription", "we don't have the SubscribeDashboard right");
 $m->follow_link_ok({text => "Basics"});
 $m->content_contains("Modify the dashboard different dashboard");
 
+# add 'Unowned Tickets' to body of 'different dashboard' dashboard
 $m->follow_link_ok({text => "Content"});
 $m->content_contains("Modify the content of dashboard different dashboard");
-my $form = $m->form_name('Dashboard-Searches-body');
-my @input = $form->find_input('Searches-body-Available');
-my ($unowned) =
-  map { ( $_->possible_values )[1] }
-  grep { ( $_->value_names )[1] =~ /Saved Search: Unowned Tickets/ } @input;
-$form->value('Searches-body-Available' => $unowned );
-$m->click_button(name => 'add');
-$m->content_contains("Dashboard updated");
+
+my ( $dashboard_id ) = ( $m->uri =~ /id=(\d+)/ );
+ok( $dashboard_id, "got a dashboard ID, $dashboard_id" );  # 8
+
+my $payload = {
+    "dashboard_id" => $dashboard_id,
+    "panes"        => {
+        "body"    => [
+            {
+              "description" => "Unowned Tickets",
+              "name" => "Unowned Tickets",
+              "searchId" => "4",
+              "searchType" => "",
+              "type" => "system"
+            },
+        ],
+        "sidebar" => [
+        ]
+    }
+};
+
+my $json = JSON::to_json( $payload );
+my $res  = $m->post(
+    $url . 'Helpers/UpdateDashboard',
+    [ content => $json ],
+);
+is( $res->code, 200, "add 'unowned tickets' to body" );
 
 my $dashboard = RT::Dashboard->new($currentuser);
-my ($id) = $m->content =~ /name="id" value="(\d+)"/;
-ok($id, "got an ID, $id");
-$dashboard->LoadById($id);
-is($dashboard->Name, "different dashboard");
+$dashboard->LoadById($dashboard_id);
+is($dashboard->Name, 'different dashboard', "'different dashboard' name is correct");
 
 is($dashboard->Privacy, 'RT::User-' . $user_obj->Id, "correct privacy");
 is($dashboard->PossibleHiddenSearches, 0, "all searches are visible");
@@ -114,19 +132,27 @@ my @searches = $dashboard->Searches;
 is(@searches, 1, "one saved search in the dashboard");
 like($searches[0]->Name, qr/newest unowned tickets/, "correct search name");
 
-$form = $m->form_name('Dashboard-Searches-body');
- at input = $form->find_input('Searches-body-Available');
-my ($my_tickets) =
-  map { ( $_->possible_values )[1] }
-  grep { ( $_->value_names )[1] =~ /Saved Search: My Tickets/ } @input;
-$form->value('Searches-body-Available' => $my_tickets );
-$m->click_button(name => 'add');
-$m->content_contains("Dashboard updated");
+push(
+    @{$payload->{panes}->{body}},
+    {
+      "description" => "My Tickets",
+      "name" => "My Tickets",
+      "searchId" => "3",
+      "searchType" => "",
+      "type" => "system"
+    },
+);
 
-$dashboard = RT::Dashboard->new($currentuser);
-$dashboard->LoadById($id);
+$json = JSON::to_json( $payload );
+$res  = $m->post(
+    $url . 'Helpers/UpdateDashboard',
+    [ content => $json ],
+);
+is( $res->code, 200, "add 'My Tickets' to body" );
 
+$dashboard->LoadById($dashboard_id);
 @searches = $dashboard->Searches;
+
 is(@searches, 2, "two saved searches in the dashboard");
 like($searches[0]->Name, qr/newest unowned tickets/, "correct existing search name");
 like($searches[1]->Name, qr/highest priority tickets I own/, "correct new search name");
@@ -139,6 +165,8 @@ $ticket->Create(
     Subject   => 'dashboard test',
 );
 
+$m->get_ok($url."Dashboards/index.html");
+$m->follow_link_ok({text => "different dashboard"});
 $m->follow_link_ok({id => 'page-show'});
 $m->content_contains("50 highest priority tickets I own");
 $m->content_contains("50 newest unowned tickets");
@@ -146,14 +174,14 @@ $m->content_unlike( qr/Bookmarked Tickets.*Bookmarked Tickets/s,
     'only dashboard queries show up' );
 $m->content_contains("dashboard test", "ticket subject");
 
-$m->get_ok("/Dashboards/$id/This fragment left intentionally blank");
+$m->get_ok("/Dashboards/$dashboard_id/This fragment left intentionally blank");
 $m->content_contains("50 highest priority tickets I own");
 $m->content_contains("50 newest unowned tickets");
 $m->content_unlike( qr/Bookmarked Tickets.*Bookmarked Tickets/s,
     'only dashboard queries show up' );
 $m->content_contains("dashboard test", "ticket subject");
 
-$m->get("/Dashboards/Modify.html?id=$id&Delete=1");
+$m->get("/Dashboards/Modify.html?id=$dashboard_id&Delete=1");
 is($m->status, HTTP::Status::HTTP_FORBIDDEN);
 $m->content_contains("Permission Denied", "unable to delete dashboard because we lack DeleteOwnDashboard");
 
@@ -161,16 +189,16 @@ $m->warning_like(qr/Couldn't delete dashboard.*Permission Denied/, "got a permis
 
 $user_obj->PrincipalObj->GrantRight(Right => 'DeleteOwnDashboard', Object => $RT::System);
 
-$m->get_ok("/Dashboards/Modify.html?id=$id");
+$m->get_ok("/Dashboards/Modify.html?id=$dashboard_id");
 $m->content_contains('Delete', "Delete button shows because we have DeleteOwnDashboard");
 
 $m->form_name('ModifyDashboard');
 $m->click_button(name => 'Delete');
 $m->content_contains("Deleted dashboard");
 
-$m->get("/Dashboards/Modify.html?id=$id");
+$m->get("/Dashboards/Modify.html?id=$dashboard_id");
 $m->content_lacks("different dashboard", "dashboard was deleted");
-$m->content_contains("Could not load dashboard $id");
+$m->content_contains("Could not load dashboard $dashboard_id");
 
 $m->next_warning_like(qr/Failed to load dashboard/, "the dashboard was deleted");
 $m->next_warning_like(qr/Could not load dashboard/, "the dashboard was deleted");
@@ -202,16 +230,40 @@ $m->content_contains("Saved dashboard system dashboard");
 
 $m->follow_link_ok({id => 'page-content'});
 
-$form = $m->form_name('Dashboard-Searches-body');
- at input = $form->find_input('Searches-body-Available');
-my ($personal) =
-  map { ( $_->possible_values )[1] }
-  grep { ( $_->value_names )[1] =~ /Saved Search: personal search/ } @input;
-$form->value('Searches-body-Available' => $personal );
-$m->click_button(name => 'add');
-$m->content_contains("Dashboard updated");
+my ( $system_dashboard_id ) = ( $m->uri =~ /id=(\d+)/ );
+ok( $system_dashboard_id, "got a dashboard ID for the system dashboard, $system_dashboard_id" );  # 10
+
+# get the saved search name from the content
+my ( $saved_search_name ) = ( $m->content =~ /(RT::User-\d+-SavedSearch-\d+)/ );
+ok( $saved_search_name, "got a saved search name, $saved_search_name" );  # RT::User-27-SavedSearch-9
+
+# add 'personal search' to 'system dashboard' dashboard
+$payload = {
+    "dashboard_id" => $system_dashboard_id,
+    "panes"        => {
+        "body"    => [
+            {
+              "description" => "personal search",
+              "name" => $saved_search_name,
+              "searchId" => "4",
+              "searchType" => "Ticket",
+              "type" => "saved"
+            },
+        ],
+        "sidebar" => [
+        ]
+    }
+};
+
+$json = JSON::to_json( $payload );
+$res  = $m->post(
+    $url . 'Helpers/UpdateDashboard',
+    [ content => $json ],
+);
+is( $res->code, 200, "add 'personal search' to body" );
 
-$m->content_contains("The following queries may not be visible to all users who can see this dashboard.");
+$m->get_ok($url."Dashboards/Queries.html?id=$system_dashboard_id");
+$m->content_contains("Warning: may not be visible to all viewers");
 
 $m->follow_link_ok({id => 'page-show'});
 $m->content_contains("personal search", "saved search shows up");
@@ -231,7 +283,7 @@ $omech->content_lacks("dashboard test", "matched ticket doesn't show up");
 $omech->warning_like(qr/User .* tried to load container user /, "can't see other users' personal searches");
 
 # make sure that navigating to dashboard pages with bad IDs throws an error
-my ($bad_id) = $personal =~ /^search-(\d+)/;
+my $bad_id = $system_dashboard_id + 1;
 
 for my $page (qw/Modify Queries Render Subscription/) {
     $m->get("/Dashboards/$page.html?id=$bad_id");
diff --git a/t/web/dashboards-search-cache.t b/t/web/dashboards-search-cache.t
index 18989d54e..66d5d7e6e 100644
--- a/t/web/dashboards-search-cache.t
+++ b/t/web/dashboards-search-cache.t
@@ -1,7 +1,7 @@
 use strict;
 use warnings;
 
-use RT::Test tests => 33;
+use RT::Test tests => undef;
 my ($baseurl, $m) = RT::Test->started_ok;
 
 my $url = $m->rt_base_url;
@@ -21,48 +21,78 @@ $m->field(SavedSearchDescription => 'Original Name');
 $m->click('SavedSearchSave');
 
 # create the inner dashboard
-$m->get_ok("$url/Dashboards/Modify.html?Create=1");
+$m->get_ok($url . "Dashboards/Modify.html?Create=1");
 $m->form_name('ModifyDashboard');
 $m->field('Name' => 'inner dashboard');
 $m->click_button(value => 'Create');
 $m->text_contains('Saved dashboard inner dashboard');
 
 my ($inner_id) = $m->content =~ /name="id" value="(\d+)"/;
-ok($inner_id, "got an ID, $inner_id");
+ok($inner_id, "got an ID for inner dashboard, $inner_id");
 
 # create a dashboard
-$m->get_ok("$url/Dashboards/Modify.html?Create=1");
+$m->get_ok($url . "Dashboards/Modify.html?Create=1");
 $m->form_name('ModifyDashboard');
 $m->field('Name' => 'cachey dashboard');
 $m->click_button(value => 'Create');
 $m->text_contains('Saved dashboard cachey dashboard');
 
 my ($dashboard_id) = $m->content =~ /name="id" value="(\d+)"/;
-ok($dashboard_id, "got an ID, $dashboard_id");
+ok($dashboard_id, "got an ID for cachey dashboard, $dashboard_id");
 
 # add the search to the dashboard
 $m->follow_link_ok({text => 'Content'});
-my $form = $m->form_name('Dashboard-Searches-body');
-my @input = $form->find_input('Searches-body-Available');
-my ($search_value) =
-  map { ( $_->possible_values )[1] }
-  grep { ( $_->value_names )[1] =~ /Saved Search: Original Name/ } @input;
-$form->value('Searches-body-Available' => $search_value );
-$m->click_button(name => 'add');
-$m->text_contains('Dashboard updated');
 
-# add the dashboard to the dashboard
-$m->follow_link_ok({text => 'Content'});
-$form = $m->form_name('Dashboard-Searches-body');
- at input = $form->find_input('Searches-body-Available');
-my ($dashboard_value) =
-  map { ( $_->possible_values )[1] }
-  grep { ( $_->value_names )[1] =~ /Dashboard: inner dashboard/ } @input;
-$form->value('Searches-body-Available' => $dashboard_value );
-$m->click_button(name => 'add');
-$m->text_contains('Dashboard updated');
+# we need to get the saved search id from the content before submitting the json.
+my ($saved_search_id) = $m->content =~ /data-type="saved" data-name="RT::User-14-SavedSearch-(\d+)"/;
+ok($saved_search_id, "got an ID for the saved search, $saved_search_id");
+
+# add 'Original Name' portlet to body
+my $payload = {
+    "dashboard_id" => $dashboard_id,
+    "panes"        => {
+        "body"    => [
+            {
+              "description" => "Original Name",
+              "name" => "RT::User-14-SavedSearch-" . $saved_search_id,
+              "searchId" => "",
+              "searchType" => "Ticket",
+              "type" => "saved"
+            },
+        ],
+        "sidebar" => [
+        ]
+    }
+};
+
+my $json = JSON::to_json( $payload );
+my $res  = $m->post(
+    $url . 'Helpers/UpdateDashboard',
+    [ content => $json ],
+);
+is( $res->code, 200, "add 'Original Name' portlet to body" );
+
+# add 'inner dashboard' portlet to body
+push(
+    @{$payload->{panes}->{body}},
+    {
+      "description" => "inner dashboard",
+      "name" => "dashboard-" . $inner_id . "-RT::User-14",
+      "searchId" => "",
+      "searchType" => "",
+      "type" => "dashboard"
+    },
+);
+
+$json = JSON::to_json( $payload );
+$res  = $m->post(
+    $url . 'Helpers/UpdateDashboard',
+    [ content => $json ],
+);
+is( $res->code, 200, "add 'inner dashboard' portlet to body" );
 
 # subscribe to the dashboard
+$m->get_ok($url . "Dashboards/" . $dashboard_id . "/cachey%20dashboard");
 $m->follow_link_ok({text => 'Subscription'});
 $m->text_contains('Saved Search: Original Name');
 $m->text_contains('Dashboard: inner dashboard');
@@ -72,9 +102,9 @@ $m->text_contains('Subscribed to dashboard cachey dashboard');
 
 # rename the search
 $m->follow_link_ok({text => 'Tickets'}, 'to query builder');
-$form = $m->form_name('BuildQuery');
- at input = $form->find_input('SavedSearchLoad');
-($search_value) =
+my $form = $m->form_name('BuildQuery');
+my @input = $form->find_input('SavedSearchLoad');
+my ($search_value) =
   map { ( $_->possible_values )[1] }
   grep { ( $_->value_names )[1] =~ /Original Name/ } @input;
 $form->value('SavedSearchLoad' => $search_value );
@@ -87,16 +117,16 @@ $m->click_button(value => 'Update');
 $m->text_contains('Updated saved search "New Name"');
 
 # rename the dashboard
-$m->get_ok("/Dashboards/Modify.html?id=$inner_id");
+$m->get_ok($url . "Dashboards/Modify.html?id=$inner_id");
 $m->form_name('ModifyDashboard');
 $m->field('Name' => 'recursive dashboard');
 $m->click_button(value => 'Save Changes');
 $m->text_contains('Dashboard recursive dashboard updated');
 
 # check subscription page again
-$m->get_ok("/Dashboards/Subscription.html?id=$dashboard_id");
 TODO: {
     local $TODO = 'we cache search names too aggressively';
+    $m->get_ok($url . "Dashboards/Subscription.html?id=$dashboard_id");
     $m->text_contains('Saved Search: New Name');
     $m->text_unlike(qr/Saved Search: Original Name/); # t-w-m lacks text_lacks
 
@@ -104,6 +134,8 @@ TODO: {
     $m->text_unlike(qr/Dashboard: inner dashboard/); # t-w-m lacks text_lacks
 }
 
-$m->get_ok("/Dashboards/Render.html?id=$dashboard_id");
+$m->get_ok($url . "Dashboards/Render.html?id=$dashboard_id");
 $m->text_contains('New Name');
 $m->text_unlike(qr/Original Name/); # t-w-m lacks text_lacks
+
+done_testing();

-----------------------------------------------------------------------


More information about the rt-commit mailing list