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
This commit is contained in:
commit
3d2dad449d
|
@ -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({
|
||||||
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({
|
||||||
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 {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.createIssues(data.issues);
|
this.createIssues(data.issues);
|
||||||
|
|
||||||
|
return data;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
color: $placeholder-text-color;
|
color: $placeholder-text-color;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.lh-100 { line-height: 1; }
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -1,29 +1,11 @@
|
||||||
.issue-count-badge {
|
.issue-count-badge {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: stretch;
|
border-radius: $border-radius-base;
|
||||||
height: 24px;
|
border: 1px solid $border-color;
|
||||||
|
padding: 5px $gl-padding-8;
|
||||||
}
|
}
|
||||||
|
|
||||||
.issue-count-badge-count {
|
.issue-count-badge-count {
|
||||||
display: flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.issue-count-badge-add-button {
|
|
||||||
display: 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]
|
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
|
||||||
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
|
||||||
|
|
||||||
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 @@
|
||||||
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 @@
|
||||||
"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
|
||||||
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
|
||||||
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', () => {
|
||||||
}, 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
|
||||||
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 }
|
||||||
|
|
Loading…
Reference in New Issue