From 8582ddf3bb7d27ce27bbffc1ef9d9b8f8163cdf4 Mon Sep 17 00:00:00 2001 From: sunnavy Date: Wed, 21 Aug 2024 03:14:31 -0400 Subject: [PATCH 1/9] Use the structure of configurable pages for dashboards --- etc/initialdata | 66 +++--- etc/upgrade/5.9.1/content | 85 +++++++ lib/RT/Attribute.pm | 62 +++-- lib/RT/Dashboard.pm | 53 ++--- lib/RT/Handle.pm | 25 +- lib/RT/Interface/Web.pm | 198 ++++++++-------- lib/RT/Migrate/Serializer/JSON.pm | 26 +-- .../Admin/Global/SelfServiceHomePage.html | 7 +- .../Dashboards/Elements/ShowPortlet/dashboard | 50 ++-- share/html/Dashboards/Queries.html | 215 ++++++------------ share/html/Dashboards/Render.html | 55 ++--- share/html/Elements/EditPageLayout | 55 ++++- share/html/Elements/MyRT | 132 +++-------- share/html/Elements/ShowWidgets | 10 +- share/html/Widgets/SearchSelection | 77 ------- share/static/css/elevator/dashboards.css | 27 --- share/static/js/pagelayout.js | 6 +- 17 files changed, 475 insertions(+), 674 deletions(-) create mode 100644 etc/upgrade/5.9.1/content diff --git a/etc/initialdata b/etc/initialdata index 6947c22dd32..9e6c98c478a 100644 --- a/etc/initialdata +++ b/etc/initialdata @@ -1004,10 +1004,9 @@ The following tokens will expire within the next 7 days: if ($search) { push @searches, { - pane => 'body', portlet_type => 'search', id => $search->Id, - description => "Saved Search: $search_name", + description => "Ticket: $search_name", privacy => join( '-', ref( RT->System ), RT->System->Id ), }; } @@ -1016,40 +1015,43 @@ The following tokens will expire within the next 7 days: } } - my $panes = { - body => [ - @searches, - { pane => 'body', - portlet_type => 'component', - component => 'QuickCreate', - description => 'QuickCreate', - path => '/Elements/QuickCreate', - }, + my @elements = { + Layout => 'col-md-8,col-md-4', + Elements => [ + [ + @searches, + { + portlet_type => 'component', + component => 'QuickCreate', + description => 'QuickCreate', + path => '/Elements/QuickCreate', + }, + ], + [ + { + portlet_type => 'component', + component => 'MyReminders', + description => 'MyReminders', + path => '/Elements/MyReminders', + }, + { + portlet_type => 'component', + component => 'QueueList', + description => 'QueueList', + path => '/Elements/QueueList', + }, + { + portlet_type => 'component', + component => 'Dashboards', + description => 'Dashboards', + path => '/Elements/Dashboards', + }, + ] ], - 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 => 'Dashboards', - description => 'Dashboards', - path => '/Elements/Dashboards', - }, - ] }; # fill content - my ( $ret, $msg ) = $dashboard->Update( Panes => $panes ); + my ( $ret, $msg ) = $dashboard->Update( Elements => \@elements ); if ( !$ret ) { RT->Logger->error("Couldn't update content for dashboard Homepage: $msg"); } diff --git a/etc/upgrade/5.9.1/content b/etc/upgrade/5.9.1/content new file mode 100644 index 00000000000..323b25f1337 --- /dev/null +++ b/etc/upgrade/5.9.1/content @@ -0,0 +1,85 @@ +use strict; +use warnings; + +our @Final = ( + sub { + + # Previously only the corresponding panes in inner dashboards were + # rendered. New layout is much more flexible and there are no + # correponding panes, so here we expand inner dashboards to make the + # content the same as before. + + my $attrs = RT::Attributes->new( RT->SystemUser ); + $attrs->Limit( FIELD => 'Name', VALUE => [ 'Dashboard', 'SelfServiceDashboard' ], OPERATOR => 'IN' ); + while ( my $attr = $attrs->Next ) { + my $content = $attr->Content; + if ( $content && $content->{Panes} ) { + my $new_content = {}; + my $changed; + for my $pane ( sort keys %{ $content->{Panes} } ) { + my @new_panes; + for my $portlet ( @{ $content->{Panes}->{$pane} } ) { + if ( $portlet->{portlet_type} eq 'dashboard' ) { + $changed ||= 1; + my $dashboard = RT::Attribute->new( RT->SystemUser ); + $dashboard->Load( $portlet->{id} ); + if ( $dashboard->Id ) { + push @new_panes, @{ $dashboard->Content->{'Panes'}{$pane} || [] } + if $dashboard->Content; + } + else { + RT->Logger->error( + "Couldn't find dashboard $portlet->{id}, removing from dashboard #" + . $attr->Id ); + } + } + else { + push @new_panes, $portlet; + } + } + $content->{Panes}->{$pane} = \@new_panes; + } + if ($changed) { + my ( $ret, $msg ) = $attr->SetContent($content); + RT->Logger->error( "Couldn't update dashboard #" . $attr->Id . ":$msg" ) unless $ret; + } + } + } + }, + sub { + my $attrs = RT::Attributes->new( RT->SystemUser ); + $attrs->Limit( FIELD => 'Name', VALUE => [ 'Dashboard', 'SelfServiceDashboard' ], OPERATOR => 'IN' ); + while ( my $attr = $attrs->Next ) { + my $content = $attr->Content; + if ( $content && $content->{Panes} ) { + my $layout; + if ( $content->{Width} ) { + $layout = join ',', map {"col-md-$_"} map { $content->{Width}{$_} || () } qw/body sidebar/; + } + $layout ||= 'col-md-8,col-md-4'; + + my @cols; + for my $pane ( sort keys %{ $content->{Panes} } ) { + my @elements; + for my $portlet ( @{ $content->{Panes}->{$pane} } ) { + delete $portlet->{pane}; + $portlet->{description} =~ s!^Saved Search:!Ticket:!; + push @elements, $portlet; + } + push @cols, \@elements; + } + my ( $ret, $msg ) = $attr->SetContent( + { + Elements => [ + { + Layout => $layout, + Elements => \@cols, + } + ] + } + ); + RT->Logger->error( "Couldn't update dashboard #" . $attr->Id . ":$msg" ) unless $ret; + } + } + }, +); diff --git a/lib/RT/Attribute.pm b/lib/RT/Attribute.pm index fd8d12217fb..8056830f5af 100644 --- a/lib/RT/Attribute.pm +++ b/lib/RT/Attribute.pm @@ -844,14 +844,11 @@ sub FindDependencies { } # dashboards have dependencies on all the searches and dashboards they use elsif ($self->Name eq 'Dashboard' || $self->Name eq 'SelfServiceDashboard') { - my $content = $self->Content; - for my $pane (values %{ $content->{Panes} || {} }) { - for my $component (@$pane) { - if ($component->{portlet_type} eq 'search' || $component->{portlet_type} eq 'dashboard') { - my $attr = RT::Attribute->new($self->CurrentUser); - $attr->LoadById($component->{id}); - $deps->Add( out => $attr ); - } + for my $component ( RT::Dashboard->Portlets( $self->Content->{Elements} || [] ) ) { + if ( $component->{portlet_type} eq 'search' || $component->{portlet_type} eq 'dashboard' ) { + my $attr = RT::Attribute->new( $self->CurrentUser ); + $attr->LoadById( $component->{id} ); + $deps->Add( out => $attr ); } } } @@ -1013,25 +1010,24 @@ sub PostInflateFixup { elsif ($self->Name eq 'Dashboard') { my $content = $self->Content; - for my $pane (values %{ $content->{Panes} || {} }) { - for (@$pane) { - if (ref($_->{uid}) eq 'SCALAR') { - my $uid = $_->{uid}; - my $attr = $importer->LookupObj($$uid); + for ( RT::Dashboard->Portlets( $content->{Elements} || [] ) ) { + if ( ref( $_->{uid} ) eq 'SCALAR' ) { + my $uid = $_->{uid}; + my $attr = $importer->LookupObj($$uid); - if ($attr) { - # update with the new id numbers assigned to us - $_->{id} = $attr->Id; - $_->{privacy} = join '-', $attr->ObjectType, $attr->ObjectId; - delete $_->{uid}; - } - else { - $importer->Postpone( - for => $$uid, - uid => $spec->{uid}, - method => 'PostInflateFixup', - ); - } + if ($attr) { + + # update with the new id numbers assigned to us + $_->{id} = $attr->Id; + $_->{privacy} = join '-', $attr->ObjectType, $attr->ObjectId; + delete $_->{uid}; + } + else { + $importer->Postpone( + for => $$uid, + uid => $spec->{uid}, + method => 'PostInflateFixup', + ); } } } @@ -1115,13 +1111,11 @@ sub Serialize { # encode saved searches and dashboards to be UIDs elsif ($store{Name} eq 'Dashboard') { my $content = $self->_DeserializeContent($store{Content}) || {}; - for my $pane (values %{ $content->{Panes} || {} }) { - for (@$pane) { - if ($_->{portlet_type} eq 'search' || $_->{portlet_type} eq 'dashboard') { - $_->{uid} = \( join '-', 'RT::Attribute', $RT::Organization, $_->{id} ); - } - # pass through everything else (e.g. component) + for ( RT::Dashboard->Portlets( $content->{Elements} || [] ) ) { + if ($_->{portlet_type} eq 'search' || $_->{portlet_type} eq 'dashboard') { + $_->{uid} = \( join '-', 'RT::Attribute', $RT::Organization, $_->{id} ); } + # pass through everything else (e.g. component) } $store{Content} = $self->_SerializeContent($content); } @@ -1189,8 +1183,8 @@ sub _SyncLinks { if ( $name eq 'Dashboard' ) { my $content = $self->_DeserializeContent( $self->__Value('Content') ); - my %searches = map { $_->{id} => 1 } grep { $_->{portlet_type} eq 'search' } @{ $content->{Panes}{body} }, - @{ $content->{Panes}{sidebar} }; + my %searches = map { $_->{id} => 1 } + grep { $_->{portlet_type} eq 'search' } RT::Dashboard->Portlets( $content->{Elements} || [] ); my $links = $self->DependsOn; while ( my $link = $links->Next ) { diff --git a/lib/RT/Dashboard.pm b/lib/RT/Dashboard.pm index 86560cce65f..a190bf23d31 100644 --- a/lib/RT/Dashboard.pm +++ b/lib/RT/Dashboard.pm @@ -105,8 +105,7 @@ sub SaveAttribute { 'Name' => 'Dashboard', 'Description' => $args->{'Name'}, 'Content' => { - Panes => $args->{'Panes'}, - Width => $args->{'Width'}, + Elements => $args->{'Elements'}, }, ); } @@ -116,10 +115,9 @@ sub UpdateAttribute { my $args = shift; my ($status, $msg) = (1, undef); - if (defined $args->{'Panes'}) { + if (defined $args->{'Elements'}) { ($status, $msg) = $self->{'Attribute'}->SetSubValues( - Panes => $args->{'Panes'}, - Width => $args->{'Width'}, + Elements => $args->{'Elements'}, ); } @@ -158,18 +156,6 @@ sub PostLoadValidate { return 1; } -=head2 Panes - -Returns a hashref of pane name to portlets - -=cut - -sub Panes { - my $self = shift; - return unless ref($self->{'Attribute'}) eq 'RT::Attribute'; - return $self->{'Attribute'}->SubValue('Panes') || {}; -} - =head2 Portlets Returns the list of this dashboard's portlets, each a hashref with key @@ -178,20 +164,25 @@ C being C or C. =cut sub Portlets { - my $self = shift; - return map { @$_ } values %{ $self->Panes }; -} - -=head2 Width - -Returns a hashref of column widths - -=cut - -sub Width { - my $self = shift; - return unless ref($self->{'Attribute'}) eq 'RT::Attribute'; - return $self->{'Attribute'}->SubValue('Width') || {}; + my $self = shift; + my $elements = shift || $self->{Attribute}->SubValue('Elements'); + my @widgets; + for my $element (@$elements) { + if ( ref $element && $element->{Elements} ) { + if ( ref $element && ref $element->{Elements}[0] eq 'ARRAY' ) { + for my $list ( @{ $element->{Elements} } ) { + push @widgets, @$list; + } + } + else { + push @widgets, @{ $element->{Elements} }; + } + } + else { + push @widgets, $element; + } + } + return @widgets; } =head2 Dashboards diff --git a/lib/RT/Handle.pm b/lib/RT/Handle.pm index 323a72bb80f..7d88cf33e07 100644 --- a/lib/RT/Handle.pm +++ b/lib/RT/Handle.pm @@ -3166,23 +3166,18 @@ sub _CanonilizeAttributeContent { my $self = shift; my $item = shift or return; 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->Clone; - $attributes->Limit( FIELD => 'Description', VALUE => $entry->{Description} ); - if ( my $attribute = $attributes->First ) { - $entry->{id} = $attribute->id; - $entry->{privacy} = ref( $object ) . '-' . $object->Id; - } - } - delete $entry->{$_} for qw/ObjectType ObjectId Description/; + for my $entry ( RT::Dashboard->Portlets( $item->{Content}{Elements} || [] ) ) { + 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->Clone; + $attributes->Limit( FIELD => 'Description', VALUE => $entry->{Description} ); + if ( my $attribute = $attributes->First ) { + $entry->{id} = $attribute->id; + $entry->{privacy} = ref($object) . '-' . $object->Id; } } + delete $entry->{$_} for qw/ObjectType ObjectId Description/; } } } diff --git a/lib/RT/Interface/Web.pm b/lib/RT/Interface/Web.pm index 97b8eb0b462..6f44611fddb 100644 --- a/lib/RT/Interface/Web.pm +++ b/lib/RT/Interface/Web.pm @@ -5355,114 +5355,6 @@ sub GetDefaultQueue { return defined $queue_obj->Name ? $queue_obj->Id : undef; } -=head2 UpdateDashboard - -Update global and user-level dashboard preferences. - -For arguments, takes submitted args from the page and a hashref of available -items. - -Gets additional information for submitted items from the hashref of -available items, since the args can't contain all information about the -item. - -=cut - -sub UpdateDashboard { - my $args = shift; - my $available_items = shift; - - my $id = $args->{dashboard_id}; - - my $data = { - "dashboard_id" => $id, - "panes" => { - "body" => [], - "sidebar" => [] - }, - "width" => { - body => $args->{body_width}, - sidebar => $args->{sidebar_width}, - }, - }; - - foreach my $arg (qw{ body sidebar }) { - my $pane = $arg; - my $values = $args->{$pane}; - - next unless $values; - - # force value to an arrayref so we can handle both single and multiple members of each pane. - $values = [$values] unless ref $values; - - foreach my $value ( @{$values} ) { - $value =~ m/^(\w+)-(.+)$/i; - my $type = $1; - my $name = $2; - push @{ $data->{panes}->{$pane} }, { type => $type, name => $name }; - } - } - - my ( $ok, $msg ); - 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; - } - - $content->{$pane_name} = \@pane; - } - - return ( $ok, $msg ) = $Dashboard->Update( Panes => $content, Width => $data->{ width } ); -} - =head2 ListOfReports Returns the list of reports registered with RT. Alias for @@ -6383,6 +6275,96 @@ sub GetAvailableWidgets { Page => 'Display', @_, ); + + if ( $args{Class} eq 'RT::Dashboard' ) { + + my @widgets = map { + section => 'Component', + label => loc($_), + portlet_type => 'component', + component => $_, + description => $_, + path => "/Elements/$_", + }, + ( $args{Page} // '' ) eq 'SelfService' + ? @{ RT->Config->Get('SelfServicePageComponents') || [] } + : @{ RT->Config->Get('HomepageComponents') || [] }; + + 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; + my $section + = $object eq $sys ? loc('System') + : $object->isa('RT::Group') ? $object->Label + : $object->Name; + + + # saved searches and charts + for ( $m->comp( "/Search/Elements/SearchesForObject", Object => $object ) ) { + my ( $desc, $loc_desc, $search ) = @$_; + + my $type = 'Ticket'; + if ( ( ref( $search->Content ) || '' ) eq 'HASH' ) { + $type = $search->Content->{'SearchType'} + if $search->Content->{'SearchType'}; + } + else { + RT->Logger->debug( "Search " . $search->id . " ($desc) appears to have no Content" ); + } + + my $setting = RT::SavedSearch->new( $session{CurrentUser} ); + $setting->Load( $object, $search->Id ); + + my $item = { + section => $section, + label => join( ': ', loc($type), $loc_desc ), + portlet_type => 'search', + id => $search->Id, + privacy => join( '-', ref $object, $object->Id ), + description => join( ': ', $type, $desc ), + }; + + $item->{tooltip} = loc('Warning: may not be visible to all viewers') + unless $setting->IsVisibleTo( $args{Dashboard}->Privacy ); + push @items, $item; + } + + for my $dashboard ( $m->comp( "/Dashboards/Elements/DashboardsForObject", Object => $object, Flat => 1 ) ) { + + # Users *can* set up mutually recursive dashboards, but don't make it + # THIS easy for them to shoot themselves in the foot. + next if $dashboard->Id == $args{Dashboard}->id; + + my $item = { + section => $section, + label => join( ': ', loc('Dashboard'), $dashboard->Name ), + portlet_type => 'dashboard', + id => $dashboard->Id, + privacy => join( '-', ref $object, $object->Id ), + description => $dashboard->Name, + }; + + $item->{tooltip} = loc('Warning: may not be visible to all viewers') + unless $dashboard->IsVisibleTo( $args{Dashboard}->Privacy ); + + push @items, $item; + } + + push @widgets, sort { lc( $a->{label} ) cmp lc( $b->{label} ) } @items; + } + return @widgets; + } + my $widget_path; $widget_path = "/$1/Widgets/$args{Page}/" if $args{Class} =~ /RT::(.+)/; my %widget; diff --git a/lib/RT/Migrate/Serializer/JSON.pm b/lib/RT/Migrate/Serializer/JSON.pm index c14a45995f9..06ede4c72cb 100644 --- a/lib/RT/Migrate/Serializer/JSON.pm +++ b/lib/RT/Migrate/Serializer/JSON.pm @@ -495,23 +495,19 @@ sub CanonicalizeAttributes { } else { if ( $record->{Name} eq 'Dashboard' ) { - my $content = $record->{Content}{Panes}; - for my $type ( qw/body sidebar/ ) { - if ( $content->{$type} && ref $content->{$type} eq 'ARRAY' ) { - for my $item ( @{ $content->{$type} } ) { - if ( my $id = $item->{id} ) { - 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->{$_} for qw/id privacy/; - } - } - delete $item->{uid}; + my $content = $record->{Content}{Elements}; + for my $item ( RT::Dashboard->Portlets( $record->{Content}{Elements} || [] ) ) { + if ( my $id = $item->{id} ) { + 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->{$_} for qw/id privacy/; } } + delete $item->{uid}; } } elsif ( $record->{Name} =~ /^(?:Pref-)?DashboardsInMenu$/ ) { diff --git a/share/html/Admin/Global/SelfServiceHomePage.html b/share/html/Admin/Global/SelfServiceHomePage.html index b31fcf4a805..7c357e72745 100644 --- a/share/html/Admin/Global/SelfServiceHomePage.html +++ b/share/html/Admin/Global/SelfServiceHomePage.html @@ -70,12 +70,7 @@ my ($ok, $msg) = $attr->LoadByNameAndObject(Object => $RT::System, Name => 'SelfServiceDashboard'); if (!$ok) { - my $blank_dashboard = { - Panes => { - body => [], - sidebar => [], - } - }; + my $blank_dashboard = { Elements => [] }; # Doesn't exist... try creating an empty one ($ok, $msg) = $Dashboard->Save( Privacy => $RT::System, diff --git a/share/html/Dashboards/Elements/ShowPortlet/dashboard b/share/html/Dashboards/Elements/ShowPortlet/dashboard index c64a79c2eea..619b7e1c551 100644 --- a/share/html/Dashboards/Elements/ShowPortlet/dashboard +++ b/share/html/Dashboards/Elements/ShowPortlet/dashboard @@ -47,7 +47,6 @@ %# END BPS TAGGED BLOCK }}} <%args> $Dashboard -$Pane $Portlet $Rows => 20 $Preview => 0 @@ -69,38 +68,25 @@ else { } } -my @panes = @{ $current_dashboard->Panes->{$Pane} || [] }; - -Abort("Possible recursive dashboard detected.") if $Depth > 8; +if ( $Depth > 8 ) { + RT->Logger->error("Possible recursive dashboard detected."); + return; +} -<%perl> -local $session{CurrentUser} = $session{ContextUser}; - -$m->callback(CallbackName => 'BeforePanes', Dashboard => $current_dashboard, - HasResults => $HasResults, Portlet => $Portlet, ARGSRef => \%ARGS); - -for my $portlet (@panes) { +% $m->callback( +% CallbackName => 'BeforePanes', +% Dashboard => $current_dashboard, +% HasResults => $HasResults, +% Portlet => $Portlet, +% ARGSRef => \%ARGS, +% ); - my $skip_processing = 0; - $m->callback(CallbackName => 'BeforeComponent', Dashboard => $current_dashboard, - HasResults => $HasResults, Portlet => $portlet, ARGSRef => \%ARGS, - SkipProcessing => \$skip_processing); - last if $skip_processing; - - $m->comp($portlet->{portlet_type}, - Portlet => $portlet, - Rows => $Rows, - Preview => $Preview, - Dashboard => $current_dashboard, - Pane => $Pane, - Depth => $Depth + 1, - HasResults => $HasResults - ); - - $m->callback(CallbackName => 'AfterComponent', Dashboard => $current_dashboard, - HasResults => $HasResults, Portlet => $portlet, ARGSRef => \%ARGS); - -} - +<& /Elements/ShowWidgets, + %ARGS, + Layout => '', + Object => $current_dashboard, + Elements => $current_dashboard->{Attribute}->SubValue('Elements') || [[]], + Dashboard => $current_dashboard, +&> diff --git a/share/html/Dashboards/Queries.html b/share/html/Dashboards/Queries.html index 34e2ff6b66a..704c0c00564 100644 --- a/share/html/Dashboards/Queries.html +++ b/share/html/Dashboards/Queries.html @@ -49,18 +49,13 @@ <& /Elements/Tabs &> <& /Elements/ListActions, actions => \@results &> -
- <& /Widgets/SearchSelection, - pane_name => \%pane_name, - sections => \@sections, - selected => \%selected, - filters => \@filters, - dashboard_setup => 1, - body_width => $Dashboard->Width->{ body }, - sidebar_width => $Dashboard->Width->{ sidebar }, - &> - <& /Elements/Submit, Name => "UpdateSearches", Label => loc('Save') &> -
+<& /Elements/EditPageLayout, + Class => 'RT::Dashboard', + id => $id, + Content => $content, + AvailableWidgets => \@available_widgets, + PassArguments => [qw/id/], +&> <%INIT> my @results; @@ -85,165 +80,85 @@ } else { $title = loc("Modify the content of dashboard [_1]", $Dashboard->Name); } +my $content = $Dashboard->{Attribute}->SubValue('Elements'); -my @sections; -my %item_for; +if ( $ARGS{AddRow} || $ARGS{Update} ) { -my @components; + if ( $ARGS{AddRow} ) { + push @$content, { Elements => [], map { $_ => $ARGS{$_} } grep { $ARGS{$_} } qw/Layout Title/ }; -if ($self_service_dashboard) { - @components = map { type => "component", name => $_, label => loc($_) }, @{RT->Config->Get('SelfServicePageComponents') || []}; -} else { - @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; - - # saved searches and charts - for ($m->comp("/Search/Elements/SearchesForObject", Object => $object)) { - my ($desc, $loc_desc, $search) = @$_; + if ( $ARGS{SeparatedColumns} ) { + push @{ $content->[-1]{Elements} }, [] for 1 .. $ARGS{Columns}; + } - my $SearchType = 'Ticket'; - if ((ref($search->Content)||'') eq 'HASH') { - $SearchType = $search->Content->{'SearchType'} - if $search->Content->{'SearchType'}; + my ( $ret, $msg ) = $Dashboard->Update( Elements => $content ); + if ($self_service_dashboard) { + push @results, $ret ? loc('Self-Service home page updated') : $msg; } else { - $RT::Logger->debug("Search ".$search->id." ($desc) appears to have no Content"); + push @results, $ret ? loc('Dashboard updated') : $msg; } - - my $item; - my $oid = $object_id.'-SavedSearch-'.$search->Id; - $item = { type => 'saved', name => $oid, search_type => $SearchType, label => $loc_desc }; - - my $setting = RT::SavedSearch->new($session{CurrentUser}); - $setting->Load($object, $search->Id); - - $item->{possibly_hidden} = !$setting->IsVisibleTo($Dashboard->Privacy); - - $item_for{ $item->{type} }{ $item->{name} } = $item; - push @items, $item; - } - - for my $dashboard ($m->comp("/Dashboards/Elements/DashboardsForObject", Object => $object, Flat => 1)) { - # Users *can* set up mutually recursive dashboards, but don't make it - # THIS easy for them to shoot themselves in the foot. - next if $dashboard->Id == $Dashboard->id; - - my $name = 'dashboard-' . $dashboard->Id . '-' . $dashboard->Privacy; - - my $item = { type => 'dashboard', name => $name, label => $dashboard->Name }; - $item->{possibly_hidden} = !$dashboard->IsVisibleTo($Dashboard->Privacy); - - $item_for{ $item->{type} }{ $item->{name} } = $item; - push @items, $item; } + else { - 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 %pane_name = ( - 'body' => loc('Body'), - 'sidebar' => loc('Sidebar'), -); - -my %selected; -do { - my $panes = $Dashboard->Panes; - for my $pane (keys %$panes) { - my @items; - for my $saved (@{ $panes->{$pane} }) { - my $item; - if ($saved->{portlet_type} eq 'component') { - $item = $item_for{ $saved->{portlet_type} }{ $saved->{component} }; - } - elsif ($saved->{portlet_type} eq 'search') { - my $name = join '-', $saved->{privacy}, 'SavedSearch', $saved->{id}; - $item = $item_for{saved}{$name}; - } - else { - my $type = $saved->{portlet_type}; - my $name = join '-', $type, $saved->{id}, $saved->{privacy}; - $item = $item_for{$type}{$name}; + my $new_content = eval { JSON::from_json( $ARGS{Content} ) }; + if ($@) { + push @results, loc("Couldn't decode JSON"); + } + else { + if ( JSON::to_json( $new_content, { canonical => 1, pretty => 1 } ) ne + JSON::to_json( $content, { canonical => 1, pretty => 1 } ) ) + { + + my ( $ret, $msg ) = $Dashboard->Update( Elements => $new_content ); + if ($self_service_dashboard) { + push @results, $ret ? loc('Self-Service home page updated') : $msg; + } + else { + push @results, $ret ? loc('Dashboard updated') : $msg; + } } + } - if ($item) { - push @items, $item; - } - else { - push @results, loc('Unable to find [_1] [_2]', $saved->{portlet_type}, $saved->{description}); - } + my $path; + my $args; + if ($self_service_dashboard) { + $path = '/Admin/Global/SelfServiceHomePage.html'; + $args = {}; } - $selected{$pane} = \@items; + else { + $path = '/Dashboards/Queries.html'; + $args = { id => $id }; + } + MaybeRedirectForResults( + Actions => \@results, + Path => $path, + Arguments => $args, + ); } -}; +} -my @filters = ( - [ 'component' => loc('Components') ], - [ 'dashboard' => loc('Dashboards') ], - [ 'ticket' => loc('Tickets') ], - [ 'chart' => loc('Charts') ], +my @available_widgets = GetAvailableWidgets( + Class => 'RT::Dashboard', + Page => $self_service_dashboard ? 'SelfService' : '', + Dashboard => $Dashboard, ); + $m->callback( - CallbackName => 'Default', - pane_name => \%pane_name, - sections => \@sections, - selected => \%selected, - filters => \@filters, + CallbackName => 'Default', + AvailableWidgets => \@available_widgets, + Content => $content, + Dashboard => $Dashboard, ); -if ( $ARGS{UpdateSearches} ) { - $ARGS{dashboard_id} = $id; - $ARGS{self_service_dashboard} = $self_service_dashboard; - my ($ok, $msg) = UpdateDashboard( \%ARGS, \%item_for ); - if ($self_service_dashboard) { - push @results, $ok ? loc('Self-Service home page updated') : $msg; - } else { - push @results, $ok ? loc('Dashboard updated') : $msg; - } - - my $path; - my $args; - if ($self_service_dashboard) { - $path = '/Admin/Global/SelfServiceHomePage.html'; - $args = { }; - } else { - $path = '/Dashboards/Queries.html'; - $args = { id => $id }; +my %available_widgets = map { join( '-', $_->{portlet_type}, $_->{component} || $_->{id} ) => $_ } + @available_widgets; +for my $widget ( $Dashboard->Portlets ) { + if ( !$available_widgets{ join( '-', $widget->{portlet_type}, $widget->{component} || $widget->{id} )} ) { + push @results, loc('Unable to find [_1] [_2]', $widget->{portlet_type}, $widget->{description}); } - MaybeRedirectForResults( - Actions => \@results, - Path => $path, - Arguments => $args, - ); } - <%ARGS> $id => '' unless defined $id diff --git a/share/html/Dashboards/Render.html b/share/html/Dashboards/Render.html index 03f430725d7..d0b9cd5371e 100644 --- a/share/html/Dashboards/Render.html +++ b/share/html/Dashboards/Render.html @@ -66,29 +66,29 @@ % } % } -% $m->callback(CallbackName => 'BeforeTable', Dashboard => $Dashboard, show_cb => $show_cb); - -
- -% $m->callback(CallbackName => 'BeforePanes', Dashboard => $Dashboard, show_cb => $show_cb); - -% my $body = $show_cb->('body'); -% my $sidebar = $show_cb->('sidebar'); -
- <% $body |n %> -
- -% if ( $sidebar =~ /\S/ ) { -
- <% $sidebar |n %> -
-% } +% $m->callback(CallbackName => 'BeforeTable', Dashboard => $Dashboard); + +
+ +% $m->callback(CallbackName => 'BeforePanes', Dashboard => $Dashboard); + +<& /Elements/ShowWidgets, + Object => $Dashboard, + Layout => '', + Elements => $Dashboard->{Attribute}->SubValue('Elements') || [[]], + Rows => $rows, + Preview => $Preview, + Dashboard => $Dashboard, + Depth => 0, + HasResults => $HasResults, + PassArguments => [ qw/Object Rows Preview Dashboard Depth HasResults PassArguments/ ], +&> -% $m->callback(CallbackName => 'AfterPanes', Dashboard => $Dashboard, show_cb => $show_cb); +% $m->callback(CallbackName => 'AfterPanes', Dashboard => $Dashboard);
-% $m->callback(CallbackName => 'AfterTable', Dashboard => $Dashboard, show_cb => $show_cb); +% $m->callback(CallbackName => 'AfterTable', Dashboard => $Dashboard); % if (!$Preview) { @@ -150,10 +150,6 @@ Abort(loc("Could not load dashboard [_1]", $id), Code => HTTP::Status::HTTP_NOT_FOUND); } -# Pick reasonable defaults -my $body_width = $Dashboard->Width->{body} // 8; -my $sidebar_width = $Dashboard->Width->{sidebar} // 4; - # Remove in RT 6.2. See Deprecation notice in ProcessQuickCreate. my $path = '/Dashboards/' . $Dashboard->id . '/' . $Dashboard->Name; unless ( $skip_create ) { @@ -192,19 +188,6 @@ $title = loc('[_1] Dashboard', $Dashboard->Name); } -my $show_cb = sub { - my $pane = shift; - return $m->scomp('Elements/ShowPortlet/dashboard', - Portlet => $Dashboard, - Rows => $rows, - Preview => $Preview, - Dashboard => $Dashboard, - Pane => $pane, - Depth => 0, - HasResults => $HasResults, - ); -}; - my $Refresh = $Preview ? $session{'home_refresh_interval'} || RT->Config->Get('HomePageRefreshInterval', $session{'CurrentUser'}) diff --git a/share/html/Elements/EditPageLayout b/share/html/Elements/EditPageLayout index af0bb5d9ecf..4cd6007561f 100644 --- a/share/html/Elements/EditPageLayout +++ b/share/html/Elements/EditPageLayout @@ -56,13 +56,28 @@
+% my $section = ''; % for my $item ( @AvailableWidgets ) { -
+% my $value; +% if ( ref $item ) { +% $value = {%$item}; +% delete $value->{$_} for qw/section label tooltip/; +% } else { +% $value = $item; +% } + +% if ( ref $item && ($item->{section} // '') ne $section ) { +% $section = $item->{section}; +
<% $section %>
+% } +

<% loc('Place here') %>

- <% $item %> + <% ref $item ? $item->{label} : $item %> -% if ( $item eq 'CustomFieldCustomGroupings' ) { +% if ( ref $item && $item->{tooltip} ) { + <% GetSVGImage( Name => 'info', Title => $item->{tooltip} ) |n %> +% } elsif ( $item eq 'CustomFieldCustomGroupings' ) { <% GetSVGImage( Name => 'info', Title => ' ' ) |n %> <% GetSVGImage( Name => 'pencil', Title => loc('Edit') ) |n %> @@ -219,7 +234,15 @@

<%INIT> -my %available_widgets = map { $_ => 1 } @AvailableWidgets; +my $widget_key = sub { + my $widget = shift; + return + ref $widget + ? join( '-', grep defined, $widget->{portlet_type}, $widget->{component} || $widget->{id} ) + : $widget =~ /^([^:]*)/ && $1; +}; + +my %available_widgets = map { $widget_key->($_) => $_ } @AvailableWidgets; my @rows; my @items; @@ -240,7 +263,7 @@ for my $item (@$Content) { Title => $item->{Title}, Layout => $item->{Layout}, Class => $column_classes[ $col % @column_classes ], - Elements => [ grep { /^([^:]*)/ && $available_widgets{$1} } @{ $item->{Elements}[$col] } ], + Elements => [ map { $available_widgets{ $widget_key->($_) } || () } @{ $item->{Elements}[$col] } ], }; } push @rows, $new_row; @@ -251,12 +274,12 @@ for my $item (@$Content) { Title => $item->{Title}, Layout => $item->{Layout}, Classes => \@column_classes, - Elements => [ grep { /^([^:]*)/ && $available_widgets{$1} } @{ $item->{Elements} } ], + Elements => [ map { $available_widgets{ $widget_key->($_) } || () } @{ $item->{Elements} } ], }; } } else { - push @items, $item if $item =~ /^([^:]*)/ && $available_widgets{$1}; + push @items, $available_widgets{ $widget_key->($item) } || (); } } push @rows, \@items if @items; @@ -279,12 +302,14 @@ $Content => [] <%METHOD EditWidget> -
+

<% loc('Place here') %>

- <% $Widget =~ /(.+):/ ? $1 : $Widget %> + <% ref $Widget ? $Widget->{label} : $Widget =~ /(.+):/ ? $1 : $Widget %> -% if ( $Widget =~ /^CustomFieldCustomGroupings\b/ ) { +% if ( ref $Widget && $Widget->{tooltip} ) { + <% GetSVGImage( Name => 'info', Title => $Widget->{tooltip} ) |n %> +% } elsif ( $Widget =~ /^CustomFieldCustomGroupings\b/ ) { <% GetSVGImage( Name => 'info', $Widget =~ /.*:(.+)/ ? ( Title => $1 ) : ( Title => ' ', ExtraClasses => 'hidden' ) ) |n %> <% GetSVGImage( Name => 'pencil', Title => loc('Edit') ) |n %> @@ -298,6 +323,16 @@ $Content => [] <& SELF:EditWidgetCustomFieldCustomGroupings, %ARGS &> % }

+<%INIT> +my $value; +if ( ref $Widget ) { + $value = {%$Widget}; + delete $value->{$_} for qw/section label tooltip/; +} +else { + $value = $Widget; +} + <%ARGS> $Widget => undef $Index => undef diff --git a/share/html/Elements/MyRT b/share/html/Elements/MyRT index 926976534ed..6f8dc51a806 100644 --- a/share/html/Elements/MyRT +++ b/share/html/Elements/MyRT @@ -46,115 +46,53 @@ %# %# END BPS TAGGED BLOCK }}} % $m->callback( ARGSRef => \%ARGS, CallbackName => 'BeforeTable' ); -
+
-
-% $show_cb->($_) foreach @$body; -
- -% if ( $sidebar ) { -
-% $show_cb->($_) foreach @$sidebar; -
-% } +<& /Elements/ShowWidgets, + Object => $dashboard, + Layout => '', + Elements => $dashboard->{Attribute}->SubValue('Elements'), + Rows => $Rows, + Preview => 1, + Dashboard => $dashboard, + Depth => 0, + HasResults => undef, + PassArguments => [ qw/Object Rows Preview Dashboard Depth HasResults/ ], +&>
% $m->callback( ARGSRef => \%ARGS, CallbackName => 'AfterTable' ); <%INIT> -my $body_width = 8; -my $sidebar_width = 4; - -my %allowed_components = map {$_ => 1} @{RT->Config->Get('HomepageComponents')}; - my $user = $session{'CurrentUser'}->UserObj; -unless ( $Portlets ) { - 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 ) { - my $user_msg = loc('Unable to load selected dashboard, it may have been deleted'); - if ( $dashboard_id == $system_default_id ) { - RT->Logger->warn("Unable to load dashboard: $msg"); - $m->out($m->scomp('/Elements/ListActions', actions => $user_msg)); - return; - } - else { - my ( $ok, $sys_msg ) = $dashboard->LoadById( $system_default_id ); - if ( $ok ) { - $m->out($m->scomp('/Elements/ListActions', actions => [$user_msg, loc('Setting homepage 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 { - RT->Logger->warn("Unable to load dashboard: $msg $sys_msg"); - $m->out($m->scomp('/Elements/ListActions', actions => $user_msg)); - return; - } - } - } - $Portlets = $dashboard->Panes; - $body_width = $dashboard->Width->{body} // 8; - $sidebar_width = $dashboard->Width->{sidebar} // 4; -} -$m->callback( CallbackName => 'MassagePortlets', Portlets => $Portlets ); +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; -my ($body, $sidebar) = @{$Portlets}{qw(body sidebar)}; -unless( $body && @$body ) { - $body = $sidebar || []; - $sidebar = undef; -} -$sidebar = undef unless $sidebar && @$sidebar; - -my $Rows = $user->Preferences( 'SummaryRows', ( RT->Config->Get('DefaultSummaryRows') || 10 ) ); - -my $show_cb = RT::Util::RecursiveSub(sub { - my $self_cb = shift; - my $entry = shift; - my $type; - my $name; - - # Normal handling for RT 5.0.2 and newer - my $depth = shift || 0; - Abort("Possible recursive dashboard detected.", SuppressHeader => 1) if $depth > 8; - - $type = $entry->{portlet_type}; - $name = $entry->{component}; - if ( $type eq 'component' ) { - if (!$allowed_components{$name}) { - $m->out( $m->interp->apply_escapes( loc("Invalid portlet [_1]", $name), "h" ) ); - RT->Logger->info("Invalid portlet $name found on user " . $user->Name . "'s homepage"); - if ($name eq 'QueueList' && $allowed_components{Quicksearch}) { - RT->Logger->warning("You may need to replace the component 'Quicksearch' in the HomepageComponents config with 'QueueList'. See the UPGRADING-4.4 document."); - } +# 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 ) { + my $user_msg = loc('Unable to load selected dashboard, it may have been deleted'); + if ( $dashboard_id == $system_default_id ) { + RT->Logger->warn("Unable to load dashboard: $msg"); + $m->out($m->scomp('/Elements/ListActions', actions => $user_msg)); + return; + } + else { + my ( $ok, $sys_msg ) = $dashboard->LoadById( $system_default_id ); + if ( $ok ) { + $m->out($m->scomp('/Elements/ListActions', actions => [$user_msg, loc('Setting homepage 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 { - local $m->notes->{HTMXLoadComponent} = $name; - $m->comp( $name, %{ $entry->{arguments} || {} } ); - } - } elsif ( $type eq 'search' ) { - $m->comp( '/Elements/ShowSearch', RT::Dashboard->ShowSearchName($entry), HTMXLoad => 1, 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); + RT->Logger->warn("Unable to load dashboard: $msg $sys_msg"); + $m->out($m->scomp('/Elements/ListActions', actions => $user_msg)); return; } - my @panes = @{ $current_dashboard->Panes->{$entry->{pane}} || [] }; - for my $portlet (@panes) { - $self_cb->($portlet, $depth + 1); - } - } else { - $RT::Logger->error("unknown portlet type '$type'"); } -}); +} +my $Rows = $user->Preferences( 'SummaryRows', ( RT->Config->Get('DefaultSummaryRows') || 10 ) ); -<%ARGS> -$Portlets => undef - diff --git a/share/html/Elements/ShowWidgets b/share/html/Elements/ShowWidgets index 31c241a81be..f60cb52e8db 100644 --- a/share/html/Elements/ShowWidgets +++ b/share/html/Elements/ShowWidgets @@ -57,13 +57,21 @@ % for my $widget ( ref $Content->[$col] eq 'ARRAY' ? @{$Content->[$col]} : $Content->[$col] ) { % my $path; -% if ( ref $widget eq 'HASH' ) { +% if ( ref $widget eq 'HASH' && $widget->{Elements}) { % if ( $widget->{Title} || ($widget->{Type} // '') eq 'Section' ) { % $path = '/Elements/ShowWidgetSection'; % } else { % $path = '/Elements/ShowWidgetRow'; % } % } +% elsif ( $Object->isa('RT::Dashboard') ) { +% $ARGS{Depth}++; +% $m->comp("/Dashboards/Elements/ShowPortlet/$widget->{portlet_type}", +% Portlet => $widget, +% %ARGS, +% ); +% next; +% } % else { % $path = $path_prefix . $widget; % } diff --git a/share/html/Widgets/SearchSelection b/share/html/Widgets/SearchSelection index adc5fc5d80a..3ad2698fddb 100644 --- a/share/html/Widgets/SearchSelection +++ b/share/html/Widgets/SearchSelection @@ -85,58 +85,7 @@
-% if ($dashboard_setup) { - -% }
% for my $pane (sort keys %pane_name) { @@ -151,18 +100,6 @@
-% if ( $dashboard_setup ) { -
-
-
-
- <% loc("[_1] Width:", ucfirst $pane) %> - - <% loc('/ 12 Columns') %> -
-
-
-% } % } @@ -172,19 +109,6 @@ <%INIT> use utf8; - -# Defaults needed here so the editor controls can setup properly for dashboards -# without column widths that are explicitly set. -my ( $body_width, $sidebar_width ); -if ( $dashboard_setup ) { - $body_width = $ARGS{ body_width } // 8; - $sidebar_width = $ARGS{ sidebar_width } // 4; -} else { - # This is some other use of this widget; not for setting up a dashboard - $body_width = 12; - $sidebar_width = 0; -} - $m->callback( CallbackName => 'Default', sections => \@sections, @@ -197,5 +121,4 @@ $m->callback( @filters @sections %selected -$dashboard_setup => 0 diff --git a/share/static/css/elevator/dashboards.css b/share/static/css/elevator/dashboards.css index 4ede5dd3023..731c36cd038 100644 --- a/share/static/css/elevator/dashboards.css +++ b/share/static/css/elevator/dashboards.css @@ -5,30 +5,3 @@ table.dashboard { #body>table.dashboard { margin-top: inherit } - -/* Dashboard sliders */ -.ui-slider-handle { - padding-top: 15px; - padding-bottom: 5px; -} - -.width-slider { - margin-top: 10px; - padding-top: 15px; -} - -.selectionbox-js .width-slider-wrapper .input-group>.input-group-text { - border: none; -} - -.selectionbox-js .width-slider-wrapper .input-group>.form-control { - width: 50px; - flex: none; -} - - -[data-bs-theme=light] { - .selectionbox-js .width-slider-wrapper .input-group>.input-group-text { - background-color: #fff; - } -} diff --git a/share/static/js/pagelayout.js b/share/static/js/pagelayout.js index f630229ddc1..da2496ce39e 100644 --- a/share/static/js/pagelayout.js +++ b/share/static/js/pagelayout.js @@ -181,7 +181,7 @@ pageLayout = { const form = this; const modal = form.closest('.pagelayout-widget-modal'); const widget = document.querySelector('#' + modal.getAttribute('id').replace(/-modal$/, '')); - if (widget.getAttribute('data-value').match(/^CustomFieldCustomGroupings\b/)) { + if (JSON.parse(widget.getAttribute('data-value')).match(/^CustomFieldCustomGroupings\b/)) { const options = form.querySelector('select[name=Groupings]').options; const groupings = Array.from(options).filter((option) => option.selected).map((option) => option.value); if (groupings.length) { @@ -319,7 +319,7 @@ pageLayout = { row.querySelectorAll('.pagelayout-content').forEach((elt) => { const items = []; elt.querySelectorAll('.pagelayout-widget').forEach((elt) => { - items.push(elt.getAttribute('data-value')); + items.push(JSON.parse(elt.getAttribute('data-value'))); }); widgets.push(items); }); @@ -333,7 +333,7 @@ pageLayout = { } else { row.querySelectorAll('.pagelayout-widget').forEach((elt) => { - content.push(elt.getAttribute('data-value')); + content.push(JSON.parse(elt.getAttribute('data-value'))); }); } }); From d2a3382a58448ffbe91e66f89a101867b87dcb00 Mon Sep 17 00:00:00 2001 From: sunnavy Date: Wed, 21 Aug 2024 01:08:47 -0400 Subject: [PATCH 2/9] Update dashboard tests for the dashboard structure change --- t/mail/dashboard-chart-with-utf8.t | 44 ++++++--- t/mail/dashboard-empty.t | 45 +++++++-- t/mail/dashboards.t | 35 +++++-- t/web/custom_frontpage.t | 123 ++++++++++++++++++------ t/web/dashboards-basics.t | 68 +++++++++---- t/web/dashboards-deleted-saved-search.t | 73 +++++++++----- t/web/dashboards-search-cache.t | 55 ++++++----- 7 files changed, 315 insertions(+), 128 deletions(-) diff --git a/t/mail/dashboard-chart-with-utf8.t b/t/mail/dashboard-chart-with-utf8.t index 027ef5071ee..1d9eae5ec0a 100644 --- a/t/mail/dashboard-chart-with-utf8.t +++ b/t/mail/dashboard-chart-with-utf8.t @@ -2,6 +2,7 @@ use strict; use warnings; use RT::Test tests => undef; +use JSON; plan skip_all => 'GD required' unless RT::StaticUtil::RequireModule("GD"); @@ -28,6 +29,8 @@ $m->submit_form( button => 'SavedSearchSave', ); +my ( $privacy, $saved_search_id ) = $m->content =~ /value="(RT::User-\d+)-SavedSearch-(\d+)"/; + # first, create and populate a dashboard $m->get_ok('/Dashboards/Modify.html?Create=1'); $m->form_name('ModifyDashboard'); @@ -39,21 +42,34 @@ ok( $dashboard_id, "got an ID for the dashboard, $dashboard_id" ); $m->follow_link_ok( { text => 'Content' } ); -# add content, Chart: chart foo, to dashboard body -# we need to get the saved search id from the content before submitting the form. -my $regex = qr/data-type="(\w+)" data-name="RT::User-/ . $root->id . qr/-SavedSearch-(\d+)"/; -my ( $saved_search_type, $saved_search_id ) = $m->content =~ /$regex/; -ok( $saved_search_type, "got a type for the saved search, $saved_search_type" ); -ok( $saved_search_id, "got an ID for the saved search, $saved_search_id" ); - -$m->submit_form_ok({ - form_name => 'UpdateSearches', - fields => { - dashboard_id => $dashboard_id, - body => $saved_search_type . "-" . "RT::User-" . $root->id . "-SavedSearch-" . $saved_search_id, +$m->submit_form_ok( + { + form_id => 'pagelayout-form-modify', + fields => { + id => $dashboard_id, + Content => JSON::encode_json( + [ + { + Layout => 'col-md-8, col-md-4', + Elements => [ + [ + { + portlet_type => 'search', + id => $saved_search_id, + description => 'Chart: chart foo', + privacy => $privacy, + } + ], + [], + ], + } + ] + ), + }, + button => 'Update', }, - button => 'UpdateSearches', -}, "add content 'Chart: chart foo' to dashboard body" ); + "add content 'Chart: chart foo' to dashboard" +); like( $m->uri, qr/results=[A-Za-z0-9]{32}/, 'URL redirected for results' ); $m->content_contains( 'Dashboard updated' ); diff --git a/t/mail/dashboard-empty.t b/t/mail/dashboard-empty.t index cd8d5a3d196..9be2bf2def7 100644 --- a/t/mail/dashboard-empty.t +++ b/t/mail/dashboard-empty.t @@ -3,6 +3,7 @@ use warnings; use RT::Test tests => undef; use RT::Dashboard::Mailer; +use JSON; my $root = RT::Test->load_or_create_user( Name => 'root' ); @@ -27,21 +28,45 @@ sub create_dashboard { my $component_name = shift; my $arg; + my @elements; if ( $component_name eq 'My Tickets' ) { - $arg = 'saved-' . $m->dom->find('[data-description="My Tickets"]')->first->attr('data-name'), + my ($search) = RT::System->new( RT->SystemUser )->Attributes->Named( 'Search - ' . $component_name ); + push @elements, + { + portlet_type => 'search', + id => $search->Id, + description => "Ticket: $component_name", + privacy => join( '-', ref( RT->System ), RT->System->Id ), + }; } else { # component_name is 'My Assets' - $arg = 'component-MyAssets'; + push @elements, + { + portlet_type => 'component', + component => 'MyAssets', + description => 'MyAssets', + path => '/Elements/MyAssets', + }; } - $m->submit_form_ok({ - form_name => 'UpdateSearches', - fields => { - dashboard_id => $dashboard_id, - body => $arg, - }, - button => 'UpdateSearches', - }, "added '$component_name' to dashboard '$name'" ); + $m->submit_form_ok( + { + form_id => 'pagelayout-form-modify', + fields => { + id => $dashboard_id, + Content => JSON::encode_json( + [ + { + Layout => 'col-md-8, col-md-4', + Elements => [ \@elements, [], ], + } + ] + ), + }, + button => 'Update', + }, + "added '$component_name' to dashboard '$name'" + ); like( $m->uri, qr/results=[A-Za-z0-9]{32}/, 'URL redirected for results' ); $m->content_contains( 'Dashboard updated' ); diff --git a/t/mail/dashboards.t b/t/mail/dashboards.t index e2eafaa2b26..6630d97707e 100644 --- a/t/mail/dashboards.t +++ b/t/mail/dashboards.t @@ -23,14 +23,35 @@ sub create_dashboard { $m->follow_link_ok({text => 'Content'}); $m->title_is('Modify the content of dashboard Testing!'); - $m->submit_form_ok({ - form_name => 'UpdateSearches', - fields => { - dashboard_id => $dashboard_id, - body => 'component-Dashboards', + $m->submit_form_ok( + { + form_id => 'pagelayout-form-modify', + fields => { + id => $dashboard_id, + Content => JSON::encode_json( + [ + { + Layout => 'col-md-8, col-md-4', + Elements => [ + [ + { + portlet_type => 'component', + component => 'Dashboards', + description => 'Dashboards', + path => '/Elements/Dashboards', + } + + ], + [], + ], + } + ] + ), + }, + button => 'Update', }, - button => 'UpdateSearches', - }, "added 'Dashboards' to dashboard 'Testing!'" ); + "added 'Dashboards' to dashboard 'Testing!'" + ); like( $m->uri, qr/results=[A-Za-z0-9]{32}/, 'URL redirected for results' ); $m->content_contains( 'Dashboard updated' ); diff --git a/t/web/custom_frontpage.t b/t/web/custom_frontpage.t index 7d9f22db3fd..1ef8e058f6b 100644 --- a/t/web/custom_frontpage.t +++ b/t/web/custom_frontpage.t @@ -2,6 +2,7 @@ use strict; use warnings; use RT::Test tests => undef; +use JSON; my ($baseurl, $m) = RT::Test->started_ok; my $url = $m->rt_base_url; @@ -50,23 +51,30 @@ $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", - body => [], - sidebar => [], -}; +my %searches; +for my $search_name ( 'My Tickets', 'Unowned Tickets', 'Bookmarked Tickets' ) { + my ($search) = RT::System->new( RT->SystemUser )->Attributes->Named( 'Search - ' . $search_name ); + $searches{$search_name} = { + portlet_type => 'search', + id => $search->Id, + description => "Ticket: $search_name", + privacy => join( '-', ref( RT->System ), RT->System->Id ), + }; +} + +my $content = [ + { + Layout => 'col-md-8, col-md-4', + Elements => [ [ $searches{'Unowned Tickets'} ], [], ], + } +]; # remove all portlets from the body pane except 'newest unowned tickets' $m->follow_link_ok( { text => 'Content' } ); -push( - @{$args->{body}}, - "saved-" . $m->dom->find('[data-description="Unowned Tickets"]')->first->attr('data-name'), -); my $res = $m->post( $url . "Dashboards/Queries.html?id=$id", - $args, + { Update => 1, Content => JSON::encode_json($content) }, ); is( $res->code, 200, "remove all portlets from body except 'newest unowned tickets'" ); @@ -92,20 +100,32 @@ $m->get_ok( $url . "Dashboards/Queries.html?id=$id" ); # add back the previously removed portlets push( - @{$args->{body}}, - "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", + @{$content->[0]{Elements}[0]}, + $searches{'My Tickets'}, + $searches{'Bookmarked Tickets'}, + { + portlet_type => 'component', + component => 'QuickCreate', + description => 'QuickCreate', + path => '/Elements/QuickCreate', + + } ); push( - @{$args->{sidebar}}, - ( "component-MyReminders", "component-QueueList", "component-Dashboards", "component-RefreshHomepage", ) + @{$content->[0]{Elements}[1]}, + map { + portlet_type => 'component', + component => $_, + description => $_, + path => "/Elements/$_", + }, + qw/MyReminders QueueList Dashboards RefreshHomepage/ ); $res = $m->post( $url . "Dashboards/Queries.html?id=$id", - $args, + { Update => 1, Content => JSON::encode_json($content) }, ); is( $res->code, 200, 'add back previously removed portlets' ); @@ -124,7 +144,7 @@ $m->form_name('BuildQuery'); $m->field( "ValueOfAttachment" => 'stupid' ); $m->field( "SavedSearchDescription" => 'special chars [test] [_1] ~[_1~]' ); $m->click_button( name => 'SavedSearchSave' ); -my ($name) = $m->content =~ /value="(RT::User-\d+-SavedSearch-\d+)"/; +my ( $name, $privacy, $search_id ) = $m->content =~ /value="((RT::User-\d+)-SavedSearch-(\d+))"/; ok( $name, 'saved search name' ); $m->get_ok( $url . "Dashboards/Queries.html?id=$id" ); @@ -133,15 +153,21 @@ $m->content_contains( 'special chars [test] [_1] ~[_1~]', # add saved search to body push( - @{$args->{body}}, - ( "saved-" . $name ) + @{$content->[0]{Elements}[0]}, + { + portlet_type => 'search', + id => $search_id, + description => "Ticket: $name", + privacy => $privacy, + } ); $res = $m->post( $url . "Dashboards/Queries.html?id=$id", - $args, + { Update => 1, Content => JSON::encode_json($content) }, ); + 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( 'Dashboard updated' ); @@ -179,6 +205,13 @@ $m->submit_form( button => 'SavedSearchSave', ); $m->content_contains("Chart first chart saved", 'saved first chart' ); +( $search_id ) = $m->content =~ /value="RT::System-1-SavedSearch-(\d+)"/; +$searches{'first chart'} = { + portlet_type => 'search', + id => $search_id, + description => "Chart: first chart", + privacy => join( '-', ref( RT->System ), RT->System->Id ), +}; $m->get_ok( $url . "/Search/Build.html?Class=RT::Transactions&Query=" . 'TicketId=1' ); @@ -193,6 +226,14 @@ $m->submit_form( # We don't show saved message on page :/ $m->content_contains("Save as New", 'saved first txn search' ); +( $search_id ) = $m->content =~ /value="RT::System-1-SavedSearch-(\d+)"/; +$searches{'first txn search'} = { + portlet_type => 'search', + id => $search_id, + description => "Transaction: first txn search", + privacy => join( '-', ref( RT->System ), RT->System->Id ), +}; + $m->get_ok( $url . "/Search/Chart.html?Class=RT::Transactions&Query=" . 'id>1' ); $m->submit_form( @@ -205,6 +246,14 @@ $m->submit_form( ); $m->content_contains("Chart first txn chart saved", 'saved first txn chart' ); +( $search_id ) = $m->content =~ /value="RT::System-1-SavedSearch-(\d+)"/; +$searches{'first txn chart'} = { + portlet_type => 'search', + id => $search_id, + description => "Chart: first txn chart", + privacy => join( '-', ref( RT->System ), RT->System->Id ), +}; + # Add asset saved searches $m->get_ok( $url . "/Search/Build.html?Class=RT::Assets&Query=" . 'id>0' ); @@ -219,6 +268,14 @@ $m->submit_form( # We don't show saved message on page :/ $m->content_contains("Save as New", 'saved first asset search' ); +( $search_id ) = $m->content =~ /value="RT::System-1-SavedSearch-(\d+)"/; +$searches{'first asset search'} = { + portlet_type => 'search', + id => $search_id, + description => "Asset: first asset search", + privacy => join( '-', ref( RT->System ), RT->System->Id ), +}; + $m->get_ok( $url . "/Search/Chart.html?Class=RT::Assets&Query=" . 'id>0' ); $m->submit_form( @@ -230,20 +287,28 @@ $m->submit_form( button => 'SavedSearchSave', ); $m->content_contains("Chart first asset chart saved", 'saved first txn chart' ); +( $search_id ) = $m->content =~ /value="RT::System-1-SavedSearch-(\d+)"/; +$searches{'first asset chart'} = { + portlet_type => 'search', + id => $search_id, + description => "Asset: first asset chart", + privacy => join( '-', ref( RT->System ), RT->System->Id ), +}; + $m->get_ok( $url . "Dashboards/Queries.html?id=$id" ); push( - @{$args->{body}}, - "saved-" . $m->dom->find('[data-description="first chart"]')->first->attr('data-name'), - "saved-" . $m->dom->find('[data-description="first txn search"]')->first->attr('data-name'), - "saved-" . $m->dom->find('[data-description="first txn chart"]')->first->attr('data-name'), - "saved-" . $m->dom->find('[data-description="first asset search"]')->first->attr('data-name'), - "saved-" . $m->dom->find('[data-description="first asset chart"]')->first->attr('data-name'), + @{ $content->[0]{Elements}[0] }, + map { $searches{$_} } 'first chart', + 'first txn search', + 'first txn chart', + 'first asset search', + 'first asset chart' ); $res = $m->post( $url . "Dashboards/Queries.html?id=$id", - $args, + { Update => 1, Content => JSON::encode_json($content) }, ); is( $res->code, 200, 'add system saved searches to body' ); diff --git a/t/web/dashboards-basics.t b/t/web/dashboards-basics.t index aac674df06a..176b1b6d21e 100644 --- a/t/web/dashboards-basics.t +++ b/t/web/dashboards-basics.t @@ -35,6 +35,7 @@ for my $user ($user_obj, $onlooker) { } } +my %searches; # Add some system non-ticket searches ok $m->login('root'), "logged in as root"; $m->get_ok( $url . "/Search/Chart.html?Query=" . 'id=1' ); @@ -48,6 +49,13 @@ $m->submit_form( button => 'SavedSearchSave', ); $m->content_contains("Chart first chart saved", 'saved first chart' ); +my ( $search_id ) = $m->content =~ /value="RT::System-1-SavedSearch-(\d+)"/; +$searches{'first chart'} = { + portlet_type => 'search', + id => $search_id, + description => "Chart: first chart", + privacy => join( '-', ref( RT->System ), RT->System->Id ), +}; $m->get_ok( $url . "/Search/Build.html?Class=RT::Transactions&Query=" . 'TicketId=1' ); @@ -62,6 +70,13 @@ $m->submit_form( # We don't show saved message on page :/ $m->content_contains("Save as New", 'saved first txn search' ); +( $search_id ) = $m->content =~ /value="RT::System-1-SavedSearch-(\d+)"/; +$searches{'first txn search'} = { + portlet_type => 'search', + id => $search_id, + description => "Transaction: first txn search", + privacy => join( '-', ref( RT->System ), RT->System->Id ), +}; ok $m->login(customer => 'customer', logout => 1), "logged in"; @@ -127,15 +142,25 @@ $m->content_contains("Modify the content of dashboard different dashboard"); my ( $id ) = ( $m->uri =~ /id=(\d+)/ ); ok( $id, "got a dashboard ID, $id" ); # 8 -my $args = { - UpdateSearches => "Save", - body => ["saved-" . $m->dom->find('[data-description="Unowned Tickets"]')->first->attr('data-name')], - sidebar => [], -}; +for my $search_name ( 'My Tickets', 'Unowned Tickets', 'Bookmarked Tickets' ) { + my ($search) = RT::System->new( RT->SystemUser )->Attributes->Named( 'Search - ' . $search_name ); + $searches{$search_name} = { + portlet_type => 'search', + id => $search->Id, + description => "Ticket: $search_name", + privacy => join( '-', ref( RT->System ), RT->System->Id ), + }; +} +my $content = [ + { + Layout => 'col-md-8, col-md-4', + Elements => [ [ $searches{'Unowned Tickets'} ], [], ], + } +]; my $res = $m->post( $url . "Dashboards/Queries.html?id=$id", - $args, + { Update => 1, Content => JSON::encode_json($content) }, ); is( $res->code, 200, "add 'unowned tickets' to body" ); @@ -153,16 +178,11 @@ my @searches = $dashboard->Searches; is(@searches, 1, "one saved search in the dashboard"); like($searches[0]->Name, qr/newest unowned tickets/, "correct search name"); -push( - @{$args->{body}}, - "saved-" . $m->dom->find('[data-description="My Tickets"]')->first->attr('data-name'), - "saved-" . $m->dom->find('[data-description="first chart"]')->first->attr('data-name'), - "saved-" . $m->dom->find('[data-description="first txn search"]')->first->attr('data-name'), -); +push @{ $content->[0]{Elements}[0] }, map { $searches{$_} } 'My Tickets', 'first chart', 'first txn search'; $res = $m->post( $url . 'Dashboards/Queries.html?id=' . $id, - $args, + { Update => 1, Content => JSON::encode_json($content) }, ); is( $res->code, 200, "add more searches to body" ); @@ -240,6 +260,16 @@ $m->form_with_fields('SavedSearchDescription'); $m->field(SavedSearchDescription => "personal search"); $m->click_button(name => "SavedSearchSave"); +# get the saved search name from the content +( my $saved_search_name, my $privacy, $search_id ) = ( $m->content =~ /((RT::User-\d+)-SavedSearch-(\d+))/ ); +ok( $saved_search_name, "got a saved search name, $saved_search_name" ); # RT::User-27-SavedSearch-9 +$searches{'personal search'} = { + portlet_type => 'search', + id => $search_id, + description => "Ticket: personal search", + privacy => $privacy, +}; + # then the system-wide dashboard $m->get_ok($url."Dashboards/Modify.html?Create=1"); @@ -256,18 +286,14 @@ $m->follow_link_ok({id => 'page-content'}); my ( $system_id ) = ( $m->uri =~ /id=(\d+)/ ); ok( $system_id, "got a dashboard ID for the system dashboard, $system_id" ); -# get the saved search name from the content -my ( $saved_search_name ) = ( $m->content =~ /(RT::User-\d+-SavedSearch-\d+)/ ); -ok( $saved_search_name, "got a saved search name, $saved_search_name" ); # RT::User-27-SavedSearch-9 - push( - @{$args->{body}}, - ( "saved-" . $saved_search_name, ) + @{ $content->[0]{Elements}[0] }, + $searches{'personal search'}, ); $res = $m->post( - $url . 'Dashboards/Queries.html?id=' . $system_id, - $args, + $url . "Dashboards/Queries.html?id=$system_id", + { Update => 1, Content => JSON::encode_json($content) }, ); is( $res->code, 200, "add 'personal search' to body" ); diff --git a/t/web/dashboards-deleted-saved-search.t b/t/web/dashboards-deleted-saved-search.t index a961363fab9..dc726445d4f 100644 --- a/t/web/dashboards-deleted-saved-search.t +++ b/t/web/dashboards-deleted-saved-search.t @@ -2,6 +2,7 @@ use strict; use warnings; use RT::Test tests => undef; +use JSON; my ( $url, $m ) = RT::Test->started_ok; ok( $m->login, 'logged in' ); @@ -47,22 +48,40 @@ $m->content_lacks( 'value="Update"', 'no update button' ); # add foo saved search to the dashboard -my $args = { - "dashboard_id" => $dashboard_id, - "body" => "saved-" . "RT::User-" . $user_id . "-SavedSearch-" . $search_id, -}; - -$m->submit_form_ok({ - form_name => 'UpdateSearches', - fields => $args, - button => 'UpdateSearches', -}, "added search foo to dashboard bar" ); - -like( $m->uri, qr/results=[A-Za-z0-9]{32}/, 'URL redirected for results' ); +my $content = [ + { + Layout => 'col-md-8, col-md-4', + Elements => [ + [ + { + portlet_type => 'search', + id => $search_id, + description => "Ticket: foo", + privacy => join( '-', 'RT::User', $user_id ), + } + ], + [], + ], + } +]; + +$m->submit_form_ok( + { + form_id => 'pagelayout-form-modify', + fields => { + id => $dashboard_id, + Content => JSON::encode_json($content), + }, + button => 'Update', + }, + "removed search foo from dashboard" +); $m->content_contains( 'Dashboard updated' ); +$m->get_ok( $url . "/Dashboards/Queries.html?id=$dashboard_id" ); # delete the created search +$m->get_ok( $url ); # Get rid of CSRF page $m->get_ok( $url . "/Search/Build.html?Query=" . 'id=1' ); $m->submit_form( form_name => 'BuildQuery', @@ -78,22 +97,28 @@ $m->content_lacks( $search_uri, 'deleted search foo' ); # here is what we really want to test $m->get_ok( $url . "/Dashboards/Queries.html?id=$dashboard_id" ); -$m->content_contains('Unable to find search Saved Search: foo', 'found deleted message' ); - -$args = { - "dashboard_id" => $dashboard_id, -}; - -$m->submit_form_ok({ - form_name => 'UpdateSearches', - fields => $args, - button => 'UpdateSearches', -}, "removed search foo from dashboard" ); +$m->content_contains('Unable to find search Ticket: foo', 'found deleted message' ); + +# Only the message contains Ticket: foo. +$m->text_unlike( qr/Ticket: foo.*Ticket: foo/s, 'no deleted search on page' ); + +delete $content->[0]{Elements}[0][0]; +$m->submit_form_ok( + { + form_id => 'pagelayout-form-modify', + fields => { + id => $dashboard_id, + Content => JSON::encode_json($content), + }, + button => 'Update', + }, + "removed search foo from dashboard" +); like( $m->uri, qr/results=[A-Za-z0-9]{32}/, 'URL redirected for results' ); $m->content_contains( 'Dashboard updated' ); $m->get_ok( $url . "/Dashboards/Queries.html?id=$dashboard_id" ); -$m->content_lacks('Unable to find search Saved Search: foo', 'deleted message is gone' ); +$m->content_lacks('Unable to find search Ticket: foo', 'deleted message is gone' ); done_testing; diff --git a/t/web/dashboards-search-cache.t b/t/web/dashboards-search-cache.t index 8950cece76a..4376dd94da3 100644 --- a/t/web/dashboards-search-cache.t +++ b/t/web/dashboards-search-cache.t @@ -22,6 +22,15 @@ $m->form_name('BuildQuery'); $m->field(SavedSearchDescription => 'Original Name'); $m->click('SavedSearchSave'); +# get the saved search name from the content +my ( $privacy, $search_id ) = ( $m->content =~ /(RT::User-\d+)-SavedSearch-(\d+)/ ); +my $search_widget = { + portlet_type => 'search', + id => $search_id, + description => "Ticket: Original Name", + privacy => $privacy, +}; + # create the inner dashboard $m->get_ok("$url/Dashboards/Modify.html?Create=1"); $m->form_name('ModifyDashboard'); @@ -32,6 +41,13 @@ $m->text_contains('Saved dashboard inner dashboard'); my ($inner_id) = $m->content =~ /name="id" value="(\d+)"/; ok($inner_id, "got an ID, $inner_id"); +my $dashboard_widget = { + portlet_type => 'dashboard', + id => $inner_id, + description => "Dashboard: inner dashboard", + privacy => $privacy, +}; + # create a dashboard $m->get_ok("$url/Dashboards/Modify.html?Create=1"); $m->form_name('ModifyDashboard'); @@ -45,30 +61,23 @@ ok($dashboard_id, "got an ID, $dashboard_id"); # add the search to the dashboard $m->follow_link_ok({text => 'Content'}); -# we need to get the saved search id from the content before submitting the args. -my $regex = 'data-type="saved" data-name="RT::User-' . $root->id . '-SavedSearch-(\d+)"'; -my ($saved_search_id) = $m->content =~ /$regex/; -ok($saved_search_id, "got an ID for the saved search, $saved_search_id"); +my $content = [ + { + Layout => 'col-md-8, col-md-4', + Elements => [ + [ + $search_widget, + $dashboard_widget, -my $args = { - UpdateSearches => "Save", - dashboard_id => $dashboard_id, - body => [], - sidebar => [], -}; - -# add 'Original Name' and 'inner dashboard' portlets to body -push( - @{$args->{body}}, - ( - "saved-" . "RT::User-" . $root->id . "-SavedSearch-" . $saved_search_id, - "dashboard-dashboard-" . $inner_id . "-RT::User-" . $root->id, - ) -); + ], + [], + ], + } +]; my $res = $m->post( $url . 'Dashboards/Queries.html?id=' . $dashboard_id, - $args, + { Update => 1, Content => JSON::encode_json($content) }, ); is( $res->code, 200, "add 'Original Name' and 'inner dashboard' portlets to body" ); @@ -77,7 +86,7 @@ $m->content_contains( 'Dashboard updated' ); # subscribe to the dashboard $m->follow_link_ok({text => 'Subscription'}); -$m->text_contains('Saved Search: Original Name'); +$m->text_contains('Ticket: Original Name'); $m->text_contains('Dashboard: inner dashboard'); $m->form_name('SubscribeDashboard'); $m->click_button(name => 'Save'); @@ -110,8 +119,8 @@ $m->text_contains('Dashboard recursive dashboard updated'); $m->get_ok("/Dashboards/Subscription.html?id=$dashboard_id"); TODO: { local $TODO = 'we cache search names too aggressively'; - $m->text_contains('Saved Search: New Name'); - $m->text_unlike(qr/Saved Search: Original Name/); # t-w-m lacks text_lacks + $m->text_contains('Ticket: New Name'); + $m->text_unlike(qr/Ticket: Original Name/); # t-w-m lacks text_lacks $m->text_contains('Dashboard: recursive dashboard'); $m->text_unlike(qr/Dashboard: inner dashboard/); # t-w-m lacks text_lacks From 5cdea3df127fe5276deb99f1a3f5ec13d954c4ac Mon Sep 17 00:00:00 2001 From: sunnavy Date: Thu, 22 Aug 2024 21:40:45 -0400 Subject: [PATCH 3/9] Canonicalize dashboards in dashboard content after import Previously we only canonicalized save searches and missed dashboards. To canonicalize inner dashboards properly, we need to sort dashboards to make sure inner ones are created first. --- lib/RT/Handle.pm | 39 ++++++++++++++++++++++++++++++++------- 1 file changed, 32 insertions(+), 7 deletions(-) diff --git a/lib/RT/Handle.pm b/lib/RT/Handle.pm index 7d88cf33e07..07547cb1b6b 100644 --- a/lib/RT/Handle.pm +++ b/lib/RT/Handle.pm @@ -1884,18 +1884,36 @@ sub InsertData { 'Subscription' => 2, ); - my $order = sub { - my $name = shift; - return $order{$name} if exists $order{$name}; + my $sort = sub { + my ( $a, $b ) = @_; # Handle customized default dashboards like RTIRDefaultDashboard later than Dashboards. - if ( $name =~ /DefaultDashboard$/ ) { - return 2; + my $a_name = $a->{Name} =~ /DefaultDashboard$/ ? 'DefaultDashboard' : $a->{Name}; + my $b_name = $b->{Name} =~ /DefaultDashboard$/ ? 'DefaultDashboard' : $b->{Name}; + + my $order = ( $order{$a_name} || 0 ) <=> ( $order{$b_name} || 0 ); + return $order if $order; + + if ( $a_name eq 'Dashboard' ) { + my %a_inner_dashboards = map { $_->{description} => 1 } + grep { $_->{portlet_type} eq 'dashboard' } RT::Dashboard->Portlets( $a->{Content}{Elements} || [] ); + my %b_inner_dashboards = map { $_->{description} => 1 } + grep { $_->{portlet_type} eq 'dashboard' } RT::Dashboard->Portlets( $b->{Content}{Elements} || [] ); + + # Create b first if a contains b + for my $key ( keys %a_inner_dashboards ) { + return 1 if $key eq $b->{Description}; + } + + # Create a first if b contains a + for my $key ( keys %b_inner_dashboards ) { + return -1 if $key eq $a->{Description}; + } } return 0; }; - for my $item ( sort { $order->( $a->{Name} ) <=> $order->( $b->{Name} ) } @Attributes ) { + for my $item ( sort { $sort->( $a, $b ) } @Attributes ) { if ( $item->{_Original} ) { $self->_UpdateOrDeleteObject( 'RT::Attribute', $item ); next; @@ -3167,10 +3185,17 @@ sub _CanonilizeAttributeContent { my $item = shift or return; if ( $item->{Name} eq 'Dashboard' ) { for my $entry ( RT::Dashboard->Portlets( $item->{Content}{Elements} || [] ) ) { - next unless $entry->{portlet_type} eq 'search'; + next unless $entry->{portlet_type} =~ /^(?:dashboard|search)$/; if ( $entry->{ObjectType} && $entry->{ObjectId} && $entry->{Description} ) { if ( my $object = $self->_LoadObject( $entry->{ObjectType}, $entry->{ObjectId} ) ) { my $attributes = $object->Attributes->Clone; + if ( $entry->{portlet_type} eq 'dashboard' ) { + $attributes->Limit( FIELD => 'Name', VALUE => 'Dashboard' ); + } + else { + $attributes->Limit( FIELD => 'Name', VALUE => 'SavedSearch' ); + $attributes->Limit( FIELD => 'Name', VALUE => 'Search - ', OPERATOR => 'STARTSWITH' ); + } $attributes->Limit( FIELD => 'Description', VALUE => $entry->{Description} ); if ( my $attribute = $attributes->First ) { $entry->{id} = $attribute->id; From 54ce502ad1efd86a7c2f57bb8f8c415a4396dbba Mon Sep 17 00:00:00 2001 From: sunnavy Date: Tue, 24 Sep 2024 11:28:34 -0400 Subject: [PATCH 4/9] Show HideUnsetFields menu only for display pages by default It's meaningless to show it on pages like dashboards. --- share/html/Elements/DropdownMenu | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/share/html/Elements/DropdownMenu b/share/html/Elements/DropdownMenu index be108d377f8..639468d149f 100644 --- a/share/html/Elements/DropdownMenu +++ b/share/html/Elements/DropdownMenu @@ -77,6 +77,6 @@ <%ARGS> $ShowSearchResults => 1 -@ShowOptions => ('HideUnsetFields') +@ShowOptions => ( $ARGS{Page} // '' ) eq 'Display' ? 'HideUnsetFields' : () From 2af1fd1e5d457a6b7224e9f8fe11f53a8520a291 Mon Sep 17 00:00:00 2001 From: sunnavy Date: Tue, 24 Sep 2024 13:31:46 -0400 Subject: [PATCH 5/9] Limit height of "Available Widgets" in case there are too many saved searches A scrollable "Available Widgets" is more convenient compared to a fully expanded long list. --- share/static/css/elevator/pagelayout.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/share/static/css/elevator/pagelayout.css b/share/static/css/elevator/pagelayout.css index 4289338fbcb..326f62dd136 100644 --- a/share/static/css/elevator/pagelayout.css +++ b/share/static/css/elevator/pagelayout.css @@ -1,4 +1,8 @@ .pagelayout-editor { + .pagelayout-widget-menu { + max-height: 1000px; + overflow-y: auto; + } .pagelayout-widget-menu .float-end svg[title=" "], .pagelayout-widget-menu .float-end svg[data-bs-original-title=" "], From fb0415326b5053bdf7cbd6d316548750dba1c090 Mon Sep 17 00:00:00 2001 From: sunnavy Date: Wed, 25 Sep 2024 10:51:37 -0400 Subject: [PATCH 6/9] Show default dashboard's description on homepage --- lib/RT/Interface/Web.pm | 40 ++++++++++++++++++++++++++++++++++++++ share/html/Elements/MyRT | 42 ++++++++-------------------------------- share/html/index.html | 8 ++++++-- 3 files changed, 54 insertions(+), 36 deletions(-) diff --git a/lib/RT/Interface/Web.pm b/lib/RT/Interface/Web.pm index 6f44611fddb..6ecc3f6ecfc 100644 --- a/lib/RT/Interface/Web.pm +++ b/lib/RT/Interface/Web.pm @@ -6408,6 +6408,46 @@ sub UpdateConfig { return ( $ret, $msg ); } +=head2 GetDefaultDashboard + +Returns an array of current user's default dashboard object(undef if not +found) and possible messages. + +=cut + +sub GetDefaultDashboard { + my $user = $session{'CurrentUser'}->UserObj; + + 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 ) { + my $user_msg = loc('Unable to load selected dashboard, it may have been deleted'); + if ( $dashboard_id == $system_default_id ) { + RT->Logger->warn("Unable to load dashboard: $msg"); + return( undef, $user_msg ); + } + else { + my ( $ok, $sys_msg ) = $dashboard->LoadById($system_default_id); + if ($ok) { + my ( $ok, $msg ) = $user->DeletePreferences('DefaultDashboard'); + RT->Logger->error( "Couldn't delete DefaultDashboard of user " . $user->Name . ": $msg" ) unless $ok; + return( $dashboard, $user_msg, loc('Setting homepage to system default homepage') ); + } + else { + RT->Logger->warn("Unable to load dashboard: $msg $sys_msg"); + return( undef, $user_msg ); + } + } + } + return $dashboard; +} + package RT::Interface::Web; RT::Base->_ImportOverlays(); diff --git a/share/html/Elements/MyRT b/share/html/Elements/MyRT index 6f8dc51a806..b97c409f1eb 100644 --- a/share/html/Elements/MyRT +++ b/share/html/Elements/MyRT @@ -49,12 +49,12 @@
<& /Elements/ShowWidgets, - Object => $dashboard, + Object => $Dashboard, Layout => '', - Elements => $dashboard->{Attribute}->SubValue('Elements'), + Elements => $Dashboard->{Attribute}->SubValue('Elements'), Rows => $Rows, Preview => 1, - Dashboard => $dashboard, + Dashboard => $Dashboard, Depth => 0, HasResults => undef, PassArguments => [ qw/Object Rows Preview Dashboard Depth HasResults/ ], @@ -63,36 +63,10 @@
% $m->callback( ARGSRef => \%ARGS, CallbackName => 'AfterTable' ); <%INIT> -my $user = $session{'CurrentUser'}->UserObj; -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 ) { - my $user_msg = loc('Unable to load selected dashboard, it may have been deleted'); - if ( $dashboard_id == $system_default_id ) { - RT->Logger->warn("Unable to load dashboard: $msg"); - $m->out($m->scomp('/Elements/ListActions', actions => $user_msg)); - return; - } - else { - my ( $ok, $sys_msg ) = $dashboard->LoadById( $system_default_id ); - if ( $ok ) { - $m->out($m->scomp('/Elements/ListActions', actions => [$user_msg, loc('Setting homepage 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 { - RT->Logger->warn("Unable to load dashboard: $msg $sys_msg"); - $m->out($m->scomp('/Elements/ListActions', actions => $user_msg)); - return; - } - } -} - -my $Rows = $user->Preferences( 'SummaryRows', ( RT->Config->Get('DefaultSummaryRows') || 10 ) ); +my $Rows = $session{CurrentUser}->Preferences( 'SummaryRows', ( RT->Config->Get('DefaultSummaryRows') || 10 ) ); + +<%ARGS> +$Dashboard => GetDefaultDashboard(); + diff --git a/share/html/index.html b/share/html/index.html index 12c7d1dc7df..1422a733a54 100644 --- a/share/html/index.html +++ b/share/html/index.html @@ -1,7 +1,7 @@ @@ -75,7 +75,9 @@

You're almost there!

%# END BPS TAGGED BLOCK }}} <& /Elements/Tabs &> <& /Elements/ListActions, actions => \@results &> -<& /Elements/MyRT &> +% if ( $dashboard ) { +<& /Elements/MyRT, Dashboard => $dashboard &> +% } <%init> if ( RT::Interface::Web->MobileClient()) { @@ -98,6 +100,8 @@

You're almost there!

RT::Interface::Web::Redirect(RT->Config->Get('WebURL')."Search/Simple.html?q=".$m->interp->apply_escapes($ARGS{q}, 'u')); } +my ( $dashboard, @msgs ) = GetDefaultDashboard(); +push @results, @msgs; %# --> From 92075c1cd7788f5a00f8404105c98eaf80ce1479 Mon Sep 17 00:00:00 2001 From: sunnavy Date: Wed, 25 Sep 2024 10:52:22 -0400 Subject: [PATCH 7/9] Change "RT at a glance" to "Homepage" for consistency As we switched to default dashboard's description as title on homepage, "RT at a glance" is confusing now. --- docs/customizing/assets/tutorial.pod | 2 +- docs/dashboards.pod | 12 ++++++------ docs/working_with_rt.pod | 2 +- etc/RT_Config.pm.in | 2 +- lib/RT/Config.pm | 4 ++-- lib/RT/Interface/Web/MenuBuilder.pm | 8 ++++---- share/html/Admin/Elements/EditRights | 2 +- share/html/Admin/Global/MyRT.html | 2 +- share/html/Admin/Users/MyRT.html | 2 +- share/html/Prefs/CatalogList.html | 2 +- share/html/Prefs/MyRT.html | 2 +- share/html/Prefs/QueueList.html | 2 +- share/html/index.html | 2 +- t/security/CVE-2011-5092-installmode.t | 4 ++-- t/web/csrf-rest.t | 6 +++--- t/web/dashboards-in-menu.t | 2 +- t/web/install.t | 2 +- t/web/login.t | 2 +- 18 files changed, 30 insertions(+), 30 deletions(-) diff --git a/docs/customizing/assets/tutorial.pod b/docs/customizing/assets/tutorial.pod index dee46ced847..6c9021a9452 100644 --- a/docs/customizing/assets/tutorial.pod +++ b/docs/customizing/assets/tutorial.pod @@ -278,7 +278,7 @@ status. Then when the ticket is resolved, flip it back to 'in-use'. If an end user contacts us with some problem with their laptop, RT makes it easy to find the correct laptop record and create a ticket for them. Since our support staff do this frequently, they have added the Find User portlet to -their RT at a glance page and can quickly search for the user and go to their +their homepage and can quickly search for the user and go to their User Summary page. We have added the Assigned Assets portlet to the User Summary page, so the diff --git a/docs/dashboards.pod b/docs/dashboards.pod index dac83daa7ca..930d3623dd9 100644 --- a/docs/dashboards.pod +++ b/docs/dashboards.pod @@ -17,9 +17,9 @@ outstanding invoice tickets. There are several different rights you can grant to allow users access to the features described here. These rights are described in L. -=head2 RT at a glance +=head2 Homepage -RT's homepage, called RT at a glance, is a dashboard that has been set as your +RT's homepage is a dashboard that has been set as your current homepage. By default, all users see the system-level dashboard set by the RT administrator. If you have the "ModifySelf" right, you can easily change your homepage by clicking on the gear icon. You'll see a page showing you all dashboards @@ -86,7 +86,7 @@ F] F] Click Show in the submenu and you'll see your new dashboard. Click Home to -return to the "RT at a glance" page and you'll see your new dashboard is in the +return to the "Homepage" page and you'll see your new dashboard is in the Dashboards portlet on the right side of the page. On dashboard pages, you can click on the title of any section and go to the @@ -130,9 +130,9 @@ the Components section when you create and modify dashboards. =head2 Dashboard Menu Entries -In addition to having dashboards available on the "RT at a glance" page, you -can also add them to the Reports menu. To modify the Reports menu, select Reports > -"Update This Menu" or "Logged in as" > Settings > "Dashboards in menu". You'll +In addition to having dashboards available on the homepage, you +can also add them to the Reports menu. To modify the Reports menu, select Reports > +"Update This Menu" or "Logged in as" > Settings > "Dashboards in menu". You'll see the Customize dashboard page which is similar to the Dashboard Content page. =for html Customize dashboard menu C<$HomepageComponents> is an arrayref of allowed components on a -user's customized homepage ("RT at a glance"). +user's customized homepage. =cut diff --git a/lib/RT/Config.pm b/lib/RT/Config.pm index 6210f65445d..2010f00ea61 100644 --- a/lib/RT/Config.pm +++ b/lib/RT/Config.pm @@ -452,9 +452,9 @@ our %META; }, }, - # User overridable options for RT at a glance + # User overridable options for Homepage HomePageRefreshInterval => { - Section => 'RT at a glance', #loc + Section => 'Homepage', #loc Overridable => 1, SortOrder => 2, Widget => '/Widgets/Form/Select', diff --git a/lib/RT/Interface/Web/MenuBuilder.pm b/lib/RT/Interface/Web/MenuBuilder.pm index d6ce7dacd40..c20741222ef 100644 --- a/lib/RT/Interface/Web/MenuBuilder.pm +++ b/lib/RT/Interface/Web/MenuBuilder.pm @@ -302,7 +302,7 @@ sub BuildMainNav { $settings->child( auth_tokens => title => loc('Auth Tokens'), path => '/Prefs/AuthTokens.html' ); } $settings->child( search_options => title => loc('Search options'), path => '/Prefs/SearchOptions.html' ); - $settings->child( myrt => title => loc('RT at a glance'), path => '/Prefs/MyRT.html' ); + $settings->child( myrt => title => loc('Homepage'), path => '/Prefs/MyRT.html' ); $settings->child( dashboards_in_menu => title => loc('Modify Reports menu'), path => '/Prefs/DashboardsInMenu.html', @@ -1416,8 +1416,8 @@ sub _BuildAdminTopMenu { path => '/Admin/Global/UserRights.html', ); $admin_global->child( 'my-rt' => - title => loc('RT at a glance'), - description => loc('Modify the default "RT at a glance" view'), + title => loc('Homepage'), + description => loc('Modify the default "Homepage" view'), path => '/Admin/Global/MyRT.html', ); @@ -1593,7 +1593,7 @@ sub _BuildAdminPageMenu { $page->child( basics => title => loc('Basics'), path => "/Admin/Users/Modify.html?id=" . $id ); $page->child( memberships => title => loc('Memberships'), path => "/Admin/Users/Memberships.html?id=" . $id ); $page->child( history => title => loc('History'), path => "/Admin/Users/History.html?id=" . $id ); - $page->child( 'my-rt' => title => loc('RT at a glance'), path => "/Admin/Users/MyRT.html?id=" . $id ); + $page->child( 'my-rt' => title => loc('Homepage'), path => "/Admin/Users/MyRT.html?id=" . $id ); $page->child( 'dashboards-in-menu' => title => loc('Modify Reports menu'), path => '/Admin/Users/DashboardsInMenu.html?id=' . $id, diff --git a/share/html/Admin/Elements/EditRights b/share/html/Admin/Elements/EditRights index cec2714aaec..34002ea358e 100644 --- a/share/html/Admin/Elements/EditRights +++ b/share/html/Admin/Elements/EditRights @@ -332,7 +332,7 @@ if ( $AddPrincipal ) {