[Rt-commit] rt 02/10: Migrate "RT at a glance" homepage to dashboard

sunnavy sunnavy at bestpractical.com
Fri Jul 9 16:45:26 EDT 2021


This is an automated email from the git hooks/post-receive script.

sunnavy pushed a commit to branch 5.0/use-dashboard-for-homepage-select-ui
in repository rt.

commit 4ee541e2cb8e8c30fb7c7fdd28c7f28c00337bd4
Author: sunnavy <sunnavy at bestpractical.com>
AuthorDate: Fri May 14 02:10:29 2021 +0800

    Migrate "RT at a glance" homepage to dashboard
    
    Technically, "HomepageSettings" attributes are converted to dashboards
    named "Homepage", and "DefaultDashboard" attributes keep the id of the
    chosen dashboards to render "RT at a glance".
    
    As there is no special format for "HomepageSettings", method
    UpdateDashboard is simplifed to handle dashboards only.
---
 etc/initialdata                          | 136 ++++++++++++++-------
 etc/upgrade/5.0.2/content                | 149 +++++++++++++++++++++++
 lib/RT/Attribute.pm                      | 180 ++++++++++-----------------
 lib/RT/Dashboard.pm                      |  14 +++
 lib/RT/Handle.pm                         |  41 +++----
 lib/RT/Interface/Web.pm                  | 201 ++++++++++++++++++++-----------
 lib/RT/Migrate/Serializer/JSON.pm        |  37 +++---
 share/html/Admin/Global/MyRT.html        | 133 +++-----------------
 share/html/Admin/Users/MyRT.html         | 137 +++------------------
 share/html/Elements/MyRT                 |  43 +++++--
 share/html/Elements/SelectDashboard      |  91 ++++++++++++++
 share/html/Elements/ShowSearch           |  19 ++-
 share/html/Prefs/MyRT.html               | 150 ++++-------------------
 share/html/Search/Elements/EditSearches  |   6 +-
 share/static/css/elevator-light/misc.css |   4 +
 15 files changed, 684 insertions(+), 657 deletions(-)

diff --git a/etc/initialdata b/etc/initialdata
index 61522db700..d69b1ef33e 100644
--- a/etc/initialdata
+++ b/etc/initialdata
@@ -900,50 +900,6 @@ Hour:         { $SubscriptionObj->SubValue('Hour') }
         OrderBy => 'LastUpdated',
         Order   => 'DESC' },
     },
-    {
-        Name        => 'HomepageSettings',
-        Description => 'HomepageSettings',
-        Content     => {
-            'body' =>                               # loc_left_pair
-              [
-                {
-                    type => 'system',
-                    name => 'My Tickets',           # loc
-                },
-                {
-                    type => 'system',
-                    name => 'Unowned Tickets'       # loc
-                },
-                {
-                    type => 'system',
-                    name => 'Bookmarked Tickets'    # loc
-                },
-                {
-                    type => 'component',
-                    name => 'QuickCreate'           # loc
-                },
-              ],
-            'sidebar' =>                            # loc_left_pair
-              [
-                {
-                    type => 'component',
-                    name => 'MyReminders'           # loc
-                },
-                {
-                    type => 'component',
-                    name => 'QueueList'           # loc
-                },
-                {
-                    type => 'component',
-                    name => 'Dashboards'            # loc
-                },
-                {
-                    type => 'component',
-                    name => 'RefreshHomepage'       # loc
-                },
-              ],
-        },
-    },
 # initial reports
     { Name => 'ReportsInMenu',
       Description => 'Content of the Reports menu', #loc
@@ -988,3 +944,95 @@ Hour:         { $SubscriptionObj->SubValue('Hour') }
         MaxValues         => 1,
     },
 );
+
+ at Final = (
+    sub {
+        my $dashboard = RT::Dashboard->new( RT->SystemUser );
+        my ( $ret, $msg ) = $dashboard->Save(
+            Name    => 'Homepage',
+            Privacy => join( '-', ref( RT->System ), RT->System->Id ),
+        );
+
+        if ($ret) {
+            my @searches;
+            for my $search_name ( 'My Tickets', 'Unowned Tickets', 'Bookmarked Tickets' ) {
+                my ($search) = RT::System->new( RT->SystemUser )->Attributes->Named( 'Search - ' . $search_name );
+                if ($search) {
+                    push @searches,
+                        {
+                        pane         => 'body',
+                        portlet_type => 'search',
+                        id           => $search->Id,
+                        description  => "Saved Search: $search_name",
+                        privacy      => join( '-', ref( RT->System ), RT->System->Id ),
+                        };
+                }
+                else {
+                    RT->Logger->error("Couldn't find search $search_name");
+                }
+            }
+
+            my $panes = {
+                body => [
+                    @searches,
+                    {   pane         => 'body',
+                        portlet_type => 'component',
+                        component    => 'QuickCreate',
+                        description  => 'QuickCreate',
+                        path         => '/Elements/QuickCreate',
+                    },
+                ],
+                sidebar => [
+                    {   pane         => 'sidebar',
+                        portlet_type => 'component',
+                        component    => 'MyReminders',
+                        description  => 'MyReminders',
+                        path         => '/Elements/MyReminders',
+                    },
+                    {   pane         => 'sidebar',
+                        portlet_type => 'component',
+                        component    => 'QueueList',
+                        description  => 'QueueList',
+                        path         => '/Elements/QueueList',
+                    },
+                    {   pane         => 'sidebar',
+                        portlet_type => 'component',
+                        component    => 'MyReminders',
+                        description  => 'MyReminders',
+                        path         => '/Elements/MyReminders',
+                    },
+                    {   pane         => 'sidebar',
+                        portlet_type => 'component',
+                        component    => 'Dashboards',
+                        description  => 'Dashboards',
+                        path         => '/Elements/Dashboards',
+                    },
+                    {   pane         => 'sidebar',
+                        portlet_type => 'component',
+                        component    => 'RefreshHomepage',
+                        description  => 'RefreshHomepage',
+                        path         => '/Elements/RefreshHomepage',
+                    },
+                ]
+            };
+
+            # fill content
+            my ( $ret, $msg ) = $dashboard->Update( Panes => $panes );
+            if ( !$ret ) {
+                RT->Logger->error("Couldn't update content for dashboard Homepage: $msg");
+            }
+
+            ( $ret, $msg ) = RT->System->SetAttribute(
+                'Name'        => 'DefaultDashboard',
+                'Description' => 'Default Dashboard',
+                'Content'     => $dashboard->Id,
+            );
+            if ( !$ret ) {
+                RT->Logger->error("Couldn't set DefaultDashboard: $msg");
+            }
+        }
+        else {
+            RT->Logger->error("Couldn't create dashboard Homepage: $msg");
+        }
+    },
+);
diff --git a/etc/upgrade/5.0.2/content b/etc/upgrade/5.0.2/content
index ad6f07edda..f768c9f69a 100644
--- a/etc/upgrade/5.0.2/content
+++ b/etc/upgrade/5.0.2/content
@@ -26,3 +26,152 @@ our @Initial = (
         }
     }
 );
+
+our @Final = (
+    sub {
+        RT->Logger->debug("Converting homepages to dashboards");
+        my $attrs = RT::Attributes->new( RT->SystemUser );
+        $attrs->Limit( FIELD => 'Name', VALUE => [ 'Pref-HomepageSettings', 'HomepageSettings' ], OPERATOR => 'IN' );
+    OUTER: while ( my $attr = $attrs->Next ) {
+            my $attr_id = $attr->Id;
+            my $object  = $attr->Object;
+            my $content = $attr->Content;
+
+            if ( $object && ( $object->isa('RT::User') || $object->isa('RT::System') ) && $content ) {
+                my $dashboard = RT::Dashboard->new( RT->SystemUser );
+                my $panes     = {};
+
+                for my $pane ( sort keys %$content ) {
+                    my $list = $content->{$pane} or next;
+                    for my $entry (@$list) {
+                        my $new_entry = { pane => $pane };
+                        if ( $entry->{type} eq 'system' ) {
+                            if ( my $name = $entry->{name} ) {
+                                my ($search)
+                                    = RT::System->new( RT->SystemUser )->Attributes->Named( 'Search - ' . $name );
+
+                                # Check user created system searches
+                                if ( !$search ) {
+                                    my (@searches)
+                                        = RT::System->new( RT->SystemUser )->Attributes->Named('SavedSearch');
+                                    for my $custom (@searches) {
+                                        if ( $custom->Description eq $entry->{name} ) {
+                                            $search = $custom;
+                                            last;
+                                        }
+                                    }
+                                }
+
+                                if ( $search ) {
+                                    $new_entry->{portlet_type} = 'search';
+                                    $new_entry->{id}           = $search->Id;
+                                    $new_entry->{description}  = "Saved Search: $name";
+                                    $new_entry->{privacy}      = 'RT::System-1';
+                                }
+                                else {
+                                    RT->Logger->warning(
+                                        "System search $name in attribute #$attr_id not found, skipping");
+                                    next;
+                                }
+                            }
+                            else {
+                                RT->Logger->warning("Missing system search name in attribute #$attr_id, skipping");
+                                next;
+                            }
+                        }
+                        elsif ( $entry->{type} eq 'saved' ) {
+                            if ( my $name = $entry->{name} ) {
+                                if ( $name =~ /(.+)-SavedSearch-(\d+)/ ) {
+                                    $new_entry->{privacy}      = $1;
+                                    $new_entry->{id}           = $2;
+                                    $new_entry->{portlet_type} = 'search';
+                                    my $search = RT::Attribute->new( RT->SystemUser );
+                                    $search->Load( $new_entry->{id} );
+                                    if ( $search->Id ) {
+                                        $new_entry->{description} = "Saved Search: " . $search->Description;
+                                    }
+                                    else {
+                                        RT->Logger->warning(
+                                            "Saved search $name in attribute #$attr_id not found, skipping");
+                                        next;
+                                    }
+                                }
+                                else {
+                                    RT->Logger->warning(
+                                        "System search $name in attribute #$attr_id not found, skipping");
+                                    next;
+                                }
+                            }
+                            else {
+                                RT->Logger->warning("Missing system search name in attribute #$attr_id, skipping");
+                                next;
+                            }
+                        }
+                        elsif ( $entry->{type} eq 'component' ) {
+                            $new_entry->{portlet_type} = 'component';
+                            $new_entry->{component}    = $entry->{name};
+                            $new_entry->{description}  = $entry->{name};
+                            $new_entry->{path}         = "/Elements/$entry->{name}";
+                        }
+                        else {
+                            RT->Logger->warning("Unsupported type $entry->{type} in attribute #$attr_id, skipping");
+                            next;
+                        }
+                        push @{$panes->{$pane}}, $new_entry;
+                    }
+                }
+
+                $RT::Handle->BeginTransaction;
+                my %new_values = (
+                    'Name'        => 'Dashboard',
+                    'Description' => 'Homepage',
+                    'Content'     => { Panes => $panes },
+                );
+
+                for my $field ( sort keys %new_values ) {
+                    my $method = "Set$field";
+                    my ( $ret, $msg ) = $attr->$method( $new_values{$field} );
+                    if ( !$ret ) {
+                        RT->Logger->error("Couldn't update $field of attribute #$attr_id: $msg");
+                        $RT::Handle->Rollback;
+                        next OUTER;
+                    }
+                }
+
+                my ( $id, $msg ) = $object->SetAttribute(
+                    'Name'        => $object->isa('RT::User') ? 'Pref-DefaultDashboard' : 'DefaultDashboard',
+                    'Description' => 'Default Dashboard',
+                    'Content'     => $attr_id,
+                );
+                if ($id) {
+                    $RT::Handle->Commit;
+                }
+                else {
+                    RT->Logger->error("Couldn't set DefaultDashboard to $id for attribute #$attr_id: $msg");
+                    $RT::Handle->Rollback;
+                }
+            }
+        }
+    },
+    sub {
+        my $acl = RT::ACL->new(RT->SystemUser);
+
+        # Grant dashboard rights so users with ModifySelf can still
+        # customize MyRT
+        RT->Logger->debug("Granting dashboard rights to users with ModifySelf");
+        $acl->Limit( FIELD => 'RightName', VALUE => 'ModifySelf' );
+        while ( my $ace = $acl->Next ) {
+            my $object = $ace->Object;
+            my $principal = $ace->PrincipalObj;
+
+            for my $right ( 'SeeOwnDashboard', 'CreateOwnDashboard', 'ModifyOwnDashboard', 'DeleteOwnDashboard' ) {
+                if ( !$principal->HasRight( Object => $object, Right => $right ) ) {
+                    my ( $ret, $msg ) = $principal->GrantRight( Object => $object, Right => $right );
+                    if ( !$ret ) {
+                        RT->Logger->error( "Couldn't grant $right to user #" . $object->Id . ": $msg" );
+                    }
+                }
+            }
+        }
+    },
+);
diff --git a/lib/RT/Attribute.pm b/lib/RT/Attribute.pm
index 21db2c8778..d3032a6162 100644
--- a/lib/RT/Attribute.pm
+++ b/lib/RT/Attribute.pm
@@ -418,10 +418,10 @@ sub Delete {
     # Get values even if current user doesn't have right to see
     my $name = $self->__Value('Name');
     my @links;
-    if ( $name =~ /^(Dashboard|(?:Pref-)?HomepageSettings)$/ ) {
+    if ( $name =~ /^(?:Dashboard|(?:Pref-)?DefaultDashboard)$/ ) {
         push @links, @{ $self->DependsOn->ItemsArrayRef };
     }
-    elsif ( $name eq 'SavedSearch' ) {
+    elsif ( $name =~ /^(?:Dashboard|SavedSearch)$/ ) {
         push @links, @{ $self->DependedOnBy->ItemsArrayRef };
     }
 
@@ -814,32 +814,11 @@ sub FindDependencies {
             }
         }
     }
-    # homepage settings attribute has dependencies on each of the searches in it
-    elsif ($self->Name eq RT::User::_PrefName("HomepageSettings")) {
-        my $content = $self->Content;
-        for my $pane (values %{ $content || {} }) {
-            for my $component (@$pane) {
-                # this hairy code mirrors what's in the saved search loader
-                # in /Elements/ShowSearch
-                if ($component->{type} eq 'saved') {
-                    if ($component->{name} =~ /^(.*?)-(\d+)-SavedSearch-(\d+)$/) {
-                        my $attr = RT::Attribute->new($self->CurrentUser);
-                        $attr->LoadById($3);
-                        $deps->Add( out => $attr );
-                    }
-                }
-                elsif ($component->{type} eq 'system') {
-                    my ($search) = RT::System->new($self->CurrentUser)->Attributes->Named( 'Search - ' . $component->{name} );
-                    unless ( $search && $search->Id ) {
-                        my (@custom_searches) = RT::System->new($self->CurrentUser)->Attributes->Named('SavedSearch');
-                        foreach my $custom (@custom_searches) {
-                            if ($custom->Description eq $component->{name}) { $search = $custom; last }
-                        }
-                    }
-                    $deps->Add( out => $search ) if $search;
-                }
-            }
-        }
+    # DefaultDashboard has id of dashboard it uses
+    elsif ($self->Name =~ /^(?:Pref-)?DefaultDashboard$/) {
+        my $attr = RT::Attribute->new( $self->CurrentUser );
+        $attr->LoadById($self->Content);
+        $deps->Add( out => $attr ) if $attr->Id;
     }
     # dashboards have dependencies on all the searches and dashboards they use
     elsif ($self->Name eq 'Dashboard' || $self->Name eq 'SelfServiceDashboard') {
@@ -930,36 +909,23 @@ sub PostInflateFixup {
         }
         $self->SetContent($content);
     }
-    # decode UIDs to be saved searches
-    elsif ($self->Name eq RT::User::_PrefName("HomepageSettings")) {
+    elsif ( $self->Name =~ /^(?:Pref-)?DefaultDashboard$/ ) {
         my $content = $self->Content;
-
-        for my $pane (values %{ $content || {} }) {
-            for (@$pane) {
-                if (ref($_->{uid}) eq 'SCALAR') {
-                    my $uid = $_->{uid};
-                    my $attr = $importer->LookupObj($$uid);
-
-                    if ($attr) {
-                        if ($_->{type} eq 'saved') {
-                            $_->{name} = join '-', $attr->ObjectType, $attr->ObjectId, 'SavedSearch', $attr->id;
-                        }
-                        # if type is system, name doesn't need to change
-                        # if type is anything else, pass it through as is
-                        delete $_->{uid};
-                    }
-                    else {
-                        $importer->Postpone(
-                            for    => $$uid,
-                            uid    => $spec->{uid},
-                            method => 'PostInflateFixup',
-                        );
-                    }
-                }
+        if ( ref($content) eq 'SCALAR' ) {
+            my $attr = $importer->LookupObj($$_);
+            if ($attr) {
+                $content = $attr->Id;
+            }
+            else {
+                $importer->Postpone(
+                    for    => $$content,
+                    uid    => $spec->{uid},
+                    method => 'PostInflateFixup',
+                );
             }
         }
-        $self->SetContent($content);
     }
+    # decode UIDs to be saved searches
     elsif ($self->Name eq 'Dashboard') {
         my $content = $self->Content;
 
@@ -1034,38 +1000,11 @@ sub Serialize {
         }
         $store{Content} = $self->_SerializeContent($content);
     }
-    # encode saved searches to be UIDs
-    elsif ($store{Name} eq RT::User::_PrefName("HomepageSettings")) {
-        my $content = $self->_DeserializeContent($store{Content});
-        for my $pane (values %{ $content || {} }) {
-            for (@$pane) {
-                # this hairy code mirrors what's in the saved search loader
-                # in /Elements/ShowSearch
-                if ($_->{type} eq 'saved') {
-                    if ($_->{name} =~ /^(.*?)-(\d+)-SavedSearch-(\d+)$/) {
-                        my $attr = RT::Attribute->new($self->CurrentUser);
-                        $attr->LoadById($3);
-                        $_->{uid} = \($attr->UID);
-                    }
-                    # if we can't parse the name, just pass it through
-                }
-                elsif ($_->{type} eq 'system') {
-                    my ($search) = RT::System->new($self->CurrentUser)->Attributes->Named( 'Search - ' . $_->{name} );
-                    unless ( $search && $search->Id ) {
-                        my (@custom_searches) = RT::System->new($self->CurrentUser)->Attributes->Named('SavedSearch');
-                        foreach my $custom (@custom_searches) {
-                            if ($custom->Description eq $_->{name}) { $search = $custom; last }
-                        }
-                    }
-                    # if we can't load the search, just pass it through
-                    if ($search) {
-                        $_->{uid} = \($search->UID);
-                    }
-                }
-                # pass through everything else (e.g. component)
-            }
-        }
-        $store{Content} = $self->_SerializeContent($content);
+    elsif ( $store{Name} =~ /^(?:Pref-)?DefaultDashboard$/ ) {
+        my $content = $store{Content};
+        my $attr    = RT::Attribute->new( $self->CurrentUser );
+        $attr->LoadById($content);
+        $store{Content} = \$attr->UID;
     }
     # encode saved searches and dashboards to be UIDs
     elsif ($store{Name} eq 'Dashboard') {
@@ -1122,41 +1061,11 @@ sub _SyncLinks {
 
     my $success;
 
-    if ( $name =~ /^(Dashboard|(?:Pref-)?HomepageSettings)$/ ) {
-        my $type    = $1;
+    if ( $name eq 'Dashboard' ) {
         my $content = $self->_DeserializeContent( $self->__Value('Content') );
 
-        my %searches;
-        if ( $type eq 'Dashboard' ) {
-            %searches
-                = map { $_->{id} => 1 } grep { $_->{portlet_type} eq 'search' } @{ $content->{Panes}{body} },
-                @{ $content->{Panes}{sidebar} };
-        }
-        else {
-            for my $item ( @{ $content->{body} }, @{ $content->{sidebar} } ) {
-                if ( $item->{type} eq 'saved' ) {
-                    if ( $item->{name} =~ /SavedSearch-(\d+)/ ) {
-                        $searches{$1} ||= 1;
-                    }
-                }
-                elsif ( $item->{type} eq 'system' ) {
-                    if ( my $attr
-                        = RT::System->new( $self->CurrentUser )->FirstAttribute( 'Search - ' . $item->{name} ) )
-                    {
-                        $searches{ $attr->id } ||= 1;
-                    }
-                    else {
-                        my $attrs = RT::System->new( $self->CurrentUser )->Attributes;
-                        $attrs->Limit( FIELD => 'Name',        VALUE => 'SavedSearch' );
-                        $attrs->Limit( FIELD => 'Description', VALUE => $item->{name} );
-                        if ( my $attr = $attrs->First ) {
-                            $searches{ $attr->id } ||= 1;
-                        }
-
-                    }
-                }
-            }
-        }
+        my %searches = map { $_->{id} => 1 } grep { $_->{portlet_type} eq 'search' } @{ $content->{Panes}{body} },
+            @{ $content->{Panes}{sidebar} };
 
         my $links = $self->DependsOn;
         while ( my $link = $links->Next ) {
@@ -1182,6 +1091,39 @@ sub _SyncLinks {
             }
         }
     }
+    elsif ( $name =~ /^(?:Pref-)?DefaultDashboard$/ ) {
+        my $id    = $self->__Value('Content');
+        my $links = $self->DependsOn;
+        my $found;
+        while ( my $link = $links->Next ) {
+            if ( $link->TargetObj->id == $id ) {
+                $found = 1;
+            }
+            else {
+
+                my ( $ret, $msg ) = $link->Delete;
+                if ( !$ret ) {
+                    RT->Logger->error( "Couldn't delete link #" . $link->id . ": $msg" );
+                    $success //= 0;
+                }
+            }
+        }
+
+        if ( !$found ) {
+            my $link      = RT::Link->new( $self->CurrentUser );
+            my $attribute = RT::Attribute->new( $self->CurrentUser );
+            $attribute->Load($id);
+            if ( $attribute->id ) {
+                my ( $ret, $msg )
+                    = $link->Create( Type => 'DependsOn', Base => 'attribute:' . $self->id, Target => "attribute:$id" );
+                if ( !$ret ) {
+                    RT->Logger->error( "Couldn't create link for attribute #:" . $self->id . ": $msg" );
+                    $success //= 0;
+                }
+            }
+        }
+    }
+
     return $success // 1;
 }
 
diff --git a/lib/RT/Dashboard.pm b/lib/RT/Dashboard.pm
index b76c8e8370..7c3465f38b 100644
--- a/lib/RT/Dashboard.pm
+++ b/lib/RT/Dashboard.pm
@@ -460,6 +460,20 @@ sub Delete {
         while ( my $subscription = $subscriptions->Next ) {
             $subscription->Delete();
         }
+
+        my $homepages = RT::Attributes->new( RT->SystemUser );
+        $homepages->Limit(
+            FIELD    => 'Name',
+            VALUE    => [ 'DefaultDashboard', 'Pref-DefaultDashboard' ],
+            OPERATOR => 'IN',
+        );
+        $homepages->Limit(
+            FIELD => 'Content',
+            VALUE => $id,
+        );
+        while ( my $homepage = $homepages->Next ) {
+            $homepage->Delete();
+        }
     }
 
     return ( $status, $msg );
diff --git a/lib/RT/Handle.pm b/lib/RT/Handle.pm
index 24593f566f..4b36c123b1 100644
--- a/lib/RT/Handle.pm
+++ b/lib/RT/Handle.pm
@@ -1800,7 +1800,8 @@ sub InsertData {
 
         my %order = (
             'Dashboard'             => 1,
-            'HomepageSettings'      => 1,
+            'DefaultDashboard'      => 2,
+            'Pref-DefaultDashboard' => 2,
             'DashboardsInMenu'      => 2,
             'Pref-DashboardsInMenu' => 2,
             'Subscription'          => 2,
@@ -2798,36 +2799,15 @@ sub _LoadObject {
 sub _CanonilizeAttributeContent {
     my $self = shift;
     my $item = shift or return;
-    if ( $item->{Name} =~ /HomepageSettings$/ ) {
-        my $content = $item->{Content};
-        for my $type ( qw/body sidebar/ ) {
-            if ( $content->{$type} && ref $content->{$type} eq 'ARRAY' ) {
-                for my $entry ( @{ $content->{$type} } ) {
-                    if ( $entry->{ObjectType} && $entry->{ObjectId} && $entry->{Description} ) {
-                        if ( my $object = $self->_LoadObject( $entry->{ObjectType}, $entry->{ObjectId} ) ) {
-                            my $attributes = $object->Attributes;
-                            $attributes->Limit( FIELD => 'Name', VALUE => 'SavedSearch' );
-                            $attributes->Limit( FIELD => 'Description', VALUE => $entry->{Description} );
-                            if ( my $attribute = $attributes->First ) {
-                                $entry->{name} =
-                                  ref( $object ) . '-' . $object->Id . '-SavedSearch-' . $attribute->Id;
-                            }
-                        }
-                        delete $entry->{$_} for qw/ObjectType ObjectId Description/;
-                    }
-                }
-            }
-        }
-    }
-    elsif ( $item->{Name} eq 'Dashboard' ) {
+    if ( $item->{Name} eq 'Dashboard' ) {
         my $content = $item->{Content}{Panes};
         for my $type ( qw/body sidebar/ ) {
             if ( $content->{$type} && ref $content->{$type} eq 'ARRAY' ) {
                 for my $entry ( @{ $content->{$type} } ) {
+                    next unless $entry->{portlet_type} eq 'search';
                     if ( $entry->{ObjectType} && $entry->{ObjectId} && $entry->{Description} ) {
                         if ( my $object = $self->_LoadObject( $entry->{ObjectType}, $entry->{ObjectId} ) ) {
                             my $attributes = $object->Attributes;
-                            $attributes->Limit( FIELD => 'Name', VALUE => 'SavedSearch' );
                             $attributes->Limit( FIELD => 'Description', VALUE => $entry->{Description} );
                             if ( my $attribute = $attributes->First ) {
                                 $entry->{id} = $attribute->id;
@@ -2856,6 +2836,19 @@ sub _CanonilizeAttributeContent {
         }
         $item->{Content}{dashboards} = \@dashboards;
     }
+    elsif ( $item->{Name} =~ /^(?:Pref-)?DefaultDashboard$/ ) {
+        my $entry = $item->{Content};
+        if ( $entry->{ObjectType} && $entry->{ObjectId} && $entry->{Description} ) {
+            if ( my $object = $self->_LoadObject( $entry->{ObjectType}, $entry->{ObjectId} ) ) {
+                my $attributes = $object->Attributes;
+                $attributes->Limit( FIELD => 'Name',        VALUE => 'Dashboard' );
+                $attributes->Limit( FIELD => 'Description', VALUE => $entry->{Description} );
+                if ( my $attribute = $attributes->First ) {
+                    $item->{Content} = $attribute->id;
+                }
+            }
+        }
+    }
     elsif ( $item->{Name} eq 'Subscription' ) {
         my $entry = $item->{Content}{DashboardId};
         if ( $entry->{ObjectType} && $entry->{ObjectId} && $entry->{Description} ) {
diff --git a/lib/RT/Interface/Web.pm b/lib/RT/Interface/Web.pm
index a14a2cc69d..e82e0bf697 100644
--- a/lib/RT/Interface/Web.pm
+++ b/lib/RT/Interface/Web.pm
@@ -4838,81 +4838,63 @@ sub UpdateDashboard {
     }
 
     my ( $ok, $msg );
-    if ( $id eq 'MyRT' ) {
-        my $user = $session{CurrentUser};
-
-        if ( my $user_id = $args->{user_id} ) {
-            my $UserObj = RT::User->new( $session{'CurrentUser'} );
-            ( $ok, $msg ) = $UserObj->Load($user_id);
-            return ( $ok, $msg ) unless $ok;
-
-            return ( $ok, $msg ) = $UserObj->SetPreferences( 'HomepageSettings', $data->{panes} );
-        } elsif ( $args->{is_global} ) {
-            my $sys = RT::System->new( $session{'CurrentUser'} );
-            my ($default_portlets) = $sys->Attributes->Named('HomepageSettings');
-            return ( $ok, $msg ) = $default_portlets->SetContent( $data->{panes} );
-        } else {
-            return ( $ok, $msg ) = $user->SetPreferences( 'HomepageSettings', $data->{panes} );
-        }
-    } else {
-        my $class = $args->{self_service_dashboard} ? 'RT::Dashboard::SelfService' : 'RT::Dashboard';
-        my $Dashboard = $class->new( $session{'CurrentUser'} );
-        ( $ok, $msg ) = $Dashboard->LoadById($id);
-
-        # report error at the bottom
-        return ( $ok, $msg ) unless $ok && $Dashboard->Id;
-
-        my $content;
-        for my $pane_name ( keys %{ $data->{panes} } ) {
-            my @pane;
-
-            for my $item ( @{ $data->{panes}{$pane_name} } ) {
-                my %saved;
-                $saved{pane}         = $pane_name;
-                $saved{portlet_type} = $item->{type};
-
-                $saved{description} = $available_items->{ $item->{type} }{ $item->{name} }{label};
-
-                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 'saved' ) {
-                    $saved{portlet_type} = 'search';
-
-                    $item->{searchType} = $available_items->{ $item->{type} }{ $item->{name} }{search_type}
-                                          if exists $available_items->{ $item->{type} }{ $item->{name} }{search_type};
-
-                    my $type = $item->{searchType};
-                    $type = 'Saved Search' if !$type || $type eq 'Ticket';
-                    $saved{description} = loc($type) . ': ' . $saved{description};
-
-                    $item->{searchId} = $available_items->{ $item->{type} }{ $item->{name} }{search_id}
-                                        if exists $available_items->{ $item->{type} }{ $item->{name} }{search_id};
-
-                    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;
+    my $class = $args->{self_service_dashboard} ? 'RT::Dashboard::SelfService' : 'RT::Dashboard';
+    my $Dashboard = $class->new( $session{'CurrentUser'} );
+    ( $ok, $msg ) = $Dashboard->LoadById($id);
+
+    # report error at the bottom
+    return ( $ok, $msg ) unless $ok && $Dashboard->Id;
+
+    my $content;
+    for my $pane_name ( keys %{ $data->{panes} } ) {
+        my @pane;
+
+        for my $item ( @{ $data->{panes}{$pane_name} } ) {
+            my %saved;
+            $saved{pane}         = $pane_name;
+            $saved{portlet_type} = $item->{type};
+
+            $saved{description} = $available_items->{ $item->{type} }{ $item->{name} }{label};
+
+            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 'saved' ) {
+                $saved{portlet_type} = 'search';
+
+                $item->{searchType} = $available_items->{ $item->{type} }{ $item->{name} }{search_type}
+                                      if exists $available_items->{ $item->{type} }{ $item->{name} }{search_type};
+
+                my $type = $item->{searchType};
+                $type = 'Saved Search' if !$type || $type eq 'Ticket';
+                $saved{description} = loc($type) . ': ' . $saved{description};
+
+                $item->{searchId} = $available_items->{ $item->{type} }{ $item->{name} }{search_id}
+                                    if exists $available_items->{ $item->{type} }{ $item->{name} }{search_id};
+
+                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};
             }
 
-            $content->{$pane_name} = \@pane;
+            push @pane, \%saved;
         }
 
-        return ( $ok, $msg ) = $Dashboard->Update( Panes => $content );
+        $content->{$pane_name} = \@pane;
     }
+
+    return ( $ok, $msg ) = $Dashboard->Update( Panes => $content );
 }
 
 =head2 ListOfReports
@@ -5228,6 +5210,85 @@ sub PreprocessTimeUpdates {
     RT::Interface::Web::PreprocessTimeUpdates(@_);
 }
 
+
+=head2 GetDashboards Objects => ARRAY, CurrentUser => CURRENT_USER
+
+Return available dashboards that are saved in the name of objects for
+specified user.
+
+=cut
+
+sub GetDashboards {
+    my %args = (
+        Objects     => undef,
+        CurrentUser => $session{CurrentUser},
+        @_,
+    );
+
+    return unless $args{CurrentUser};
+
+    $args{Objects} ||= [ RT::Dashboard->new( $args{CurrentUser} )->ObjectsForLoading( IncludeSuperuserGroups => 1 ) ];
+
+    my ($system_default) = RT::System->new( $args{'CurrentUser'} )->Attributes->Named('DefaultDashboard');
+    my $default_dashboard_id = $system_default ? $system_default->Content : 0;
+
+    my $found_system_default;
+
+    require RT::Dashboards;
+    my %dashboards;
+    my %system_default;
+    foreach my $object ( @{ $args{Objects} } ) {
+        my $list = RT::Dashboards->new( $args{CurrentUser} );
+        $list->LimitToPrivacy( join( '-', ref($object), $object->Id ) );
+        my $section;
+        if ( ref $object eq 'RT::User' && $object->Id == $session{CurrentUser}->Id ) {
+            $section = loc("My dashboards");
+        }
+        else {
+            $section = loc( "[_1]'s dashboards", $object->Name );
+        }
+
+        while ( my $dashboard = $list->Next ) {
+            # Use current logged in user to determine if to return link or not
+            $dashboard->CurrentUser( $session{CurrentUser} );
+            push @{ $dashboards{$section} },
+                {   id        => $dashboard->Id,
+                    name      => $dashboard->Name,
+                    view_link => $dashboard->CurrentUserCanSee()
+                    ? join( '/', RT->Config->Get('WebPath'), 'Dashboards', $dashboard->Id, $dashboard->Name )
+                    : '',
+                    edit_link => $dashboard->CurrentUserCanModify()
+                    ? join( '/', RT->Config->Get('WebPath'), 'Dashboards', 'Queries.html?id=' . $dashboard->Id )
+                    : '',
+                };
+
+            if ( $dashboard->Id == $default_dashboard_id ) {
+                %system_default = ( section => $section, %{ $dashboards{$section}[-1] } );
+            }
+        }
+    }
+
+    if (%system_default) {
+        push @{ $dashboards{ $system_default{section} } },
+            {   id        => 0,
+                name      => loc('System Default') . " ($system_default{name})",
+                view_link => $system_default{view_link},
+                edit_link => $system_default{edit_link},
+            };
+    }
+    else {
+        push @{$dashboards{"System's dashboards"}}, {
+            id   => 0,
+            name => loc('System Default'),
+        };
+    }
+
+    for my $section ( keys %dashboards ) {
+        @{ $dashboards{$section} } = sort { lc $a->{name} cmp lc $b->{name} } @{ $dashboards{$section} };
+    }
+    return \%dashboards;
+}
+
 package RT::Interface::Web;
 RT::Base->_ImportOverlays();
 
diff --git a/lib/RT/Migrate/Serializer/JSON.pm b/lib/RT/Migrate/Serializer/JSON.pm
index c12eac1dd3..d163ceb9cf 100644
--- a/lib/RT/Migrate/Serializer/JSON.pm
+++ b/lib/RT/Migrate/Serializer/JSON.pm
@@ -483,28 +483,7 @@ sub CanonicalizeAttributes {
                 );
             }
             else {
-                if ( $record->{Name} =~ /HomepageSettings$/ ) {
-                    my $content = $record->{Content};
-                    for my $type ( qw/body sidebar/ ) {
-                        if ( $content->{$type} && ref $content->{$type} eq 'ARRAY' ) {
-                            for my $item ( @{ $content->{$type} } ) {
-                                if ( $item->{type} eq 'saved' && $item->{name} =~ /RT::\w+-\d+-SavedSearch-(\d+)/ ) {
-                                    my $id        = $1;
-                                    my $attribute = RT::Attribute->new( RT->SystemUser );
-                                    $attribute->Load( $id );
-                                    if ( $attribute->id ) {
-                                        $item->{ObjectType}  = $attribute->ObjectType;
-                                        $item->{ObjectId}    = $attribute->Object->Name;
-                                        $item->{Description} = $attribute->Description;
-                                        delete $item->{name};
-                                    }
-                                }
-                                delete $item->{uid};
-                            }
-                        }
-                    }
-                }
-                elsif ( $record->{Name} eq 'Dashboard' ) {
+                if ( $record->{Name} eq 'Dashboard' ) {
                     my $content = $record->{Content}{Panes};
                     for my $type ( qw/body sidebar/ ) {
                         if ( $content->{$type} && ref $content->{$type} eq 'ARRAY' ) {
@@ -559,6 +538,20 @@ sub CanonicalizeAttributes {
                 }
             }
         }
+        elsif ( $record->{Name} =~ /^(?:Pref-)?DefaultDashboard$/ ) {
+            if ( ref $record->{Content} eq 'SCALAR' && ${$record->{Content}} =~ /(\d+)$/ ) {
+                my $id        = $1;
+                my $attribute = RT::Attribute->new( RT->SystemUser );
+                $attribute->Load($id);
+                my $dashboard = {};
+                if ( $attribute->id ) {
+                    $dashboard->{ObjectType}  = $attribute->ObjectType;
+                    $dashboard->{ObjectId}    = $attribute->Object->Name;
+                    $dashboard->{Description} = $attribute->Description;
+                    $record->{Content}        = $dashboard;
+                }
+            }
+        }
 
         if ( ${$record->{Object}} =~ /^(RT::\w+)-/ ) {
             $record->{ObjectType} = $1;
diff --git a/share/html/Admin/Global/MyRT.html b/share/html/Admin/Global/MyRT.html
index f5728279a1..bd375c3b8e 100644
--- a/share/html/Admin/Global/MyRT.html
+++ b/share/html/Admin/Global/MyRT.html
@@ -49,129 +49,32 @@
 <& /Elements/Tabs &>
 <& /Elements/ListActions, actions => \@results &>
 
-<form method="post" name="UpdateSearches" class="mx-auto max-width-lg">
-  <& /Widgets/SearchSelection,
-    pane_name => \%pane_name,
-    sections  => \@sections,
-    selected  => \%selected,
-    filters   => \@filters,
-  &>
-  <input type="hidden" name="dashboard_id" value="MyRT">
-  <input type="hidden" name="is_global" value="1">
-  <& /Elements/Submit, Name => "UpdateSearches", Label => loc('Save') &>
+<&|/Widgets/TitleBox, title => loc('Set Homepage'), bodyclass => "", class => "mx-auto max-width-lg" &>
+<form method="post" name="UpdateDefaultDashboard" class="mx-auto max-width-lg">
+  <div class="form-row">
+    <div class="col-12">
+      <& /Elements/SelectDashboard, Dashboards => GetDashboards( Objects => [ RT->System ] ), Default => $default_dashboard_id, ShowEmpty => 0 &>
+    </div>
+  </div>
 </form>
+</&>
 
 <%INIT>
+Abort( loc("Permission Denied") ) unless $session{CurrentUser}->HasRight( Right => 'SuperUser', Object => RT->System );
+
 my @results;
 my $title = loc("Customize").' '.loc("Global RT at a glance");
 
-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;
-
-    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 %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'),
-);
-
-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,
-);
-
-if ($ARGS{UpdateSearches}) {
-    my ($ok, $msg) = UpdateDashboard( \%ARGS, \%item_for );
-    push @results, $ok ? loc('Preferences saved.') : $msg;
+my ($system_default) = RT::System->new( $session{'CurrentUser'} )->Attributes->Named('DefaultDashboard');
+my $default_dashboard_id = $system_default ? $system_default->Content : 0;
 
+my ($default) = map { /^DefaultDashboard-(\d+)/ ? $1 : () } keys %ARGS;
+if ( $default ) {
+    my ( $ret, $msg ) = RT->System->SetAttribute( Name => 'DefaultDashboard', Description => 'Default Dashboard', Content => $default );
+    push @results, $ret ? loc('Preferences saved.') : $msg;
     MaybeRedirectForResults(
-        Actions   => \@results,
-        Path      => "/Admin/Global/MyRT.html",
+        Actions => \@results,
+        Path    => "/Admin/Global/MyRT.html",
     );
 }
 
diff --git a/share/html/Admin/Users/MyRT.html b/share/html/Admin/Users/MyRT.html
index 7f97e567a1..e832f9dcd9 100644
--- a/share/html/Admin/Users/MyRT.html
+++ b/share/html/Admin/Users/MyRT.html
@@ -49,138 +49,41 @@
 <& /Elements/Tabs &>
 <& /Elements/ListActions, actions => \@actions &>
 
+<&|/Widgets/TitleBox, title => loc('Set Homepage'), bodyclass => "", class => "mx-auto max-width-lg" &>
 <form method="post" action="MyRT.html" name="UpdateSearches" class="mx-auto max-width-lg">
-  <& /Widgets/SearchSelection,
-                    pane_name => \%pane_name,
-                    sections  => \@sections,
-                    selected  => \%selected,
-                    filters   => \@filters,
-    &>
-<input type="hidden" name="id" value="<% $id %>"/>
-<input type="hidden" name="dashboard_id" value="MyRT">
-<& /Elements/Submit, Name => "UpdateSearches", Label => loc('Save') &>
-</form>
-<form method="post" action="MyRT.html?id=<% $id %>" class="mx-auto max-width-lg">
-  <input type="hidden" name="Reset" value="1" />
-  <& /Elements/Submit, Label => loc('Reset to default') &>
+  <input type="hidden" name="id" value="<% $id %>" />
+  <div class="form-row">
+    <div class="col-12">
+      <& /Elements/SelectDashboard, Dashboards => GetDashboards( CurrentUser => $UserObj ), Default => $default_dashboard_id &>
+    </div>
+  </div>
 </form>
+</&>
 
 <%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}) {
-    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 @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'});
-$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;
-
-     for ($m->comp("/Search/Elements/SearchesForObject", Object => $object)) {
-         my ($desc, $loc_desc, $search) = @$_;
+my $default_dashboard_id = $UserObj->Preferences( DefaultDashboard => 0 );
 
-         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;
+my ($default) = map { /^DefaultDashboard-(\d+)/ ? $1 : () } keys %ARGS;
+if ( defined $default ) {
+    my ( $ret, $msg );
+    if ( $default ) {
+        ( $ret, $msg ) = $UserObj->SetPreferences( 'DefaultDashboard', $default );
+    }
+    else {
+        if ( $default_dashboard_id ) {
+            ( $ret, $msg ) = $UserObj->DeletePreferences( 'DefaultDashboard' );
         }
         else {
-            push @actions, loc('Unable to find [_1] [_2]', $saved->{type}, $saved->{name});
+            $ret = 1;
         }
     }
-    $selected{$pane} = \@items;
-    }
-
-my %pane_name = (
-  'body'    => loc('Body'),
-  'sidebar' => loc('Sidebar'),
-);
-
-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,
-);
 
-if ( $ARGS{UpdateSearches} ) {
-    $ARGS{user_id} = $ARGS{id};
-    my ($ok, $msg) = UpdateDashboard( \%ARGS, \%item_for );
-    push @actions, $ok ? loc('Preferences saved for user [_1].', $UserObj->Name) : $msg;
+    push @actions, $ret ? loc('Preferences saved for user [_1].', $UserObj->Name) : $msg;
 
     MaybeRedirectForResults(
         Actions   => \@actions,
diff --git a/share/html/Elements/MyRT b/share/html/Elements/MyRT
index 85271e7275..5d20c5b747 100644
--- a/share/html/Elements/MyRT
+++ b/share/html/Elements/MyRT
@@ -66,10 +66,18 @@ my %allowed_components = map {$_ => 1} @{RT->Config->Get('HomepageComponents')};
 
 my $user = $session{'CurrentUser'}->UserObj;
 unless ( $Portlets ) {
-    my ($defaults) = RT::System->new($session{'CurrentUser'})->Attributes->Named('HomepageSettings');
-    $Portlets = $user->Preferences(
-        HomepageSettings => $defaults ? $defaults->Content : {}
-    );
+    my ($system_default) = RT::System->new($session{'CurrentUser'})->Attributes->Named('DefaultDashboard');
+    my $system_default_id = $system_default ? $system_default->Content : 0;
+    my $dashboard_id = $user->Preferences( DefaultDashboard => $system_default_id ) or return;
+
+    # Allow any user to read system default dashboard
+    my $dashboard = RT::Dashboard->new($system_default_id == $dashboard_id ? RT->SystemUser : $session{'CurrentUser'});
+    my ( $ok, $msg ) = $dashboard->LoadById( $dashboard_id );
+    if ( !$ok ) {
+        $m->out($msg);
+        return;
+    }
+    $Portlets = $dashboard->Panes;
 }
 
 $m->callback( CallbackName => 'MassagePortlets', Portlets => $Portlets );
@@ -83,10 +91,14 @@ $sidebar = undef unless $sidebar && @$sidebar;
 
 my $Rows = $user->Preferences( 'SummaryRows', ( RT->Config->Get('DefaultSummaryRows') || 10 ) );
 
-my $show_cb = sub {
+my $show_cb;
+$show_cb = sub {
     my $entry = shift;
-    my $type  = $entry->{type};
-    my $name = $entry->{'name'};
+    my $depth = shift || 0;
+    Abort("Possible recursive dashboard detected.", SuppressHeader => 1) if $depth > 8;
+
+    my $type  = $entry->{portlet_type};
+    my $name = $entry->{component};
     if ( $type eq 'component' ) {
         if (!$allowed_components{$name}) {
             $m->out( $m->interp->apply_escapes( loc("Invalid portlet [_1]", $name), "h" ) );
@@ -98,10 +110,19 @@ my $show_cb = sub {
         else {
             $m->comp( $name, %{ $entry->{arguments} || {} } );
         }
-    } elsif ( $type eq 'system' ) {
-        $m->comp( '/Elements/ShowSearch', Name => $name, Override => { Rows => $Rows } );
-    } elsif ( $type eq 'saved' ) {
-        $m->comp( '/Elements/ShowSearch', SavedSearch => $name, Override => { Rows => $Rows } );
+    } elsif ( $type eq 'search' ) {
+        $m->comp( '/Elements/ShowSearch', RT::Dashboard->ShowSearchName($entry), Override => { Rows => $Rows } );
+    } elsif ( $type eq 'dashboard' ) {
+        my $current_dashboard = RT::Dashboard->new($session{CurrentUser});
+        my ($ok, $msg) = $current_dashboard->LoadById($entry->{id});
+        if (!$ok) {
+            $m->out($msg);
+            return;
+        }
+        my @panes = @{ $current_dashboard->Panes->{$entry->{pane}} || [] };
+        for my $portlet (@panes) {
+            $show_cb->($portlet, $depth + 1);
+        }
     } else {
         $RT::Logger->error("unknown portlet type '$type'");
     }
diff --git a/share/html/Elements/SelectDashboard b/share/html/Elements/SelectDashboard
new file mode 100644
index 0000000000..0814f3a90e
--- /dev/null
+++ b/share/html/Elements/SelectDashboard
@@ -0,0 +1,91 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2021 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 }}}
+
+% for my $section ( sort keys %Dashboards ) {
+<table class="table table-striped table-hover collection-as-table">
+  <thead>
+    <th class="w-50 collection-as-table"><% $section %></th>
+    <th class="w-15 collection-as-table"></th>
+    <th class="w-15 collection-as-table"></th>
+    <th class="w-15 collection-as-table"></th>
+  </thead>
+  <tbody>
+%   for my $dashboard ( @{$Dashboards{$section}} ) {
+%     next if !$ShowEmpty && !$dashboard->{id};
+    <tr>
+      <td class="collection-as-table"><% $dashboard->{name} %></td>
+      <td class="collection-as-table">
+%     if ( $dashboard->{view_link} ) {
+        <a href="<% $dashboard->{view_link} %>" target="_blank"><&|/l&>View Dashboard</&></a>
+%     }
+      </td>
+      <td class="collection-as-table">
+%     if ( $dashboard->{edit_link} ) {
+        <a href="<% $dashboard->{edit_link} %>" target="_blank"><&|/l&>Edit Dashboard</&></a>
+%     }
+      </td>
+      <td class="collection-as-table">
+%     if ( $dashboard->{id} == $Default ) {
+        <&|/l&>Current Homepage</&>
+%     } else {
+        <input name="DefaultDashboard-<% $dashboard->{id} %>" class="button btn btn-primary form-control" type="submit" value="<&|/l&>Set as Homepage</&>" />
+%     }
+      </td>
+    </tr>
+%   }
+  </tbody>
+</table>
+% }
+
+
+<%ARGS>
+$Name => 'DefaultDashboard'
+%Dashboards => ()
+$Default => 0
+$ShowEmpty => 1
+</%ARGS>
diff --git a/share/html/Elements/ShowSearch b/share/html/Elements/ShowSearch
index f8c05d5ab2..d647a1c82f 100644
--- a/share/html/Elements/ShowSearch
+++ b/share/html/Elements/ShowSearch
@@ -78,6 +78,11 @@ if ($SavedSearch) {
         $m->out(loc("Saved search [_1] not found", $m->interp->apply_escapes($SavedSearch, 'h'))) unless $IgnoreMissing;
         return;
     }
+
+    if ( $search->Object->isa('RT::System') ) {
+        $SearchArg = $user->Preferences( $search, $search->Content );
+    }
+
     $SearchArg->{'SavedSearchId'} ||= $SavedSearch;
     $SearchArg->{'SearchType'} ||= 'Ticket';
     if ( $SearchArg->{SearchType} eq 'Transaction' ) {
@@ -108,11 +113,19 @@ if ($SavedSearch) {
         $query_link_url = RT->Config->Get('WebPath') . "/Search/$SearchArg->{SearchType}.html";
         $ShowCount = 0;
     } elsif ($ShowCustomize) {
-        $customize = RT->Config->Get('WebPath') . '/Search/Build.html?'
-            . $m->comp( '/Elements/QueryString',
-            SavedSearchLoad => $SavedSearch );
+        if ( $search->Object->isa('RT::System') ) {
+            $customize = RT->Config->Get('WebPath') . '/Prefs/Search.html?'
+                . $m->comp( '/Elements/QueryString',
+                    name => ref($search) . '-' . $search->Id );
+        }
+        else {
+            $customize = RT->Config->Get('WebPath') . '/Search/Build.html?'
+                . $m->comp( '/Elements/QueryString',
+                SavedSearchLoad => $SavedSearch );
+        }
     }
 } else {
+    RT->Deprecated( Message => 'Passing bare $Name is deprecated', Instead => '$SavedSearch', Remove => '5.2' );
     ($search) = RT::System->new( $session{'CurrentUser'} ) ->Attributes->Named( 'Search - ' . $Name );
     unless ( $search && $search->Id ) {
         my (@custom_searches) = RT::System->new( $session{'CurrentUser'} )->Attributes->Named('SavedSearch');
diff --git a/share/html/Prefs/MyRT.html b/share/html/Prefs/MyRT.html
index 2e38498e70..d46f0dfd7a 100644
--- a/share/html/Prefs/MyRT.html
+++ b/share/html/Prefs/MyRT.html
@@ -49,19 +49,18 @@
 <& /Elements/Tabs &>
 <& /Elements/ListActions, actions => \@results &>
 
-<form method="post" name="UpdateSearches" class="mx-auto max-width-lg">
-  <& /Widgets/SearchSelection,
-    pane_name => \%pane_name,
-    sections  => \@sections,
-    selected  => \%selected,
-    filters   => \@filters,
-  &>
-  <input type="hidden" name="dashboard_id" value="MyRT">
-  <& /Elements/Submit, Name => "UpdateSearches", Label => loc('Save') &>
+<&|/Widgets/TitleBox, title => loc('Set Homepage'), bodyclass => "", class => "mx-auto max-width-xl" &>
+<form method="post" name="UpdateDefaultDashboard" class="mx-auto max-width-xl">
+  <div class="form-row">
+    <div class="col-12">
+      <& /Elements/SelectDashboard, Dashboards => GetDashboards(), Default => $default_dashboard_id &>
+    </div>
+  </div>
 </form>
+</&>
 
-<&|/Widgets/TitleBox, title => loc('Options'), bodyclass => "", class => "mx-auto max-width-lg" &>
-<form method="post" action="MyRT.html">
+<&|/Widgets/TitleBox, title => loc('Options'), bodyclass => "", class => "mx-auto max-width-xl" &>
+<form method="post" action="MyRT.html" class="mx-auto max-width-xl">
 <div class="form-row">
   <div class="label col-auto">
     <&|/l&>Rows per box</&>:
@@ -75,13 +74,6 @@
 </div>
 </form>
 </&>
-<&|/Widgets/TitleBox, title => loc("Reset RT at a glance"), class => "mx-auto max-width-lg" &>
-<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') %>">
-</form>
-</&>
-
 
 <%INIT>
 my @results;
@@ -100,124 +92,28 @@ if ( $ARGS{'UpdateSummaryRows'} ) {
 }
 $ARGS{'SummaryRows'} ||= $user->Preferences('SummaryRows', RT->Config->Get('DefaultSummaryRows'));
 
-if ($ARGS{Reset}) {
-    for my $pref_name ('HomepageSettings', 'SummaryRows') {
-        next unless $user->Preferences($pref_name);
-        my ($ok, $msg) = $user->DeletePreferences($pref_name);
-        push @results, $msg unless $ok;
-    }
-    push @results, loc('Preferences saved.') unless @results;
-}
-
-my $portlets = $user->Preferences('HomepageSettings');
-unless ($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;
-
-    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 $default_dashboard_id = $session{'CurrentUser'}->Preferences( DefaultDashboard => 0 );
 
-        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 ($default) = map { /^DefaultDashboard-(\d+)/ ? $1 : () } keys %ARGS;
+if ( defined $default ) {
+    my ( $ret, $msg );
+    if ( $default ) {
+        ( $ret, $msg ) = $session{CurrentUser}->SetPreferences( 'DefaultDashboard', $default );
     }
-
-    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 {
+        if ( $default_dashboard_id ) {
+            ( $ret, $msg ) = $session{CurrentUser}->DeletePreferences( 'DefaultDashboard' );
         }
         else {
-            push @results, loc('Unable to find [_1] [_2]', $saved->{type}, $saved->{name});
+            $ret = 1;
         }
     }
 
-    $selected{$pane} = \@items;
-}
-
-my %pane_name = (
-    'body'    => loc('Body'),
-    'sidebar' => loc('Sidebar'),
-);
-
-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,
-);
-
-if ($ARGS{UpdateSearches}) {
-    my ($ok, $msg) = UpdateDashboard( \%ARGS, \%item_for );
-    push @results, $ok ? loc('Preferences saved.') : $msg;
+    push @results, $ret ? loc('Preferences saved.') : $msg;
 
     MaybeRedirectForResults(
-        Actions   => \@results,
-        Path      => "/Prefs/MyRT.html",
+        Actions => \@results,
+        Path    => "/Prefs/MyRT.html",
     );
 }
 
diff --git a/share/html/Search/Elements/EditSearches b/share/html/Search/Elements/EditSearches
index a196333127..6a89bc19a9 100644
--- a/share/html/Search/Elements/EditSearches
+++ b/share/html/Search/Elements/EditSearches
@@ -432,7 +432,7 @@ return @results;
 <%METHOD GetDependedOnByList>
 
   <p>
-    <&|/l&>This search is used in these dashboards/homepages</&>:
+    <&|/l&>This search is used in these dashboards</&>:
   </p>
   <ul class="saved-search-depended-on-by-list list-group-compact">
 % my $links = $Object->DependedOnBy;
@@ -440,10 +440,6 @@ return @results;
     <li class="list-group-item">
 %   if ( $link->BaseObj->Name eq 'Dashboard' ) {
       <a href="<% $link->BaseURI->Resolver->HREF %>" target="_blank"><% $link->BaseURI->AsString %></a>
-%   } elsif ( $link->BaseObj->ObjectType eq 'RT::System' ) {
-      <% loc('Global') %>
-%   } elsif ( $link->BaseObj->ObjectType eq 'RT::User' ) {
-      <% loc('User') %>: <& /Elements/ShowUser, User => $link->BaseObj->Object, LinkTarget => '_blank' &>
 %   } else {
       <% $link->BaseObj->ObjectType %>: #<% $link->BaseObj->ObjectId %>
 %   }
diff --git a/share/static/css/elevator-light/misc.css b/share/static/css/elevator-light/misc.css
index 58f82460bd..1219bff839 100644
--- a/share/static/css/elevator-light/misc.css
+++ b/share/static/css/elevator-light/misc.css
@@ -160,3 +160,7 @@ span.pagenum {
     padding-top: 0;
     padding-bottom: 0;
 }
+
+.w-15 {
+    width: 15% !important;
+}

-- 
To stop receiving notification emails like this one, please contact
sysadmin at bestpractical.com.


More information about the rt-commit mailing list