Assign assets (with autocomplete)

Would be nice to have it.
It is not user friendly to write to asset IDs into tickets maually.

There are functions missing:

  • assign asset by Scrip based on asset-name in subject
  • assign asset manually from webUI with autocompletion
  • assign asset by email

What are the best approaches for implementing these features?

Peter

I agree with Peter. Some type of autocomplete would be useful for new tickets. If my understanding is correct, to assign an Asset to a ticket I must:

1.Lookup the Asset ID.
2. Select the ticket.
3. Select ‘Links’.
4. type asset:ID in ‘Refers To’.
5. Save.
6. Click ‘Display’ to return to the ticket.

With use of @AssetQueues in RT_SiteConfig.pm is a little simpler.
Still need to search for Asset ID and put it into the ticket as Asset reference.

We have a custom solution from BestPractical that provides an Actions menu on the Display page for each Asset. I guess I’ve never seen the vanilla Assets functionality and am surprised to learn how difficult it is to link an Asset to a Ticket. Maybe the solutions written for us could be be rolled into core or I guess maybe cheaply customized and provided to you/others?

Here’s the functionality we use:

-From each Asset Display page we can do Actions > Create Linked Ticket which opens a little window asking which Queue to create the ticket in and telling you that it will set the Asset Owner as the Ticket Requestor. Then it takes you to the Ticket Create screen for the selected queue, ready for a subject, more Requestors, Reply/Comment, etc.

-From an Asset Search > Bulk Update screen we have a button to Create Linked Ticket which behaves the same as it does from the Asset Display Actions menu.

-From inside each Ticket we have a tiny box to “Add an asset to this ticket” which accepts an Asset # or multiple Asset #s separated by spaces. No autocomplete, which hasn’t been a problem.

With those 3 pieces of functionality we never have to directly use Links to link an asset and a ticket.

The first and third points works the same way in vanilla Assets.
Still missing autocomplete on Ticket edit/create page which is the most used way the IT operational ticket asset relation is managed.

About assets autocompletion, I’ve posted a pull request some times ago: Add autocomplete for assets by gibus · Pull Request #203 · bestpractical/rt · GitHub

May I ask some help about this solution? I’m newbie, but really would like to use autocomplete to assign an asset to the ticket at links. :slight_smile:

Hi,

I applied your patch to my 4.4.4 system and it works quite nicely. Unfortunately, it will only use the catalog specified in the DefaultCatalog configuration parameter. For other SimpleSearch functions, the value is sticky so if you search in a new catalog that will remain the default for additional searches. It looks like Assets.pm just reads the DefaultCatalog value directly. Do you know what would need to be changed to pull the current value of DefaulCatalog from the $session cache?

Regards,
Ken

Hi,

I applied the following patch to your html/Helpers/Autocomplete/Assets to make the search use the sticky DefaultCatalog value if available.

--- Assets.ASSETAUTO	2022-01-28 17:07:16.732533645 -0600
+++ Assets.LOADDEFCAT	2022-01-31 10:18:37.691732152 -0600
@@ -31,7 +31,12 @@
 
 $assets->RowsPerPage($max);
 $assets->LimitToActiveStatus;
-$assets->SimpleSearch(Term => $term);
+
+if ( $session{'DefaultCatalog'} ) {
+    $assets->SimpleSearch(Term => $term, Catalog => $session{'DefaultCatalog'});
+} else {
+    $assets->SimpleSearch(Term => $term);
+}
 
 # Exclude assets we don't want
 foreach (split /\s*,\s*/, $exclude) {

Hope this helps others.

Regards,
Ken

Hi Gerald,
We have been using your autocomplete for assets patch with 4.4.4 and it works very well. We are preparing to upgrade to 5.0.4 but there are enough differences that I am not sure that I know how to re-base your patches. You haven’t by any chance updated them for RT 5?

Regards,
Ken

Hi Ken,

Indeed, I’ve updated this patch for a local installation of RT 5.0.4:

diff --git a/rt/share/html/Elements/AddLinks b/rt/share/html/Elements/AddLinks
index 4fd52791..cdf2c24a 100644
--- a/rt/share/html/Elements/AddLinks
+++ b/rt/share/html/Elements/AddLinks
@@ -55,8 +55,9 @@ my $id = ($Object and $Object->id)
     ? $Object->id
     : "new";
 
-my $exclude = qq| data-autocomplete="Tickets" data-autocomplete-multiple="1"|;
-$exclude .= qq| data-autocomplete-exclude="$id"| if $Object->id;
+my $exclude = qq| data-autocomplete="TicketsAssets" data-autocomplete-multiple="1"|;
+my @excludes;
+push @excludes, $id if $Object->id;
 </%init>
 % if (ref($Object) eq 'RT::Ticket') {
 <i><&|/l&>Enter tickets or URIs to link tickets to. Separate multiple entries with spaces.</&>
@@ -76,24 +77,54 @@ $exclude .= qq| data-autocomplete-exclude="$id"| if $Object->id;
 
 
 <&| /Elements/LabeledValue, RawLabel => $m->scomp('ShowRelationLabel', Object => $Object, Label => loc('Depends on'), Relation => 'DependsOn') &>
-  <input type="text" class="form-control" name="<%$id%>-DependsOn" value="<% $ARGSRef->{"$id-DependsOn"} || '' %>" <% $exclude |n%>/>
+% my @excludes_dependson;
+% while (my $link = $Object->DependsOn->Next) {
+%   push @excludes_dependson, ((UNIVERSAL::isa($link->TargetObj, 'RT::Asset') ? 'asset:' : '') . $link->TargetObj->id) if $link->TargetObj;
+% }
+% my $exclude_dependson = $exclude . ' data-autocomplete-exclude="' .join(' ', (@excludes, @excludes_dependson)). '"' if @excludes_dependson || @excludes;
+  <input type="text" class="form-control" name="<%$id%>-DependsOn" value="<% $ARGSRef->{"$id-DependsOn"} || '' %>" <% $exclude_dependson |n%>/>
 </&>
 <&| /Elements/LabeledValue, RawLabel => $m->scomp('ShowRelationLabel', Object => $Object, Label => loc('Depended on by'), Relation => 'DependedOnBy') &>
-  <input type="text" class="form-control" name="DependsOn-<%$id%>" value="<% $ARGSRef->{"DependsOn-$id"} || '' %>" <% $exclude |n%>/>
+% my @excludes_dependedonby;
+% while (my $link = $Object->DependedOnBy->Next) {
+%   push @excludes_dependedonby, ((UNIVERSAL::isa($link->BaseObj, 'RT::Asset') ? 'asset:' : '') . $link->BaseObj->id) if $link->BaseObj;
+% }
+% my $exclude_dependonby = $exclude . ' data-autocomplete-exclude="' .join(' ', (@excludes, @excludes_dependedonby)). '"' if @excludes_dependedonby || @excludes;
+  <input type="text" class="form-control" name="DependsOn-<%$id%>" value="<% $ARGSRef->{"DependsOn-$id"} || '' %>" <% $exclude_dependonby |n%>/>
 </&>
 
 <&| /Elements/LabeledValue, RawLabel => $m->scomp('ShowRelationLabel', Object => $Object, Label => loc('Parents'), Relation => 'Parents') &>
-  <input type="text" class="form-control" name="<%$id%>-MemberOf" value="<% $ARGSRef->{"$id-MemberOf"} || '' %>" <% $exclude |n%>/>
+% my @excludes_memberof;
+% while (my $link = $Object->MemberOf->Next) {
+%   push @excludes_memberof, ((UNIVERSAL::isa($link->TargetObj, 'RT::Asset') ? 'asset:' : '') . $link->TargetObj->id) if $link->TargetObj;
+% }
+% my $exclude_memberof = $exclude . ' data-autocomplete-exclude="' .join(' ', (@excludes, @excludes_memberof)). '"' if @excludes_memberof || @excludes;
+  <input type="text" class="form-control" name="<%$id%>-MemberOf" value="<% $ARGSRef->{"$id-MemberOf"} || '' %>" <% $exclude_memberof |n%>/>
 </&>
 <&| /Elements/LabeledValue, RawLabel => $m->scomp('ShowRelationLabel', Object => $Object, Label => loc('Children'), Relation => 'Children') &>
-  <input type="text" class="form-control" name="MemberOf-<%$id%>" value="<% $ARGSRef->{"MemberOf-$id"} || '' %>" <% $exclude |n%>/>
+% my @excludes_members;
+% while (my $link = $Object->Members->Next) {
+%   push @excludes_members, ((UNIVERSAL::isa($link->BaseObj, 'RT::Asset') ? 'asset:' : '') . $link->BaseObj->id) if $link->BaseObj;
+% }
+% my $exclude_members = $exclude . ' data-autocomplete-exclude="' .join(' ', (@excludes, @excludes_members)). '"' if @excludes_members || @excludes;
+  <input type="text" class="form-control" name="MemberOf-<%$id%>" value="<% $ARGSRef->{"MemberOf-$id"} || '' %>" <% $exclude_members |n%>/>
 </&>
 
 <&| /Elements/LabeledValue, RawLabel => $m->scomp('ShowRelationLabel', Object => $Object, Label => loc('Refers to'), Relation => 'RefersTo') &>
-  <input type="text" class="form-control" name="<%$id%>-RefersTo" value="<% $ARGSRef->{"$id-RefersTo"} || '' %>" <% $exclude |n%>/>
+% my @excludes_refersto;
+% while (my $link = $Object->RefersTo->Next) {
+%   push @excludes_refersto, ((UNIVERSAL::isa($link->TargetObj, 'RT::Asset') ? 'asset:' : '') . $link->TargetObj->id) if $link->TargetObj;
+% }
+% my $exclude_refersto = $exclude . ' data-autocomplete-exclude="' .join(' ', (@excludes, @excludes_refersto)). '"' if @excludes_refersto || @excludes;
+  <input type="text" class="form-control" name="<%$id%>-RefersTo" value="<% $ARGSRef->{"$id-RefersTo"} || '' %>" <% $exclude_refersto |n%>/>
 </&>
 <&| /Elements/LabeledValue, RawLabel => $m->scomp('ShowRelationLabel', Object => $Object, Label => loc('Referred to by'), Relation => 'ReferredToBy') &>
-  <input type="text" class="form-control" name="RefersTo-<%$id%>" value="<% $ARGSRef->{"RefersTo-$id"} || '' %>" <% $exclude |n%>/>
+% my @excludes_referredtoby;
+% while (my $link = $Object->ReferredToBy->Next) {
+%   push @excludes_referredtoby, ((UNIVERSAL::isa($link->BaseObj, 'RT::Asset') ? 'asset:' : '') . $link->BaseObj->id) if $link->BaseObj;
+% }
+% my $exclude_referredtoby = $exclude . ' data-autocomplete-exclude="' .join(' ', (@excludes, @excludes_referredtoby)). '"' if @excludes_referredtoby || @excludes;
+  <input type="text" class="form-control" name="RefersTo-<%$id%>" value="<% $ARGSRef->{"RefersTo-$id"} || '' %>" <% $exclude_referredtoby |n%>/>
 </&>
 
   <& /Elements/EditCustomFields,
diff --git a/rt/share/html/Helpers/Autocomplete/Assets b/rt/share/html/Helpers/Autocomplete/Assets
index ef93af5a..4a26cb29 100644
--- a/rt/share/html/Helpers/Autocomplete/Assets
+++ b/rt/share/html/Helpers/Autocomplete/Assets
@@ -52,10 +52,12 @@
 <%ARGS>
 $term       => undef
 $max        => 10
-$op         => 'STARTSWITH'
+$exclude    => ''
+$op         => 'LIKE'
 $right      => undef
 $return     => 'id'
 $queue      => undef
+$return_suggestions => 0
 </%ARGS>
 
 <%INIT>
@@ -68,7 +70,7 @@ $m->abort unless defined $return
              and length $term;
 
 # Sanity check the operator
-$op = 'STARTSWITH' unless $op =~ /^(?:LIKE|(?:START|END)SWITH|=|!=)$/i;
+$op = 'LIKE' unless $op =~ /^(?:LIKE|(?:START|END)SWITH|=|!=)$/i;
 
 my $assets = RT::Assets->new( $session{CurrentUser} );
 
@@ -81,10 +83,16 @@ $assets->Limit(
     CASESENSITIVE   => 0,
 );
 
+# Exclude assets we don't want
+foreach (split /\s*,\s*/, $exclude) {
+    $assets->Limit(FIELD => 'id', VALUE => $_, OPERATOR => '!=', ENTRYAGGREGATOR => 'AND', SUBCLAUSE => 'excludeautocomplete');
+}
+
 my @suggestions;
 while (my $a = $assets->Next) {
     next if $right and not $a->CurrentUserHasRight($right);
     my $value = $a->$return;
     push @suggestions, { label => $a->Name, value => $value };
 }
+return @suggestions if $return_suggestions;
 </%INIT>
diff --git a/rt/share/html/Helpers/Autocomplete/Tickets b/rt/share/html/Helpers/Autocomplete/Tickets
index 8e9462ce..7429b031 100644
--- a/rt/share/html/Helpers/Autocomplete/Tickets
+++ b/rt/share/html/Helpers/Autocomplete/Tickets
@@ -54,6 +54,7 @@ $term => undef
 $max => undef
 $exclude => ''
 $limit => undef
+$return_suggestions => 0
 </%ARGS>
 <%INIT>
 # Only allow certain return fields
@@ -64,6 +65,7 @@ $m->abort unless defined $return
              and defined $term
              and length $term;
 
+
 my $CurrentUser = $session{'CurrentUser'};
 
 # Require privileged users
@@ -110,5 +112,6 @@ while ( my $ticket = $tickets->Next ) {
     my $formatted = loc("#[_1]: [_2]", $ticket->Id, $ticket->Subject);
     push @suggestions, { label => $formatted, value => $ticket->$return };
 }
+return @suggestions if $return_suggestions;
 
 </%INIT>
diff --git a/rt/share/html/Helpers/Autocomplete/TicketsAssets b/rt/share/html/Helpers/Autocomplete/TicketsAssets
new file mode 100644
index 00000000..cb69c712
--- /dev/null
+++ b/rt/share/html/Helpers/Autocomplete/TicketsAssets
@@ -0,0 +1,26 @@
+% $r->content_type('application/json; charset=utf-8');
+<% JSON( \@suggestions ) |n %>
+% $m->abort;
+<%args>
+$return => ''
+$term => undef
+$max => undef
+$exclude => ''
+</%args>
+<%init>
+my @suggestions;
+my @excludes;
+
+(my $prev, my $type, $term) = $term =~ /^((?:(asset:)?\d+\s+)*)(.*)/;
+@excludes = split ' ', $prev if $prev;
+push @excludes, split ' ', $exclude if $exclude;
+
+if ($term =~ /^asset:./) {
+    my $exclude_assets = join(',', map(/(\d+)/, grep(/^asset:\d+$/, @excludes)));
+    @suggestions = $m->comp('Assets', term => substr($term, 6), max => $max, exclude => $exclude_assets, return_suggestions => 1);
+    @suggestions = map { {id => $_->{id}, label => $_->{label}, value => 'asset:' . $_->{value}} } @suggestions;
+} else {
+    my $exclude_tickets = join(' ', grep(/^\d+$/, @excludes));
+    @suggestions = $m->comp('Tickets', return => $return, term => $term, max => $max, exclude => $exclude_tickets, return_suggestions => 1);
+}
+</%init>
diff --git a/rt/share/static/js/autocomplete.js b/rt/share/static/js/autocomplete.js
index c967050d..ed7bbafc 100644
--- a/rt/share/static/js/autocomplete.js
+++ b/rt/share/static/js/autocomplete.js
@@ -9,6 +9,7 @@ window.RT.Autocomplete.Classes = {
     Queues: 'queues',
     Articles: 'articles',
     Assets: 'assets',
+    TicketsAssets: 'tickets-assets',
     Principals: 'principals'
 };
 
@@ -150,7 +151,7 @@ window.RT.Autocomplete.bind = function(from) {
         }
 
         if (input.is('[data-autocomplete-multiple]')) {
-            if ( what != 'Tickets' ) {
+            if ( what != 'Tickets' && what != 'TicketsAssets' ) {
                 queryargs.push("delim=,");
             }
 
@@ -160,22 +161,23 @@ window.RT.Autocomplete.bind = function(from) {
             }
 
             options.select = function(event, ui) {
-                var terms = this.value.split(what == 'Tickets' ? /\s+/ : /,\s*/);
+                var terms = this.value.split((what == 'Tickets' || what == 'TicketsAssets') ? /\s+/ : /,\s*/);
                 terms.pop();                    // remove current input
-                if ( what == 'Tickets' ) {
+                if ( what == 'Tickets' || what == 'TicketsAssets' ) {
                     // remove non-integers in case subject search with spaces in (like "foo bar")
                     var new_terms = [];
                     for ( var i = 0; i < terms.length; i++ ) {
-                        if ( terms[i].match(/\D/) ) {
-                            break; // Items after the first non-integers are all parts of search string
+                        if ( terms[i].match(/^(?:asset:)?\d+$/) ) {
+                            new_terms.push(terms[i]);
+                            continue;
                         }
-                        new_terms.push(terms[i]);
+                        break; // Items after the first non-integers / asset:integer are all parts of search string
                     }
                     terms = new_terms;
                 }
                 terms.push( ui.item.value );    // add selected item
                 terms.push(''); // add trailing delimeter so user can input another value directly
-                this.value = terms.join(what == 'Tickets' ? ' ' : ", ");
+                this.value = terms.join((what == 'Tickets' || what == 'TicketsAssets') ? ' ' : ", ");
                 jQuery(this).change();
 
                 return false;

Hi Gerald,

That is super! I will give it a try. Thank you.

Regards,
Ken

Just to follow up. The updated patch worked great! Thank you again.

Regards,
Ken