[Rt-commit] rt branch, 4.6/core-group-management-extensions, created. rt-4.4.4-294-gc7a151b20
Blaine Motsinger
blaine at bestpractical.com
Mon Aug 26 13:28:36 EDT 2019
The branch, 4.6/core-group-management-extensions has been created
at c7a151b20b0efd0cf0c2083200624f29187e04d7 (commit)
- Log -----------------------------------------------------------------
commit 5e988d87198fd1b16e0a8ef6d48a519342010e48
Author: Blaine Motsinger <blaine at bestpractical.com>
Date: Thu May 23 09:58:52 2019 -0500
Core RT-Extension-GroupLinks
diff --git a/lib/RT/Group.pm b/lib/RT/Group.pm
index 19a41ef7a..5dfc3fe7a 100644
--- a/lib/RT/Group.pm
+++ b/lib/RT/Group.pm
@@ -74,12 +74,11 @@ use warnings;
use base 'RT::Record';
use Role::Basic 'with';
-with "RT::Record::Role::Rights";
+with "RT::Record::Role::Rights",
+ "RT::Record::Role::Links";
sub Table {'Groups'}
-
-
use RT::Users;
use RT::GroupMembers;
use RT::Principals;
@@ -96,6 +95,7 @@ __PACKAGE__->AddRight( Staff => SeeGroupDashboard => 'View group dashboards')
__PACKAGE__->AddRight( Admin => CreateGroupDashboard => 'Create group dashboards'); # loc
__PACKAGE__->AddRight( Admin => ModifyGroupDashboard => 'Modify group dashboards'); # loc
__PACKAGE__->AddRight( Admin => DeleteGroupDashboard => 'Delete group dashboards'); # loc
+__PACKAGE__->AddRight( Staff => ModifyGroupLinks => 'Modify group links' ); # loc
=head1 METHODS
@@ -1712,6 +1712,22 @@ sub _CustomRoleObj {
return;
}
+sub ModifyLinkRight {'ModifyGroupLinks'}
+
+=head2 URI
+
+Returns this group's URI
+
+=cut
+
+sub URI {
+ my $self = shift;
+
+ require RT::URI::group;
+ my $uri = RT::URI::group->new($self->CurrentUser);
+ return $uri->URIForObject($self);
+}
+
RT::Base->_ImportOverlays();
1;
diff --git a/lib/RT/Interface/Web/MenuBuilder.pm b/lib/RT/Interface/Web/MenuBuilder.pm
index 5e9fbed9c..754cf8deb 100644
--- a/lib/RT/Interface/Web/MenuBuilder.pm
+++ b/lib/RT/Interface/Web/MenuBuilder.pm
@@ -2,7 +2,7 @@
#
# COPYRIGHT:
#
-# This software is Copyright (c) 1996-2016 Best Practical Solutions, LLC
+# This software is Copyright (c) 1996-2019 Best Practical Solutions, LLC
# <sales at bestpractical.com>
#
# (Except where explicitly superseded by other copyright notices)
@@ -1077,9 +1077,19 @@ sub _BuildAdminMenu {
$page->child( basics => title => loc('Basics'), path => "/Admin/Groups/Modify.html?id=" . $obj->id );
$page->child( members => title => loc('Members'), path => "/Admin/Groups/Members.html?id=" . $obj->id );
$page->child( memberships => title => loc('Memberships'), path => "/Admin/Groups/Memberships.html?id=" . $obj->id );
+ $page->child( 'links' =>
+ title => loc("Links"),
+ path => "/Admin/Groups/ModifyLinks.html?id=" . $obj->id,
+ description => loc("Group links"),
+ );
$page->child( 'group-rights' => title => loc('Group Rights'), path => "/Admin/Groups/GroupRights.html?id=" . $obj->id );
$page->child( 'user-rights' => title => loc('User Rights'), path => "/Admin/Groups/UserRights.html?id=" . $obj->id );
$page->child( history => title => loc('History'), path => "/Admin/Groups/History.html?id=" . $obj->id );
+ $page->child( 'summary' =>
+ title => loc("Group Summary"),
+ path => "/Group/Summary.html?id=" . $obj->id,
+ description => loc("Group summary page"),
+ );
}
}
}
diff --git a/lib/RT/URI/group.pm b/lib/RT/URI/group.pm
new file mode 100644
index 000000000..67a8e8139
--- /dev/null
+++ b/lib/RT/URI/group.pm
@@ -0,0 +1,213 @@
+# BEGIN BPS TAGGED BLOCK {{{
+#
+# COPYRIGHT:
+#
+# This software is Copyright (c) 1996-2019 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 }}}
+
+use strict;
+use warnings;
+
+package RT::URI::group;
+use base qw/RT::URI::base/;
+
+require RT::Group;
+
+=head1 NAME
+
+RT::URI::group - Internal URIs for linking to an L<RT::Group>
+
+=head1 DESCRIPTION
+
+This class should rarely be used directly, but via L<RT::URI> instead.
+
+Represents, parses, and generates internal RT URIs such as:
+
+ group:42
+ group://example.com/42
+
+These URIs are used to link between objects in RT such as associating a group
+with another group.
+
+=head1 METHODS
+
+Much of the interface below is dictated by L<RT::URI> and L<RT::URI::base>.
+
+=head2 Scheme
+
+Return the URI scheme for groups
+
+=cut
+
+sub Scheme { "group" }
+
+=head2 LocalURIPrefix
+
+Returns the site-specific prefix for a local group URI
+
+=cut
+
+sub LocalURIPrefix {
+ my $self = shift;
+ return $self->Scheme . "://" . RT->Config->Get('Organization');
+}
+
+=head2 IsLocal
+
+Returns a true value, the grouup ID, if this object represents a local group,
+undef otherwise.
+
+=cut
+
+sub IsLocal {
+ my $self = shift;
+ my $prefix = $self->LocalURIPrefix;
+ return $1 if $self->{uri} =~ /^\Q$prefix\E(\d+)/i;
+ return undef;
+}
+
+=head2 URIForObject RT::Group
+
+Returns the URI for a local L<RT::Group> object
+
+=cut
+
+sub URIForObject {
+ my $self = shift;
+ my $obj = shift;
+ return $self->LocalURIPrefix . $obj->Id;
+}
+
+=head2 ParseURI URI
+
+Primarily used by L<RT::URI> to set internal state.
+
+Figures out from an C<group:> URI whether it refers to a local group and the
+group ID.
+
+Returns the group ID if local, otherwise returns false.
+
+=cut
+
+sub ParseURI {
+ my $self = shift;
+ my $uri = shift;
+
+ my $scheme = $self->Scheme;
+
+ # canonicalize "42" and "group:42" -> group://example.com/42
+ if ($uri =~ /^(?:\Q$scheme\E:)?(\d+)$/i) {
+ $self->{'uri'} = $self->LocalURIPrefix . $1;
+ }
+ else {
+ $self->{'uri'} = $uri;
+ }
+
+ my $group = RT::Group->new( $self->CurrentUser );
+ if ( my $id = $self->IsLocal ) {
+ $group->Load($id);
+
+ if ($group->id) {
+ $self->{'object'} = $group;
+ } else {
+ RT->Logger->error("Can't load Group #$id by URI '$uri'");
+ return;
+ }
+ }
+ return $group->id;
+}
+
+=head2 Object
+
+Returns the object for this URI, if it's local. Otherwise returns undef.
+
+=cut
+
+sub Object {
+ my $self = shift;
+ return $self->{'object'};
+}
+
+=head2 HREF
+
+If this is a local group, return an HTTP URL for it.
+
+Otherwise, return its URI.
+
+=cut
+
+sub HREF {
+ my $self = shift;
+ if ($self->IsLocal and $self->Object) {
+ return RT->Config->Get('WebURL')
+# . ( $self->CurrentUser->Privileged ? "" : "SelfService/" )
+# . "Admin/Groups/Modify.html?id="
+ . "Group/Summary.html?id="
+ . $self->Object->Id;
+ } else {
+ return $self->URI;
+ }
+}
+
+=head2 AsString
+
+Returns a description of this object
+
+=cut
+
+sub AsString {
+ my $self = shift;
+ if ($self->IsLocal and $self->Object) {
+ my $object = $self->Object;
+ if ( $object->Name ) {
+ return $self->loc('[_1] (Group #[_2])', $object->Name, $object->id);
+ } else {
+ return $self->loc('Group #[_1]', $object->id);
+ }
+ } else {
+ return $self->SUPER::AsString(@_);
+ }
+}
+
+1;
diff --git a/lib/RT/URI/user.pm b/lib/RT/URI/user.pm
new file mode 100644
index 000000000..28f8998d5
--- /dev/null
+++ b/lib/RT/URI/user.pm
@@ -0,0 +1,211 @@
+# BEGIN BPS TAGGED BLOCK {{{
+#
+# COPYRIGHT:
+#
+# This software is Copyright (c) 1996-2019 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 }}}
+
+use strict;
+use warnings;
+
+package RT::URI::user;
+use base qw/RT::URI::base/;
+
+require RT::User;
+
+=head1 NAME
+
+RT::URI::user - Internal URIs for linking to an L<RT::User>
+
+=head1 DESCRIPTION
+
+This class should rarely be used directly, but via L<RT::URI> instead.
+
+Represents, parses, and generates internal RT URIs such as:
+
+ user:42
+ user://example.com/42
+
+These URIs are used to link between objects in RT such as referencing an RT user
+record from a ticket in the Links section.
+
+=head1 METHODS
+
+Much of the interface below is dictated by L<RT::URI> and L<RT::URI::base>.
+
+=head2 Scheme
+
+Return the URI scheme for groups
+
+=cut
+
+sub Scheme { "user" }
+
+=head2 LocalURIPrefix
+
+Returns the site-specific prefix for a local group URI
+
+=cut
+
+sub LocalURIPrefix {
+ my $self = shift;
+ return $self->Scheme . "://" . RT->Config->Get('Organization');
+}
+
+=head2 IsLocal
+
+Returns a true value, the grouup ID, if this object represents a local group,
+undef otherwise.
+
+=cut
+
+sub IsLocal {
+ my $self = shift;
+ my $prefix = $self->LocalURIPrefix;
+ return $1 if $self->{uri} =~ /^\Q$prefix\E(\d+)/i;
+ return undef;
+}
+
+=head2 URIForObject RT::Group
+
+Returns the URI for a local L<RT::Group> object
+
+=cut
+
+sub URIForObject {
+ my $self = shift;
+ my $obj = shift;
+ return $self->LocalURIPrefix . $obj->Id;
+}
+
+=head2 ParseURI URI
+
+Primarily used by L<RT::URI> to set internal state.
+
+Figures out from an C<user:> URI whether it refers to a local user and the
+user ID.
+
+Returns the user ID if local, otherwise returns false.
+
+=cut
+
+sub ParseURI {
+ my $self = shift;
+ my $uri = shift;
+
+ my $scheme = $self->Scheme;
+
+ # canonicalize "42" and "user:42" -> user://example.com/42
+ if ($uri =~ /^(?:\Q$scheme\E:)?(\d+)$/i) {
+ $self->{'uri'} = $self->LocalURIPrefix . $1;
+ }
+ else {
+ $self->{'uri'} = $uri;
+ }
+
+ my $user = RT::User->new( $self->CurrentUser );
+ if ( my $id = $self->IsLocal ) {
+ $user->Load($id);
+
+ if ($user->id) {
+ $self->{'object'} = $user;
+ } else {
+ RT->Logger->error("Can't load User #$id by URI '$uri'");
+ return;
+ }
+ }
+ return $user->id;
+}
+
+=head2 Object
+
+Returns the object for this URI, if it's local. Otherwise returns undef.
+
+=cut
+
+sub Object {
+ my $self = shift;
+ return $self->{'object'};
+}
+
+=head2 HREF
+
+If this is a local group, return an HTTP URL for it.
+
+Otherwise, return its URI.
+
+=cut
+
+sub HREF {
+ my $self = shift;
+ if ($self->IsLocal and $self->Object) {
+ return RT->Config->Get('WebURL')
+ . "User/Summary.html?id="
+ . $self->Object->Id;
+ } else {
+ return $self->URI;
+ }
+}
+
+=head2 AsString
+
+Returns a description of this object
+
+=cut
+
+sub AsString {
+ my $self = shift;
+ if ($self->IsLocal and $self->Object) {
+ my $object = $self->Object;
+ if ( $object->Name ) {
+ return $self->loc('[_1] (User #[_2])', $object->Name, $object->id);
+ } else {
+ return $self->loc('User #[_1]', $object->id);
+ }
+ } else {
+ return $self->SUPER::AsString(@_);
+ }
+}
+
+1;
diff --git a/share/html/Elements/ShowPrincipal b/share/html/Admin/Elements/AddLinks
similarity index 57%
copy from share/html/Elements/ShowPrincipal
copy to share/html/Admin/Elements/AddLinks
index f8d1e0ed1..06b0285af 100644
--- a/share/html/Elements/ShowPrincipal
+++ b/share/html/Admin/Elements/AddLinks
@@ -45,28 +45,42 @@
%# those contributions and any derivatives thereof.
%#
%# END BPS TAGGED BLOCK }}}
-%# Released under the terms of version 2 of the GNU Public License
-<%args>
-$Object
-$PostUser => undef
-$Separator => ", "
-$Link => 1
-</%args>
-<%init>
-if ($Object->isa("RT::Group")) {
- # Link the users (non-recursively)
- my @ret = map {$m->scomp("ShowPrincipal", Object => $_->[1], PostUser => $PostUser, Link => $Link)}
- sort {$a->[0] cmp $b->[0]}
- map {+[($_->EmailAddress||''), $_]}
- @{ $Object->UserMembersObj( Recursively => 0 )->ItemsArrayRef };
+% if (ref($Object) eq 'RT::Group') {
+<i><&|/l&>Enter names or IDs of other groups to link. Start typing a group name to see matching groups. Separate multiple entries with a comma.</&>
+% $m->callback( CallbackName => 'ExtraLinkInstructions' );
+</i><br />
+% } else {
+<i><&|/l&>Enter objects or URIs to link objects to. Separate multiple entries with spaces.</&></i><br />
+% }
+<table>
+ <tr>
+ <td class="label"><& /Elements/ShowRelationLabel, Object => $Object, Label => loc('Links to').':', Relation => 'RefersTo' &></td>
+ <td class="entry"><input name="<%$id%>-RefersTo" value="<% $ARGSRef->{"$id-RefersTo"} || '' %>" <% $exclude |n%>/></td>
+ </tr>
+ <tr>
+ <td class="label"><& /Elements/ShowRelationLabel, Object => $Object, Label => loc('Linked to by').':', Relation => 'ReferredToBy' &></td>
+ <td class="entry"> <input name="RefersTo-<%$id%>" value="<% $ARGSRef->{"RefersTo-$id"} || '' %>" <% $exclude |n%>/></td>
+ </tr>
+ <& /Elements/EditCustomFields,
+ Object => $Object,
+ Grouping => 'Links',
+ InTable => 1,
+ ($CustomFields
+ ? (CustomFields => $CustomFields)
+ : ()),
+ &>
+% $m->callback( CallbackName => 'NewLink' );
+</table>
+<%INIT>
+my $id = ($Object and $Object->id)
+ ? $Object->id
+ : "new";
- # But don't link the groups
- push @ret, sort map {$m->interp->apply_escapes( loc("Group: [_1]", $_->Name), 'h' )}
- @{ $Object->GroupMembersObj( Recursively => 0)->ItemsArrayRef };
-
- $m->out( join($Separator, @ret) );
-} else {
- $m->comp("/Elements/ShowUser", User => $Object, Link => $Link);
- $m->out( $PostUser->($Object) ) if $PostUser;
-}
-</%init>
+my $exclude = qq| data-autocomplete="Groups" data-autocomplete-multiple="1" |;
+$exclude .= qq| data-autocomplete-exclude="$id"| if $Object->id;
+</%INIT>
+<%ARGS>
+$Object => undef
+$CustomFields => undef
+$ARGSRef => $DECODED_ARGS
+</%ARGS>
diff --git a/share/html/Elements/ShowPrincipal b/share/html/Admin/Elements/EditLinks
similarity index 66%
copy from share/html/Elements/ShowPrincipal
copy to share/html/Admin/Elements/EditLinks
index f8d1e0ed1..4671f3ebe 100644
--- a/share/html/Elements/ShowPrincipal
+++ b/share/html/Admin/Elements/EditLinks
@@ -45,28 +45,41 @@
%# those contributions and any derivatives thereof.
%#
%# END BPS TAGGED BLOCK }}}
-%# Released under the terms of version 2 of the GNU Public License
-<%args>
-$Object
-$PostUser => undef
-$Separator => ", "
-$Link => 1
-</%args>
-<%init>
-if ($Object->isa("RT::Group")) {
- # Link the users (non-recursively)
- my @ret = map {$m->scomp("ShowPrincipal", Object => $_->[1], PostUser => $PostUser, Link => $Link)}
- sort {$a->[0] cmp $b->[0]}
- map {+[($_->EmailAddress||''), $_]}
- @{ $Object->UserMembersObj( Recursively => 0 )->ItemsArrayRef };
+<table width="100%">
+ <tr>
+ <td valign="top" width="50%">
+ <h3><&|/l&>Current Links</&></h3>
- # But don't link the groups
- push @ret, sort map {$m->interp->apply_escapes( loc("Group: [_1]", $_->Name), 'h' )}
- @{ $Object->GroupMembersObj( Recursively => 0)->ItemsArrayRef };
+<table>
+ <tr>
+ <td class="labeltop"><& /Elements/ShowRelationLabel, Object => $Object, Label => loc('Links to').':', Relation => 'RefersTo' &></td>
+ <td class="value">
+% while (my $link = $Object->RefersTo->Next) {
+ <& /Elements/EditLink, Link => $link, Mode => 'Target' &>
+%}
+ </td>
+ </tr>
+ <tr>
+ <td class="labeltop group-link edit-referredtoby"><& /Elements/ShowRelationLabel, Object => $Object, Label => loc('Linked to by').':', Relation => 'ReferredToBy' &></td>
+ <td class="value group-link edit-referredtoby">
+% while (my $link = $Object->ReferredToBy->Next) {
+ <& /Elements/EditLink, Link => $link, Mode => 'Base' &>
+% }
+ </td>
+ </tr>
+ <tr>
+ <td></td>
+ <td><i><&|/l&>(Check box to remove link)</&></i></td>
+ </tr>
+</table>
- $m->out( join($Separator, @ret) );
-} else {
- $m->comp("/Elements/ShowUser", User => $Object, Link => $Link);
- $m->out( $PostUser->($Object) ) if $PostUser;
-}
-</%init>
+</td>
+<td valign="top">
+<h3><&|/l&>New Links</&></h3>
+<& AddLinks, %ARGS &>
+</td>
+</tr>
+</table>
+<%ARGS>
+$Object => undef
+</%ARGS>
diff --git a/share/html/Admin/Groups/ModifyLinks.html b/share/html/Admin/Groups/ModifyLinks.html
new file mode 100644
index 000000000..77a44d57d
--- /dev/null
+++ b/share/html/Admin/Groups/ModifyLinks.html
@@ -0,0 +1,116 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2019 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 }}}
+<& /Admin/Elements/Header, Title => $title &>
+
+<& /Elements/Tabs &>
+<& /Elements/ListActions, actions => \@results &>
+
+% $m->callback(CallbackName => 'BeforeActionList', Actions => \@results, ARGSRef => \%ARGS, GroupObj => $Group);
+<& /Elements/ListActions, actions => \@results &>
+
+<form action="<% RT->Config->Get('WebPath') %>/Admin/Groups/ModifyLinks.html" name="ModifyLinks" method="post">
+<input type="hidden" class="hidden" name="id" value="<%$Group->Id%>" />
+
+% $m->callback( CallbackName => 'FormStart', ARGSRef => \%ARGS );
+
+<&| /Widgets/TitleBox, title => loc('Manage Links for Group [_1]', $Group->Label) &>
+
+<& /Admin/Elements/EditLinks, Object => $Group &>
+</&>
+
+<& /Elements/Submit, Name => 'SubmitGroupLinks', Label => loc('Save Changes') &>
+</form>
+
+% $m->callback(CallbackName => 'AfterForm', ARGSRef => \%ARGS, GroupObj => $Group);
+
+<%INIT>
+my $Group = RT::Group->new($session{'CurrentUser'});
+$Group->Load($id) || Abort(loc('Could not load group'));
+my @results;
+
+$m->callback(CallbackName => 'Init', GroupObj => $Group, ARGSRef => \%ARGS, Results => \@results);
+
+my $title = loc("Modify Links for group [_1]", $Group->Label);
+
+if ( $ARGS{'SubmitGroupLinks'} ){
+
+ foreach my $link_type ( "RefersTo-$id", "$id-RefersTo" ){
+ next unless $ARGS{$link_type};
+
+ # List is comma delimited, which allows for group names with spaces
+ my @values = split ', ', $ARGS{$link_type};
+ foreach my $input ( @values ) {
+ if ( $input =~ /^\d+$/ ){
+ # Default scheme for link ids assumes a ticket. Since we're on the group
+ # links page, allow ids as input and prepend 'group:' here to
+ # create group links
+ $input = 'group:' . $input;
+ next;
+ }
+ else {
+ # Could be a group name. Try to look it up.
+ my $group = RT::Group->new($session{'CurrentUser'});
+ my ($ret, $msg) = $group->LoadUserDefinedGroup($input);
+ RT::Logger->info("Unable to load group from name $input: $msg") unless $ret;
+ $input = 'group:' . $group->Id if $ret and $group->Id;
+ }
+ }
+ $ARGS{$link_type} = join ' ', @values;
+ }
+
+ (@results) = ProcessRecordLinks(RecordObj => $Group, ARGSRef => \%ARGS);
+
+ MaybeRedirectForResults(
+ Actions => \@results,
+ Arguments => { id => $id },
+ );
+}
+</%INIT>
+<%ARGS>
+$id => undef
+</%ARGS>
diff --git a/share/html/Elements/AddLinks b/share/html/Elements/AddLinks
index ce5228b86..76efeb312 100644
--- a/share/html/Elements/AddLinks
+++ b/share/html/Elements/AddLinks
@@ -62,6 +62,8 @@ $exclude .= qq| data-autocomplete-exclude="$id"| if $Object->id;
<i><&|/l&>Enter tickets or URIs to link tickets to. Separate multiple entries with spaces.</&>
<br /><&|/l&>You may enter links to Articles as "a:###", where ### represents the number of the Article.</&>
<br /><&|/l&>Enter links to assets as "asset:###", where ### represents the asset ID.</&>
+<br /><&|/l&>Enter links to groups as "group:###", where ### represents the group ID.</&>
+<br /><&|/l&>Enter links to users as "user:###", where ### represents the user ID.</&>
% $m->callback( CallbackName => 'ExtraLinkInstructions' );
</i><br />
% } elsif (ref($Object) eq 'RT::Queue') {
diff --git a/share/html/Elements/ShowPrincipal b/share/html/Elements/ShowPrincipal
index f8d1e0ed1..329266512 100644
--- a/share/html/Elements/ShowPrincipal
+++ b/share/html/Elements/ShowPrincipal
@@ -60,8 +60,9 @@ if ($Object->isa("RT::Group")) {
map {+[($_->EmailAddress||''), $_]}
@{ $Object->UserMembersObj( Recursively => 0 )->ItemsArrayRef };
- # But don't link the groups
- push @ret, sort map {$m->interp->apply_escapes( loc("Group: [_1]", $_->Name), 'h' )}
+ # Link to the group summary page
+ my $href = RT->Config->Get("WebPath") . "/Group/Summary.html?id=";
+ push @ret, sort map { "<a href=\"" . $href . $_->id . "\">" . loc("Group:") . " " . $_->Name . "</a>" }
@{ $Object->GroupMembersObj( Recursively => 0)->ItemsArrayRef };
$m->out( join($Separator, @ret) );
commit 2f39b030934a9e10f47a40dd02f3674cb8953e2d
Author: Blaine Motsinger <blaine at bestpractical.com>
Date: Wed May 29 13:16:08 2019 -0500
Core RT-Extension-GroupSummary
diff --git a/etc/RT_Config.pm.in b/etc/RT_Config.pm.in
index 46c00e409..da139b13c 100644
--- a/etc/RT_Config.pm.in
+++ b/etc/RT_Config.pm.in
@@ -1355,7 +1355,7 @@ user's customized homepage ("RT at a glance").
Set(
$HomepageComponents,
[
- qw(QuickCreate QueueList MyAdminQueues MySupportQueues MyReminders RefreshHomepage Dashboards SavedSearches FindUser MyAssets FindAsset) # loc_qw
+ qw(QuickCreate QueueList MyAdminQueues MySupportQueues MyReminders RefreshHomepage Dashboards SavedSearches FindUser MyAssets FindAsset FindGroup) # loc_qw
]
);
@@ -1813,6 +1813,97 @@ Set($HideUnsetFieldsOnDisplay, 0);
=back
+=head2 Group Summary Configuration
+
+Below are configuration options for the Group Summary page.
+
+=over
+
+=item C<$GroupSearchResultFormat>
+
+This controls the display of lists of groups returned from the Group
+Summary Search. The display of groups in the Admin interface is
+controlled by C<%AdminSearchResultFormat>.
+
+=cut
+
+Set($GroupSearchResultFormat,
+ q{ '<a href="__WebPath__/Group/Summary.html?id=__id__">__id__</a>/TITLE:#'}
+ .q{,'<a href="__WebPath__/Group/Summary.html?id=__id__">__Name__</a>/TITLE:Name'}
+);
+
+=item C<@GroupSummaryPortlets>
+
+A list of portlets to be displayed on the Group Summary page.
+By default, we show all of the available portlets.
+Extensions may provide their own portlets for this page.
+
+=cut
+
+Set(@GroupSummaryPortlets, (qw/ExtraInfo CreateTicket ActiveTickets InactiveTickets GroupAssets /));
+
+=item C<$GroupSummaryExtraInfo>
+
+This controls what information is displayed on the Group Summary
+portal. By default the group Name and Description are displayed.
+
+=cut
+
+Set($GroupSummaryExtraInfo, "id, Name, Description");
+
+=item C<$GroupSummaryTicketListFormat>
+
+Control the appearance of the Active and Inactive ticket lists in the
+Group Summary.
+
+=cut
+
+Set($GroupSummaryTicketListFormat, q{
+ '<B><A HREF="__WebPath__/Ticket/Display.html?id=__id__">__id__</a></B>/TITLE:#',
+ '<B><A HREF="__WebPath__/Ticket/Display.html?id=__id__">__Subject__</a></B>/TITLE:Subject',
+ Status,
+ QueueName,
+ Owner,
+ Priority,
+ '__NEWLINE__',
+ '',
+ '<small>__Requestors__</small>',
+ '<small>__CreatedRelative__</small>',
+ '<small>__ToldRelative__</small>',
+ '<small>__LastUpdatedRelative__</small>',
+ '<small>__TimeLeft__</small>'
+});
+
+=item C<$GroupSearchFields>
+
+Specifies which fields of L<RT::Group> to match against and how to match
+each field when performing a quick search on groups. Valid match
+methods are LIKE, STARTSWITH, ENDSWITH, =, and !=. Valid search fields
+are id, Name, Description, or custom fields, which are specified as
+"CF.1234" or "CF.Name"
+
+=cut
+
+Set($GroupSearchFields, {
+ id => '=',
+ Name => 'LIKE',
+ Description => 'LIKE',
+});
+
+=item C<$AllowGroupAutocompleteForUnprivileged>
+
+Defines whether unprivileged users (users of SelfService) are allowed
+to autocomplete groups when searching. Setting this option to 1 means
+unprivileged users will be able to search all your user created
+group names. Users will also need the SeeGroup privilege to use
+this feature.
+
+=cut
+
+Set($AllowGroupAutocompleteForUnprivileged, 0);
+
+=back
+
=head2 Self Service Interface
The Self Service Interface is a view automatically presented to Unprivileged
@@ -1938,7 +2029,6 @@ Users also need the ModifySelf right to have access to this page.
Set( $SelfServiceDownloadUserData, 0 );
-
=back
=head2 Articles
diff --git a/lib/RT/Groups.pm b/lib/RT/Groups.pm
index 0d9784bc6..ae80e0e53 100644
--- a/lib/RT/Groups.pm
+++ b/lib/RT/Groups.pm
@@ -433,6 +433,110 @@ sub _DoSearch {
}
+=head2 SimpleSearch
+
+Does a 'simple' search of Groups against a specified Term.
+
+This Term is compared to a number of fields using various types of SQL
+comparison operators.
+
+Ensures that the returned collection of Groups will have a value for Return.
+
+This method is passed the following. You must specify a Term and a Return.
+
+ Fields - Hashref of data - defaults to C<$GroupSearchFields> emulate that if you want to override
+ Term - String that is in the fields specified by Fields
+ Return - What field on the User you want to be sure isn't empty
+ Exclude - Array reference of ids to exclude
+ Max - Size to limit this collection to
+
+=cut
+
+sub SimpleSearch {
+ my $self = shift;
+ my %args = (
+ Fields => RT->Config->Get('GroupSearchFields'),
+ Term => undef,
+ Exclude => [],
+ Return => 'Name',
+ Max => 10,
+ @_
+ );
+
+ return $self unless defined $args{Return}
+ and defined $args{Term}
+ and length $args{Term};
+
+ $self->RowsPerPage( $args{Max} );
+
+ $self->LimitToUserDefinedGroups();
+
+ while (my ($name, $op) = each %{$args{Fields}}) {
+ $op = 'STARTSWITH'
+ unless $op =~ /^(?:LIKE|(?:START|END)SWITH|=|!=)$/i;
+
+ if ($name =~ /^CF\.(?:\{(.*)}|(.*))$/) {
+ my $cfname = $1 || $2;
+ my $cf = RT::CustomField->new(RT->SystemUser);
+ my ($ok, $msg) = $cf->LoadByName( Name => $cfname, LookupType => 'RT::Group');
+ if ( $ok ) {
+ $self->LimitCustomField(
+ CUSTOMFIELD => $cf->Id,
+ OPERATOR => $op,
+ VALUE => $args{Term},
+ ENTRYAGGREGATOR => 'OR',
+ SUBCLAUSE => 'autocomplete',
+ CASESENSITIVE => 0,
+ );
+ } else {
+ RT->Logger->warning("Asked to search custom field $name but unable to load a Group CF with the name $cfname: $msg");
+ }
+ } elsif ($name eq 'id' and $op =~ /(?:LIKE|(?:START|END)SWITH)$/i) {
+ $self->Limit(
+ FUNCTION => "CAST( main.$name AS TEXT )",
+ OPERATOR => $op,
+ VALUE => $args{Term},
+ ENTRYAGGREGATOR => 'OR',
+ SUBCLAUSE => 'autocomplete',
+ CASESENSITIVE => 0,
+ ) if $args{Term} =~ /^\d+$/;
+ } else {
+ $self->Limit(
+ FIELD => $name,
+ OPERATOR => $op,
+ VALUE => $args{Term},
+ ENTRYAGGREGATOR => 'OR',
+ SUBCLAUSE => 'autocomplete',
+ CASESENSITIVE => 0,
+ ) unless $args{Term} =~ /\D/ and $name eq 'id';
+ }
+ }
+
+ # Exclude groups we don't want
+ $self->Limit(FIELD => 'id', OPERATOR => 'NOT IN', VALUE => $args{Exclude} )
+ if @{$args{Exclude}};
+
+ if ( RT->Config->Get('DatabaseType') eq 'Oracle' ) {
+ $self->Limit(
+ FIELD => $args{Return},
+ OPERATOR => 'IS NOT',
+ VALUE => 'NULL',
+ );
+ }
+ else {
+ $self->Limit( FIELD => $args{Return}, OPERATOR => '!=', VALUE => '', CASESENSITIVE => 0, );
+ $self->Limit(
+ FIELD => $args{Return},
+ OPERATOR => 'IS NOT',
+ VALUE => 'NULL',
+ ENTRYAGGREGATOR => 'AND',
+ CASESENSITIVE => 0,
+ );
+ }
+
+ return $self;
+}
+
RT::Base->_ImportOverlays();
1;
diff --git a/lib/RT/Interface/Web/MenuBuilder.pm b/lib/RT/Interface/Web/MenuBuilder.pm
index 754cf8deb..df9b475f2 100644
--- a/lib/RT/Interface/Web/MenuBuilder.pm
+++ b/lib/RT/Interface/Web/MenuBuilder.pm
@@ -167,6 +167,12 @@ sub BuildMainNav {
$search->child( users => title => loc('Users'), path => "/User/Search.html" );
+ $search->child( groups =>
+ title => loc('Groups'),
+ path => "/Group/Search.html",
+ description => 'Group search'
+ );
+
$search->child( assets => title => loc("Assets"), path => "/Asset/Search/" )
if $current_user->HasRight( Right => 'ShowAssetsMenu', Object => RT->System );
@@ -1067,7 +1073,7 @@ sub _BuildAdminMenu {
}
- if ( $request_path =~ m{^/Admin/Groups} ) {
+ if ( $request_path =~ m{^(/Admin/Groups|/Group/(Summary|History)\.html)} ) {
if ( $HTML::Mason::Commands::DECODED_ARGS->{'id'} && $HTML::Mason::Commands::DECODED_ARGS->{'id'} =~ /^\d+$/ ) {
my $id = $HTML::Mason::Commands::DECODED_ARGS->{'id'};
my $obj = RT::Group->new( $current_user );
diff --git a/share/html/Elements/FindGroup b/share/html/Elements/FindGroup
new file mode 100644
index 000000000..d5c0eeaac
--- /dev/null
+++ b/share/html/Elements/FindGroup
@@ -0,0 +1,50 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2019 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 }}}
+<&|/Widgets/TitleBox, title => loc('Find a group')&>
+<& /Elements/GotoGroup &>
+</&>
diff --git a/share/html/Elements/GotoGroup b/share/html/Elements/GotoGroup
new file mode 100644
index 000000000..6c84066de
--- /dev/null
+++ b/share/html/Elements/GotoGroup
@@ -0,0 +1,62 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2019 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 }}}
+<form name="GroupSearch" method="post" action="<% RT->Config->Get('WebPath') %>/Group/Search.html">
+<input type="text" name="GroupString" value="<% $Default %>" data-autocomplete="Groups" data-autocomplete-return="Name" id="autocomplete-GroupString" />
+<script type="text/javascript">
+jQuery(function(){
+ // Jump directly to the page if a group is chosen
+ jQuery("#autocomplete-GroupString").on("autocompleteselect", function( event, ui ) {
+ document.location = RT.Config.WebPath + "/Group/Summary.html?id=" + ui.item.id;
+ });
+});
+</script>
+<input type="submit" name="GroupSearch" value="<&|/l&>Search</&>" class="button" />
+</form>
+<%ARGS>
+$Default => ''
+</%ARGS>
diff --git a/share/html/Group/Elements/AssetList b/share/html/Group/Elements/AssetList
new file mode 100644
index 000000000..e88443bf5
--- /dev/null
+++ b/share/html/Group/Elements/AssetList
@@ -0,0 +1,80 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2019 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 }}}
+<%INIT>
+my $assets = RT::Assets->new($session{CurrentUser});
+$m->callback( CallbackName => 'ModifyAssetSearch', %ARGS, Assets => $assets, Roles => \@Roles );
+for my $role (@Roles) {
+ $assets->RoleLimit(
+ TYPE => $role,
+ VALUE => $Group->PrincipalId,
+ SUBCLAUSE => "Role$role",
+ );
+}
+my $Format = q[
+ '<b><a href="__WebHomePath__/Asset/Display.html?id=__id__">__id__</a></b>/TITLE:#',
+ '<b><a href="__WebHomePath__/Asset/Display.html?id=__id__">__Name__</a></b>/TITLE:Name',
+ Description,
+];
+$m->callback( CallbackName => 'ModifyFormat', %ARGS, Format => \$Format );
+</%INIT>
+<&| /Widgets/TitleBox, title => $Title, class => "group asset-list" &>
+ <& /Elements/CollectionList,
+ Collection => $assets,
+ OrderBy => 'id',
+ Order => 'ASC',
+ Format => $Format,
+ AllowSorting => 0,
+ HasResults => $HasResults,
+ &>
+</&>
+<%ARGS>
+$Group
+$Title
+ at Roles
+$HasResults => undef
+</%ARGS>
diff --git a/share/html/Group/Elements/GroupInfo b/share/html/Group/Elements/GroupInfo
new file mode 100644
index 000000000..b594d146d
--- /dev/null
+++ b/share/html/Group/Elements/GroupInfo
@@ -0,0 +1,64 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2019 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 }}}
+<& /Elements/ShowRecord,
+ Object => $Group,
+ Format => $format,
+ TrustFormat => 1, # Only modifiable by the RT server admin, so no need to scrub.
+ Class => "$ClassPrefix-extra",
+ &>
+<%INIT>
+return unless blessed($Group) and $Group->id;
+return unless $FormatConfig;
+my $format = RT->Config->Get($FormatConfig);
+return unless $format;
+</%INIT>
+<%ARGS>
+$Group => undef
+$FormatConfig => undef
+$ClassPrefix => undef
+</%ARGS>
diff --git a/share/html/Group/Elements/Portlets/ActiveTickets b/share/html/Group/Elements/Portlets/ActiveTickets
new file mode 100644
index 000000000..0cf5317ec
--- /dev/null
+++ b/share/html/Group/Elements/Portlets/ActiveTickets
@@ -0,0 +1,68 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2019 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 }}}
+<& /Group/Elements/TicketList ,
+ Group => $Group,
+ conditions => $conditions,
+ Rows => $Rows,
+ WatcherTypes => [qw(Watcher)],
+ Class => "group active-tickets",
+ Title => loc('Active Tickets'),
+ TitleBox => 1,
+ ShowHeader => 1,
+ Format => RT->Config->Get('GroupSummaryTicketListFormat'),
+&>
+<%INIT>
+unless ( @$conditions ) {
+ push @$conditions, { cond => "Status = '__Active__'" };
+}
+</%INIT>
+<%ARGS>
+$Group => undef
+$conditions => []
+$Rows => 10
+</%ARGS>
diff --git a/share/html/Group/Elements/Portlets/CreateTicket b/share/html/Group/Elements/Portlets/CreateTicket
new file mode 100644
index 000000000..eace3c9bb
--- /dev/null
+++ b/share/html/Group/Elements/Portlets/CreateTicket
@@ -0,0 +1,58 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2019 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 }}}
+<&| /Widgets/TitleBox, title => loc('Quick ticket creation'), class => "group create-ticket" &>
+<form name="CreateTicket" action="<%RT->Config->Get('WebPath')%>/Ticket/Create.html">
+<&|/l&>Create a ticket with this group as Cc in Queue</&>
+<input type="hidden" name="AddGroupCc" value="<%$Group->Id%>">
+<& /Elements/SelectNewTicketQueue &>
+<input type="submit" value="<&|/l&>Create</&>">
+</form>
+</&>
+<%ARGS>
+$Group
+</%ARGS>
diff --git a/share/html/Group/Elements/Portlets/ExtraInfo b/share/html/Group/Elements/Portlets/ExtraInfo
new file mode 100644
index 000000000..c065ccaea
--- /dev/null
+++ b/share/html/Group/Elements/Portlets/ExtraInfo
@@ -0,0 +1,56 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2019 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 }}}
+<&| /Widgets/TitleBox, title => loc('Group Information'), class => "group extra-info" &>
+
+% $m->callback( Group => $Group, CallbackName => 'BeforeExtraInfo' );
+<& /Group/Elements/GroupInfo, Group => $Group, FormatConfig => 'GroupSummaryExtraInfo', ClassPrefix => 'group-summary' &>
+
+</&>
+<%ARGS>
+$Group
+</%ARGS>
diff --git a/share/html/Group/Elements/Portlets/GroupAssets b/share/html/Group/Elements/Portlets/GroupAssets
new file mode 100644
index 000000000..88aa7a2ee
--- /dev/null
+++ b/share/html/Group/Elements/Portlets/GroupAssets
@@ -0,0 +1,52 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2019 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 }}}
+%# Roles => [''] triggers the magical RoleLimit behavior that matches any role
+<& /Group/Elements/AssetList, Group => $Group, Roles => [''], Title => loc('Assigned Assets') &>
+<%ARGS>
+$Group
+</%ARGS>
diff --git a/share/html/Group/Elements/Portlets/InactiveTickets b/share/html/Group/Elements/Portlets/InactiveTickets
new file mode 100644
index 000000000..902b82614
--- /dev/null
+++ b/share/html/Group/Elements/Portlets/InactiveTickets
@@ -0,0 +1,68 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2019 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 }}}
+<& /Group/Elements/TicketList ,
+ Group => $Group,
+ conditions => $conditions,
+ Rows => $Rows,
+ WatcherTypes => [qw(Watcher)],
+ Class => "group inactive-tickets",
+ Title => loc('Inactive Tickets'),
+ TitleBox => 1,
+ ShowHeader => 1,
+ Format => RT->Config->Get('GroupSummaryTicketListFormat'),
+&>
+<%INIT>
+unless ( @$conditions ) {
+ push @$conditions, { cond => "Status = '__Inactive__'" };
+}
+</%INIT>
+<%ARGS>
+$Group => undef
+$conditions => []
+$Rows => 10
+</%ARGS>
diff --git a/share/html/Group/Elements/TicketList b/share/html/Group/Elements/TicketList
new file mode 100644
index 000000000..faaec0d8e
--- /dev/null
+++ b/share/html/Group/Elements/TicketList
@@ -0,0 +1,114 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2019 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 }}}
+% if ( $TitleBox ) {
+ <& /Widgets/TitleBoxStart, title => $Title, title_href => $url, class => $Class &>
+% } else {
+ <span class="label"><a href="<% $url %>"><% $Title %>:</a></span>
+% }
+
+<& /Elements/CollectionList,
+ %QueryProperties,
+ Class => 'RT::Tickets',
+ Page => 1,
+ AllowSorting => 0,
+ ShowNavigation => 0,
+&>
+
+% if ( $TitleBox ) {
+ <& /Widgets/TitleBoxEnd &>
+% }
+<%INIT>
+
+my $sql = '';
+
+$sql = join(' OR ', map { "$_.id = ".$Group->Id } @WatcherTypes );
+$sql = "( $sql )";
+
+$m->callback( CallbackName => 'ModifyWatcherSQL',
+ %ARGS,
+ sql => \$sql,
+);
+
+if (@$conditions) {
+ $sql .= " AND (".join( " OR ", map $_->{cond}, @$conditions).")";
+}
+
+my %QueryProperties = (
+ Query => $sql,
+ OrderBy => 'Priority|id',
+ Order => 'DESC|DESC',
+ Rows => $Rows || 10,
+ ShowHeader => $ShowHeader,
+ Format => $Format,
+);
+
+$m->callback( CallbackName => 'ModifyQueryProperties',
+ %ARGS,
+ QueryProperties => \%QueryProperties,
+);
+
+my $url = RT->Config->Get('WebPath') . '/Search/Results.html?';
+ $url .= $m->comp('/Elements/QueryString',
+ Query => $QueryProperties{'Query'},
+ OrderBy => $QueryProperties{'OrderBy'},
+ Order => $QueryProperties{'Order'},
+ );
+
+</%INIT>
+<%ARGS>
+$Title => ''
+$Class => ''
+ at WatcherTypes => (qw(Watcher))
+$Group => undef
+$conditions
+$Rows => 10
+$Description => ''
+$TitleBox => 0
+$Format => ''
+$ShowHeader => 0
+</%ARGS>
diff --git a/share/html/Group/Search.html b/share/html/Group/Search.html
new file mode 100644
index 000000000..0716c7298
--- /dev/null
+++ b/share/html/Group/Search.html
@@ -0,0 +1,100 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2019 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 }}}
+<& /Elements/Header, Title => loc('Group Search'), Focus => '#autocomplete-GroupString' &>
+<& /Elements/Tabs &>
+
+<& /Elements/GotoGroup, Default => $GroupString||'' &>
+
+<p> <&|/l&>This will search for groups by looking in the following fields:</&> <% $search_fields %></p>
+
+% if ($GroupString) {
+
+% unless ( $groups->Count ) {
+<p><&|/l&>No groups matching search criteria found.</&></p>
+% } else {
+<p><&|/l&>Select a group</&>:</p>
+
+<& /Elements/CollectionList,
+ OrderBy => 'Name',
+ Order => 'ASC',
+ Rows => 100,
+ %ARGS,
+ Format => $Format,
+ Collection => $groups,
+ AllowSorting => 1,
+ PassArguments => [qw(Format Rows Page Order OrderBy GroupString)],
+&>
+
+% }
+% }
+
+<%INIT>
+my $groups;
+my $Format;
+if ( $GroupString ) {
+ $groups = RT::Groups->new($session{'CurrentUser'});
+ $groups->LimitToUserDefinedGroups();
+
+ $groups->SimpleSearch( Return => 'Name',
+ Term => $GroupString,
+ Max => 100 );
+ my $first = $groups->First;
+ RT::Interface::Web::Redirect(RT->Config->Get('WebURL')."Group/Summary.html?id=".$first->Id)
+ if $groups->Count == 1;
+ $groups->GotoFirstItem;
+ $Format = RT->Config->Get('GroupSearchResultFormat');
+}
+
+my $search_fields = join ", ",
+ sort map {s/^CF\.(?:\{(.*)}|(.*))/$1 || $2/e; loc($_)}
+ keys %{RT->Config->Get('GroupSearchFields')};
+
+</%INIT>
+<%ARGS>
+$GroupString => undef
+</%ARGS>
diff --git a/share/html/Group/Summary.html b/share/html/Group/Summary.html
new file mode 100644
index 000000000..6048fc954
--- /dev/null
+++ b/share/html/Group/Summary.html
@@ -0,0 +1,103 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2019 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 }}}
+<& /Elements/Header, Title => loc('Group: [_1]', $Group->Name) &>
+<& /Elements/Tabs &>
+
+<& /Elements/GotoGroup &>
+<& /Elements/ListActions, actions => \@results &>
+
+<%perl>
+$m->callback( CallbackName => 'BeforePortlets', ARGSRef => \%ARGS, Group => $Group, Portlets => $portlets );
+for my $portlet (@$portlets) {
+ $show_portlet->($portlet);
+}
+$m->callback( CallbackName => 'AfterPortlets', ARGSRef => \%ARGS, Group => $Group, Portlets => $portlets );
+</%perl>
+
+<%INIT>
+my $Group = RT::Group->new( $session{'CurrentUser'} );
+my ($status, $msg) = $Group->Load($id);
+unless ($status) {
+ RT->Logger->error("Unable to load group $id: $msg");
+ Abort("Unable to load Group $id");
+}
+
+unless ( $Group->CurrentUserHasRight('SeeGroup') ){
+ Abort("No permission to view group");
+}
+
+my @results;
+if ( $Group->Disabled ){
+ if ( $session{'CurrentUser'}->HasRight(
+ Object => RT->System, Right => 'AdminGroup' ) ){
+ push @results, loc('Group [_1] is currently disabled. Edit the group and check "Enabled" to enable.', $Group->Name);
+ }
+ else{
+ push @results, loc('Group [_1] is currently disabled.', $Group->Name);
+ }
+}
+
+my $portlets = RT->Config->Get('GroupSummaryPortlets');
+
+my $show_portlet = sub {
+ my $portlet = shift;
+ my $full_path = "/Group/Elements/Portlets/$portlet";
+ unless ( RT::Interface::Web->ComponentPathIsSafe($full_path) ) {
+ RT->Logger->error("unsafe portlet $portlet specified in GroupSummaryPortlets");
+ return;
+ }
+ unless ( $m->comp_exists($full_path) ) {
+ RT->Logger->error("Unable to find $portlet in /Group/Elements/Portlets - specified in GroupSummaryPortlets");
+ return;
+ }
+ $m->comp( $full_path, Group => $Group );
+};
+</%INIT>
+<%ARGS>
+$id => undef
+</%ARGS>
diff --git a/share/html/Ticket/Create.html b/share/html/Ticket/Create.html
index 55a220f07..1503fb280 100644
--- a/share/html/Ticket/Create.html
+++ b/share/html/Ticket/Create.html
@@ -56,7 +56,11 @@
<input type="submit" name="SubmitTicket" value="Create" style="display:none">
<input type="hidden" class="hidden" name="id" value="new" />
<input type="hidden" class="hidden" name="Token" value="<% $ARGS{'Token'} %>" />
-
+
+% if ( $ARGS{'AddGroupCc'} ){
+<input type="hidden" class="hidden" name="AddGroupCc" value="<% $ARGS{'AddGroupCc'} %>" />
+% }
+
% $m->callback( CallbackName => 'FormStart', QueueObj => $QueueObj, ARGSRef => \%ARGS );
% if ($gnupg_widget) {
@@ -506,4 +510,5 @@ $DependedOnBy => undef
$MemberOf => undef
$QuoteTransaction => undef
$CloneTicket => undef
+$AddGroupCc => undef
</%ARGS>
diff --git a/share/html/Ticket/Display.html b/share/html/Ticket/Display.html
index 2582871c6..8c1920b2f 100644
--- a/share/html/Ticket/Display.html
+++ b/share/html/Ticket/Display.html
@@ -216,6 +216,26 @@ if ($ARGS{'id'} eq 'new') {
$title = loc("#[_1]: [_2]", $TicketObj->Id, $TicketObj->Subject || '');
+if ( $ARGS{'id'} and $ARGS{'id'} eq 'new' ) {
+ if ( $ARGS{'AddGroupCc'} ){
+ my $group = RT::Group->new($session{'CurrentUser'});
+ my ($ret, $msg) = $group->LoadUserDefinedGroup($ARGS{'AddGroupCc'});
+
+ unless ( $ret ){
+ RT::Logger->warn("Unable to load group " . $ARGS{'AddGroupCc'} . ", $msg. Not adding as Cc.");
+ return;
+ }
+
+ ( $ret, $msg ) = $TicketObj->AddWatcher(
+ Type => 'Cc',
+ PrincipalId => $group->Id
+ );
+
+ RT::Logger->warn("Unable to add group " . $group->Name . ": " . $group->Id . " as a Cc: $msg")
+ unless $ret;
+ }
+}
+
$m->callback(
CallbackName => 'BeforeDisplay',
TicketObj => \$TicketObj,
commit c07dc62c3a5c42e39fbf8b07263852dc51edfe58
Author: Blaine Motsinger <blaine at bestpractical.com>
Date: Wed May 29 18:32:18 2019 -0500
Core RT-Extension-GroupSelfService
diff --git a/etc/RT_Config.pm.in b/etc/RT_Config.pm.in
index da139b13c..ee93fe91c 100644
--- a/etc/RT_Config.pm.in
+++ b/etc/RT_Config.pm.in
@@ -2029,6 +2029,15 @@ Users also need the ModifySelf right to have access to this page.
Set( $SelfServiceDownloadUserData, 0 );
+=item C<$SelfServiceShowGroupTickets>
+
+Set this option to true to show a section with group tickets
+on self service pages.
+
+=cut
+
+Set($SelfServiceShowGroupTickets, 1);
+
=back
=head2 Articles
diff --git a/lib/RT/System.pm b/lib/RT/System.pm
index 05ef84738..d19be82c1 100644
--- a/lib/RT/System.pm
+++ b/lib/RT/System.pm
@@ -93,6 +93,7 @@ __PACKAGE__->AddRight( Staff => ShowGlobalTemplates => 'Show global templates'
__PACKAGE__->AddRight( General => LoadSavedSearch => 'Allow loading of saved searches'); # loc
__PACKAGE__->AddRight( General => CreateSavedSearch => 'Allow creation of saved searches'); # loc
__PACKAGE__->AddRight( Admin => ExecuteCode => 'Allow writing Perl code in templates, scrips, etc'); # loc
+__PACKAGE__->AddRight( Staff => SeeSelfServiceGroupTicket => 'See tickets for other group members in SelfService' ); # loc
=head2 AvailableRights
diff --git a/share/html/SelfService/Elements/MyRequests b/share/html/SelfService/Elements/MyGroupRequests
similarity index 66%
copy from share/html/SelfService/Elements/MyRequests
copy to share/html/SelfService/Elements/MyGroupRequests
index 0cd4e3781..8285cece2 100644
--- a/share/html/SelfService/Elements/MyRequests
+++ b/share/html/SelfService/Elements/MyGroupRequests
@@ -59,9 +59,47 @@
</&>
<%INIT>
+
+unless ( RT->Config->Get('SelfServiceShowGroupTickets')
+ and $session{'CurrentUser'}->HasRight(Right => 'SeeSelfServiceGroupTicket', Object => $RT::System) ){
+ return;
+}
+
$title ||= loc("My [_1] tickets", $friendly_status);
-my $id = $session{'CurrentUser'}->id;
-my $Query = "( Watcher.id = $id )";
+
+# Load a system user to see all groups without a rights check on whether
+# the current user has ShowGroup.
+my $user = RT::User->new(RT->SystemUser);
+my ($ret, $msg) = $user->Load($session{'CurrentUser'}->Id);
+unless ( $ret ){
+ RT::Logger->error("Unable to load user record for user: " . $session{'CurrentUser'}->Name . " :$msg");
+ return;
+}
+my $groups_obj = $user->OwnGroups;
+
+my $Query = '';
+
+if ( $groups_obj->Count ){
+ my $group = $groups_obj->Next;
+
+ # Confirm we got a group. Count can report available groups, but
+ # if the current user doesn't have SeeGroup, it won't be loaded.
+ if ( $group ){
+ $Query = "(( WatcherGroup = " . $group->Id . " )";
+ }
+
+ # Handle multiple groups
+ while ( $group = $groups_obj->Next ){
+ $Query .= " OR ( WatcherGroup = " . $group->Id . " )";
+ }
+
+ $Query .= ")" if $Query;
+}
+
+# Exclude tickets where current user is requestor or cc since they will
+# appear in the My open tickets list
+$Query .= " AND" if $Query;
+$Query .= " $SortByRole.id != " . $session{'CurrentUser'}->Id;
if ($status) {
$status =~ s/(['\\])/\\$1/g;
@@ -72,11 +110,12 @@ my $Format = RT->Config->Get('DefaultSelfServiceSearchResultFormat');
</%INIT>
<%ARGS>
$title => undef
-$friendly_status => loc('open')
+$friendly_status => loc("group's")
$status => undef
$BaseURL => undef
$Page => 1
@Order => ('ASC')
@OrderBy => ('Created')
$Rows => 50
+$SortByRole => 'Requestor' # Role to use when determining "My" tickets
</%ARGS>
diff --git a/share/html/SelfService/Elements/MyRequests b/share/html/SelfService/Elements/MyRequests
index 0cd4e3781..0aa6bf344 100644
--- a/share/html/SelfService/Elements/MyRequests
+++ b/share/html/SelfService/Elements/MyRequests
@@ -61,7 +61,7 @@
<%INIT>
$title ||= loc("My [_1] tickets", $friendly_status);
my $id = $session{'CurrentUser'}->id;
-my $Query = "( Watcher.id = $id )";
+my $Query = "( $SortByRole.id = $id )";
if ($status) {
$status =~ s/(['\\])/\\$1/g;
@@ -79,4 +79,5 @@ $Page => 1
@Order => ('ASC')
@OrderBy => ('Created')
$Rows => 50
+$SortByRole => 'Requestor' # Role to use when determining "My" tickets
</%ARGS>
diff --git a/share/html/SelfService/index.html b/share/html/SelfService/index.html
index 40f389538..9a2e3c1d2 100644
--- a/share/html/SelfService/index.html
+++ b/share/html/SelfService/index.html
@@ -59,6 +59,16 @@
% $m->callback(CallbackName => 'AfterMyRequests', ARGSRef => \%ARGS, Page => $Page);
+<& /SelfService/Elements/MyGroupRequests,
+ %ARGS,
+ status => '__Active__',
+ title => loc('My group\'s tickets'),
+ BaseURL => RT->Config->Get('WebPath') ."/SelfService/?",
+ Page => $Page,
+&>
+
+% $m->callback(CallbackName => 'AfterMyGroupRequests', ARGSRef => \%ARGS, Page => $Page);
+
<%ARGS>
$Page => 1
</%ARGS>
commit ab820c9a4e4a9f15f43245eab619387113e403ab
Author: Blaine Motsinger <blaine at bestpractical.com>
Date: Thu Jun 6 12:56:58 2019 -0500
Add note to UPGRADING-4.6 for callback changes
diff --git a/devel/docs/UPGRADING-4.6 b/devel/docs/UPGRADING-4.6
new file mode 100644
index 000000000..0e9175793
--- /dev/null
+++ b/devel/docs/UPGRADING-4.6
@@ -0,0 +1,19 @@
+=head1 UPGRADING FROM RT 4.4.0 and greater
+
+This documentation notes internals changes between the 4.4 and 4.6
+series that are primarily of interest to developers writing extensions
+or local customizations. It is not an exhaustive list.
+
+=over
+
+=item *
+
+New group options were added to the ticket listings pages in SelfService. With
+the additions, the C<AfterMyRequests> callback is no longer at the bottom of the
+page. If you previously used this callback to add to the bottom of the SelfService
+page, a new callback C<AfterMyGroupRequests> is now available below the new group
+ticket listing.
+
+=back
+
+=cut
commit 70a92466b98e51f79645aeb86b0f51073e19ae33
Author: Blaine Motsinger <blaine at bestpractical.com>
Date: Tue Jun 11 12:50:07 2019 -0500
Update test for new group nav links
diff --git a/t/web/group_create.t b/t/web/group_create.t
index 4a7bc9cd9..b09529701 100644
--- a/t/web/group_create.t
+++ b/t/web/group_create.t
@@ -94,6 +94,9 @@ ok($m->logout(), 'Logged out');
ok($tester->PrincipalObj->GrantRight(Right => 'SeeGroup', Object => $RT::System), 'Grant SeeGroup');
load_group_admin_pages($m, $group_id, '200');
+
+ $m->get("/Group/Summary.html?id=$group_id");
+ is( $m->status, 200, "Got 200 for Group Summary page");
}
sub load_group_admin_pages{
@@ -101,7 +104,7 @@ sub load_group_admin_pages{
my $group_id = shift;
my $status = shift;
- foreach my $page (qw(GroupRights Members Modify History Memberships UserRights)){
+ foreach my $page (qw(GroupRights Members Modify History Memberships ModifyLinks UserRights)){
$m->get("/Admin/Groups/$page.html?id=$group_id");
is( $m->status, $status, "Got $status for $page page");
}
commit 05d6cbc6fec621eb3d6b6150a8c78129c45a3b10
Author: Blaine Motsinger <blaine at bestpractical.com>
Date: Tue Jun 11 13:17:39 2019 -0500
Add test for group summary
diff --git a/t/web/group_summary.t b/t/web/group_summary.t
new file mode 100644
index 000000000..b2fe93ff2
--- /dev/null
+++ b/t/web/group_summary.t
@@ -0,0 +1,69 @@
+use strict;
+use warnings;
+
+use RT::Test tests => undef;
+
+my ( $baseurl, $m ) = RT::Test->started_ok;
+ok $m->login, 'logged in as root';
+my $root = RT::User->new(RT->SystemUser);
+ok( $root->Load('root'), 'load root user' );
+
+my $group_name = 'test group';
+my $group_id;
+
+diag( 'Group Summary access and ticket creation' );
+{
+ $m->follow_link( id => 'admin-groups-create');
+
+ $m->submit_form(
+ form_name => 'ModifyGroup',
+ fields => {
+ Name => $group_name,
+ },
+ );
+ $m->content_contains( 'Group created', 'created group successfully' );
+
+ $group_id = $m->form_name( 'ModifyGroup' )->value( 'id' );
+ ok( $group_id, "Found id of the group in the form, #$group_id" );
+
+ $m->follow_link_ok({ id => 'page-summary', url_regex => qr|/Group/Summary\.html\?id=$group_id$!| },
+ 'Followed Group Summary link');
+
+ $m->submit_form_ok({ form_name => 'CreateTicket' },
+ "Submitted form to create ticket with group $group_id as Cc" );
+ like( $m->uri, qr{/Ticket/Create\.html\?AddGroupCc=$group_id&Queue=1$},
+ "now on /Ticket/Create\.html with param AddGroupCc=$group_id" );
+
+ my $subject = 'test AddGroupCc ticket';
+ $m->submit_form_ok({
+ form_name => 'TicketCreate',
+ fields => {
+ Subject => $subject,
+ },
+ }, 'Submitted form to create ticket with group cc');
+ like( $m->uri, qr{/Ticket/Display\.html\?id},
+ "now on /Ticket/Display\.html" );
+
+ $m->get( "/Group/Summary.html?id=$group_id" );
+ $m->content_contains( 'test AddGroupCc ticket', 'Group Cc ticket was found on Group Summary page' );
+}
+
+ok( $m->logout(), 'Logged out' );
+
+diag( 'Access Group Summary with non-root user' );
+{
+ my $tester = RT::Test->load_or_create_user( Name => 'staff1', Password => 'password' );
+ ok( $m->login( $tester->Name, 'password' ), 'Logged in' );
+
+ $m->get( "/Group/Summary.html?id=$group_id" );
+ is( $m->status, 200, "Got 200 for Group Summary page" );
+ $m->warning_like( qr/No permission to view group/, "Got permission denied warning without SeeGroup right" );
+
+ ok( $tester->PrincipalObj->GrantRight( Right => 'SeeGroup', Object => $RT::System ), 'Grant SeeGroup' );
+
+ $m->get( "/Group/Summary.html?id=$group_id" );
+ is( $m->status, 200, "Got 200 for Group Summary page" );
+ $m->no_warnings_ok( "No warning with SeeGroup right" );
+}
+
+done_testing();
commit 7766cdb63b0f8c5725df37bfee0f24ed46512dbe
Author: Blaine Motsinger <blaine at bestpractical.com>
Date: Mon Aug 19 19:35:54 2019 -0500
Migrate RT-Extension-GroupSummary pages to elevator themes
diff --git a/share/html/Elements/GotoGroup b/share/html/Elements/GotoGroup
index 6c84066de..8985780c4 100644
--- a/share/html/Elements/GotoGroup
+++ b/share/html/Elements/GotoGroup
@@ -45,8 +45,6 @@
%# those contributions and any derivatives thereof.
%#
%# END BPS TAGGED BLOCK }}}
-<form name="GroupSearch" method="post" action="<% RT->Config->Get('WebPath') %>/Group/Search.html">
-<input type="text" name="GroupString" value="<% $Default %>" data-autocomplete="Groups" data-autocomplete-return="Name" id="autocomplete-GroupString" />
<script type="text/javascript">
jQuery(function(){
// Jump directly to the page if a group is chosen
@@ -55,7 +53,15 @@ jQuery(function(){
});
});
</script>
-<input type="submit" name="GroupSearch" value="<&|/l&>Search</&>" class="button" />
+<form name="GroupSearch" method="post" action="<% RT->Config->Get('WebPath') %>/Group/Search.html">
+ <div class="form-row">
+ <div class="col-md-auto">
+ <input type="text" class="form-control" name="GroupString" value="<% $Default %>" data-autocomplete="Groups" data-autocomplete-return="Name" id="autocomplete-GroupString" />
+ </div>
+ <div class="col-md-auto">
+ <input type="submit" name="GroupSearch" value="<&|/l&>Search</&>" class="form-control btn btn-primary button" />
+ </div>
+ </div>
</form>
<%ARGS>
$Default => ''
diff --git a/share/html/Group/Elements/Portlets/CreateTicket b/share/html/Group/Elements/Portlets/CreateTicket
index eace3c9bb..6acad36f6 100644
--- a/share/html/Group/Elements/Portlets/CreateTicket
+++ b/share/html/Group/Elements/Portlets/CreateTicket
@@ -47,10 +47,16 @@
%# END BPS TAGGED BLOCK }}}
<&| /Widgets/TitleBox, title => loc('Quick ticket creation'), class => "group create-ticket" &>
<form name="CreateTicket" action="<%RT->Config->Get('WebPath')%>/Ticket/Create.html">
-<&|/l&>Create a ticket with this group as Cc in Queue</&>
<input type="hidden" name="AddGroupCc" value="<%$Group->Id%>">
-<& /Elements/SelectNewTicketQueue &>
-<input type="submit" value="<&|/l&>Create</&>">
+ <div class="form-row">
+ <div class="col-md-auto">
+ <&|/l&>Create a ticket with this group as Cc in Queue</&>
+ <& /Elements/SelectNewTicketQueue &>
+ </div>
+ <div class="col-md-auto">
+ <input type="submit" class="form-control btn btn-primary button" value="<&|/l&>Create</&>">
+ </div>
+ </div>
</form>
</&>
<%ARGS>
commit c7a151b20b0efd0cf0c2083200624f29187e04d7
Author: Blaine Motsinger <blaine at bestpractical.com>
Date: Thu Aug 22 17:33:27 2019 -0500
Migrate RT-Extension-GroupLinks pages to elevator themes
diff --git a/share/html/Admin/Elements/AddLinks b/share/html/Admin/Elements/AddLinks
index 06b0285af..ae80bfa0d 100644
--- a/share/html/Admin/Elements/AddLinks
+++ b/share/html/Admin/Elements/AddLinks
@@ -52,15 +52,24 @@
% } else {
<i><&|/l&>Enter objects or URIs to link objects to. Separate multiple entries with spaces.</&></i><br />
% }
-<table>
- <tr>
- <td class="label"><& /Elements/ShowRelationLabel, Object => $Object, Label => loc('Links to').':', Relation => 'RefersTo' &></td>
- <td class="entry"><input name="<%$id%>-RefersTo" value="<% $ARGSRef->{"$id-RefersTo"} || '' %>" <% $exclude |n%>/></td>
- </tr>
- <tr>
- <td class="label"><& /Elements/ShowRelationLabel, Object => $Object, Label => loc('Linked to by').':', Relation => 'ReferredToBy' &></td>
- <td class="entry"> <input name="RefersTo-<%$id%>" value="<% $ARGSRef->{"RefersTo-$id"} || '' %>" <% $exclude |n%>/></td>
- </tr>
+
+ <div class="form-row">
+ <div class="col-md-3 label">
+ <& /Elements/ShowRelationLabel, Object => $Object, Label => loc('Links to').':', Relation => 'RefersTo' &>
+ </div>
+ <div class="col-md-9 value">
+ <input type="text" class="form-control" name="<%$id%>-RefersTo" value="<% $ARGSRef->{"$id-RefersTo"} || '' %>" <% $exclude |n%>/>
+ </div>
+ </div>
+
+ <div class="form-row">
+ <div class="col-md-3 label">
+ <& /Elements/ShowRelationLabel, Object => $Object, Label => loc('Linked to by').':', Relation => 'ReferredToBy' &>
+ </div>
+ <div class="col-md-9 value">
+ <input type="text" class="form-control" name="RefersTo-<%$id%>" value="<% $ARGSRef->{"RefersTo-$id"} || '' %>" <% $exclude |n%>/>
+ </div>
+ </div>
<& /Elements/EditCustomFields,
Object => $Object,
Grouping => 'Links',
@@ -70,7 +79,7 @@
: ()),
&>
% $m->callback( CallbackName => 'NewLink' );
-</table>
+
<%INIT>
my $id = ($Object and $Object->id)
? $Object->id
diff --git a/share/html/Admin/Elements/EditLinks b/share/html/Admin/Elements/EditLinks
index 4671f3ebe..2ca19141d 100644
--- a/share/html/Admin/Elements/EditLinks
+++ b/share/html/Admin/Elements/EditLinks
@@ -45,41 +45,65 @@
%# those contributions and any derivatives thereof.
%#
%# END BPS TAGGED BLOCK }}}
-<table width="100%">
- <tr>
- <td valign="top" width="50%">
- <h3><&|/l&>Current Links</&></h3>
+<div class="row">
-<table>
- <tr>
- <td class="labeltop"><& /Elements/ShowRelationLabel, Object => $Object, Label => loc('Links to').':', Relation => 'RefersTo' &></td>
- <td class="value">
+ <div class="col-md-6">
+ <div class="form-row">
+ <div class="col-md-12">
+ <h3><&|/l&>Current Links</&></h3>
+ </div>
+ </div>
+ <div class="form-row">
+ <div class="col-md-3 label">
+ <& /Elements/ShowRelationLabel, Object => $Object, Label => loc('Links to').':', Relation => 'RefersTo' &>
+ </div>
+ <div class="col-md-9 value">
+ <div class="form-row">
+ <div class="col-md-auto">
% while (my $link = $Object->RefersTo->Next) {
- <& /Elements/EditLink, Link => $link, Mode => 'Target' &>
+ <& /Elements/EditLink, Link => $link, Mode => 'Target' &>
%}
- </td>
- </tr>
- <tr>
- <td class="labeltop group-link edit-referredtoby"><& /Elements/ShowRelationLabel, Object => $Object, Label => loc('Linked to by').':', Relation => 'ReferredToBy' &></td>
- <td class="value group-link edit-referredtoby">
+ </div>
+ </div>
+ </div>
+ </div>
+ <div class="form-row">
+ <div class="col-md-3 label">
+ <& /Elements/ShowRelationLabel, Object => $Object, Label => loc('Linked to by').':', Relation => 'ReferredToBy' &>
+ </div>
+ <div class="col-md-9 value">
+ <div class="form-row">
+ <div class="col-md-auto">
% while (my $link = $Object->ReferredToBy->Next) {
- <& /Elements/EditLink, Link => $link, Mode => 'Base' &>
+ <& /Elements/EditLink, Link => $link, Mode => 'Base' &>
% }
- </td>
- </tr>
- <tr>
- <td></td>
- <td><i><&|/l&>(Check box to remove link)</&></i></td>
- </tr>
-</table>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div class="form-row">
+ <div class="col-md-3 label"></div>
+ <div class="col-md-9 value">
+ <i><&|/l&>(Check box to remove link)</&></i>
+ </div>
+ </div>
+ </div>
+
+ <div class="col-md-6">
+ <div class="form-row">
+ <div class="col-md-12">
+ <h3><&|/l&>New Links</&></h3>
+ </div>
+ </div>
+ <div class="form-row">
+ <div class="col-md-12">
+ <& AddLinks, %ARGS &>
+ </div>
+ </div>
+ </div>
+
+</div>
-</td>
-<td valign="top">
-<h3><&|/l&>New Links</&></h3>
-<& AddLinks, %ARGS &>
-</td>
-</tr>
-</table>
<%ARGS>
$Object => undef
</%ARGS>
diff --git a/share/html/Admin/Groups/ModifyLinks.html b/share/html/Admin/Groups/ModifyLinks.html
index 77a44d57d..e36b865eb 100644
--- a/share/html/Admin/Groups/ModifyLinks.html
+++ b/share/html/Admin/Groups/ModifyLinks.html
@@ -63,7 +63,11 @@
<& /Admin/Elements/EditLinks, Object => $Group &>
</&>
-<& /Elements/Submit, Name => 'SubmitGroupLinks', Label => loc('Save Changes') &>
+ <div class="form-row">
+ <div class="col-md-12">
+ <& /Elements/Submit, Name => 'SubmitGroupLinks', Label => loc('Save Changes') &>
+ </div>
+ </div>
</form>
% $m->callback(CallbackName => 'AfterForm', ARGSRef => \%ARGS, GroupObj => $Group);
-----------------------------------------------------------------------
More information about the rt-commit
mailing list