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 @@
data () {
return {
scrollOffset: 250,
filters: Store.state.filters
filters: Store.state.filters,
showCount: false
};
},
watch: {
......@@ -30,6 +31,15 @@
this.$els.list.scrollTop = 0;
},
deep: true
},
issues () {
this.$nextTick(() => {
if (this.scrollHeight() > this.listHeight()) {
this.showCount = true;
} else {
this.showCount = false;
}
});
}
},
methods: {
......@@ -58,6 +68,7 @@
group: 'issues',
sort: false,
disabled: this.disabled,
filter: '.board-list-count',
onStart: (e) => {
const card = this.$refs.issue[e.oldIndex];
......
......@@ -11,6 +11,7 @@ class List {
this.loading = true;
this.loadingMore = false;
this.issues = [];
this.issuesSize = 0;
if (obj.label) {
this.label = new ListLabel(obj.label);
......@@ -51,7 +52,7 @@ class List {
}
nextPage () {
if (Math.floor(this.issues.length / 20) === this.page) {
if (this.issuesSize > this.issues.length) {
this.page++;
return this.getIssues(false);
......@@ -80,12 +81,13 @@ class List {
.then((resp) => {
const data = resp.json();
this.loading = false;
this.issuesSize = data.size;
if (emptyIssues) {
this.issues = [];
}
this.createIssues(data);
this.createIssues(data.issues);
});
}
......@@ -96,14 +98,20 @@ class List {
}
addIssue (issue, listFrom) {
this.issues.push(issue);
if (!this.findIssue(issue.id)) {
this.issues.push(issue);
if (this.label) {
issue.addLabel(this.label);
}
if (this.label) {
issue.addLabel(this.label);
}
if (listFrom) {
gl.boardService.moveIssue(issue.id, listFrom.id, this.id);
if (listFrom) {
this.issuesSize++;
gl.boardService.moveIssue(issue.id, listFrom.id, this.id)
.then(() => {
listFrom.getIssues(false);
});
}
}
}
......@@ -116,6 +124,7 @@ class List {
const matchesRemove = removeIssue.id === issue.id;
if (matchesRemove) {
this.issuesSize--;
issue.removeLabel(this.label);
}
......
......@@ -142,11 +142,6 @@
}
}
.board-header-loading-spinner {
margin-right: 10px;
color: $gray-darkest;
}
.board-inner-container {
border-bottom: 1px solid $border-color;
padding: $gl-padding;
......@@ -304,3 +299,22 @@
margin-right: 8px;
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
issues = ::Boards::Issues::ListService.new(project, current_user, filter_params).execute
issues = issues.page(params[:page])
render json: issues.as_json(
only: [:iid, :title, :confidential],
include: {
assignee: { only: [:id, :name, :username], methods: [:avatar_url] },
labels: { only: [:id, :title, :description, :color, :priority], methods: [:text_color] }
})
render json: {
issues: issues.as_json(
only: [:iid, :title, :confidential],
include: {
assignee: { only: [:id, :name, :username], methods: [:avatar_url] },
labels: { only: [:id, :title, :description, :color, :priority], methods: [:text_color] }
}),
size: issues.total_count
}
end
def update
......
......@@ -13,19 +13,13 @@
%h3.board-title.js-board-handle{ ":class" => "{ 'user-can-drag': (!disabled && !list.preset) }" }
{{ list.title }}
%span.pull-right{ "v-if" => "list.type !== 'blank'" }
{{ list.issues.length }}
{{ list.issuesSize }}
- if can?(current_user, :admin_list, @project)
%board-delete{ "inline-template" => true,
":list" => "list",
"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" }
= 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,
"v-if" => "list.type !== 'blank'",
":list" => "list",
......@@ -39,5 +33,11 @@
"v-show" => "!loading",
":data-board" => "list.id" }
= 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)
= render "projects/boards/components/blank_state"
......@@ -143,14 +143,21 @@ describe 'Issue Boards', feature: true, js: true do
wait_for_vue_resource
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_content('Showing 20 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.find('.board-header')).to have_content('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
......@@ -466,13 +473,19 @@ describe 'Issue Boards', feature: true, js: true do
wait_for_vue_resource
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_content('Showing 20 of 51 issues')
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_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
......
{
"type": "array",
"items": { "$ref": "issue.json" }
"type": "object",
"required" : [
"issues",
"size"
],
"properties" : {
"issues": {
"type": "array",
"items": { "$ref": "issue.json" }
},
"size": { "type": "integer" }
},
"additionalProperties": false
}
......@@ -26,12 +26,15 @@ const listObjDuplicate = {
const BoardsMockData = {
'GET': {
'/test/issue-boards/board/lists{/id}/issues': [{
title: 'Testing',
iid: 1,
confidential: false,
labels: []
}]
'/test/issue-boards/board/lists{/id}/issues': {
issues: [{
title: 'Testing',
iid: 1,
confidential: false,
labels: []
}],
size: 1
}
},
'POST': {
'/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