Merge branch '43246-checkfilter' into 'master'

Resolve "Show a message when loading the issues / merge requests dashboard without filters"

Closes #43246

See merge request gitlab-org/gitlab-ce!17961
This commit is contained in:
Sean McGivern 2018-04-06 13:25:18 +00:00
commit e5d32c2c0c
21 changed files with 316 additions and 258 deletions

View File

@ -233,21 +233,21 @@ export default class SearchAutocomplete {
const issueItems = [ const issueItems = [
{ {
text: 'Issues assigned to me', text: 'Issues assigned to me',
url: `${issuesPath}/?assignee_username=${userName}`, url: `${issuesPath}/?assignee_id=${userId}`,
}, },
{ {
text: "Issues I've created", text: "Issues I've created",
url: `${issuesPath}/?author_username=${userName}`, url: `${issuesPath}/?author_id=${userId}`,
}, },
]; ];
const mergeRequestItems = [ const mergeRequestItems = [
{ {
text: 'Merge requests assigned to me', text: 'Merge requests assigned to me',
url: `${mrPath}/?assignee_username=${userName}`, url: `${mrPath}/?assignee_id=${userId}`,
}, },
{ {
text: "Merge requests I've created", text: "Merge requests I've created",
url: `${mrPath}/?author_username=${userName}`, url: `${mrPath}/?author_id=${userId}`,
}, },
]; ];

View File

@ -20,7 +20,7 @@
width: 100%; width: 100%;
} }
$image-widths: 80 250 306 394 430; $image-widths: 80 130 250 306 394 430;
@each $width in $image-widths { @each $width in $image-widths {
&.svg-#{$width} { &.svg-#{$width} {
img, img,

View File

@ -2,9 +2,17 @@ class DashboardController < Dashboard::ApplicationController
include IssuesAction include IssuesAction
include MergeRequestsAction include MergeRequestsAction
FILTER_PARAMS = [
:author_id,
:assignee_id,
:milestone_title,
:label_name
].freeze
before_action :event_filter, only: :activity before_action :event_filter, only: :activity
before_action :projects, only: [:issues, :merge_requests] before_action :projects, only: [:issues, :merge_requests]
before_action :set_show_full_reference, only: [:issues, :merge_requests] before_action :set_show_full_reference, only: [:issues, :merge_requests]
before_action :check_filters_presence!, only: [:issues, :merge_requests]
respond_to :html respond_to :html
@ -39,4 +47,15 @@ class DashboardController < Dashboard::ApplicationController
def set_show_full_reference def set_show_full_reference
@show_full_reference = true @show_full_reference = true
end end
def check_filters_presence!
@no_filters_set = FILTER_PARAMS.none? { |k| params.key?(k) }
return unless @no_filters_set
respond_to do |format|
format.html
format.atom { head :bad_request }
end
end
end end

View File

@ -228,9 +228,7 @@ module ApplicationHelper
scope: params[:scope], scope: params[:scope],
milestone_title: params[:milestone_title], milestone_title: params[:milestone_title],
assignee_id: params[:assignee_id], assignee_id: params[:assignee_id],
assignee_username: params[:assignee_username],
author_id: params[:author_id], author_id: params[:author_id],
author_username: params[:author_username],
search: params[:search], search: params[:search],
label_name: params[:label_name] label_name: params[:label_name]
} }

View File

@ -159,16 +159,18 @@ module IssuablesHelper
label_names.join(', ') label_names.join(', ')
end end
def issuables_state_counter_text(issuable_type, state) def issuables_state_counter_text(issuable_type, state, display_count)
titles = { titles = {
opened: "Open" opened: "Open"
} }
state_title = titles[state] || state.to_s.humanize state_title = titles[state] || state.to_s.humanize
count = issuables_count_for_state(issuable_type, state)
html = content_tag(:span, state_title) html = content_tag(:span, state_title)
html << " " << content_tag(:span, number_with_delimiter(count), class: 'badge')
if display_count
count = issuables_count_for_state(issuable_type, state)
html << " " << content_tag(:span, number_with_delimiter(count), class: 'badge')
end
html.html_safe html.html_safe
end end
@ -191,24 +193,10 @@ module IssuablesHelper
end end
end end
def issuable_filter_params
[
:search,
:author_id,
:assignee_id,
:milestone_title,
:label_name
]
end
def issuable_reference(issuable) def issuable_reference(issuable)
@show_full_reference ? issuable.to_reference(full: true) : issuable.to_reference(@group || @project) @show_full_reference ? issuable.to_reference(full: true) : issuable.to_reference(@group || @project)
end end
def issuable_filter_present?
issuable_filter_params.any? { |k| params.key?(k) }
end
def issuable_initial_data(issuable) def issuable_initial_data(issuable)
data = { data = {
endpoint: issuable_path(issuable), endpoint: issuable_path(issuable),

View File

@ -1,15 +1,19 @@
- @hide_top_links = true - @hide_top_links = true
- page_title "Issues" - page_title _("Issues")
- header_title "Issues", issues_dashboard_path(assignee_id: current_user.id) - @breadcrumb_link = issues_dashboard_path(assignee_id: current_user.id)
= content_for :meta_tags do = content_for :meta_tags do
= auto_discovery_link_tag(:atom, params.merge(rss_url_options), title: "#{current_user.name} issues") = auto_discovery_link_tag(:atom, params.merge(rss_url_options), title: "#{current_user.name} issues")
.top-area .top-area
= render 'shared/issuable/nav', type: :issues = render 'shared/issuable/nav', type: :issues, display_count: !@no_filters_set
.nav-controls .nav-controls
= link_to params.merge(rss_url_options), class: 'btn has-tooltip', data: { container: 'body' }, title: 'Subscribe' do = link_to params.merge(rss_url_options), class: 'btn has-tooltip', data: { container: 'body' }, title: 'Subscribe' do
= icon('rss') = icon('rss')
= render 'shared/new_project_item_select', path: 'issues/new', label: "New issue", with_feature_enabled: 'issues', type: :issues = render 'shared/new_project_item_select', path: 'issues/new', label: "New issue", with_feature_enabled: 'issues', type: :issues
= render 'shared/issuable/filter', type: :issues = render 'shared/issuable/filter', type: :issues
= render 'shared/issues'
- if current_user && @no_filters_set
= render 'shared/dashboard/no_filter_selected'
- else
= render 'shared/issues'

View File

@ -1,11 +1,15 @@
- @hide_top_links = true - @hide_top_links = true
- page_title "Merge Requests" - page_title _("Merge Requests")
- header_title "Merge Requests", merge_requests_dashboard_path(assignee_id: current_user.id) - @breadcrumb_link = merge_requests_dashboard_path(assignee_id: current_user.id)
.top-area .top-area
= render 'shared/issuable/nav', type: :merge_requests = render 'shared/issuable/nav', type: :merge_requests, display_count: !@no_filters_set
.nav-controls .nav-controls
= render 'shared/new_project_item_select', path: 'merge_requests/new', label: "New merge request", with_feature_enabled: 'merge_requests', type: :merge_requests = render 'shared/new_project_item_select', path: 'merge_requests/new', label: "New merge request", with_feature_enabled: 'merge_requests', type: :merge_requests
= render 'shared/issuable/filter', type: :merge_requests = render 'shared/issuable/filter', type: :merge_requests
= render 'shared/merge_requests'
- if current_user && @no_filters_set
= render 'shared/dashboard/no_filter_selected'
- else
= render 'shared/merge_requests'

View File

@ -0,0 +1,8 @@
.row.empty-state.text-center
.col-xs-12
.svg-130.prepend-top-default
= image_tag 'illustrations/issue-dashboard_results-without-filter.svg'
.col-xs-12
.text-content
%h4
= _("Please select at least one filter to see results")

View File

@ -24,12 +24,9 @@
.filter-item.inline.labels-filter .filter-item.inline.labels-filter
= render "shared/issuable/label_dropdown", selected: selected_labels, use_id: false, selected_toggle: params[:label_name], data_options: { field_name: "label_name[]" } = render "shared/issuable/label_dropdown", selected: selected_labels, use_id: false, selected_toggle: params[:label_name], data_options: { field_name: "label_name[]" }
- if issuable_filter_present? - unless @no_filters_set
.filter-item.inline.reset-filters .pull-right
%a{ href: page_filter_path(without: issuable_filter_params) } Reset filters = render 'shared/sort_dropdown'
.pull-right
= render 'shared/sort_dropdown'
- has_labels = @labels && @labels.any? - has_labels = @labels && @labels.any?
.row-content-block.second-block.filtered-labels{ class: ("hidden" unless has_labels) } .row-content-block.second-block.filtered-labels{ class: ("hidden" unless has_labels) }

View File

@ -1,22 +1,23 @@
- type = local_assigns.fetch(:type, :issues) - type = local_assigns.fetch(:type, :issues)
- page_context_word = type.to_s.humanize(capitalize: false) - page_context_word = type.to_s.humanize(capitalize: false)
- display_count = local_assigns.fetch(:display_count, :true)
%ul.nav-links.issues-state-filters.mobile-separator %ul.nav-links.issues-state-filters.mobile-separator
%li{ class: active_when(params[:state] == 'opened') }> %li{ class: active_when(params[:state] == 'opened') }>
= link_to page_filter_path(state: 'opened', label: true), id: 'state-opened', title: "Filter by #{page_context_word} that are currently opened.", data: { state: 'opened' } do = link_to page_filter_path(state: 'opened', label: true), id: 'state-opened', title: "Filter by #{page_context_word} that are currently opened.", data: { state: 'opened' } do
#{issuables_state_counter_text(type, :opened)} #{issuables_state_counter_text(type, :opened, display_count)}
- if type == :merge_requests - if type == :merge_requests
%li{ class: active_when(params[:state] == 'merged') }> %li{ class: active_when(params[:state] == 'merged') }>
= link_to page_filter_path(state: 'merged', label: true), id: 'state-merged', title: 'Filter by merge requests that are currently merged.', data: { state: 'merged' } do = link_to page_filter_path(state: 'merged', label: true), id: 'state-merged', title: 'Filter by merge requests that are currently merged.', data: { state: 'merged' } do
#{issuables_state_counter_text(type, :merged)} #{issuables_state_counter_text(type, :merged, display_count)}
%li{ class: active_when(params[:state] == 'closed') }> %li{ class: active_when(params[:state] == 'closed') }>
= link_to page_filter_path(state: 'closed', label: true), id: 'state-closed', title: 'Filter by merge requests that are currently closed and unmerged.', data: { state: 'closed' } do = link_to page_filter_path(state: 'closed', label: true), id: 'state-closed', title: 'Filter by merge requests that are currently closed and unmerged.', data: { state: 'closed' } do
#{issuables_state_counter_text(type, :closed)} #{issuables_state_counter_text(type, :closed, display_count)}
- else - else
%li{ class: active_when(params[:state] == 'closed') }> %li{ class: active_when(params[:state] == 'closed') }>
= link_to page_filter_path(state: 'closed', label: true), id: 'state-closed', title: 'Filter by issues that are currently closed.', data: { state: 'closed' } do = link_to page_filter_path(state: 'closed', label: true), id: 'state-closed', title: 'Filter by issues that are currently closed.', data: { state: 'closed' } do
#{issuables_state_counter_text(type, :closed)} #{issuables_state_counter_text(type, :closed, display_count)}
= render 'shared/issuable/nav_links/all', page_context_word: page_context_word, counter: issuables_state_counter_text(type, :all) = render 'shared/issuable/nav_links/all', page_context_word: page_context_word, counter: issuables_state_counter_text(type, :all, display_count)

View File

@ -0,0 +1,6 @@
---
title: Require at least one filter when listing issues or merge requests on dashboard
page
merge_request:
author:
type: performance

View File

@ -11,9 +11,11 @@ describe DashboardController do
describe 'GET issues' do describe 'GET issues' do
it_behaves_like 'issuables list meta-data', :issue, :issues it_behaves_like 'issuables list meta-data', :issue, :issues
it_behaves_like 'issuables requiring filter', :issues
end end
describe 'GET merge requests' do describe 'GET merge requests' do
it_behaves_like 'issuables list meta-data', :merge_request, :merge_requests it_behaves_like 'issuables list meta-data', :merge_request, :merge_requests
it_behaves_like 'issuables requiring filter', :merge_requests
end end
end end

View File

@ -13,17 +13,26 @@ describe "Dashboard Issues Feed" do
end end
describe "atom feed" do describe "atom feed" do
it "renders atom feed via personal access token" do it "returns 400 if no filter is used" do
personal_access_token = create(:personal_access_token, user: user) personal_access_token = create(:personal_access_token, user: user)
visit issues_dashboard_path(:atom, private_token: personal_access_token.token) visit issues_dashboard_path(:atom, private_token: personal_access_token.token)
expect(response_headers['Content-Type']).to have_content('application/atom+xml')
expect(page.status_code).to eq(400)
end
it "renders atom feed via personal access token" do
personal_access_token = create(:personal_access_token, user: user)
visit issues_dashboard_path(:atom, private_token: personal_access_token.token, assignee_id: user.id)
expect(response_headers['Content-Type']).to have_content('application/atom+xml') expect(response_headers['Content-Type']).to have_content('application/atom+xml')
expect(body).to have_selector('title', text: "#{user.name} issues") expect(body).to have_selector('title', text: "#{user.name} issues")
end end
it "renders atom feed via RSS token" do it "renders atom feed via RSS token" do
visit issues_dashboard_path(:atom, rss_token: user.rss_token) visit issues_dashboard_path(:atom, rss_token: user.rss_token, assignee_id: user.id)
expect(response_headers['Content-Type']).to have_content('application/atom+xml') expect(response_headers['Content-Type']).to have_content('application/atom+xml')
expect(body).to have_selector('title', text: "#{user.name} issues") expect(body).to have_selector('title', text: "#{user.name} issues")
@ -44,7 +53,7 @@ describe "Dashboard Issues Feed" do
let!(:issue2) { create(:issue, author: user, assignees: [assignee], project: project2, description: 'test desc') } let!(:issue2) { create(:issue, author: user, assignees: [assignee], project: project2, description: 'test desc') }
it "renders issue fields" do it "renders issue fields" do
visit issues_dashboard_path(:atom, rss_token: user.rss_token) visit issues_dashboard_path(:atom, rss_token: user.rss_token, assignee_id: assignee.id)
entry = find(:xpath, "//feed/entry[contains(summary/text(),'#{issue2.title}')]") entry = find(:xpath, "//feed/entry[contains(summary/text(),'#{issue2.title}')]")
@ -67,7 +76,7 @@ describe "Dashboard Issues Feed" do
end end
it "renders issue label and milestone info" do it "renders issue label and milestone info" do
visit issues_dashboard_path(:atom, rss_token: user.rss_token) visit issues_dashboard_path(:atom, rss_token: user.rss_token, assignee_id: assignee.id)
entry = find(:xpath, "//feed/entry[contains(summary/text(),'#{issue1.title}')]") entry = find(:xpath, "//feed/entry[contains(summary/text(),'#{issue1.title}')]")

View File

@ -17,6 +17,12 @@ feature 'Dashboard Issues filtering', :js do
visit_issues visit_issues
end end
context 'without any filter' do
it 'shows error message' do
expect(page).to have_content 'Please select at least one filter to see results'
end
end
context 'filtering by milestone' do context 'filtering by milestone' do
it 'shows all issues with no milestone' do it 'shows all issues with no milestone' do
show_milestone_dropdown show_milestone_dropdown
@ -27,15 +33,6 @@ feature 'Dashboard Issues filtering', :js do
expect(page).to have_selector('.issue', count: 1) expect(page).to have_selector('.issue', count: 1)
end end
it 'shows all issues with any milestone' do
show_milestone_dropdown
click_link 'Any Milestone'
expect(page).to have_issuable_counts(open: 2, closed: 0, all: 2)
expect(page).to have_selector('.issue', count: 2)
end
it 'shows all issues with the selected milestone' do it 'shows all issues with the selected milestone' do
show_milestone_dropdown show_milestone_dropdown
@ -68,13 +65,6 @@ feature 'Dashboard Issues filtering', :js do
let(:label) { create(:label, project: project) } let(:label) { create(:label, project: project) }
let!(:label_link) { create(:label_link, label: label, target: issue) } let!(:label_link) { create(:label_link, label: label, target: issue) }
it 'shows all issues without filter' do
page.within 'ul.content-list' do
expect(page).to have_content issue.title
expect(page).to have_content issue2.title
end
end
it 'shows all issues with the selected label' do it 'shows all issues with the selected label' do
page.within '.labels-filter' do page.within '.labels-filter' do
find('.dropdown').click find('.dropdown').click
@ -89,9 +79,13 @@ feature 'Dashboard Issues filtering', :js do
end end
context 'sorting' do context 'sorting' do
it 'shows sorted issues' do before do
visit_issues(assignee_id: user.id)
end
it 'remembers last sorting value' do
sort_by('Created date') sort_by('Created date')
visit_issues visit_issues(assignee_id: user.id)
expect(find('.issues-filters')).to have_content('Created date') expect(find('.issues-filters')).to have_content('Created date')
end end

View File

@ -51,15 +51,6 @@ RSpec.describe 'Dashboard Issues' do
expect(page).not_to have_content(other_issue.title) expect(page).not_to have_content(other_issue.title)
end end
it 'shows all issues' do
click_link('Reset filters')
expect(page).to have_content(authored_issue.title)
expect(page).to have_content(authored_issue_on_public_project.title)
expect(page).to have_content(assigned_issue.title)
expect(page).to have_content(other_issue.title)
end
it 'state filter tabs work' do it 'state filter tabs work' do
find('#state-closed').click find('#state-closed').click
expect(page).to have_current_path(issues_dashboard_url(assignee_id: current_user.id, state: 'closed'), url: true) expect(page).to have_current_path(issues_dashboard_url(assignee_id: current_user.id, state: 'closed'), url: true)

View File

@ -103,15 +103,11 @@ feature 'Dashboard Merge Requests' do
expect(page).not_to have_content(other_merge_request.title) expect(page).not_to have_content(other_merge_request.title)
end end
it 'shows all merge requests', :js do it 'shows error message without filter', :js do
filter_item_select('Any Assignee', '.js-assignee-search') filter_item_select('Any Assignee', '.js-assignee-search')
filter_item_select('Any Author', '.js-author-search') filter_item_select('Any Author', '.js-author-search')
expect(page).to have_content(authored_merge_request.title) expect(page).to have_content('Please select at least one filter to see results')
expect(page).to have_content(authored_merge_request_from_fork.title)
expect(page).to have_content(assigned_merge_request.title)
expect(page).to have_content(assigned_merge_request_from_fork.title)
expect(page).to have_content(other_merge_request.title)
end end
it 'shows sorted merge requests' do it 'shows sorted merge requests' do

View File

@ -9,49 +9,25 @@ describe 'User uses header search field' do
before do before do
project.add_reporter(user) project.add_reporter(user)
sign_in(user) sign_in(user)
visit(project_path(project))
end end
it 'starts searching by pressing the enter key', :js do context 'when user is in a global scope', :js do
fill_in('search', with: 'gitlab')
find('#search').native.send_keys(:enter)
page.within('.breadcrumbs-sub-title') do
expect(page).to have_content('Search')
end
end
it 'contains location badge' do
expect(page).to have_selector('.has-location-badge')
end
context 'when clicking the search field', :js do
before do before do
visit(root_path)
page.find('#search').click page.find('#search').click
end end
it 'shows category search dropdown' do
expect(page).to have_selector('.dropdown-header', text: /#{project.name}/i)
end
context 'when clicking issues' do context 'when clicking issues' do
let!(:issue) { create(:issue, project: project, author: user, assignees: [user]) }
it 'shows assigned issues' do it 'shows assigned issues' do
find('.dropdown-menu').click_link('Issues assigned to me') find('.search-input-container .dropdown-menu').click_link('Issues assigned to me')
expect(page).to have_selector('.filtered-search') expect(find('.js-assignee-search')).to have_content(user.name)
expect_tokens([assignee_token(user.name)])
expect_filtered_search_input_empty
end end
it 'shows created issues' do it 'shows created issues' do
find('.dropdown-menu').click_link("Issues I've created") find('.search-input-container .dropdown-menu').click_link("Issues I've created")
expect(page).to have_selector('.filtered-search') expect(find('.js-author-search')).to have_content(user.name)
expect_tokens([author_token(user.name)])
expect_filtered_search_input_empty
end end
end end
@ -59,32 +35,97 @@ describe 'User uses header search field' do
let!(:merge_request) { create(:merge_request, source_project: project, author: user, assignee: user) } let!(:merge_request) { create(:merge_request, source_project: project, author: user, assignee: user) }
it 'shows assigned merge requests' do it 'shows assigned merge requests' do
find('.dropdown-menu').click_link('Merge requests assigned to me') find('.search-input-container .dropdown-menu').click_link('Merge requests assigned to me')
expect(page).to have_selector('.merge-requests-holder') expect(find('.js-assignee-search')).to have_content(user.name)
expect_tokens([assignee_token(user.name)])
expect_filtered_search_input_empty
end end
it 'shows created merge requests' do it 'shows created merge requests' do
find('.dropdown-menu').click_link("Merge requests I've created") find('.search-input-container .dropdown-menu').click_link("Merge requests I've created")
expect(page).to have_selector('.merge-requests-holder') expect(find('.js-author-search')).to have_content(user.name)
expect_tokens([author_token(user.name)])
expect_filtered_search_input_empty
end end
end end
end end
context 'when entering text into the search field', :js do context 'when user is in a project scope' do
before do before do
page.within('.search-input-wrap') do visit(project_path(project))
fill_in('search', with: project.name[0..3]) end
it 'starts searching by pressing the enter key', :js do
fill_in('search', with: 'gitlab')
find('#search').native.send_keys(:enter)
page.within('.breadcrumbs-sub-title') do
expect(page).to have_content('Search')
end end
end end
it 'does not display the category search dropdown' do it 'contains location badge' do
expect(page).not_to have_selector('.dropdown-header', text: /#{project.name}/i) expect(page).to have_selector('.has-location-badge')
end
context 'when clicking the search field', :js do
before do
page.find('#search').click
end
it 'shows category search dropdown' do
expect(page).to have_selector('.dropdown-header', text: /#{project.name}/i)
end
context 'when clicking issues' do
let!(:issue) { create(:issue, project: project, author: user, assignees: [user]) }
it 'shows assigned issues' do
find('.dropdown-menu').click_link('Issues assigned to me')
expect(page).to have_selector('.filtered-search')
expect_tokens([assignee_token(user.name)])
expect_filtered_search_input_empty
end
it 'shows created issues' do
find('.dropdown-menu').click_link("Issues I've created")
expect(page).to have_selector('.filtered-search')
expect_tokens([author_token(user.name)])
expect_filtered_search_input_empty
end
end
context 'when clicking merge requests' do
let!(:merge_request) { create(:merge_request, source_project: project, author: user, assignee: user) }
it 'shows assigned merge requests' do
find('.dropdown-menu').click_link('Merge requests assigned to me')
expect(page).to have_selector('.merge-requests-holder')
expect_tokens([assignee_token(user.name)])
expect_filtered_search_input_empty
end
it 'shows created merge requests' do
find('.dropdown-menu').click_link("Merge requests I've created")
expect(page).to have_selector('.merge-requests-holder')
expect_tokens([author_token(user.name)])
expect_filtered_search_input_empty
end
end
end
context 'when entering text into the search field', :js do
before do
page.within('.search-input-wrap') do
fill_in('search', with: project.name[0..3])
end
end
it 'does not display the category search dropdown' do
expect(page).not_to have_selector('.dropdown-header', text: /#{project.name}/i)
end
end end
end end
end end

View File

@ -40,22 +40,22 @@ describe IssuablesHelper do
end end
it 'returns "Open" when state is :opened' do it 'returns "Open" when state is :opened' do
expect(helper.issuables_state_counter_text(:issues, :opened)) expect(helper.issuables_state_counter_text(:issues, :opened, true))
.to eq('<span>Open</span> <span class="badge">42</span>') .to eq('<span>Open</span> <span class="badge">42</span>')
end end
it 'returns "Closed" when state is :closed' do it 'returns "Closed" when state is :closed' do
expect(helper.issuables_state_counter_text(:issues, :closed)) expect(helper.issuables_state_counter_text(:issues, :closed, true))
.to eq('<span>Closed</span> <span class="badge">42</span>') .to eq('<span>Closed</span> <span class="badge">42</span>')
end end
it 'returns "Merged" when state is :merged' do it 'returns "Merged" when state is :merged' do
expect(helper.issuables_state_counter_text(:merge_requests, :merged)) expect(helper.issuables_state_counter_text(:merge_requests, :merged, true))
.to eq('<span>Merged</span> <span class="badge">42</span>') .to eq('<span>Merged</span> <span class="badge">42</span>')
end end
it 'returns "All" when state is :all' do it 'returns "All" when state is :all' do
expect(helper.issuables_state_counter_text(:merge_requests, :all)) expect(helper.issuables_state_counter_text(:merge_requests, :all, true))
.to eq('<span>All</span> <span class="badge">42</span>') .to eq('<span>All</span> <span class="badge">42</span>')
end end
end end
@ -101,27 +101,6 @@ describe IssuablesHelper do
end end
end end
describe '#issuable_filter_present?' do
it 'returns true when any key is present' do
allow(helper).to receive(:params).and_return(
ActionController::Parameters.new(milestone_title: 'Velit consectetur asperiores natus delectus.',
project_id: 'gitlabhq',
scope: 'all')
)
expect(helper.issuable_filter_present?).to be_truthy
end
it 'returns false when no key is present' do
allow(helper).to receive(:params).and_return(
ActionController::Parameters.new(project_id: 'gitlabhq',
scope: 'all')
)
expect(helper.issuable_filter_present?).to be_falsey
end
end
describe '#updated_at_by' do describe '#updated_at_by' do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:unedited_issuable) { create(:issue) } let(:unedited_issuable) { create(:issue) }

View File

@ -6,8 +6,21 @@ import SearchAutocomplete from '~/search_autocomplete';
import '~/lib/utils/common_utils'; import '~/lib/utils/common_utils';
import * as urlUtils from '~/lib/utils/url_utility'; import * as urlUtils from '~/lib/utils/url_utility';
(function() { describe('Search autocomplete dropdown', () => {
var assertLinks, dashboardIssuesPath, dashboardMRsPath, groupIssuesPath, groupMRsPath, groupName, mockDashboardOptions, mockGroupOptions, mockProjectOptions, projectIssuesPath, projectMRsPath, projectName, userId, widget; var assertLinks,
dashboardIssuesPath,
dashboardMRsPath,
groupIssuesPath,
groupMRsPath,
groupName,
mockDashboardOptions,
mockGroupOptions,
mockProjectOptions,
projectIssuesPath,
projectMRsPath,
projectName,
userId,
widget;
var userName = 'root'; var userName = 'root';
widget = null; widget = null;
@ -66,133 +79,126 @@ import * as urlUtils from '~/lib/utils/url_utility';
// Mock `gl` object in window for dashboard specific page. App code will need it. // Mock `gl` object in window for dashboard specific page. App code will need it.
mockDashboardOptions = function() { mockDashboardOptions = function() {
window.gl || (window.gl = {}); window.gl || (window.gl = {});
return window.gl.dashboardOptions = { return (window.gl.dashboardOptions = {
issuesPath: dashboardIssuesPath, issuesPath: dashboardIssuesPath,
mrPath: dashboardMRsPath mrPath: dashboardMRsPath,
}; });
}; };
// Mock `gl` object in window for project specific page. App code will need it. // Mock `gl` object in window for project specific page. App code will need it.
mockProjectOptions = function() { mockProjectOptions = function() {
window.gl || (window.gl = {}); window.gl || (window.gl = {});
return window.gl.projectOptions = { return (window.gl.projectOptions = {
'gitlab-ce': { 'gitlab-ce': {
issuesPath: projectIssuesPath, issuesPath: projectIssuesPath,
mrPath: projectMRsPath, mrPath: projectMRsPath,
projectName: projectName projectName: projectName,
} },
}; });
}; };
mockGroupOptions = function() { mockGroupOptions = function() {
window.gl || (window.gl = {}); window.gl || (window.gl = {});
return window.gl.groupOptions = { return (window.gl.groupOptions = {
'gitlab-org': { 'gitlab-org': {
issuesPath: groupIssuesPath, issuesPath: groupIssuesPath,
mrPath: groupMRsPath, mrPath: groupMRsPath,
projectName: groupName projectName: groupName,
} },
}; });
}; };
assertLinks = function(list, issuesPath, mrsPath) { assertLinks = function(list, issuesPath, mrsPath) {
var a1, a2, a3, a4, issuesAssignedToMeLink, issuesIHaveCreatedLink, mrsAssignedToMeLink, mrsIHaveCreatedLink;
if (issuesPath) { if (issuesPath) {
issuesAssignedToMeLink = issuesPath + "/?assignee_username=" + userName; const issuesAssignedToMeLink = `a[href="${issuesPath}/?assignee_id=${userId}"]`;
issuesIHaveCreatedLink = issuesPath + "/?author_username=" + userName; const issuesIHaveCreatedLink = `a[href="${issuesPath}/?author_id=${userId}"]`;
a1 = "a[href='" + issuesAssignedToMeLink + "']"; expect(list.find(issuesAssignedToMeLink).length).toBe(1);
a2 = "a[href='" + issuesIHaveCreatedLink + "']"; expect(list.find(issuesAssignedToMeLink).text()).toBe('Issues assigned to me');
expect(list.find(a1).length).toBe(1); expect(list.find(issuesIHaveCreatedLink).length).toBe(1);
expect(list.find(a1).text()).toBe('Issues assigned to me'); expect(list.find(issuesIHaveCreatedLink).text()).toBe("Issues I've created");
expect(list.find(a2).length).toBe(1);
expect(list.find(a2).text()).toBe("Issues I've created");
} }
mrsAssignedToMeLink = mrsPath + "/?assignee_username=" + userName; const mrsAssignedToMeLink = `a[href="${mrsPath}/?assignee_id=${userId}"]`;
mrsIHaveCreatedLink = mrsPath + "/?author_username=" + userName; const mrsIHaveCreatedLink = `a[href="${mrsPath}/?author_id=${userId}"]`;
a3 = "a[href='" + mrsAssignedToMeLink + "']"; expect(list.find(mrsAssignedToMeLink).length).toBe(1);
a4 = "a[href='" + mrsIHaveCreatedLink + "']"; expect(list.find(mrsAssignedToMeLink).text()).toBe('Merge requests assigned to me');
expect(list.find(a3).length).toBe(1); expect(list.find(mrsIHaveCreatedLink).length).toBe(1);
expect(list.find(a3).text()).toBe('Merge requests assigned to me'); expect(list.find(mrsIHaveCreatedLink).text()).toBe("Merge requests I've created");
expect(list.find(a4).length).toBe(1);
return expect(list.find(a4).text()).toBe("Merge requests I've created");
}; };
describe('Search autocomplete dropdown', function() { preloadFixtures('static/search_autocomplete.html.raw');
preloadFixtures('static/search_autocomplete.html.raw'); beforeEach(function() {
beforeEach(function() { loadFixtures('static/search_autocomplete.html.raw');
loadFixtures('static/search_autocomplete.html.raw');
// Prevent turbolinks from triggering within gl_dropdown // Prevent turbolinks from triggering within gl_dropdown
spyOn(urlUtils, 'visitUrl').and.returnValue(true); spyOn(urlUtils, 'visitUrl').and.returnValue(true);
window.gon = {}; window.gon = {};
window.gon.current_user_id = userId; window.gon.current_user_id = userId;
window.gon.current_username = userName; window.gon.current_username = userName;
return widget = new SearchAutocomplete(); return (widget = new SearchAutocomplete());
});
afterEach(function() {
// Undo what we did to the shared <body>
removeBodyAttributes();
window.gon = {};
});
it('should show Dashboard specific dropdown menu', function() {
var list;
addBodyAttributes();
mockDashboardOptions();
widget.searchInput.triggerHandler('focus');
list = widget.wrap.find('.dropdown-menu').find('ul');
return assertLinks(list, dashboardIssuesPath, dashboardMRsPath);
});
it('should show Group specific dropdown menu', function() {
var list;
addBodyAttributes('group');
mockGroupOptions();
widget.searchInput.triggerHandler('focus');
list = widget.wrap.find('.dropdown-menu').find('ul');
return assertLinks(list, groupIssuesPath, groupMRsPath);
});
it('should show Project specific dropdown menu', function() {
var list;
addBodyAttributes('project');
mockProjectOptions();
widget.searchInput.triggerHandler('focus');
list = widget.wrap.find('.dropdown-menu').find('ul');
return assertLinks(list, projectIssuesPath, projectMRsPath);
});
it('should show only Project mergeRequest dropdown menu items when project issues are disabled', function() {
addBodyAttributes('project');
disableProjectIssues();
mockProjectOptions();
widget.searchInput.triggerHandler('focus');
const list = widget.wrap.find('.dropdown-menu').find('ul');
assertLinks(list, null, projectMRsPath);
});
it('should not show category related menu if there is text in the input', function() {
var link, list;
addBodyAttributes('project');
mockProjectOptions();
widget.searchInput.val('help');
widget.searchInput.triggerHandler('focus');
list = widget.wrap.find('.dropdown-menu').find('ul');
link = "a[href='" + projectIssuesPath + "/?assignee_id=" + userId + "']";
return expect(list.find(link).length).toBe(0);
});
return it('should not submit the search form when selecting an autocomplete row with the keyboard', function() {
var ENTER = 13;
var DOWN = 40;
addBodyAttributes();
mockDashboardOptions(true);
var submitSpy = spyOnEvent('form', 'submit');
widget.searchInput.triggerHandler('focus');
widget.wrap.trigger($.Event('keydown', { which: DOWN }));
var enterKeyEvent = $.Event('keydown', { which: ENTER });
widget.searchInput.trigger(enterKeyEvent);
// This does not currently catch failing behavior. For security reasons,
// browsers will not trigger default behavior (form submit, in this
// example) on JavaScript-created keypresses.
expect(submitSpy).not.toHaveBeenTriggered();
});
}); });
}).call(window);
afterEach(function() {
// Undo what we did to the shared <body>
removeBodyAttributes();
window.gon = {};
});
it('should show Dashboard specific dropdown menu', function() {
var list;
addBodyAttributes();
mockDashboardOptions();
widget.searchInput.triggerHandler('focus');
list = widget.wrap.find('.dropdown-menu').find('ul');
return assertLinks(list, dashboardIssuesPath, dashboardMRsPath);
});
it('should show Group specific dropdown menu', function() {
var list;
addBodyAttributes('group');
mockGroupOptions();
widget.searchInput.triggerHandler('focus');
list = widget.wrap.find('.dropdown-menu').find('ul');
return assertLinks(list, groupIssuesPath, groupMRsPath);
});
it('should show Project specific dropdown menu', function() {
var list;
addBodyAttributes('project');
mockProjectOptions();
widget.searchInput.triggerHandler('focus');
list = widget.wrap.find('.dropdown-menu').find('ul');
return assertLinks(list, projectIssuesPath, projectMRsPath);
});
it('should show only Project mergeRequest dropdown menu items when project issues are disabled', function() {
addBodyAttributes('project');
disableProjectIssues();
mockProjectOptions();
widget.searchInput.triggerHandler('focus');
const list = widget.wrap.find('.dropdown-menu').find('ul');
assertLinks(list, null, projectMRsPath);
});
it('should not show category related menu if there is text in the input', function() {
var link, list;
addBodyAttributes('project');
mockProjectOptions();
widget.searchInput.val('help');
widget.searchInput.triggerHandler('focus');
list = widget.wrap.find('.dropdown-menu').find('ul');
link = "a[href='" + projectIssuesPath + '/?assignee_id=' + userId + "']";
return expect(list.find(link).length).toBe(0);
});
it('should not submit the search form when selecting an autocomplete row with the keyboard', function() {
var ENTER = 13;
var DOWN = 40;
addBodyAttributes();
mockDashboardOptions(true);
var submitSpy = spyOnEvent('form', 'submit');
widget.searchInput.triggerHandler('focus');
widget.wrap.trigger($.Event('keydown', { which: DOWN }));
var enterKeyEvent = $.Event('keydown', { which: ENTER });
widget.searchInput.trigger(enterKeyEvent);
// This does not currently catch failing behavior. For security reasons,
// browsers will not trigger default behavior (form submit, in this
// example) on JavaScript-created keypresses.
expect(submitSpy).not.toHaveBeenTriggered();
});
});

View File

@ -5,9 +5,9 @@ shared_examples 'issuables list meta-data' do |issuable_type, action = nil|
%w[fix improve/awesome].each do |source_branch| %w[fix improve/awesome].each do |source_branch|
issuable = issuable =
if issuable_type == :issue if issuable_type == :issue
create(issuable_type, project: project) create(issuable_type, project: project, author: project.creator)
else else
create(issuable_type, source_project: project, source_branch: source_branch) create(issuable_type, source_project: project, source_branch: source_branch, author: project.creator)
end end
@issuable_ids << issuable.id @issuable_ids << issuable.id
@ -16,7 +16,7 @@ shared_examples 'issuables list meta-data' do |issuable_type, action = nil|
it "creates indexed meta-data object for issuable notes and votes count" do it "creates indexed meta-data object for issuable notes and votes count" do
if action if action
get action get action, author_id: project.creator.id
else else
get :index, namespace_id: project.namespace, project_id: project get :index, namespace_id: project.namespace, project_id: project
end end
@ -35,7 +35,7 @@ shared_examples 'issuables list meta-data' do |issuable_type, action = nil|
it "doesn't execute any queries with false conditions" do it "doesn't execute any queries with false conditions" do
get_action = get_action =
if action if action
proc { get action } proc { get action, author_id: project.creator.id }
else else
proc { get :index, namespace_id: project2.namespace, project_id: project2 } proc { get :index, namespace_id: project2.namespace, project_id: project2 }
end end

View File

@ -0,0 +1,15 @@
shared_examples 'issuables requiring filter' do |action|
it "doesn't load any issuables if no filter is set" do
expect_any_instance_of(described_class).not_to receive(:issuables_collection)
get action
expect(response).to render_template(action)
end
it "loads issuables if at least one filter is set" do
expect_any_instance_of(described_class).to receive(:issuables_collection).and_call_original
get action, author_id: user.id
end
end