Merge branch '15356-filters-should-change-issue-counts' into 'master'
Take filters in account in issuable counters ## What does this MR do? This merge request ensure we display issuable counters that take in account all the selected filters, solving #15356. ## Are there points in the code the reviewer needs to double check? There was an issue (#22414) in the original implementation (!4960) when more than one label was selected because calling `#count` when the ActiveRecordRelation contains a `.group` returns an OrderedHash. This merge request relies on [how Kaminari handle this case](https://github.com/amatsuda/kaminari/blob/master/lib/kaminari/models/active_record_relation_methods.rb#L24-L30). A few things to note: - The `COUNT` query issued by Kaminari for the pagination is now cached because it's already run by `ApplicationHelper#state_filters_text_for`, so in the end we issue one less SQL query than before; - In the case when more than one label are selected, the `COUNT` queries return an OrderedHash in the form `{ ISSUABLE_ID => COUNT_OF_SELECTED_FILTERS }` on which `#count` is called: this drawback is already in place (for instance when loading https://gitlab.com/gitlab-org/gitlab-ce/issues?scope=all&state=all&utf8=%E2%9C%93&label_name%5B%5D=bug&label_name%5B%5D=regression) since that's how Kaminari solves this, **the difference is that now we do that two more times for the two states that are not currently selected**. I will let the ~Performance team decide if that's something acceptable or not, otherwise we will have to find another solution... - The queries that count the # of issuable are a bit more complex than before, from: ``` (0.6ms) SELECT COUNT(*) FROM "issues" WHERE "issues"."deleted_at" IS NULL AND "issues"."project_id" = $1 AND ("issues"."state" IN ('opened','reopened')) [["project_id", 2]] (0.2ms) SELECT COUNT(*) FROM "issues" WHERE "issues"."deleted_at" IS NULL AND "issues"."project_id" = $1 AND ("issues"."state" IN ('closed')) [["project_id", 2]] (0.2ms) SELECT COUNT(*) FROM "issues" WHERE "issues"."deleted_at" IS NULL AND "issues"."project_id" = $1 [["project_id", 2]] ``` to ``` (0.7ms) SELECT COUNT(*) AS count_all, "issues"."id" AS issues_id FROM "issues" INNER JOIN "label_links" ON "label_links"."target_id" = "issues"."id" AND "label_links"."target_type" = $1 INNER JOIN "labels" ON "labels"."id" = "label_links"."label_id" WHERE "issues"."deleted_at" IS NULL AND ("issues"."state" IN ('opened','reopened')) AND "issues"."project_id" = 2 AND "labels"."title" IN ('bug', 'discussion') AND "labels"."project_id" = 2 GROUP BY "issues"."id" HAVING COUNT(DISTINCT labels.title) = 2 [["target_type", "Issue"]] (0.5ms) SELECT COUNT(*) AS count_all, "issues"."id" AS issues_id FROM "issues" INNER JOIN "label_links" ON "label_links"."target_id" = "issues"."id" AND "label_links"."target_type" = $1 INNER JOIN "labels" ON "labels"."id" = "label_links"."label_id" WHERE "issues"."deleted_at" IS NULL AND ("issues"."state" IN ('closed')) AND "issues"."project_id" = 2 AND "labels"."title" IN ('bug', 'discussion') AND "labels"."project_id" = 2 GROUP BY "issues"."id" HAVING COUNT(DISTINCT labels.title) = 2 [["target_type", "Issue"]] (0.5ms) SELECT COUNT(*) AS count_all, "issues"."id" AS issues_id FROM "issues" INNER JOIN "label_links" ON "label_links"."target_id" = "issues"."id" AND "label_links"."target_type" = $1 INNER JOIN "labels" ON "labels"."id" = "label_links"."label_id" WHERE "issues"."deleted_at" IS NULL AND "issues"."project_id" = 2 AND "labels"."title" IN ('bug', 'discussion') AND "labels"."project_id" = 2 GROUP BY "issues"."id" HAVING COUNT(DISTINCT labels.title) = 2 [["target_type", "Issue"]] ``` - We could cache the counters for a few minutes? The key could be `PROJECT_ID-ISSUABLE_TYPE-PARAMS`. A few possible arguments in favor of "it's an acceptable solution": - most of the time people filter with a single label => no performance problem here - when filtering with more than one label, usually the result set is reduced, limiting the performance issues ## What are the relevant issue numbers? Closes #15356 See merge request !6496
This commit is contained in:
commit
6591b594d4
14 changed files with 237 additions and 159 deletions
|
@ -13,6 +13,7 @@ v 8.13.0 (unreleased)
|
|||
- Fix robots.txt disallowing access to groups starting with "s" (Matt Harrison)
|
||||
- Use a ConnectionPool for Rails.cache on Sidekiq servers
|
||||
- Only update issuable labels if they have been changed
|
||||
- Take filters in account in issuable counters. !6496
|
||||
- Revoke button in Applications Settings underlines on hover.
|
||||
- Fix Long commit messages overflow viewport in file tree
|
||||
- Revert avoid touching file system on Build#artifacts?
|
||||
|
|
|
@ -33,7 +33,7 @@ module Projects
|
|||
|
||||
def issue
|
||||
@issue ||=
|
||||
IssuesFinder.new(current_user, project_id: project.id, state: 'all')
|
||||
IssuesFinder.new(current_user, project_id: project.id)
|
||||
.execute
|
||||
.where(iid: params[:id])
|
||||
.first!
|
||||
|
|
|
@ -137,10 +137,10 @@ class ProjectsController < Projects::ApplicationController
|
|||
noteable =
|
||||
case params[:type]
|
||||
when 'Issue'
|
||||
IssuesFinder.new(current_user, project_id: @project.id, state: 'all').
|
||||
IssuesFinder.new(current_user, project_id: @project.id).
|
||||
execute.find_by(iid: params[:type_id])
|
||||
when 'MergeRequest'
|
||||
MergeRequestsFinder.new(current_user, project_id: @project.id, state: 'all').
|
||||
MergeRequestsFinder.new(current_user, project_id: @project.id).
|
||||
execute.find_by(iid: params[:type_id])
|
||||
when 'Commit'
|
||||
@project.commit(params[:type_id])
|
||||
|
|
|
@ -183,17 +183,12 @@ class IssuableFinder
|
|||
end
|
||||
|
||||
def by_state(items)
|
||||
case params[:state]
|
||||
when 'closed'
|
||||
items.closed
|
||||
when 'merged'
|
||||
items.respond_to?(:merged) ? items.merged : items.closed
|
||||
when 'all'
|
||||
items
|
||||
when 'opened'
|
||||
items.opened
|
||||
params[:state] ||= 'all'
|
||||
|
||||
if items.respond_to?(params[:state])
|
||||
items.public_send(params[:state])
|
||||
else
|
||||
raise 'You must specify default state'
|
||||
items
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -280,32 +280,6 @@ module ApplicationHelper
|
|||
end
|
||||
end
|
||||
|
||||
def state_filters_text_for(entity, project)
|
||||
titles = {
|
||||
opened: "Open"
|
||||
}
|
||||
|
||||
entity_title = titles[entity] || entity.to_s.humanize
|
||||
|
||||
count =
|
||||
if project.nil?
|
||||
nil
|
||||
elsif current_controller?(:issues)
|
||||
project.issues.visible_to_user(current_user).send(entity).count
|
||||
elsif current_controller?(:merge_requests)
|
||||
project.merge_requests.send(entity).count
|
||||
end
|
||||
|
||||
html = content_tag :span, entity_title
|
||||
|
||||
if count.present?
|
||||
html += " "
|
||||
html += content_tag :span, number_with_delimiter(count), class: 'badge'
|
||||
end
|
||||
|
||||
html.html_safe
|
||||
end
|
||||
|
||||
def truncate_first_line(message, length = 50)
|
||||
truncate(message.each_line.first.chomp, length: length) if message
|
||||
end
|
||||
|
|
|
@ -94,6 +94,24 @@ module IssuablesHelper
|
|||
label_names.join(', ')
|
||||
end
|
||||
|
||||
def issuables_state_counter_text(issuable_type, state)
|
||||
titles = {
|
||||
opened: "Open"
|
||||
}
|
||||
|
||||
state_title = titles[state] || state.to_s.humanize
|
||||
|
||||
count =
|
||||
Rails.cache.fetch(issuables_state_counter_cache_key(issuable_type, state), expires_in: 2.minutes) do
|
||||
issuables_count_for_state(issuable_type, state)
|
||||
end
|
||||
|
||||
html = content_tag(:span, state_title)
|
||||
html << " " << content_tag(:span, number_with_delimiter(count), class: 'badge')
|
||||
|
||||
html.html_safe
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def sidebar_gutter_collapsed?
|
||||
|
@ -111,4 +129,22 @@ module IssuablesHelper
|
|||
issuable.open? ? :opened : :closed
|
||||
end
|
||||
end
|
||||
|
||||
def issuables_count_for_state(issuable_type, state)
|
||||
issuables_finder = public_send("#{issuable_type}_finder")
|
||||
issuables_finder.params[:state] = state
|
||||
|
||||
issuables_finder.execute.page(1).total_count
|
||||
end
|
||||
|
||||
IRRELEVANT_PARAMS_FOR_CACHE_KEY = %i[utf8 sort page]
|
||||
private_constant :IRRELEVANT_PARAMS_FOR_CACHE_KEY
|
||||
|
||||
def issuables_state_counter_cache_key(issuable_type, state)
|
||||
opts = params.with_indifferent_access
|
||||
opts[:state] = state
|
||||
opts.except!(*IRRELEVANT_PARAMS_FOR_CACHE_KEY)
|
||||
|
||||
hexdigest(['issuables_count', issuable_type, opts.sort].flatten.join('-'))
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,25 +1,25 @@
|
|||
- type = local_assigns.fetch(:type, :issues)
|
||||
- page_context_word = type.to_s.humanize(capitalize: false)
|
||||
- issuables = @issues || @merge_requests
|
||||
|
||||
%ul.nav-links.issues-state-filters
|
||||
- if defined?(type) && type == :merge_requests
|
||||
- page_context_word = 'merge requests'
|
||||
- else
|
||||
- page_context_word = 'issues'
|
||||
%li{class: ("active" if params[:state] == 'opened')}
|
||||
= link_to page_filter_path(state: 'opened', label: true), title: "Filter by #{page_context_word} that are currently opened." do
|
||||
#{state_filters_text_for(:opened, @project)}
|
||||
#{issuables_state_counter_text(type, :opened)}
|
||||
|
||||
- if defined?(type) && type == :merge_requests
|
||||
- if type == :merge_requests
|
||||
%li{class: ("active" if params[:state] == 'merged')}
|
||||
= link_to page_filter_path(state: 'merged', label: true), title: 'Filter by merge requests that are currently merged.' do
|
||||
#{state_filters_text_for(:merged, @project)}
|
||||
#{issuables_state_counter_text(type, :merged)}
|
||||
|
||||
%li{class: ("active" if params[:state] == 'closed')}
|
||||
= link_to page_filter_path(state: 'closed', label: true), title: 'Filter by merge requests that are currently closed and unmerged.' do
|
||||
#{state_filters_text_for(:closed, @project)}
|
||||
#{issuables_state_counter_text(type, :closed)}
|
||||
- else
|
||||
%li{class: ("active" if params[:state] == 'closed')}
|
||||
= link_to page_filter_path(state: 'closed', label: true), title: 'Filter by issues that are currently closed.' do
|
||||
#{state_filters_text_for(:closed, @project)}
|
||||
#{issuables_state_counter_text(type, :closed)}
|
||||
|
||||
%li{class: ("active" if params[:state] == 'all')}
|
||||
= link_to page_filter_path(state: 'all', label: true), title: "Show all #{page_context_word}." do
|
||||
#{state_filters_text_for(:all, @project)}
|
||||
#{issuables_state_counter_text(type, :all)}
|
||||
|
|
|
@ -108,8 +108,7 @@ module API
|
|||
|
||||
finder_params = {
|
||||
project_id: user_project.id,
|
||||
milestone_title: @milestone.title,
|
||||
state: 'all'
|
||||
milestone_title: @milestone.title
|
||||
}
|
||||
|
||||
issues = IssuesFinder.new(current_user, finder_params).execute
|
||||
|
|
|
@ -21,6 +21,7 @@ describe "Dashboard Issues filtering", feature: true, js: true do
|
|||
|
||||
click_link 'No Milestone'
|
||||
|
||||
expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
|
||||
expect(page).to have_selector('.issue', count: 1)
|
||||
end
|
||||
|
||||
|
@ -29,6 +30,7 @@ describe "Dashboard Issues filtering", feature: true, js: true do
|
|||
|
||||
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
|
||||
|
||||
|
@ -39,6 +41,7 @@ describe "Dashboard Issues filtering", feature: true, js: true do
|
|||
click_link milestone.title
|
||||
end
|
||||
|
||||
expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
|
||||
expect(page).to have_selector('.issue', count: 1)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
require 'rails_helper'
|
||||
|
||||
feature 'Issue filtering by Labels', feature: true do
|
||||
feature 'Issue filtering by Labels', feature: true, js: true do
|
||||
include WaitForAjax
|
||||
|
||||
let(:project) { create(:project, :public) }
|
||||
let!(:user) { create(:user)}
|
||||
let!(:user) { create(:user) }
|
||||
let!(:label) { create(:label, project: project) }
|
||||
|
||||
before do
|
||||
|
@ -28,156 +28,81 @@ feature 'Issue filtering by Labels', feature: true do
|
|||
visit namespace_project_issues_path(project.namespace, project)
|
||||
end
|
||||
|
||||
context 'filter by label bug', js: true do
|
||||
context 'filter by label bug' do
|
||||
before do
|
||||
page.find('.js-label-select').click
|
||||
wait_for_ajax
|
||||
execute_script("$('.dropdown-menu-labels li:contains(\"bug\") a').click()")
|
||||
page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click
|
||||
wait_for_ajax
|
||||
select_labels('bug')
|
||||
end
|
||||
|
||||
it 'shows issue "Bugfix1" and "Bugfix2" in issues list' do
|
||||
it 'apply the filter' do
|
||||
expect(page).to have_content "Bugfix1"
|
||||
expect(page).to have_content "Bugfix2"
|
||||
end
|
||||
|
||||
it 'does not show "Feature1" in issues list' do
|
||||
expect(page).not_to have_content "Feature1"
|
||||
end
|
||||
|
||||
it 'shows label "bug" in filtered-labels' do
|
||||
expect(find('.filtered-labels')).to have_content "bug"
|
||||
end
|
||||
|
||||
it 'does not show label "feature" and "enhancement" in filtered-labels' do
|
||||
expect(find('.filtered-labels')).not_to have_content "feature"
|
||||
expect(find('.filtered-labels')).not_to have_content "enhancement"
|
||||
end
|
||||
|
||||
it 'removes label "bug"' do
|
||||
find('.js-label-filter-remove').click
|
||||
wait_for_ajax
|
||||
expect(find('.filtered-labels', visible: false)).to have_no_content "bug"
|
||||
end
|
||||
end
|
||||
|
||||
context 'filter by label feature', js: true do
|
||||
context 'filter by label feature' do
|
||||
before do
|
||||
page.find('.js-label-select').click
|
||||
wait_for_ajax
|
||||
execute_script("$('.dropdown-menu-labels li:contains(\"feature\") a').click()")
|
||||
page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click
|
||||
wait_for_ajax
|
||||
select_labels('feature')
|
||||
end
|
||||
|
||||
it 'shows issue "Feature1" in issues list' do
|
||||
it 'applies the filter' do
|
||||
expect(page).to have_content "Feature1"
|
||||
end
|
||||
|
||||
it 'does not show "Bugfix1" and "Bugfix2" in issues list' do
|
||||
expect(page).not_to have_content "Bugfix2"
|
||||
expect(page).not_to have_content "Bugfix1"
|
||||
end
|
||||
|
||||
it 'shows label "feature" in filtered-labels' do
|
||||
expect(find('.filtered-labels')).to have_content "feature"
|
||||
end
|
||||
|
||||
it 'does not show label "bug" and "enhancement" in filtered-labels' do
|
||||
expect(find('.filtered-labels')).not_to have_content "bug"
|
||||
expect(find('.filtered-labels')).not_to have_content "enhancement"
|
||||
end
|
||||
end
|
||||
|
||||
context 'filter by label enhancement', js: true do
|
||||
context 'filter by label enhancement' do
|
||||
before do
|
||||
page.find('.js-label-select').click
|
||||
wait_for_ajax
|
||||
execute_script("$('.dropdown-menu-labels li:contains(\"enhancement\") a').click()")
|
||||
page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click
|
||||
wait_for_ajax
|
||||
select_labels('enhancement')
|
||||
end
|
||||
|
||||
it 'shows issue "Bugfix2" in issues list' do
|
||||
it 'applies the filter' do
|
||||
expect(page).to have_content "Bugfix2"
|
||||
end
|
||||
|
||||
it 'does not show "Feature1" and "Bugfix1" in issues list' do
|
||||
expect(page).not_to have_content "Feature1"
|
||||
expect(page).not_to have_content "Bugfix1"
|
||||
end
|
||||
|
||||
it 'shows label "enhancement" in filtered-labels' do
|
||||
expect(find('.filtered-labels')).to have_content "enhancement"
|
||||
end
|
||||
|
||||
it 'does not show label "feature" and "bug" in filtered-labels' do
|
||||
expect(find('.filtered-labels')).not_to have_content "bug"
|
||||
expect(find('.filtered-labels')).not_to have_content "feature"
|
||||
end
|
||||
end
|
||||
|
||||
context 'filter by label enhancement or feature', js: true do
|
||||
context 'filter by label enhancement and bug in issues list' do
|
||||
before do
|
||||
page.find('.js-label-select').click
|
||||
wait_for_ajax
|
||||
execute_script("$('.dropdown-menu-labels li:contains(\"enhancement\") a').click()")
|
||||
execute_script("$('.dropdown-menu-labels li:contains(\"feature\") a').click()")
|
||||
page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click
|
||||
wait_for_ajax
|
||||
select_labels('bug', 'enhancement')
|
||||
end
|
||||
|
||||
it 'does not show "Bugfix1" or "Feature1" in issues list' do
|
||||
expect(page).not_to have_content "Bugfix1"
|
||||
expect(page).not_to have_content "Feature1"
|
||||
end
|
||||
|
||||
it 'shows label "enhancement" and "feature" in filtered-labels' do
|
||||
expect(find('.filtered-labels')).to have_content "enhancement"
|
||||
expect(find('.filtered-labels')).to have_content "feature"
|
||||
end
|
||||
|
||||
it 'does not show label "bug" in filtered-labels' do
|
||||
expect(find('.filtered-labels')).not_to have_content "bug"
|
||||
end
|
||||
|
||||
it 'removes label "enhancement"' do
|
||||
find('.js-label-filter-remove', match: :first).click
|
||||
wait_for_ajax
|
||||
expect(find('.filtered-labels')).to have_no_content "enhancement"
|
||||
end
|
||||
end
|
||||
|
||||
context 'filter by label enhancement and bug in issues list', js: true do
|
||||
before do
|
||||
page.find('.js-label-select').click
|
||||
wait_for_ajax
|
||||
execute_script("$('.dropdown-menu-labels li:contains(\"enhancement\") a').click()")
|
||||
execute_script("$('.dropdown-menu-labels li:contains(\"bug\") a').click()")
|
||||
page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click
|
||||
wait_for_ajax
|
||||
end
|
||||
|
||||
it 'shows issue "Bugfix2" in issues list' do
|
||||
it 'applies the filters' do
|
||||
expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
|
||||
expect(page).to have_content "Bugfix2"
|
||||
end
|
||||
|
||||
it 'does not show "Feature1"' do
|
||||
expect(page).not_to have_content "Feature1"
|
||||
end
|
||||
|
||||
it 'shows label "bug" and "enhancement" in filtered-labels' do
|
||||
expect(find('.filtered-labels')).to have_content "bug"
|
||||
expect(find('.filtered-labels')).to have_content "enhancement"
|
||||
end
|
||||
expect(find('.filtered-labels')).not_to have_content "feature"
|
||||
|
||||
it 'does not show label "feature" in filtered-labels' do
|
||||
find('.js-label-filter-remove', match: :first).click
|
||||
wait_for_ajax
|
||||
|
||||
expect(page).to have_content "Bugfix2"
|
||||
expect(page).not_to have_content "Feature1"
|
||||
expect(page).not_to have_content "Bugfix1"
|
||||
expect(find('.filtered-labels')).not_to have_content "bug"
|
||||
expect(find('.filtered-labels')).to have_content "enhancement"
|
||||
expect(find('.filtered-labels')).not_to have_content "feature"
|
||||
end
|
||||
end
|
||||
|
||||
context 'remove filtered labels', js: true do
|
||||
context 'remove filtered labels' do
|
||||
before do
|
||||
page.within '.labels-filter' do
|
||||
click_button 'Label'
|
||||
|
@ -200,7 +125,7 @@ feature 'Issue filtering by Labels', feature: true do
|
|||
end
|
||||
end
|
||||
|
||||
context 'dropdown filtering', js: true do
|
||||
context 'dropdown filtering' do
|
||||
it 'filters by label name' do
|
||||
page.within '.labels-filter' do
|
||||
click_button 'Label'
|
||||
|
@ -214,4 +139,14 @@ feature 'Issue filtering by Labels', feature: true do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
def select_labels(*labels)
|
||||
page.find('.js-label-select').click
|
||||
wait_for_ajax
|
||||
labels.each do |label|
|
||||
execute_script("$('.dropdown-menu-labels li:contains(\"#{label}\") a').click()")
|
||||
end
|
||||
page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click
|
||||
wait_for_ajax
|
||||
end
|
||||
end
|
||||
|
|
|
@ -7,15 +7,15 @@ describe 'Filter issues', feature: true do
|
|||
let!(:user) { create(:user)}
|
||||
let!(:milestone) { create(:milestone, project: project) }
|
||||
let!(:label) { create(:label, project: project) }
|
||||
let!(:issue1) { create(:issue, project: project) }
|
||||
let!(:wontfix) { create(:label, project: project, title: "Won't fix") }
|
||||
|
||||
before do
|
||||
project.team << [user, :master]
|
||||
login_as(user)
|
||||
create(:issue, project: project)
|
||||
end
|
||||
|
||||
describe 'Filter issues for assignee from issues#index' do
|
||||
describe 'for assignee from issues#index' do
|
||||
before do
|
||||
visit namespace_project_issues_path(project.namespace, project)
|
||||
|
||||
|
@ -45,7 +45,7 @@ describe 'Filter issues', feature: true do
|
|||
end
|
||||
end
|
||||
|
||||
describe 'Filter issues for milestone from issues#index' do
|
||||
describe 'for milestone from issues#index' do
|
||||
before do
|
||||
visit namespace_project_issues_path(project.namespace, project)
|
||||
|
||||
|
@ -75,7 +75,7 @@ describe 'Filter issues', feature: true do
|
|||
end
|
||||
end
|
||||
|
||||
describe 'Filter issues for label from issues#index', js: true do
|
||||
describe 'for label from issues#index', js: true do
|
||||
before do
|
||||
visit namespace_project_issues_path(project.namespace, project)
|
||||
find('.js-label-select').click
|
||||
|
@ -115,6 +115,7 @@ describe 'Filter issues', feature: true do
|
|||
expect(page).to have_content wontfix.title
|
||||
click_link wontfix.title
|
||||
end
|
||||
|
||||
expect(find('.js-label-select .dropdown-toggle-text')).to have_content(wontfix.title)
|
||||
end
|
||||
|
||||
|
@ -146,7 +147,7 @@ describe 'Filter issues', feature: true do
|
|||
end
|
||||
end
|
||||
|
||||
describe 'Filter issues for assignee and label from issues#index' do
|
||||
describe 'for assignee and label from issues#index' do
|
||||
before do
|
||||
visit namespace_project_issues_path(project.namespace, project)
|
||||
|
||||
|
@ -226,6 +227,7 @@ describe 'Filter issues', feature: true do
|
|||
it 'filters by text and label' do
|
||||
fill_in 'issuable_search', with: 'Bug'
|
||||
|
||||
expect(page).to have_issuable_counts(open: 2, closed: 0, all: 2)
|
||||
page.within '.issues-list' do
|
||||
expect(page).to have_selector('.issue', count: 2)
|
||||
end
|
||||
|
@ -236,6 +238,7 @@ describe 'Filter issues', feature: true do
|
|||
end
|
||||
find('.dropdown-menu-close-icon').click
|
||||
|
||||
expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
|
||||
page.within '.issues-list' do
|
||||
expect(page).to have_selector('.issue', count: 1)
|
||||
end
|
||||
|
@ -244,6 +247,7 @@ describe 'Filter issues', feature: true do
|
|||
it 'filters by text and milestone' do
|
||||
fill_in 'issuable_search', with: 'Bug'
|
||||
|
||||
expect(page).to have_issuable_counts(open: 2, closed: 0, all: 2)
|
||||
page.within '.issues-list' do
|
||||
expect(page).to have_selector('.issue', count: 2)
|
||||
end
|
||||
|
@ -253,6 +257,7 @@ describe 'Filter issues', feature: true do
|
|||
click_link '8'
|
||||
end
|
||||
|
||||
expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
|
||||
page.within '.issues-list' do
|
||||
expect(page).to have_selector('.issue', count: 1)
|
||||
end
|
||||
|
@ -261,6 +266,7 @@ describe 'Filter issues', feature: true do
|
|||
it 'filters by text and assignee' do
|
||||
fill_in 'issuable_search', with: 'Bug'
|
||||
|
||||
expect(page).to have_issuable_counts(open: 2, closed: 0, all: 2)
|
||||
page.within '.issues-list' do
|
||||
expect(page).to have_selector('.issue', count: 2)
|
||||
end
|
||||
|
@ -270,6 +276,7 @@ describe 'Filter issues', feature: true do
|
|||
click_link user.name
|
||||
end
|
||||
|
||||
expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
|
||||
page.within '.issues-list' do
|
||||
expect(page).to have_selector('.issue', count: 1)
|
||||
end
|
||||
|
@ -278,6 +285,7 @@ describe 'Filter issues', feature: true do
|
|||
it 'filters by text and author' do
|
||||
fill_in 'issuable_search', with: 'Bug'
|
||||
|
||||
expect(page).to have_issuable_counts(open: 2, closed: 0, all: 2)
|
||||
page.within '.issues-list' do
|
||||
expect(page).to have_selector('.issue', count: 2)
|
||||
end
|
||||
|
@ -287,6 +295,7 @@ describe 'Filter issues', feature: true do
|
|||
click_link user.name
|
||||
end
|
||||
|
||||
expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
|
||||
page.within '.issues-list' do
|
||||
expect(page).to have_selector('.issue', count: 1)
|
||||
end
|
||||
|
@ -315,6 +324,7 @@ describe 'Filter issues', feature: true do
|
|||
find('.dropdown-menu-close-icon').click
|
||||
wait_for_ajax
|
||||
|
||||
expect(page).to have_issuable_counts(open: 2, closed: 0, all: 2)
|
||||
page.within '.issues-list' do
|
||||
expect(page).to have_selector('.issue', count: 2)
|
||||
end
|
||||
|
|
|
@ -17,6 +17,7 @@ feature 'Merge Request filtering by Milestone', feature: true do
|
|||
visit_merge_requests(project)
|
||||
filter_by_milestone(Milestone::None.title)
|
||||
|
||||
expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
|
||||
expect(page).to have_css('.merge-request', count: 1)
|
||||
end
|
||||
|
||||
|
@ -39,6 +40,7 @@ feature 'Merge Request filtering by Milestone', feature: true do
|
|||
visit_merge_requests(project)
|
||||
filter_by_milestone(Milestone::Upcoming.title)
|
||||
|
||||
expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
|
||||
expect(page).to have_css('.merge-request', count: 1)
|
||||
end
|
||||
|
||||
|
@ -61,6 +63,7 @@ feature 'Merge Request filtering by Milestone', feature: true do
|
|||
visit_merge_requests(project)
|
||||
filter_by_milestone(milestone.title)
|
||||
|
||||
expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
|
||||
expect(page).to have_css('.merge-request', count: 1)
|
||||
end
|
||||
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe IssuablesHelper do
|
||||
describe IssuablesHelper do
|
||||
let(:label) { build_stubbed(:label) }
|
||||
let(:label2) { build_stubbed(:label) }
|
||||
|
||||
context 'label tooltip' do
|
||||
describe '#issuable_labels_tooltip' do
|
||||
it 'returns label text' do
|
||||
expect(issuable_labels_tooltip([label])).to eq(label.title)
|
||||
end
|
||||
|
@ -13,4 +13,105 @@ describe IssuablesHelper do
|
|||
expect(issuable_labels_tooltip([label, label2], limit: 1)).to eq("#{label.title}, and 1 more")
|
||||
end
|
||||
end
|
||||
|
||||
describe '#issuables_state_counter_text' do
|
||||
let(:user) { create(:user) }
|
||||
|
||||
describe 'state text' do
|
||||
before do
|
||||
allow(helper).to receive(:issuables_count_for_state).and_return(42)
|
||||
end
|
||||
|
||||
it 'returns "Open" when state is :opened' do
|
||||
expect(helper.issuables_state_counter_text(:issues, :opened)).
|
||||
to eq('<span>Open</span> <span class="badge">42</span>')
|
||||
end
|
||||
|
||||
it 'returns "Closed" when state is :closed' do
|
||||
expect(helper.issuables_state_counter_text(:issues, :closed)).
|
||||
to eq('<span>Closed</span> <span class="badge">42</span>')
|
||||
end
|
||||
|
||||
it 'returns "Merged" when state is :merged' do
|
||||
expect(helper.issuables_state_counter_text(:merge_requests, :merged)).
|
||||
to eq('<span>Merged</span> <span class="badge">42</span>')
|
||||
end
|
||||
|
||||
it 'returns "All" when state is :all' do
|
||||
expect(helper.issuables_state_counter_text(:merge_requests, :all)).
|
||||
to eq('<span>All</span> <span class="badge">42</span>')
|
||||
end
|
||||
end
|
||||
|
||||
describe 'counter caching based on issuable type and params', :caching do
|
||||
let(:params) do
|
||||
{
|
||||
scope: 'created-by-me',
|
||||
state: 'opened',
|
||||
utf8: '✓',
|
||||
author_id: '11',
|
||||
assignee_id: '18',
|
||||
label_name: ['bug', 'discussion', 'documentation'],
|
||||
milestone_title: 'v4.0',
|
||||
sort: 'due_date_asc',
|
||||
namespace_id: 'gitlab-org',
|
||||
project_id: 'gitlab-ce',
|
||||
page: 2
|
||||
}.with_indifferent_access
|
||||
end
|
||||
|
||||
it 'returns the cached value when called for the same issuable type & with the same params' do
|
||||
expect(helper).to receive(:params).twice.and_return(params)
|
||||
expect(helper).to receive(:issuables_count_for_state).with(:issues, :opened).and_return(42)
|
||||
|
||||
expect(helper.issuables_state_counter_text(:issues, :opened)).
|
||||
to eq('<span>Open</span> <span class="badge">42</span>')
|
||||
|
||||
expect(helper).not_to receive(:issuables_count_for_state)
|
||||
|
||||
expect(helper.issuables_state_counter_text(:issues, :opened)).
|
||||
to eq('<span>Open</span> <span class="badge">42</span>')
|
||||
end
|
||||
|
||||
it 'does not take some keys into account in the cache key' do
|
||||
expect(helper).to receive(:params).and_return({
|
||||
author_id: '11',
|
||||
state: 'foo',
|
||||
sort: 'foo',
|
||||
utf8: 'foo',
|
||||
page: 'foo'
|
||||
}.with_indifferent_access)
|
||||
expect(helper).to receive(:issuables_count_for_state).with(:issues, :opened).and_return(42)
|
||||
|
||||
expect(helper.issuables_state_counter_text(:issues, :opened)).
|
||||
to eq('<span>Open</span> <span class="badge">42</span>')
|
||||
|
||||
expect(helper).to receive(:params).and_return({
|
||||
author_id: '11',
|
||||
state: 'bar',
|
||||
sort: 'bar',
|
||||
utf8: 'bar',
|
||||
page: 'bar'
|
||||
}.with_indifferent_access)
|
||||
expect(helper).not_to receive(:issuables_count_for_state)
|
||||
|
||||
expect(helper.issuables_state_counter_text(:issues, :opened)).
|
||||
to eq('<span>Open</span> <span class="badge">42</span>')
|
||||
end
|
||||
|
||||
it 'does not take params order into account in the cache key' do
|
||||
expect(helper).to receive(:params).and_return('author_id' => '11', 'state' => 'opened')
|
||||
expect(helper).to receive(:issuables_count_for_state).with(:issues, :opened).and_return(42)
|
||||
|
||||
expect(helper.issuables_state_counter_text(:issues, :opened)).
|
||||
to eq('<span>Open</span> <span class="badge">42</span>')
|
||||
|
||||
expect(helper).to receive(:params).and_return('state' => 'opened', 'author_id' => '11')
|
||||
expect(helper).not_to receive(:issuables_count_for_state)
|
||||
|
||||
expect(helper.issuables_state_counter_text(:issues, :opened)).
|
||||
to eq('<span>Open</span> <span class="badge">42</span>')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
21
spec/support/matchers/have_issuable_counts.rb
Normal file
21
spec/support/matchers/have_issuable_counts.rb
Normal file
|
@ -0,0 +1,21 @@
|
|||
RSpec::Matchers.define :have_issuable_counts do |opts|
|
||||
match do |actual|
|
||||
expected_counts = opts.map do |state, count|
|
||||
"#{state.to_s.humanize} #{count}"
|
||||
end
|
||||
|
||||
actual.within '.issues-state-filters' do
|
||||
expected_counts.each do |expected_count|
|
||||
expect(actual).to have_content(expected_count)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
description do
|
||||
"displays the following issuable counts: #{expected_counts.inspect}"
|
||||
end
|
||||
|
||||
failure_message do
|
||||
"expected the following issuable counts: #{expected_counts.inspect} to be displayed"
|
||||
end
|
||||
end
|
Loading…
Reference in a new issue