Commit 3d2dad44 authored by Phil Hughes's avatar Phil Hughes

Merge branch 'ce-backport-3772-total-weight' into 'master'

[Backport] View summed weights of issues in board column

See merge request gitlab-org/gitlab-ce!20860
parents 4f083434 5815c5b4
...@@ -2,6 +2,9 @@ ...@@ -2,6 +2,9 @@
import Sortable from 'sortablejs'; import Sortable from 'sortablejs';
import Vue from 'vue'; import Vue from 'vue';
import { n__ } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue';
import Tooltip from '~/vue_shared/directives/tooltip';
import AccessorUtilities from '../../lib/utils/accessor'; import AccessorUtilities from '../../lib/utils/accessor';
import boardList from './board_list.vue'; import boardList from './board_list.vue';
import BoardBlankState from './board_blank_state.vue'; import BoardBlankState from './board_blank_state.vue';
...@@ -17,6 +20,10 @@ gl.issueBoards.Board = Vue.extend({ ...@@ -17,6 +20,10 @@ gl.issueBoards.Board = Vue.extend({
boardList, boardList,
'board-delete': gl.issueBoards.BoardDelete, 'board-delete': gl.issueBoards.BoardDelete,
BoardBlankState, BoardBlankState,
Icon,
},
directives: {
Tooltip,
}, },
props: { props: {
list: { list: {
...@@ -46,6 +53,12 @@ gl.issueBoards.Board = Vue.extend({ ...@@ -46,6 +53,12 @@ gl.issueBoards.Board = Vue.extend({
filter: Store.filter, filter: Store.filter,
}; };
}, },
computed: {
counterTooltip() {
const { issuesSize } = this.list;
return `${n__('%d issue', '%d issues', issuesSize)}`;
},
},
watch: { watch: {
filter: { filter: {
handler() { handler() {
......
...@@ -136,6 +136,8 @@ class List { ...@@ -136,6 +136,8 @@ class List {
} }
this.createIssues(data.issues); this.createIssues(data.issues);
return data;
}); });
} }
......
...@@ -125,11 +125,17 @@ gl.issueBoards.BoardsStore = { ...@@ -125,11 +125,17 @@ gl.issueBoards.BoardsStore = {
} else if (listTo.type === 'backlog' && listFrom.type === 'assignee') { } else if (listTo.type === 'backlog' && listFrom.type === 'assignee') {
issue.removeAssignee(listFrom.assignee); issue.removeAssignee(listFrom.assignee);
listFrom.removeIssue(issue); listFrom.removeIssue(issue);
} else if ((listTo.type !== 'label' && listFrom.type === 'assignee') || } else if (this.shouldRemoveIssue(listFrom, listTo)) {
(listTo.type !== 'assignee' && listFrom.type === 'label')) {
listFrom.removeIssue(issue); listFrom.removeIssue(issue);
} }
}, },
shouldRemoveIssue(listFrom, listTo) {
return (
(listTo.type !== 'label' && listFrom.type === 'assignee') ||
(listTo.type !== 'assignee' && listFrom.type === 'label') ||
(listFrom.type === 'backlog')
);
},
moveIssueInList (list, issue, oldIndex, newIndex, idArray) { moveIssueInList (list, issue, oldIndex, newIndex, idArray) {
const beforeId = parseInt(idArray[newIndex - 1], 10) || null; const beforeId = parseInt(idArray[newIndex - 1], 10) || null;
const afterId = parseInt(idArray[newIndex + 1], 10) || null; const afterId = parseInt(idArray[newIndex + 1], 10) || null;
......
...@@ -444,3 +444,5 @@ textarea { ...@@ -444,3 +444,5 @@ textarea {
color: $placeholder-text-color; color: $placeholder-text-color;
} }
} }
.lh-100 { line-height: 1; }
...@@ -205,7 +205,7 @@ ...@@ -205,7 +205,7 @@
.board-title { .board-title {
margin: 0; margin: 0;
padding: 12px $gl-padding; padding: $gl-padding-8 $gl-padding;
font-size: 1em; font-size: 1em;
border-bottom: 1px solid $border-color; border-bottom: 1px solid $border-color;
display: flex; display: flex;
......
.issue-count-badge { .issue-count-badge {
display: inline-flex; display: inline-flex;
align-items: stretch;
height: 24px;
}
.issue-count-badge-count {
display: flex;
align-items: center;
padding-right: 10px;
padding-left: 10px;
border: 1px solid $border-color;
border-radius: $border-radius-base; border-radius: $border-radius-base;
line-height: 1; border: 1px solid $border-color;
padding: 5px $gl-padding-8;
&.has-btn {
border-right: 0;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
} }
.issue-count-badge-add-button { .issue-count-badge-count {
display: flex; display: inline-flex;
align-items: center; align-items: center;
border: 1px solid $border-color;
border-radius: 0 $border-radius-base $border-radius-base 0;
line-height: 1;
} }
...@@ -12,8 +12,9 @@ module Boards ...@@ -12,8 +12,9 @@ module Boards
skip_before_action :authenticate_user!, only: [:index] skip_before_action :authenticate_user!, only: [:index]
def index def index
issues = Boards::Issues::ListService.new(board_parent, current_user, filter_params).execute list_service = Boards::Issues::ListService.new(board_parent, current_user, filter_params)
issues = issues.page(params[:page]).per(params[:per] || 20) issues = list_service.execute
issues = issues.page(params[:page]).per(params[:per] || 20).without_count
make_sure_position_is_set(issues) if Gitlab::Database.read_write? make_sure_position_is_set(issues) if Gitlab::Database.read_write?
issues = issues.preload(:project, issues = issues.preload(:project,
:milestone, :milestone,
...@@ -22,10 +23,7 @@ module Boards ...@@ -22,10 +23,7 @@ module Boards
notes: [:award_emoji, :author] notes: [:award_emoji, :author]
) )
render json: { render_issues(issues, list_service.metadata)
issues: serialize_as_json(issues),
size: issues.total_count
}
end end
def create def create
...@@ -51,6 +49,13 @@ module Boards ...@@ -51,6 +49,13 @@ module Boards
private private
def render_issues(issues, metadata)
data = { issues: serialize_as_json(issues) }
data.merge!(metadata)
render json: data
end
def make_sure_position_is_set(issues) def make_sure_position_is_set(issues)
issues.each do |issue| issues.each do |issue|
issue.move_to_end && issue.save unless issue.relative_position issue.move_to_end && issue.save unless issue.relative_position
......
...@@ -3,14 +3,35 @@ ...@@ -3,14 +3,35 @@
module Boards module Boards
module Issues module Issues
class ListService < Boards::BaseService class ListService < Boards::BaseService
include Gitlab::Utils::StrongMemoize
def execute def execute
issues = IssuesFinder.new(current_user, filter_params).execute fetch_issues.order_by_position_and_priority
issues = filter(issues) end
issues.order_by_position_and_priority
def metadata
keys = metadata_fields.keys
columns = metadata_fields.values_at(*keys).join(', ')
results = Issue.where(id: fetch_issues.select('issues.id')).pluck(columns)
Hash[keys.zip(results.flatten)]
end end
private private
def metadata_fields
{ size: 'COUNT(*)' }
end
# We memoize the query here since the finder methods we use are quite complex. This does not memoize the result of the query.
def fetch_issues
strong_memoize(:fetch_issues) do
issues = IssuesFinder.new(current_user, filter_params).execute
filter(issues).reorder(nil)
end
end
def filter(issues) def filter(issues)
issues = without_board_labels(issues) unless list&.movable? || list&.closed? issues = without_board_labels(issues) unless list&.movable? || list&.closed?
issues = with_list_label(issues) if list&.label? issues = with_list_label(issues) if list&.label?
......
...@@ -32,17 +32,21 @@ ...@@ -32,17 +32,21 @@
"v-if" => "!list.preset && list.id" } "v-if" => "!list.preset && list.id" }
%button.board-delete.has-tooltip.float-right{ type: "button", title: _("Delete list"), "aria-label" => _("Delete list"), data: { placement: "bottom" }, "@click.stop" => "deleteBoard" } %button.board-delete.has-tooltip.float-right{ type: "button", title: _("Delete list"), "aria-label" => _("Delete list"), data: { placement: "bottom" }, "@click.stop" => "deleteBoard" }
= icon("trash") = icon("trash")
.issue-count-badge.clearfix{ "v-if" => 'list.type !== "blank" && list.type !== "promotion"' } .issue-count-badge.text-secondary{ "v-if" => 'list.type !== "blank" && list.type !== "promotion"', ":title": "counterTooltip", "v-tooltip": true, data: { placement: "top" } }
%span.issue-count-badge-count.float-left{ ":class" => '{ "has-btn": list.type !== "closed" && !disabled }' } %span.issue-count-badge-count
%icon.mr-1{ name: "issues" }
{{ list.issuesSize }} {{ list.issuesSize }}
- if can?(current_user, :admin_list, current_board_parent) = render_if_exists "shared/boards/components/list_weight"
%button.issue-count-badge-add-button.btn.btn-sm.btn-default.has-tooltip.js-no-trigger-collapse{ type: "button",
"@click" => "showNewIssueForm", - if can?(current_user, :admin_list, current_board_parent)
"v-if" => 'list.type !== "closed"', %button.issue-count-badge-add-button.btn.btn-sm.btn-default.ml-1.has-tooltip.js-no-trigger-collapse{ type: "button",
"aria-label" => _("New issue"), "@click" => "showNewIssueForm",
"title" => _("New issue"), "v-if" => 'list.type !== "closed"',
data: { placement: "top", container: "body" } } "aria-label" => _("New issue"),
= icon("plus", class: "js-no-trigger-collapse") "title" => _("New issue"),
data: { placement: "top", container: "body" } }
= icon("plus", class: "js-no-trigger-collapse")
%board-list{ "v-if" => 'list.type !== "blank" && list.type !== "promotion"', %board-list{ "v-if" => 'list.type !== "blank" && list.type !== "promotion"',
":list" => "list", ":list" => "list",
":issues" => "list.issues", ":issues" => "list.issues",
......
...@@ -42,7 +42,7 @@ describe Boards::IssuesController do ...@@ -42,7 +42,7 @@ describe Boards::IssuesController do
parsed_response = JSON.parse(response.body) parsed_response = JSON.parse(response.body)
expect(response).to match_response_schema('issues') expect(response).to match_response_schema('issues')
expect(parsed_response.length).to eq 2 expect(parsed_response['issues'].length).to eq 2
expect(development.issues.map(&:relative_position)).not_to include(nil) expect(development.issues.map(&:relative_position)).not_to include(nil)
end end
...@@ -80,7 +80,7 @@ describe Boards::IssuesController do ...@@ -80,7 +80,7 @@ describe Boards::IssuesController do
parsed_response = JSON.parse(response.body) parsed_response = JSON.parse(response.body)
expect(response).to match_response_schema('issues') expect(response).to match_response_schema('issues')
expect(parsed_response.length).to eq 2 expect(parsed_response['issues'].length).to eq 2
end end
end end
......
...@@ -161,6 +161,28 @@ describe('Store', () => { ...@@ -161,6 +161,28 @@ describe('Store', () => {
}, 0); }, 0);
}); });
it('moves an issue from backlog to a list', (done) => {
const backlog = gl.issueBoards.BoardsStore.addList({
...listObj,
list_type: 'backlog',
});
const listTwo = gl.issueBoards.BoardsStore.addList(listObjDuplicate);
expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(2);
setTimeout(() => {
expect(backlog.issues.length).toBe(1);
expect(listTwo.issues.length).toBe(1);
gl.issueBoards.BoardsStore.moveIssueToList(backlog, listTwo, backlog.findIssue(1));
expect(backlog.issues.length).toBe(0);
expect(listTwo.issues.length).toBe(1);
done();
}, 0);
});
it('moves issue to top of another list', (done) => { it('moves issue to top of another list', (done) => {
const listOne = gl.issueBoards.BoardsStore.addList(listObj); const listOne = gl.issueBoards.BoardsStore.addList(listObj);
const listTwo = gl.issueBoards.BoardsStore.addList(listObjDuplicate); const listTwo = gl.issueBoards.BoardsStore.addList(listObjDuplicate);
......
...@@ -7,6 +7,16 @@ shared_examples 'issues list service' do ...@@ -7,6 +7,16 @@ shared_examples 'issues list service' do
described_class.new(parent, user, params).execute described_class.new(parent, user, params).execute
end end
context '#metadata' do
it 'returns issues count for list' do
params = { board_id: board.id, id: list1.id }
metadata = described_class.new(parent, user, params).metadata
expect(metadata[:size]).to eq(3)
end
end
context 'issues are ordered by priority' do context 'issues are ordered by priority' do
it 'returns opened issues when list_id is missing' do it 'returns opened issues when list_id is missing' do
params = { board_id: board.id } params = { board_id: board.id }
......
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