[Rt-commit] rt 02/12: Migrate "RT at a glance" homepage to dashboard
sunnavy
sunnavy at bestpractical.com
Wed Jul 14 14:46:43 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 fdb195ecd091d5416324824319b2730979bca51c
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 | 181 ++++++++++------------------
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 | 57 +++++++--
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 +
14 files changed, 685 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..61bbadd4d7 100644
--- a/lib/RT/Attribute.pm
+++ b/lib/RT/Attribute.pm
@@ -418,10 +418,11 @@ 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' ) {
+
+ if ( $name =~ /^(?:Dashboard|SavedSearch)$/ ) {
push @links, @{ $self->DependedOnBy->ItemsArrayRef };
}
@@ -814,32 +815,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 +910,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 +1001,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 +1062,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 +1092,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/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..4e5571a5b7 100644
--- a/share/html/Elements/MyRT
+++ b/share/html/Elements/MyRT
@@ -66,10 +66,32 @@ 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 ) {
+ if ( $dashboard_id == $system_default_id ) {
+ $m->out($m->scomp('/Elements/ListActions', actions => $msg));
+ return;
+ }
+ else {
+ my ( $ok, $sys_msg ) = $dashboard->LoadById( $system_default_id );
+ if ( $ok ) {
+ $m->out($m->scomp('/Elements/ListActions', actions => [$msg, loc('Fallback to system default homepage')]));
+ my ( $ok, $msg ) = $user->DeletePreferences( 'DefaultDashboard' );
+ RT->Logger->error( "Couldn't delete DefaultDashboard of user " . $user->Name . ": $msg" ) unless $ok;
+ }
+ else {
+ $m->out($m->scomp('/Elements/ListActions', actions => [$msg, $sys_msg]));
+ return;
+ }
+ }
+ }
+ $Portlets = $dashboard->Panes;
}
$m->callback( CallbackName => 'MassagePortlets', Portlets => $Portlets );
@@ -83,10 +105,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 +124,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