Commit d950556b authored by Fatih Acet's avatar Fatih Acet Committed by Ruben Davila

Merge branch 'issue-boards-issues-total-count' into 'master'

Add the total number of issues in the JSON response in issue board lists

Add the total number of issues in the JSON response in issue board lists

The issue board lists should always show the total number of issues in the list, not the current amount fetched by endless scroll.

Closes #21327

See merge request !5904
Conflicts:
	app/assets/stylesheets/pages/boards.scss
	app/views/projects/boards/components/_board.html.haml
parent d4aff2e7
...@@ -20,7 +20,8 @@ ...@@ -20,7 +20,8 @@
data () { data () {
return { return {
scrollOffset: 250, scrollOffset: 250,
filters: Store.state.filters filters: Store.state.filters,
showCount: false
}; };
}, },
watch: { watch: {
...@@ -30,6 +31,15 @@ ...@@ -30,6 +31,15 @@
this.$els.list.scrollTop = 0; this.$els.list.scrollTop = 0;
}, },
deep: true deep: true
},
issues () {
this.$nextTick(() => {
if (this.scrollHeight() > this.listHeight()) {
this.showCount = true;
} else {
this.showCount = false;
}
});
} }
}, },
methods: { methods: {
...@@ -58,6 +68,7 @@ ...@@ -58,6 +68,7 @@
group: 'issues', group: 'issues',
sort: false, sort: false,
disabled: this.disabled, disabled: this.disabled,
filter: '.board-list-count',
onStart: (e) => { onStart: (e) => {
const card = this.$refs.issue[e.oldIndex]; const card = this.$refs.issue[e.oldIndex];
......
...@@ -11,6 +11,7 @@ class List { ...@@ -11,6 +11,7 @@ class List {
this.loading = true; this.loading = true;
this.loadingMore = false; this.loadingMore = false;
this.issues = []; this.issues = [];
this.issuesSize = 0;
if (obj.label) { if (obj.label) {
this.label = new ListLabel(obj.label); this.label = new ListLabel(obj.label);
...@@ -51,7 +52,7 @@ class List { ...@@ -51,7 +52,7 @@ class List {
} }
nextPage () { nextPage () {
if (Math.floor(this.issues.length / 20) === this.page) { if (this.issuesSize > this.issues.length) {
this.page++; this.page++;
return this.getIssues(false); return this.getIssues(false);
...@@ -80,12 +81,13 @@ class List { ...@@ -80,12 +81,13 @@ class List {
.then((resp) => { .then((resp) => {
const data = resp.json(); const data = resp.json();
this.loading = false; this.loading = false;
this.issuesSize = data.size;
if (emptyIssues) { if (emptyIssues) {
this.issues = []; this.issues = [];
} }
this.createIssues(data); this.createIssues(data.issues);
}); });
} }
...@@ -96,6 +98,7 @@ class List { ...@@ -96,6 +98,7 @@ class List {
} }
addIssue (issue, listFrom) { addIssue (issue, listFrom) {
if (!this.findIssue(issue.id)) {
this.issues.push(issue); this.issues.push(issue);
if (this.label) { if (this.label) {
...@@ -103,7 +106,12 @@ class List { ...@@ -103,7 +106,12 @@ class List {
} }
if (listFrom) { if (listFrom) {
gl.boardService.moveIssue(issue.id, listFrom.id, this.id); this.issuesSize++;
gl.boardService.moveIssue(issue.id, listFrom.id, this.id)
.then(() => {
listFrom.getIssues(false);
});
}
} }
} }
...@@ -116,6 +124,7 @@ class List { ...@@ -116,6 +124,7 @@ class List {
const matchesRemove = removeIssue.id === issue.id; const matchesRemove = removeIssue.id === issue.id;
if (matchesRemove) { if (matchesRemove) {
this.issuesSize--;
issue.removeLabel(this.label); issue.removeLabel(this.label);
} }
......
...@@ -142,11 +142,6 @@ ...@@ -142,11 +142,6 @@
} }
} }
.board-header-loading-spinner {
margin-right: 10px;
color: $gray-darkest;
}
.board-inner-container { .board-inner-container {
border-bottom: 1px solid $border-color; border-bottom: 1px solid $border-color;
padding: $gl-padding; padding: $gl-padding;
...@@ -304,3 +299,22 @@ ...@@ -304,3 +299,22 @@
margin-right: 8px; margin-right: 8px;
font-weight: 500; font-weight: 500;
} }
.issue-boards-search {
width: 335px;
.form-control {
display: inline-block;
width: 210px;
}
}
.board-list-count {
padding: 10px 0;
color: $gl-placeholder-color;
font-size: 13px;
> .fa {
margin-right: 5px;
}
}
...@@ -8,12 +8,15 @@ module Projects ...@@ -8,12 +8,15 @@ module Projects
issues = ::Boards::Issues::ListService.new(project, current_user, filter_params).execute issues = ::Boards::Issues::ListService.new(project, current_user, filter_params).execute
issues = issues.page(params[:page]) issues = issues.page(params[:page])
render json: issues.as_json( render json: {
issues: issues.as_json(
only: [:iid, :title, :confidential], only: [:iid, :title, :confidential],
include: { include: {
assignee: { only: [:id, :name, :username], methods: [:avatar_url] }, assignee: { only: [:id, :name, :username], methods: [:avatar_url] },
labels: { only: [:id, :title, :description, :color, :priority], methods: [:text_color] } labels: { only: [:id, :title, :description, :color, :priority], methods: [:text_color] }
}) }),
size: issues.total_count
}
end end
def update def update
......
...@@ -13,19 +13,13 @@ ...@@ -13,19 +13,13 @@
%h3.board-title.js-board-handle{ ":class" => "{ 'user-can-drag': (!disabled && !list.preset) }" } %h3.board-title.js-board-handle{ ":class" => "{ 'user-can-drag': (!disabled && !list.preset) }" }
{{ list.title }} {{ list.title }}
%span.pull-right{ "v-if" => "list.type !== 'blank'" } %span.pull-right{ "v-if" => "list.type !== 'blank'" }
{{ list.issues.length }} {{ list.issuesSize }}
- if can?(current_user, :admin_list, @project) - if can?(current_user, :admin_list, @project)
%board-delete{ "inline-template" => true, %board-delete{ "inline-template" => true,
":list" => "list", ":list" => "list",
"v-if" => "!list.preset && list.id" } "v-if" => "!list.preset && list.id" }
%button.board-delete.has-tooltip.pull-right{ type: "button", title: "Delete list", "aria-label" => "Delete list", data: { placement: "bottom" }, "@click.stop" => "deleteBoard" } %button.board-delete.has-tooltip.pull-right{ type: "button", title: "Delete list", "aria-label" => "Delete list", data: { placement: "bottom" }, "@click.stop" => "deleteBoard" }
= icon("trash") = icon("trash")
= icon("spinner spin", class: "board-header-loading-spinner pull-right", "v-show" => "list.loadingMore")
.board-inner-container.board-search-container{ "v-if" => "list.canSearch()" }
%input.form-control{ type: "text", placeholder: "Search issues", "v-model" => "query", "debounce" => "250" }
= icon("search", class: "board-search-icon", "v-show" => "!query")
%button.board-search-clear-btn{ type: "button", role: "button", "aria-label" => "Clear search", "@click" => "query = ''", "v-show" => "query" }
= icon("times", class: "board-search-clear")
%board-list{ "inline-template" => true, %board-list{ "inline-template" => true,
"v-if" => "list.type !== 'blank'", "v-if" => "list.type !== 'blank'",
":list" => "list", ":list" => "list",
...@@ -39,5 +33,11 @@ ...@@ -39,5 +33,11 @@
"v-show" => "!loading", "v-show" => "!loading",
":data-board" => "list.id" } ":data-board" => "list.id" }
= render "projects/boards/components/card" = render "projects/boards/components/card"
%li.board-list-count.text-center{ "v-if" => "showCount" }
= icon("spinner spin", "v-show" => "list.loadingMore" )
%span{ "v-if" => "list.issues.length === list.issuesSize" }
Showing all issues
%span{ "v-else" => true }
Showing {{ list.issues.length }} of {{ list.issuesSize }} issues
- if can?(current_user, :admin_list, @project) - if can?(current_user, :admin_list, @project)
= render "projects/boards/components/blank_state" = render "projects/boards/components/blank_state"
...@@ -143,14 +143,21 @@ describe 'Issue Boards', feature: true, js: true do ...@@ -143,14 +143,21 @@ describe 'Issue Boards', feature: true, js: true do
wait_for_vue_resource wait_for_vue_resource
page.within(find('.board', match: :first)) do page.within(find('.board', match: :first)) do
expect(page.find('.board-header')).to have_content('20') expect(page.find('.board-header')).to have_content('56')
expect(page).to have_selector('.card', count: 20) expect(page).to have_selector('.card', count: 20)
expect(page).to have_content('Showing 20 of 56 issues')
evaluate_script("document.querySelectorAll('.board .board-list')[0].scrollTop = document.querySelectorAll('.board .board-list')[0].scrollHeight") evaluate_script("document.querySelectorAll('.board .board-list')[0].scrollTop = document.querySelectorAll('.board .board-list')[0].scrollHeight")
wait_for_vue_resource(spinner: false) wait_for_vue_resource(spinner: false)
expect(page.find('.board-header')).to have_content('40')
expect(page).to have_selector('.card', count: 40) expect(page).to have_selector('.card', count: 40)
expect(page).to have_content('Showing 40 of 56 issues')
evaluate_script("document.querySelectorAll('.board .board-list')[0].scrollTop = document.querySelectorAll('.board .board-list')[0].scrollHeight")
wait_for_vue_resource(spinner: false)
expect(page).to have_selector('.card', count: 56)
expect(page).to have_content('Showing all issues')
end end
end end
...@@ -466,13 +473,19 @@ describe 'Issue Boards', feature: true, js: true do ...@@ -466,13 +473,19 @@ describe 'Issue Boards', feature: true, js: true do
wait_for_vue_resource wait_for_vue_resource
page.within(find('.board', match: :first)) do page.within(find('.board', match: :first)) do
expect(page.find('.board-header')).to have_content('20') expect(page.find('.board-header')).to have_content('51')
expect(page).to have_selector('.card', count: 20) expect(page).to have_selector('.card', count: 20)
expect(page).to have_content('Showing 20 of 51 issues')
evaluate_script("document.querySelectorAll('.board .board-list')[0].scrollTop = document.querySelectorAll('.board .board-list')[0].scrollHeight") evaluate_script("document.querySelectorAll('.board .board-list')[0].scrollTop = document.querySelectorAll('.board .board-list')[0].scrollHeight")
expect(page.find('.board-header')).to have_content('40')
expect(page).to have_selector('.card', count: 40) expect(page).to have_selector('.card', count: 40)
expect(page).to have_content('Showing 40 of 51 issues')
evaluate_script("document.querySelectorAll('.board .board-list')[0].scrollTop = document.querySelectorAll('.board .board-list')[0].scrollHeight")
expect(page).to have_selector('.card', count: 51)
expect(page).to have_content('Showing all issues')
end end
end end
......
{ {
"type": "object",
"required" : [
"issues",
"size"
],
"properties" : {
"issues": {
"type": "array", "type": "array",
"items": { "$ref": "issue.json" } "items": { "$ref": "issue.json" }
},
"size": { "type": "integer" }
},
"additionalProperties": false
} }
...@@ -26,12 +26,15 @@ const listObjDuplicate = { ...@@ -26,12 +26,15 @@ const listObjDuplicate = {
const BoardsMockData = { const BoardsMockData = {
'GET': { 'GET': {
'/test/issue-boards/board/lists{/id}/issues': [{ '/test/issue-boards/board/lists{/id}/issues': {
issues: [{
title: 'Testing', title: 'Testing',
iid: 1, iid: 1,
confidential: false, confidential: false,
labels: [] labels: []
}] }],
size: 1
}
}, },
'POST': { 'POST': {
'/test/issue-boards/board/lists{/id}': listObj '/test/issue-boards/board/lists{/id}': listObj
......
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