Commit 0a7e2d80 authored by Natalia Tepluhina's avatar Natalia Tepluhina

Merge branch 'bw-issue-icons' into 'master'

Display issue type icon on issue creation form

See merge request gitlab-org/gitlab!67798
parents a263d43e 340515b7
<script> <script>
import { GlFormGroup, GlDropdown, GlDropdownItem } from '@gitlab/ui'; import { GlFormGroup, GlDropdown, GlDropdownItem, GlIcon } from '@gitlab/ui';
import { capitalize } from 'lodash'; import { capitalize } from 'lodash';
import { __ } from '~/locale'; import { __ } from '~/locale';
import { IssuableTypes } from '../../constants'; import { IssuableTypes } from '../../constants';
...@@ -15,6 +15,7 @@ export default { ...@@ -15,6 +15,7 @@ export default {
IssuableTypes, IssuableTypes,
components: { components: {
GlFormGroup, GlFormGroup,
GlIcon,
GlDropdown, GlDropdown,
GlDropdownItem, GlDropdownItem,
}, },
...@@ -72,6 +73,7 @@ export default { ...@@ -72,6 +73,7 @@ export default {
is-check-item is-check-item
@click="updateIssueType(type.value)" @click="updateIssueType(type.value)"
> >
<gl-icon :name="type.icon" />
{{ type.text }} {{ type.text }}
</gl-dropdown-item> </gl-dropdown-item>
</gl-dropdown> </gl-dropdown>
......
...@@ -28,8 +28,8 @@ export const STATUS_PAGE_PUBLISHED = __('Published on status page'); ...@@ -28,8 +28,8 @@ export const STATUS_PAGE_PUBLISHED = __('Published on status page');
export const JOIN_ZOOM_MEETING = __('Join Zoom meeting'); export const JOIN_ZOOM_MEETING = __('Join Zoom meeting');
export const IssuableTypes = [ export const IssuableTypes = [
{ value: 'issue', text: __('Issue') }, { value: 'issue', text: __('Issue'), icon: 'issue-type-issue' },
{ value: 'incident', text: __('Incident') }, { value: 'incident', text: __('Incident'), icon: 'issue-type-incident' },
]; ];
export const IssueTypePath = 'issues'; export const IssueTypePath = 'issues';
......
...@@ -48,6 +48,14 @@ module IssuesHelper ...@@ -48,6 +48,14 @@ module IssuesHelper
end end
end end
def work_item_type_icon(issue_type)
if WorkItem::Type.base_types.include?(issue_type)
"issue-type-#{issue_type.to_s.dasherize}"
else
'issue-type-issue'
end
end
def confidential_icon(issue) def confidential_icon(issue)
sprite_icon('eye-slash', css_class: 'gl-vertical-align-text-bottom') if issue.confidential? sprite_icon('eye-slash', css_class: 'gl-vertical-align-text-bottom') if issue.confidential?
end end
......
...@@ -16,14 +16,14 @@ ...@@ -16,14 +16,14 @@
= _("Select type") = _("Select type")
%button.dropdown-title-button.dropdown-menu-close.gl-ml-auto{ type: 'button', "aria-label" => _('Close') } %button.dropdown-title-button.dropdown-menu-close.gl-ml-auto{ type: 'button', "aria-label" => _('Close') }
= sprite_icon('close', size: 16, css_class: 'dropdown-menu-close-icon') = sprite_icon('close', size: 16, css_class: 'dropdown-menu-close-icon')
.dropdown-content .dropdown-content{ data: { testid: 'issue-type-select-dropdown' } }
%ul %ul
%li.js-filter-issuable-type %li.js-filter-issuable-type
= link_to new_project_issue_path(@project), class: ("is-active" if issuable.issue?) do = link_to new_project_issue_path(@project), class: ("is-active" if issuable.issue?) do
= _("Issue") #{sprite_icon(work_item_type_icon(:issue), css_class: 'gl-icon')} #{_("Issue")}
%li.js-filter-issuable-type{ data: { track: { event: "select_issue_type_incident", label: "select_issue_type_incident_dropdown_option" } } } %li.js-filter-issuable-type{ data: { track: { event: "select_issue_type_incident", label: "select_issue_type_incident_dropdown_option" } } }
= link_to new_project_issue_path(@project, { issuable_template: 'incident', issue: { issue_type: 'incident' } }), class: ("is-active" if issuable.incident?) do = link_to new_project_issue_path(@project, { issuable_template: 'incident', issue: { issue_type: 'incident' } }), class: ("is-active" if issuable.incident?) do
= _("Incident") #{sprite_icon(work_item_type_icon(:incident), css_class: 'gl-icon')} #{_("Incident")}
#js-type-popover #js-type-popover
......
...@@ -6,13 +6,13 @@ RSpec.describe 'New/edit issue', :js do ...@@ -6,13 +6,13 @@ RSpec.describe 'New/edit issue', :js do
include ActionView::Helpers::JavaScriptHelper include ActionView::Helpers::JavaScriptHelper
include FormHelper include FormHelper
let!(:project) { create(:project) } let_it_be(:project) { create(:project) }
let!(:user) { create(:user)} let_it_be(:user) { create(:user)}
let!(:user2) { create(:user)} let_it_be(:user2) { create(:user)}
let!(:milestone) { create(:milestone, project: project) } let_it_be(:milestone) { create(:milestone, project: project) }
let!(:label) { create(:label, project: project) } let_it_be(:label) { create(:label, project: project) }
let!(:label2) { create(:label, project: project) } let_it_be(:label2) { create(:label, project: project) }
let!(:issue) { create(:issue, project: project, assignees: [user], milestone: milestone) } let_it_be(:issue) { create(:issue, project: project, assignees: [user], milestone: milestone) }
before do before do
stub_licensed_features(multiple_issue_assignees: false, issue_weights: false) stub_licensed_features(multiple_issue_assignees: false, issue_weights: false)
...@@ -234,6 +234,28 @@ RSpec.describe 'New/edit issue', :js do ...@@ -234,6 +234,28 @@ RSpec.describe 'New/edit issue', :js do
expect(page).to have_selector('.atwho-view') expect(page).to have_selector('.atwho-view')
end end
describe 'displays issue type options in the dropdown' do
before do
page.within('.issue-form') do
click_button 'Issue'
end
end
it 'correctly displays the Issue type option with an icon', :aggregate_failures do
page.within('[data-testid="issue-type-select-dropdown"]') do
expect(page).to have_selector('[data-testid="issue-type-issue-icon"]')
expect(page).to have_content('Issue')
end
end
it 'correctly displays the Incident type option with an icon', :aggregate_failures do
page.within('[data-testid="issue-type-select-dropdown"]') do
expect(page).to have_selector('[data-testid="issue-type-incident-icon"]')
expect(page).to have_content('Incident')
end
end
end
describe 'milestone' do describe 'milestone' do
let!(:milestone) { create(:milestone, title: '">&lt;img src=x onerror=alert(document.domain)&gt;', project: project) } let!(:milestone) { create(:milestone, title: '">&lt;img src=x onerror=alert(document.domain)&gt;', project: project) }
......
import { GlFormGroup, GlDropdown, GlDropdownItem } from '@gitlab/ui'; import { GlFormGroup, GlDropdown, GlDropdownItem, GlIcon } from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils'; import { shallowMount, createLocalVue } from '@vue/test-utils';
import VueApollo from 'vue-apollo'; import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper'; import createMockApollo from 'helpers/mock_apollo_helper';
...@@ -35,6 +35,9 @@ describe('Issue type field component', () => { ...@@ -35,6 +35,9 @@ describe('Issue type field component', () => {
const findTypeFromGroup = () => wrapper.findComponent(GlFormGroup); const findTypeFromGroup = () => wrapper.findComponent(GlFormGroup);
const findTypeFromDropDown = () => wrapper.findComponent(GlDropdown); const findTypeFromDropDown = () => wrapper.findComponent(GlDropdown);
const findTypeFromDropDownItems = () => wrapper.findAllComponents(GlDropdownItem); const findTypeFromDropDownItems = () => wrapper.findAllComponents(GlDropdownItem);
const findTypeFromDropDownItemAt = (at) => findTypeFromDropDownItems().at(at);
const findTypeFromDropDownItemIconAt = (at) =>
findTypeFromDropDownItems().at(at).findComponent(GlIcon);
const createComponent = ({ data } = {}) => { const createComponent = ({ data } = {}) => {
fakeApollo = createMockApollo([], mockResolvers); fakeApollo = createMockApollo([], mockResolvers);
...@@ -60,6 +63,15 @@ describe('Issue type field component', () => { ...@@ -60,6 +63,15 @@ describe('Issue type field component', () => {
wrapper.destroy(); wrapper.destroy();
}); });
it.each`
at | text | icon
${0} | ${IssuableTypes[0].text} | ${IssuableTypes[0].icon}
${1} | ${IssuableTypes[1].text} | ${IssuableTypes[1].icon}
`(`renders the issue type $text with an icon in the dropdown`, ({ at, text, icon }) => {
expect(findTypeFromDropDownItemIconAt(at).attributes('name')).toBe(icon);
expect(findTypeFromDropDownItemAt(at).text()).toBe(text);
});
it('renders a form group with the correct label', () => { it('renders a form group with the correct label', () => {
expect(findTypeFromGroup().attributes('label')).toBe(i18n.label); expect(findTypeFromGroup().attributes('label')).toBe(i18n.label);
}); });
......
# frozen_string_literal: true # frozen_string_literal: true
require "spec_helper" require 'spec_helper'
RSpec.describe IssuesHelper do RSpec.describe IssuesHelper do
let(:project) { create(:project) } let(:project) { create(:project) }
let(:issue) { create :issue, project: project } let(:issue) { create :issue, project: project }
let(:ext_project) { create :redmine_project } let(:ext_project) { create :redmine_project }
describe '#work_item_type_icon' do
it 'returns icon of all standard base types' do
WorkItem::Type.base_types.each do |type|
expect(work_item_type_icon(type[0])).to eq "issue-type-#{type[0].to_s.dasherize}"
end
end
it 'defaults to issue icon if type is unknown' do
expect(work_item_type_icon('invalid')).to eq 'issue-type-issue'
end
end
describe '#award_user_list' do describe '#award_user_list' do
it "returns a comma-separated list of the first X users" do it 'returns a comma-separated list of the first X users' do
user = build_stubbed(:user, name: 'Joe') user = build_stubbed(:user, name: 'Joe')
awards = Array.new(3, build_stubbed(:award_emoji, user: user)) awards = Array.new(3, build_stubbed(:award_emoji, user: user))
...@@ -24,7 +36,7 @@ RSpec.describe IssuesHelper do ...@@ -24,7 +36,7 @@ RSpec.describe IssuesHelper do
expect(award_user_list([award], nil)).to eq 'Joe' expect(award_user_list([award], nil)).to eq 'Joe'
end end
it "truncates lists" do it 'truncates lists' do
user = build_stubbed(:user, name: 'Jane') user = build_stubbed(:user, name: 'Jane')
awards = Array.new(5, build_stubbed(:award_emoji, user: user)) awards = Array.new(5, build_stubbed(:award_emoji, user: user))
...@@ -32,14 +44,14 @@ RSpec.describe IssuesHelper do ...@@ -32,14 +44,14 @@ RSpec.describe IssuesHelper do
.to eq('Jane, Jane, Jane, and 2 more.') .to eq('Jane, Jane, Jane, and 2 more.')
end end
it "displays the current user in front of other users" do it 'displays the current user in front of other users' do
current_user = build_stubbed(:user) current_user = build_stubbed(:user)
my_award = build_stubbed(:award_emoji, user: current_user) my_award = build_stubbed(:award_emoji, user: current_user)
award = build_stubbed(:award_emoji, user: build_stubbed(:user, name: 'Jane')) award = build_stubbed(:award_emoji, user: build_stubbed(:user, name: 'Jane'))
awards = Array.new(5, award).push(my_award) awards = Array.new(5, award).push(my_award)
expect(award_user_list(awards, current_user, limit: 2)) expect(award_user_list(awards, current_user, limit: 2))
.to eq("You, Jane, and 4 more.") .to eq('You, Jane, and 4 more.')
end end
end end
...@@ -54,19 +66,19 @@ RSpec.describe IssuesHelper do ...@@ -54,19 +66,19 @@ RSpec.describe IssuesHelper do
end end
end end
it "returns disabled string for unauthenticated user" do it 'returns disabled string for unauthenticated user' do
expect(helper.award_state_class(awardable, AwardEmoji.all, nil)).to eq("disabled") expect(helper.award_state_class(awardable, AwardEmoji.all, nil)).to eq('disabled')
end end
it "returns disabled for a user that does not have access to the awardable" do it 'returns disabled for a user that does not have access to the awardable' do
expect(helper.award_state_class(awardable, AwardEmoji.all, build(:user))).to eq("disabled") expect(helper.award_state_class(awardable, AwardEmoji.all, build(:user))).to eq('disabled')
end end
it "returns active string for author" do it 'returns active string for author' do
expect(helper.award_state_class(awardable, AwardEmoji.all, upvote.user)).to eq("active") expect(helper.award_state_class(awardable, AwardEmoji.all, upvote.user)).to eq('active')
end end
it "is blank for a user that has access to the awardable" do it 'is blank for a user that has access to the awardable' do
user = build(:user) user = build(:user)
expect(helper).to receive(:can?).with(user, :award_emoji, awardable).and_return(true) expect(helper).to receive(:can?).with(user, :award_emoji, awardable).and_return(true)
...@@ -74,40 +86,40 @@ RSpec.describe IssuesHelper do ...@@ -74,40 +86,40 @@ RSpec.describe IssuesHelper do
end end
end end
describe "awards_sort" do describe 'awards_sort' do
it "sorts a hash so thumbsup and thumbsdown are always on top" do it 'sorts a hash so thumbsup and thumbsdown are always on top' do
data = { "thumbsdown" => "some value", "lifter" => "some value", "thumbsup" => "some value" } data = { 'thumbsdown' => 'some value', 'lifter' => 'some value', 'thumbsup' => 'some value' }
expect(awards_sort(data).keys).to eq(%w(thumbsup thumbsdown lifter)) expect(awards_sort(data).keys).to eq(%w(thumbsup thumbsdown lifter))
end end
end end
describe "#link_to_discussions_to_resolve" do describe '#link_to_discussions_to_resolve' do
describe "passing only a merge request" do describe 'passing only a merge request' do
let(:merge_request) { create(:merge_request) } let(:merge_request) { create(:merge_request) }
it "links just the merge request" do it 'links just the merge request' do
expected_path = project_merge_request_path(merge_request.project, merge_request) expected_path = project_merge_request_path(merge_request.project, merge_request)
expect(link_to_discussions_to_resolve(merge_request, nil)).to include(expected_path) expect(link_to_discussions_to_resolve(merge_request, nil)).to include(expected_path)
end end
it "contains the reference to the merge request" do it 'contains the reference to the merge request' do
expect(link_to_discussions_to_resolve(merge_request, nil)).to include(merge_request.to_reference) expect(link_to_discussions_to_resolve(merge_request, nil)).to include(merge_request.to_reference)
end end
end end
describe "when passing a discussion" do describe 'when passing a discussion' do
let(:diff_note) { create(:diff_note_on_merge_request) } let(:diff_note) { create(:diff_note_on_merge_request) }
let(:merge_request) { diff_note.noteable } let(:merge_request) { diff_note.noteable }
let(:discussion) { diff_note.to_discussion } let(:discussion) { diff_note.to_discussion }
it "links to the merge request with first note if a single discussion was passed" do it 'links to the merge request with first note if a single discussion was passed' do
expected_path = Gitlab::UrlBuilder.build(diff_note) expected_path = Gitlab::UrlBuilder.build(diff_note)
expect(link_to_discussions_to_resolve(merge_request, discussion)).to include(expected_path) expect(link_to_discussions_to_resolve(merge_request, discussion)).to include(expected_path)
end end
it "contains both the reference to the merge request and a mention of the discussion" do it 'contains both the reference to the merge request and a mention of the discussion' do
expect(link_to_discussions_to_resolve(merge_request, discussion)).to include("#{merge_request.to_reference} (discussion #{diff_note.id})") expect(link_to_discussions_to_resolve(merge_request, discussion)).to include("#{merge_request.to_reference} (discussion #{diff_note.id})")
end end
end end
...@@ -235,13 +247,13 @@ RSpec.describe IssuesHelper do ...@@ -235,13 +247,13 @@ RSpec.describe IssuesHelper do
end end
describe '#use_startup_call' do describe '#use_startup_call' do
it "returns false when a query param is present" do it 'returns false when a query param is present' do
allow(controller.request).to receive(:query_parameters).and_return({ foo: 'bar' }) allow(controller.request).to receive(:query_parameters).and_return({ foo: 'bar' })
expect(helper.use_startup_call?).to eq(false) expect(helper.use_startup_call?).to eq(false)
end end
it "returns false when user has stored sort preference" do it 'returns false when user has stored sort preference' do
controller.instance_variable_set(:@sort, 'updated_asc') controller.instance_variable_set(:@sort, 'updated_asc')
expect(helper.use_startup_call?).to eq(false) expect(helper.use_startup_call?).to eq(false)
...@@ -265,13 +277,13 @@ RSpec.describe IssuesHelper do ...@@ -265,13 +277,13 @@ RSpec.describe IssuesHelper do
it 'returns expected result' do it 'returns expected result' do
expected = { expected = {
can_create_issue: "true", can_create_issue: 'true',
can_reopen_issue: "true", can_reopen_issue: 'true',
can_report_spam: "false", can_report_spam: 'false',
can_update_issue: "true", can_update_issue: 'true',
iid: issue.iid, iid: issue.iid,
is_issue_author: "false", is_issue_author: 'false',
issue_type: "issue", issue_type: 'issue',
new_issue_path: new_project_issue_path(project), new_issue_path: new_project_issue_path(project),
project_path: project.full_path, project_path: project.full_path,
report_abuse_path: new_abuse_report_path(user_id: issue.author.id, ref_url: issue_url(issue)), report_abuse_path: new_abuse_report_path(user_id: issue.author.id, ref_url: issue_url(issue)),
...@@ -345,7 +357,7 @@ RSpec.describe IssuesHelper do ...@@ -345,7 +357,7 @@ RSpec.describe IssuesHelper do
end end
it 'returns manual ordering class' do it 'returns manual ordering class' do
expect(helper.issue_manual_ordering_class).to eq("manual-ordering") expect(helper.issue_manual_ordering_class).to eq('manual-ordering')
end end
context 'when manual sorting disabled' do context 'when manual sorting disabled' do
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment