From 84feccba79c9f7d60376f82a25f8d4f926177af6 Mon Sep 17 00:00:00 2001
From: Jack Chapman <jachapman@gitlab.com>
Date: Mon, 10 Jun 2024 17:14:03 +0000
Subject: [PATCH] Add support for rendering icons

Adds support for rendering an icon beside
the autocomplete suggestions for issues in
markdown editor.

Changelog: added
---
 app/assets/javascripts/gfm_auto_complete.js           |  6 ++++--
 app/models/issue.rb                                   |  2 ++
 app/serializers/group_issuable_autocomplete_entity.rb |  1 +
 app/services/groups/autocomplete_service.rb           |  3 ++-
 app/services/projects/autocomplete_service.rb         |  5 ++++-
 spec/frontend/gfm_auto_complete_spec.js               | 11 +++++++++++
 .../group_issuable_autocomplete_entity_spec.rb        |  2 +-
 spec/services/groups/autocomplete_service_spec.rb     |  1 +
 spec/services/projects/autocomplete_service_spec.rb   |  7 +++++++
 9 files changed, 33 insertions(+), 5 deletions(-)

diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js
index b91bc16bed1ab..8a5f9318b69db 100644
--- a/app/assets/javascripts/gfm_auto_complete.js
+++ b/app/assets/javascripts/gfm_auto_complete.js
@@ -514,6 +514,7 @@ class GfmAutoComplete {
               title: i.title,
               reference: i.reference,
               search: `${i.iid} ${i.title}`,
+              iconName: i.icon_name,
             };
           });
         },
@@ -1155,8 +1156,9 @@ GfmAutoComplete.Issues = {
     // eslint-disable-next-line no-template-curly-in-string
     return value.reference || '${atwho-at}${id}';
   },
-  templateFunction({ id, title, reference }) {
-    return `<li><small>${escape(reference || id)}</small> ${escape(title)}</li>`;
+  templateFunction({ id, title, reference, iconName }) {
+    const icon = iconName ? spriteIcon(iconName, 'gl-text-secondary s16 gl-mr-2') : '';
+    return `<li>${icon}<small>${escape(reference || id)}</small> ${escape(title)}</li>`;
   },
 };
 // Milestones
diff --git a/app/models/issue.rb b/app/models/issue.rb
index aff368a6fad80..e66e5f6acff86 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -242,6 +242,8 @@ def most_recent
   scope :with_non_null_relative_position, -> { where.not(relative_position: nil) }
   scope :with_projects_matching_search_data, -> { where('issue_search_data.project_id = issues.project_id') }
 
+  scope :with_work_item_type, -> { joins(:work_item_type) }
+
   before_validation :ensure_namespace_id, :ensure_work_item_type
 
   after_save :ensure_metrics!, unless: :skip_metrics?
diff --git a/app/serializers/group_issuable_autocomplete_entity.rb b/app/serializers/group_issuable_autocomplete_entity.rb
index f950a7db7855c..6c47f073eb111 100644
--- a/app/serializers/group_issuable_autocomplete_entity.rb
+++ b/app/serializers/group_issuable_autocomplete_entity.rb
@@ -6,4 +6,5 @@ class GroupIssuableAutocompleteEntity < Grape::Entity
   expose :reference do |issuable, options|
     issuable.to_reference(options[:parent_group])
   end
+  expose :icon_name, safe: true
 end
diff --git a/app/services/groups/autocomplete_service.rb b/app/services/groups/autocomplete_service.rb
index ad1cfc66d6d93..d4392e3b8441e 100644
--- a/app/services/groups/autocomplete_service.rb
+++ b/app/services/groups/autocomplete_service.rb
@@ -13,7 +13,8 @@ def issues(confidential_only: false, issue_types: nil)
       IssuesFinder.new(current_user, finder_params)
         .execute
         .preload(project: :namespace)
-        .select(:iid, :title, :project_id, :namespace_id)
+        .with_work_item_type
+        .select(:iid, :title, :project_id, :namespace_id, 'work_item_types.icon_name')
     end
     # rubocop: enable CodeReuse/ActiveRecord
 
diff --git a/app/services/projects/autocomplete_service.rb b/app/services/projects/autocomplete_service.rb
index 9213866e609a7..4ccdc2e7aa767 100644
--- a/app/services/projects/autocomplete_service.rb
+++ b/app/services/projects/autocomplete_service.rb
@@ -6,7 +6,10 @@ class AutocompleteService < BaseService
     include Routing::WikiHelper
 
     def issues
-      IssuesFinder.new(current_user, project_id: project.id, state: 'opened').execute.select([:iid, :title])
+      IssuesFinder.new(current_user, project_id: project.id, state: 'opened')
+        .execute
+        .with_work_item_type
+        .select([:iid, :title, 'work_item_types.icon_name'])
     end
 
     def milestones
diff --git a/spec/frontend/gfm_auto_complete_spec.js b/spec/frontend/gfm_auto_complete_spec.js
index 7cdeea5bd67e6..f6dfe86251baa 100644
--- a/spec/frontend/gfm_auto_complete_spec.js
+++ b/spec/frontend/gfm_auto_complete_spec.js
@@ -645,6 +645,17 @@ describe('GfmAutoComplete', () => {
       ).toBe('<li><small>grp/proj#5</small> Some Issue</li>');
     });
 
+    it('should include an svg image when iconName is provided', () => {
+      const expectedHtml = `<li><svg class="gl-text-secondary s16 gl-mr-2"><use xlink:href="/icons.svg#example-icon" /></svg><small>5</small> Some Issue</li>`;
+      expect(
+        GfmAutoComplete.Issues.templateFunction({
+          id: 5,
+          title: 'Some Issue',
+          iconName: 'example-icon',
+        }),
+      ).toBe(expectedHtml);
+    });
+
     it('escapes title in the template as it is user input', () => {
       expect(
         GfmAutoComplete.Issues.templateFunction({
diff --git a/spec/serializers/group_issuable_autocomplete_entity_spec.rb b/spec/serializers/group_issuable_autocomplete_entity_spec.rb
index 977239c67da35..ccdd59200ea69 100644
--- a/spec/serializers/group_issuable_autocomplete_entity_spec.rb
+++ b/spec/serializers/group_issuable_autocomplete_entity_spec.rb
@@ -12,7 +12,7 @@
     subject { described_class.new(issue, parent_group: group).as_json }
 
     it 'includes the iid, title, and reference' do
-      expect(subject).to include(:iid, :title, :reference)
+      expect(subject).to include(:iid, :title, :reference, :icon_name)
     end
   end
 end
diff --git a/spec/services/groups/autocomplete_service_spec.rb b/spec/services/groups/autocomplete_service_spec.rb
index 4fb14b525ac95..e623c5501448d 100644
--- a/spec/services/groups/autocomplete_service_spec.rb
+++ b/spec/services/groups/autocomplete_service_spec.rb
@@ -46,6 +46,7 @@ def expect_labels_to_equal(labels, expected_labels)
 
       expect(issues.map(&:iid)).to contain_exactly(project_issue.iid, sub_group_project_issue.iid)
       expect(issues.map(&:title)).to contain_exactly(project_issue.title, sub_group_project_issue.title)
+      expect(issues.map(&:icon_name)).to contain_exactly('issue-type-issue', 'issue-type-issue')
     end
 
     it 'returns only confidential issues if confidential_only is true' do
diff --git a/spec/services/projects/autocomplete_service_spec.rb b/spec/services/projects/autocomplete_service_spec.rb
index 48748fb7da9f4..d8e5c7dd753bc 100644
--- a/spec/services/projects/autocomplete_service_spec.rb
+++ b/spec/services/projects/autocomplete_service_spec.rb
@@ -18,6 +18,13 @@
       let!(:security_issue_1) { create(:issue, :confidential, project: project, title: 'Security issue 1', author: author) }
       let!(:security_issue_2) { create(:issue, :confidential, title: 'Security issue 2', project: project, assignees: [assignee]) }
 
+      it 'includes work item icons in list' do
+        autocomplete = described_class.new(project, nil)
+        issues = autocomplete.issues.map(&:icon_name)
+
+        expect(issues).to include 'issue-type-issue'
+      end
+
       it 'does not list project confidential issues for guests' do
         autocomplete = described_class.new(project, nil)
         issues = autocomplete.issues.map(&:iid)
-- 
GitLab