[Rt-commit] rt branch, 5.0/use-dashboard-for-homepage, created. rt-5.0.1-335-g7c28a51544

? sunnavy sunnavy at bestpractical.com
Thu May 20 17:36:01 EDT 2021


The branch, 5.0/use-dashboard-for-homepage has been created
        at  7c28a51544d1c1bb883fe35669522d31dea24f24 (commit)

- Log -----------------------------------------------------------------
commit c8ba3ab6a08479c3344266f827dc8ba0d8434887
Author: sunnavy <sunnavy at bestpractical.com>
Date:   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.

diff --git a/etc/initialdata b/etc/initialdata
index 1ebe6d7fba..8e27963059 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
@@ -983,3 +939,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
new file mode 100644
index 0000000000..0fba4ab9fd
--- /dev/null
+++ b/etc/upgrade/5.0.2/content
@@ -0,0 +1,149 @@
+use strict;
+use warnings;
+
+our @Final = (
+    sub {
+        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
+        $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/Interface/Web.pm b/lib/RT/Interface/Web.pm
index 3170c298ba..a765fb27d8 100644
--- a/lib/RT/Interface/Web.pm
+++ b/lib/RT/Interface/Web.pm
@@ -4761,81 +4761,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
@@ -5146,6 +5128,84 @@ 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 $title;
+        if ( ref $object eq 'RT::User' && $object->Id == $session{CurrentUser}->Id ) {
+            $title = loc("My dashboards");
+        }
+        else {
+            $title = loc( "[_1]'s dashboards", $object->Name );
+        }
+
+        while ( my $dashboard = $list->Next ) {
+            my $name = join( ': ', $title, $dashboard->Name );
+
+            # Use current logged in user to determine if to return link or not
+            $dashboard->CurrentUser( $session{CurrentUser} );
+            push @dashboards,
+                {
+                id   => $dashboard->Id,
+                name => $name,
+                link => $dashboard->CurrentUserCanSee( $args{CurrentUser} )
+                ? join( '/', RT->Config->Get('WebPath'), 'Dashboards', $dashboard->Id, $dashboard->Name )
+                : '',
+                };
+
+            if ( $dashboard->Id == $default_dashboard_id ) {
+                %system_default = %{ $dashboards[-1] };
+            }
+        }
+    }
+
+    if (%system_default) {
+        push @dashboards,
+            {
+            id   => 0,
+            name => loc('System Default') . " ($system_default{name})",
+            link => $system_default{link},
+            };
+    }
+    else {
+        push @dashboards, {
+            id   => 0,
+            name => loc('System Default'),
+            link => '',
+        };
+    }
+
+    @dashboards = sort { lc $a->{name} cmp lc $b->{name} } @dashboards;
+    return @dashboards;
+}
+
 package RT::Interface::Web;
 RT::Base->_ImportOverlays();
 
diff --git a/share/html/Admin/Global/MyRT.html b/share/html/Admin/Global/MyRT.html
index f5728279a1..f6c469cfb8 100644
--- a/share/html/Admin/Global/MyRT.html
+++ b/share/html/Admin/Global/MyRT.html
@@ -49,129 +49,36 @@
 <& /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>
+  <div class="form-row">
+    <div class="col-12">
+      <& /Elements/Submit, Name => "UpdateDefaultDashboard", Label => loc('Save') &>
+    </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;
 
+if ( $ARGS{UpdateDefaultDashboard} && $ARGS{DefaultDashboard} ) {
+    my ( $ret, $msg ) = RT->System->SetAttribute( Name => 'DefaultDashboard', Description => 'Default Dashboard', Content => $ARGS{DefaultDashboard} );
+    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..91f14aa7d9 100644
--- a/share/html/Admin/Users/MyRT.html
+++ b/share/html/Admin/Users/MyRT.html
@@ -49,138 +49,45 @@
 <& /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>
+  <div class="form-row">
+    <div class="col-12">
+      <& /Elements/Submit, Name => "UpdateDefaultDashboard", Label => loc('Save') &>
+    </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;
+if ( $ARGS{UpdateDefaultDashboard} && defined $ARGS{DefaultDashboard} ) {
+    my ( $ret, $msg );
+    if ( $ARGS{DefaultDashboard} ) {
+        ( $ret, $msg ) = $UserObj->SetPreferences( 'DefaultDashboard', $ARGS{DefaultDashboard} );
+    }
+    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..10382a99fd
--- /dev/null
+++ b/share/html/Elements/SelectDashboard
@@ -0,0 +1,69 @@
+%# 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 $dashboard ( @Dashboards ) {
+%   next if !$ShowEmpty && !$dashboard->{id};
+  <div class="custom-control custom-radio">
+    <input type="radio" id="<% "$Name-$dashboard->{id}" %>" name="<% $Name %>" class="custom-control-input" value="<% $dashboard->{id} %>" <% $dashboard->{id} == $Default ? ' checked="checked"': '' |n %>>
+    <label class="custom-control-label" for="<% "$Name-$dashboard->{id}" %>">
+%   if ( $dashboard->{link} ) {
+      <a href="<% $dashboard->{link} %>" target="_blank"><% $dashboard->{name} %></a>
+%   } else {
+      <% $dashboard->{name} %>
+%   }
+    </label>
+  </div>
+% }
+
+
+<%ARGS>
+$Name => 'DefaultDashboard'
+ at 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..77acef718b 100644
--- a/share/html/Prefs/MyRT.html
+++ b/share/html/Prefs/MyRT.html
@@ -49,16 +49,20 @@
 <& /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-lg" &>
+<form method="post" name="UpdateDefaultDashboard" class="mx-auto max-width-lg">
+  <div class="form-row">
+    <div class="col-12">
+      <& /Elements/SelectDashboard, Dashboards => [ GetDashboards() ], Default => $default_dashboard_id &>
+    </div>
+  </div>
+  <div class="form-row">
+    <div class="col-12">
+      <& /Elements/Submit, Name => "UpdateDefaultDashboard", Label => loc('Save') &>
+    </div>
+  </div>
 </form>
+</&>
 
 <&|/Widgets/TitleBox, title => loc('Options'), bodyclass => "", class => "mx-auto max-width-lg" &>
 <form method="post" action="MyRT.html">
@@ -75,13 +79,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 +97,27 @@ 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 $default_dashboard_id = $session{'CurrentUser'}->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;
+if ( $ARGS{UpdateDefaultDashboard} && defined $ARGS{DefaultDashboard} ) {
+    my ( $ret, $msg );
+    if ( $ARGS{DefaultDashboard} ) {
+        ( $ret, $msg ) = $session{CurrentUser}->SetPreferences( 'DefaultDashboard', $ARGS{DefaultDashboard} );
     }
-
-    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",
     );
 }
 

commit 87103fa2abf0d0f1d625505282bf9e5d2be0dc82
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Sat May 15 00:59:13 2021 +0800

    Add "New Dashboard" to prefs/global MyRT page menu for convenience

diff --git a/lib/RT/Interface/Web/MenuBuilder.pm b/lib/RT/Interface/Web/MenuBuilder.pm
index 7c72d3ea07..29f165a2aa 100644
--- a/lib/RT/Interface/Web/MenuBuilder.pm
+++ b/lib/RT/Interface/Web/MenuBuilder.pm
@@ -933,6 +933,20 @@ sub BuildMainNav {
         }
     }
 
+    # Top menu already has the create link, adding it to page menu is just
+    # for convenience. As user admin page already has quite a few items in
+    # page menu and it's unlikely that admins want to create new dashboard
+    # when editing a user's preference, here we don't touch admin user page.
+    if ( $request_path =~ m{^/(Prefs|Admin/Global)/MyRT\.html} ) {
+        if ( RT::Dashboard->new($current_user)->CurrentUserCanCreateAny ) {
+            $page->child(
+                'dashboard_create' => title => loc('New Dashboard'),
+                path               => "/Dashboards/Modify.html?Create=1"
+            );
+        }
+    }
+
+
     if ( $request_path =~ /^\/(?:index.html|$)/ ) {
         my $alt = loc('Edit');
         $page->child( edit => raw_html => q[<a id="page-edit" class="menu-item" href="] . RT->Config->Get('WebPath') . qq[/Prefs/MyRT.html"><span class="fas fa-cog" alt="$alt" data-toggle="tooltip" data-placement="top" data-original-title="$alt"></span></a>] );

commit 7c28a51544d1c1bb883fe35669522d31dea24f24
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Fri May 14 06:13:19 2021 +0800

    Update tests for the migration of "RT at a glance" => dashboard

diff --git a/t/api/initialdata-roundtrip.t b/t/api/initialdata-roundtrip.t
index fbd349507d..e55223acc7 100644
--- a/t/api/initialdata-roundtrip.t
+++ b/t/api/initialdata-roundtrip.t
@@ -1026,10 +1026,10 @@ my @tests = (
         present => sub {
             # Provided in core initialdata
             my $homepage = RT::Attribute->new(RT->SystemUser);
-            $homepage->LoadByNameAndObject(Name => 'HomepageSettings', Object => RT->System);
+            $homepage->LoadByNameAndObject(Name => 'Dashboard', Description => 'Homepage', Object => RT->System);
             ok($homepage->Id, 'Loaded homepage attribute');
-            is($homepage->Name, 'HomepageSettings', 'Name is HomepageSettings');
-            is($homepage->Description, 'HomepageSettings', 'Description is HomepageSettings');
+            is($homepage->Name, 'Dashboard', 'Name is Dashboard');
+            is($homepage->Description, 'Homepage', 'Description is Homepage');
             is($homepage->ContentType, 'storable', 'ContentType is storable');
 
             my $root = RT::User->new(RT->SystemUser);
diff --git a/t/web/custom_frontpage.t b/t/web/custom_frontpage.t
index cbfcde0c55..3db735e054 100644
--- a/t/web/custom_frontpage.t
+++ b/t/web/custom_frontpage.t
@@ -16,6 +16,10 @@ $user_obj->PrincipalObj->GrantRight(Right => 'LoadSavedSearch');
 $user_obj->PrincipalObj->GrantRight(Right => 'EditSavedSearches');
 $user_obj->PrincipalObj->GrantRight(Right => 'CreateSavedSearch');
 $user_obj->PrincipalObj->GrantRight(Right => 'ModifySelf');
+$user_obj->PrincipalObj->GrantRight(Right => 'SeeDashboard');
+$user_obj->PrincipalObj->GrantRight(Right => 'SeeOwnDashboard');
+$user_obj->PrincipalObj->GrantRight(Right => 'CreateOwnDashboard');
+$user_obj->PrincipalObj->GrantRight(Right => 'ModifyOwnDashboard');
 
 ok $m->login( customer => 'customer' ), "logged in";
 
@@ -28,11 +32,24 @@ $m->field ( "ValueOfAttachment" => 'stupid');
 $m->field ( "SavedSearchDescription" => 'stupid tickets');
 $m->click_button (name => 'SavedSearchSave');
 
-$m->get ( $url.'Prefs/MyRT.html' );
+$m->get_ok( $url . "Dashboards/Modify.html?Create=1" );
+$m->form_name('ModifyDashboard');
+$m->field( Name => 'My homepage' );
+$m->click_button( value => 'Create' );
+
+$m->follow_link_ok( { text => 'Content' } );
 $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_ok( $url . "Dashboards/Modify.html?Create=1" );
+$m->form_name('ModifyDashboard');
+$m->field( Name => 'My homepage' );
+$m->click_button( value => 'Create' );
+
+my ($id) = ( $m->uri =~ /id=(\d+)/ );
+ok( $id, "got a dashboard ID, $id" );
+
 my $args = {
     UpdateSearches => "Save",
     dashboard_id   => "MyRT",
@@ -41,18 +58,29 @@ my $args = {
 };
 
 # remove all portlets from the body pane except 'newest unowned tickets'
+$m->follow_link_ok( { text => 'Content' } );
 push(
     @{$args->{body}},
-    ( "system-Unowned Tickets", )
+    "saved-" . $m->dom->find('[data-description="Unowned Tickets"]')->first->attr('data-name'),
 );
 
 my $res = $m->post(
-    $url . 'Prefs/MyRT.html',
+    $url . "Dashboards/Queries.html?id=$id",
     $args,
 );
 
 is( $res->code, 200, "remove all portlets from body except 'newest unowned tickets'" );
 like( $m->uri, qr/results=[A-Za-z0-9]{32}/, 'URL redirected for results' );
+$m->content_contains( 'Dashboard updated' );
+
+$m->get_ok( $url . 'Prefs/MyRT.html' );
+$m->submit_form_ok(
+    {   form_name => 'UpdateDefaultDashboard',
+        fields    => { DefaultDashboard => $id },
+        button    => 'UpdateDefaultDashboard',
+    },
+);
+
 $m->content_contains( 'Preferences saved' );
 
 $m->get( $url );
@@ -61,10 +89,14 @@ $m->content_lacks( 'highest priority tickets', "'highest priority tickets' is no
 $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" );
 
+$m->get_ok( $url . "Dashboards/Queries.html?id=$id" );
+
 # add back the previously removed portlets
 push(
     @{$args->{body}},
-    ( "system-My Tickets", "system-Bookmarked Tickets", "component-QuickCreate" )
+    "saved-" . $m->dom->find('[data-description="My Tickets"]')->first->attr('data-name'),
+    "saved-" . $m->dom->find('[data-description="Bookmarked Tickets"]')->first->attr('data-name'),
+    "component-QuickCreate",
 );
 
 push(
@@ -73,13 +105,13 @@ push(
 );
 
 $res = $m->post(
-    $url . 'Prefs/MyRT.html',
+    $url . "Dashboards/Queries.html?id=$id",
     $args,
 );
 
 is( $res->code, 200, 'add back previously removed portlets' );
 like( $m->uri, qr/results=[A-Za-z0-9]{32}/, 'URL redirected for results' );
-$m->content_contains( 'Preferences saved' );
+$m->content_contains( 'Dashboard updated' );
 
 $m->get( $url );
 $m->content_contains( 'newest unowned tickets', "'newest unowned tickets' is present" );
@@ -95,7 +127,8 @@ $m->field( "SavedSearchDescription" => 'special chars [test] [_1] ~[_1~]' );
 $m->click_button( name => 'SavedSearchSave' );
 my ($name) = $m->content =~ /value="(RT::User-\d+-SavedSearch-\d+)"/;
 ok( $name, 'saved search name' );
-$m->get( $url . 'Prefs/MyRT.html' );
+
+$m->get_ok( $url . "Dashboards/Queries.html?id=$id" );
 $m->content_contains( 'special chars [test] [_1] ~[_1~]',
     'saved search listed in rt at a glance items' );
 
@@ -106,13 +139,13 @@ push(
 );
 
 $res = $m->post(
-    $url . 'Prefs/MyRT.html',
+    $url . "Dashboards/Queries.html?id=$id",
     $args,
 );
 
 is( $res->code, 200, 'add saved search to body' );
 like( $m->uri, qr/results=[A-Za-z0-9]{32}/, 'URL redirected for results' );
-$m->content_contains( 'Preferences saved' );
+$m->content_contains( 'Dashboard updated' );
 
 $m->get($url);
 $m->content_like( qr/special chars \[test\] \d+ \[_1\]/,
@@ -161,7 +194,7 @@ $m->submit_form(
 # We don't show saved message on page :/
 $m->content_contains("Save as New", 'saved first txn search' );
 
-$m->get_ok( $url . 'Prefs/MyRT.html' );
+$m->get_ok( $url . "Dashboards/Queries.html?id=$id" );
 push(
     @{$args->{body}},
     "saved-" . $m->dom->find('[data-description="first chart"]')->first->attr('data-name'),
@@ -169,12 +202,12 @@ push(
 );
 
 $res = $m->post(
-    $url . 'Prefs/MyRT.html',
+    $url . "Dashboards/Queries.html?id=$id",
     $args,
 );
 
 is( $res->code, 200, 'add system saved searches to body' );
-$m->text_contains( 'Preferences saved' );
+$m->content_contains( 'Dashboard updated' );
 
 $m->get_ok($url);
 $m->text_contains('first chart');

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


More information about the rt-commit mailing list