diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js index b91bc16bed1ab0057fc1c74adef0c01cb3d3149d..8a5f9318b69db3c85e34122ffad1703ffa48c0ed 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 aff368a6fad80a744970910850dd8716a82a9075..e66e5f6acff86cdddd5e8929bd9254260de6bce2 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 f950a7db7855c3c38e53004dbb8643b007798a45..6c47f073eb1110c58217c2827297cb0b0b30963e 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 ad1cfc66d6d93aef4460b15e82663d12912ab6eb..d4392e3b8441eca9a4d9daef64493983a9710435 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 9213866e609a71f63c5ed8152701365a46a1ad89..4ccdc2e7aa76717009c35c55fe8ade60d221ff0d 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 7cdeea5bd67e63932c48d570432557f07ec63b0c..f6dfe86251baa88703ac5e786afd21b67762c380 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 977239c67da35e954630a8d0bf30dcf0ff4bfdc0..ccdd59200ea69e6c237cbbe9e301277ce52942a2 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 4fb14b525ac95dea4316c1d6ce81381ff469cd0b..e623c5501448d727a60ea810179a1c87853b59e2 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 48748fb7da9f4c2d5fd88404fa1b382b03411078..d8e5c7dd753bc281f5bad5cdbc1bfc23dcdb57aa 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)