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 @@
import Sortable from 'sortablejs';
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 boardList from './board_list.vue';
import BoardBlankState from './board_blank_state.vue';
......@@ -17,6 +20,10 @@ gl.issueBoards.Board = Vue.extend({
boardList,
'board-delete': gl.issueBoards.BoardDelete,
BoardBlankState,
Icon,
},
directives: {
Tooltip,
},
props: {
list: {
......@@ -46,6 +53,12 @@ gl.issueBoards.Board = Vue.extend({
filter: Store.filter,
};
},
computed: {
counterTooltip() {
const { issuesSize } = this.list;
return `${n__('%d issue', '%d issues', issuesSize)}`;
},
},
watch: {
filter: {
handler() {
......
......@@ -136,6 +136,8 @@ class List {
}
this.createIssues(data.issues);
return data;
});
}
......
......@@ -125,11 +125,17 @@ gl.issueBoards.BoardsStore = {
} else if (listTo.type === 'backlog' && listFrom.type === 'assignee') {
issue.removeAssignee(listFrom.assignee);
listFrom.removeIssue(issue);
} else if ((listTo.type !== 'label' && listFrom.type === 'assignee') ||
(listTo.type !== 'assignee' && listFrom.type === 'label')) {
} else if (this.shouldRemoveIssue(listFrom, listTo)) {
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) {
const beforeId = parseInt(idArray[newIndex - 1], 10) || null;
const afterId = parseInt(idArray[newIndex + 1], 10) || null;
......
......@@ -444,3 +444,5 @@ textarea {
color: $placeholder-text-color;
}
}
.lh-100 { line-height: 1; }
......@@ -205,7 +205,7 @@
.board-title {
margin: 0;
padding: 12px $gl-padding;
padding: $gl-padding-8 $gl-padding;
font-size: 1em;
border-bottom: 1px solid $border-color;
display: flex;
......
.issue-count-badge {
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;
line-height: 1;
&.has-btn {
border-right: 0;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
border: 1px solid $border-color;
padding: 5px $gl-padding-8;
}
.issue-count-badge-add-button {
display: flex;
.issue-count-badge-count {
display: inline-flex;
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
skip_before_action :authenticate_user!, only: [:index]
def index
issues = Boards::Issues::ListService.new(board_parent, current_user, filter_params).execute
issues = issues.page(params[:page]).per(params[:per] || 20)
list_service = Boards::Issues::ListService.new(board_parent, current_user, filter_params)
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?
issues = issues.preload(:project,
:milestone,
......@@ -22,10 +23,7 @@ module Boards
notes: [:award_emoji, :author]
)
render json: {
issues: serialize_as_json(issues),
size: issues.total_count
}
render_issues(issues, list_service.metadata)
end
def create
......@@ -51,6 +49,13 @@ module Boards
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)
issues.each do |issue|
issue.move_to_end && issue.save unless issue.relative_position
......
......@@ -3,14 +3,35 @@
module Boards
module Issues
class ListService < Boards::BaseService
include Gitlab::Utils::StrongMemoize
def execute
issues = IssuesFinder.new(current_user, filter_params).execute
issues = filter(issues)
issues.order_by_position_and_priority
fetch_issues.order_by_position_and_priority
end
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
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)
issues = without_board_labels(issues) unless list&.movable? || list&.closed?
issues = with_list_label(issues) if list&.label?
......
......@@ -32,17 +32,21 @@
"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" }
= icon("trash")
.issue-count-badge.clearfix{ "v-if" => 'list.type !== "blank" && list.type !== "promotion"' }
%span.issue-count-badge-count.float-left{ ":class" => '{ "has-btn": list.type !== "closed" && !disabled }' }
.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
%icon.mr-1{ name: "issues" }
{{ list.issuesSize }}
- if can?(current_user, :admin_list, current_board_parent)
%button.issue-count-badge-add-button.btn.btn-sm.btn-default.has-tooltip.js-no-trigger-collapse{ type: "button",
"@click" => "showNewIssueForm",
"v-if" => 'list.type !== "closed"',
"aria-label" => _("New issue"),
"title" => _("New issue"),
data: { placement: "top", container: "body" } }
= icon("plus", class: "js-no-trigger-collapse")
= render_if_exists "shared/boards/components/list_weight"
- if can?(current_user, :admin_list, current_board_parent)
%button.issue-count-badge-add-button.btn.btn-sm.btn-default.ml-1.has-tooltip.js-no-trigger-collapse{ type: "button",
"@click" => "showNewIssueForm",
"v-if" => 'list.type !== "closed"',
"aria-label" => _("New issue"),
"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"',
":list" => "list",
":issues" => "list.issues",
......
......@@ -42,7 +42,7 @@ describe Boards::IssuesController do
parsed_response = JSON.parse(response.body)
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)
end
......@@ -80,7 +80,7 @@ describe Boards::IssuesController do
parsed_response = JSON.parse(response.body)
expect(response).to match_response_schema('issues')
expect(parsed_response.length).to eq 2
expect(parsed_response['issues'].length).to eq 2
end
end
......
......@@ -161,6 +161,28 @@ describe('Store', () => {
}, 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) => {
const listOne = gl.issueBoards.BoardsStore.addList(listObj);
const listTwo = gl.issueBoards.BoardsStore.addList(listObjDuplicate);
......
......@@ -7,6 +7,16 @@ shared_examples 'issues list service' do
described_class.new(parent, user, params).execute
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
it 'returns opened issues when list_id is missing' do
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