Add issues to boards list
This removes the backlog list & instead creates a modal window that will list all issues that are not part of a list for easy adding onto the board Closes #26205
This commit is contained in:
parent
52ea505126
commit
a132b7d8ce
18 changed files with 438 additions and 52 deletions
|
@ -13,6 +13,7 @@
|
|||
//= require ./components/board
|
||||
//= require ./components/board_sidebar
|
||||
//= require ./components/new_list_dropdown
|
||||
//= require ./components/modal
|
||||
//= require ./vue_resource_interceptor
|
||||
|
||||
$(() => {
|
||||
|
@ -31,7 +32,8 @@ $(() => {
|
|||
el: $boardApp,
|
||||
components: {
|
||||
'board': gl.issueBoards.Board,
|
||||
'board-sidebar': gl.issueBoards.BoardSidebar
|
||||
'board-sidebar': gl.issueBoards.BoardSidebar,
|
||||
'board-add-issues-modal': gl.issueBoards.IssuesModal,
|
||||
},
|
||||
data: {
|
||||
state: Store.state,
|
||||
|
@ -55,12 +57,12 @@ $(() => {
|
|||
gl.boardService.all()
|
||||
.then((resp) => {
|
||||
resp.json().forEach((board) => {
|
||||
if (board.list_type === 'backlog') return;
|
||||
|
||||
const list = Store.addList(board);
|
||||
|
||||
if (list.type === 'done') {
|
||||
list.position = Infinity;
|
||||
} else if (list.type === 'backlog') {
|
||||
list.position = -1;
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -81,4 +83,13 @@ $(() => {
|
|||
gl.issueBoards.newListDropdownInit();
|
||||
}
|
||||
});
|
||||
|
||||
// This element is outside the Vue app
|
||||
$(document)
|
||||
.off('click', '.js-show-add-issues')
|
||||
.on('click', '.js-show-add-issues', (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
Store.modal.showAddIssuesModal = true;
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
/* eslint-disable comma-dangle, space-before-function-paren, dot-notation */
|
||||
//= require ./issue_card_inner
|
||||
/* global Vue */
|
||||
|
||||
(() => {
|
||||
|
@ -9,6 +10,9 @@
|
|||
|
||||
gl.issueBoards.BoardCard = Vue.extend({
|
||||
template: '#js-board-list-card',
|
||||
components: {
|
||||
'issue-card-inner': gl.issueBoards.IssueCardInner,
|
||||
},
|
||||
props: {
|
||||
list: Object,
|
||||
issue: Object,
|
||||
|
@ -28,31 +32,6 @@
|
|||
}
|
||||
},
|
||||
methods: {
|
||||
filterByLabel (label, e) {
|
||||
let labelToggleText = label.title;
|
||||
const labelIndex = Store.state.filters['label_name'].indexOf(label.title);
|
||||
$(e.target).tooltip('hide');
|
||||
|
||||
if (labelIndex === -1) {
|
||||
Store.state.filters['label_name'].push(label.title);
|
||||
$('.labels-filter').prepend(`<input type="hidden" name="label_name[]" value="${label.title}" />`);
|
||||
} else {
|
||||
Store.state.filters['label_name'].splice(labelIndex, 1);
|
||||
labelToggleText = Store.state.filters['label_name'][0];
|
||||
$(`.labels-filter input[name="label_name[]"][value="${label.title}"]`).remove();
|
||||
}
|
||||
|
||||
const selectedLabels = Store.state.filters['label_name'];
|
||||
if (selectedLabels.length === 0) {
|
||||
labelToggleText = 'Label';
|
||||
} else if (selectedLabels.length > 1) {
|
||||
labelToggleText = `${selectedLabels[0]} + ${selectedLabels.length - 1} more`;
|
||||
}
|
||||
|
||||
$('.labels-filter .dropdown-toggle-text').text(labelToggleText);
|
||||
|
||||
Store.updateFiltersUrl();
|
||||
},
|
||||
mouseDown () {
|
||||
this.showDetail = true;
|
||||
},
|
||||
|
|
|
@ -0,0 +1,89 @@
|
|||
/* global Vue */
|
||||
(() => {
|
||||
const Store = gl.issueBoards.BoardsStore;
|
||||
|
||||
window.gl = window.gl || {};
|
||||
window.gl.issueBoards = window.gl.issueBoards || {};
|
||||
|
||||
gl.issueBoards.IssueCardInner = Vue.extend({
|
||||
props: [
|
||||
'issue', 'issueLinkBase', 'list',
|
||||
],
|
||||
methods: {
|
||||
showLabel(label) {
|
||||
if (!this.list) return true;
|
||||
|
||||
return !this.list.label || label.id !== this.list.label.id;
|
||||
},
|
||||
filterByLabel(label, e) {
|
||||
let labelToggleText = label.title;
|
||||
const labelIndex = Store.state.filters.label_name.indexOf(label.title);
|
||||
$(e.target).tooltip('hide');
|
||||
|
||||
if (labelIndex === -1) {
|
||||
Store.state.filters.label_name.push(label.title);
|
||||
$('.labels-filter').prepend(`<input type="hidden" name="label_name[]" value="${label.title}" />`);
|
||||
} else {
|
||||
Store.state.filters.label_name.splice(labelIndex, 1);
|
||||
labelToggleText = Store.state.filters.label_name[0];
|
||||
$(`.labels-filter input[name="label_name[]"][value="${label.title}"]`).remove();
|
||||
}
|
||||
|
||||
const selectedLabels = Store.state.filters.label_name;
|
||||
if (selectedLabels.length === 0) {
|
||||
labelToggleText = 'Label';
|
||||
} else if (selectedLabels.length > 1) {
|
||||
labelToggleText = `${selectedLabels[0]} + ${selectedLabels.length - 1} more`;
|
||||
}
|
||||
|
||||
$('.labels-filter .dropdown-toggle-text').text(labelToggleText);
|
||||
|
||||
Store.updateFiltersUrl();
|
||||
},
|
||||
},
|
||||
template: `
|
||||
<div>
|
||||
<h4 class="card-title">
|
||||
<i
|
||||
class="fa fa-eye-flash"
|
||||
v-if="issue.confidential"></i>
|
||||
<a
|
||||
:href="issueLinkBase + '/' + issue.id"
|
||||
:title="issue.title">
|
||||
{{ issue.title }}
|
||||
</a>
|
||||
</h4>
|
||||
<div class="card-footer">
|
||||
<span
|
||||
class="card-number"
|
||||
v-if="issue.id">
|
||||
#{{issue.id}}
|
||||
</span>
|
||||
<a
|
||||
class="has-tooltip"
|
||||
:href="issue.assignee.username"
|
||||
:title="'Assigned to ' + issue.assignee.name"
|
||||
v-if="issue.assignee"
|
||||
data-container="body">
|
||||
<img
|
||||
class="avatar avatar-inline s20"
|
||||
:src="issue.assignee.avatar"
|
||||
width="20"
|
||||
height="20" />
|
||||
</a>
|
||||
<button
|
||||
class="label color-label has-tooltip"
|
||||
v-for="label in issue.labels"
|
||||
type="button"
|
||||
v-if="showLabel(label)"
|
||||
@click="filterByLabel(label, $event)"
|
||||
:style="{ backgroundColor: label.color, color: label.textColor }"
|
||||
:title="label.description"
|
||||
data-container="body">
|
||||
{{ label.title }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
});
|
||||
})();
|
|
@ -0,0 +1,28 @@
|
|||
/* global Vue */
|
||||
(() => {
|
||||
const Store = gl.issueBoards.BoardsStore;
|
||||
|
||||
window.gl = window.gl || {};
|
||||
window.gl.issueBoards = window.gl.issueBoards || {};
|
||||
|
||||
gl.issueBoards.DismissModal = Vue.extend({
|
||||
data() {
|
||||
return Store.modal;
|
||||
},
|
||||
methods: {
|
||||
toggleModal(toggle) {
|
||||
this.showAddIssuesModal = toggle;
|
||||
},
|
||||
},
|
||||
template: `
|
||||
<button
|
||||
type="button"
|
||||
class="close"
|
||||
data-dismiss="modal"
|
||||
aria-label="Close"
|
||||
@click="toggleModal(false)">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
`,
|
||||
});
|
||||
})();
|
33
app/assets/javascripts/boards/components/modal/footer.js.es6
Normal file
33
app/assets/javascripts/boards/components/modal/footer.js.es6
Normal file
|
@ -0,0 +1,33 @@
|
|||
/* global Vue */
|
||||
(() => {
|
||||
const Store = gl.issueBoards.BoardsStore;
|
||||
|
||||
window.gl = window.gl || {};
|
||||
window.gl.issueBoards = window.gl.issueBoards || {};
|
||||
|
||||
gl.issueBoards.ModalFooter = Vue.extend({
|
||||
data() {
|
||||
return Store.modal;
|
||||
},
|
||||
methods: {
|
||||
hideModal() {
|
||||
this.showAddIssuesModal = false;
|
||||
},
|
||||
},
|
||||
template: `
|
||||
<footer class="form-actions add-issues-footer">
|
||||
<button
|
||||
class="btn btn-success pull-left"
|
||||
type="button">
|
||||
Add issues
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-default pull-right"
|
||||
type="button"
|
||||
@click="hideModal">
|
||||
Cancel
|
||||
</button>
|
||||
</footer>
|
||||
`,
|
||||
});
|
||||
})();
|
31
app/assets/javascripts/boards/components/modal/header.js.es6
Normal file
31
app/assets/javascripts/boards/components/modal/header.js.es6
Normal file
|
@ -0,0 +1,31 @@
|
|||
//= require ./dismiss
|
||||
//= require ./tabs
|
||||
//= require ./search
|
||||
/* global Vue */
|
||||
(() => {
|
||||
const Store = gl.issueBoards.BoardsStore;
|
||||
|
||||
window.gl = window.gl || {};
|
||||
window.gl.issueBoards = window.gl.issueBoards || {};
|
||||
|
||||
gl.issueBoards.IssuesModalHeader = Vue.extend({
|
||||
data() {
|
||||
return Store.modal;
|
||||
},
|
||||
components: {
|
||||
'modal-dismiss': gl.issueBoards.DismissModal,
|
||||
'modal-tabs': gl.issueBoards.ModalTabs,
|
||||
'modal-search': gl.issueBoards.ModalSearch,
|
||||
},
|
||||
template: `
|
||||
<header class="add-issues-header">
|
||||
<h2>
|
||||
Add issues to board
|
||||
<modal-dismiss></modal-dismiss>
|
||||
</h2>
|
||||
<modal-tabs></modal-tabs>
|
||||
<modal-search></modal-search>
|
||||
</header>
|
||||
`,
|
||||
});
|
||||
})();
|
32
app/assets/javascripts/boards/components/modal/index.es6
Normal file
32
app/assets/javascripts/boards/components/modal/index.es6
Normal file
|
@ -0,0 +1,32 @@
|
|||
//= require ./header
|
||||
//= require ./list
|
||||
//= require ./footer
|
||||
/* global Vue */
|
||||
(() => {
|
||||
const Store = gl.issueBoards.BoardsStore;
|
||||
|
||||
window.gl = window.gl || {};
|
||||
window.gl.issueBoards = window.gl.issueBoards || {};
|
||||
|
||||
gl.issueBoards.IssuesModal = Vue.extend({
|
||||
data() {
|
||||
return Store.modal;
|
||||
},
|
||||
components: {
|
||||
'modal-header': gl.issueBoards.IssuesModalHeader,
|
||||
'modal-list': gl.issueBoards.ModalList,
|
||||
'modal-footer': gl.issueBoards.ModalFooter,
|
||||
},
|
||||
template: `
|
||||
<div
|
||||
class="add-issues-modal"
|
||||
v-if="showAddIssuesModal">
|
||||
<div class="add-issues-container">
|
||||
<modal-header></modal-header>
|
||||
<modal-list></modal-list>
|
||||
<modal-footer></modal-footer>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
});
|
||||
})();
|
51
app/assets/javascripts/boards/components/modal/list.js.es6
Normal file
51
app/assets/javascripts/boards/components/modal/list.js.es6
Normal file
|
@ -0,0 +1,51 @@
|
|||
/* global Vue */
|
||||
/* global ListIssue */
|
||||
(() => {
|
||||
const Store = gl.issueBoards.BoardsStore;
|
||||
|
||||
window.gl = window.gl || {};
|
||||
window.gl.issueBoards = window.gl.issueBoards || {};
|
||||
|
||||
gl.issueBoards.ModalList = Vue.extend({
|
||||
data() {
|
||||
return Store.modal;
|
||||
},
|
||||
computed: {
|
||||
loading() {
|
||||
return this.issues.length === 0;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
gl.boardService.getBacklog()
|
||||
.then((res) => {
|
||||
const data = res.json();
|
||||
|
||||
data.forEach((issueObj) => {
|
||||
this.issues.push(new ListIssue(issueObj));
|
||||
});
|
||||
});
|
||||
},
|
||||
components: {
|
||||
'issue-card-inner': gl.issueBoards.IssueCardInner,
|
||||
},
|
||||
template: `
|
||||
<section class="add-issues-list">
|
||||
<i
|
||||
class="fa fa-spinner fa-spin"
|
||||
v-if="loading"></i>
|
||||
<ul
|
||||
class="list-unstyled"
|
||||
v-if="!loading">
|
||||
<li
|
||||
class="card"
|
||||
v-for="issue in issues">
|
||||
<issue-card-inner
|
||||
:issue="issue"
|
||||
:issue-link-base="'/'">
|
||||
</issue-card-inner>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
`,
|
||||
});
|
||||
})();
|
14
app/assets/javascripts/boards/components/modal/search.js.es6
Normal file
14
app/assets/javascripts/boards/components/modal/search.js.es6
Normal file
|
@ -0,0 +1,14 @@
|
|||
/* global Vue */
|
||||
(() => {
|
||||
window.gl = window.gl || {};
|
||||
window.gl.issueBoards = window.gl.issueBoards || {};
|
||||
|
||||
gl.issueBoards.ModalSearch = Vue.extend({
|
||||
template: `
|
||||
<input
|
||||
placeholder="Search issues..."
|
||||
class="form-control"
|
||||
type="search" />
|
||||
`,
|
||||
});
|
||||
})();
|
46
app/assets/javascripts/boards/components/modal/tabs.js.es6
Normal file
46
app/assets/javascripts/boards/components/modal/tabs.js.es6
Normal file
|
@ -0,0 +1,46 @@
|
|||
/* global Vue */
|
||||
(() => {
|
||||
const Store = gl.issueBoards.BoardsStore;
|
||||
|
||||
window.gl = window.gl || {};
|
||||
window.gl.issueBoards = window.gl.issueBoards || {};
|
||||
|
||||
gl.issueBoards.ModalTabs = Vue.extend({
|
||||
data() {
|
||||
return Store.modal;
|
||||
},
|
||||
methods: {
|
||||
changeTab(tab) {
|
||||
this.activeTab = tab;
|
||||
},
|
||||
},
|
||||
template: `
|
||||
<div class="top-area">
|
||||
<ul class="nav-links issues-state-filters">
|
||||
<li :class="{ 'active': activeTab == 'all' }">
|
||||
<a
|
||||
href="#"
|
||||
role="button"
|
||||
@click.prevent="changeTab('all')">
|
||||
<span>All issues</span>
|
||||
<span class="badge">
|
||||
{{ issues.length }}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
<li :class="{ 'active': activeTab == 'selected' }">
|
||||
<a
|
||||
href="#"
|
||||
role="button"
|
||||
@click.prevent="changeTab('selected')">
|
||||
<span>Selected issues</span>
|
||||
<span class="badge">
|
||||
0
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
`,
|
||||
});
|
||||
})();
|
|
@ -3,6 +3,12 @@
|
|||
|
||||
class BoardService {
|
||||
constructor (root, boardId) {
|
||||
this.boards = Vue.resource(`${root}{/id}.json`, {}, {
|
||||
backlog: {
|
||||
method: 'GET',
|
||||
url: `${root}/${boardId}/backlog.json`
|
||||
}
|
||||
});
|
||||
this.lists = Vue.resource(`${root}/${boardId}/lists{/id}`, {}, {
|
||||
generate: {
|
||||
method: 'POST',
|
||||
|
@ -65,6 +71,10 @@ class BoardService {
|
|||
issue
|
||||
});
|
||||
}
|
||||
|
||||
getBacklog() {
|
||||
return this.boards.backlog();
|
||||
}
|
||||
}
|
||||
|
||||
window.BoardService = BoardService;
|
||||
|
|
|
@ -12,6 +12,11 @@
|
|||
detail: {
|
||||
issue: {}
|
||||
},
|
||||
modal: {
|
||||
issues: [],
|
||||
showAddIssuesModal: false,
|
||||
activeTab: 'all',
|
||||
},
|
||||
moving: {
|
||||
issue: {},
|
||||
list: {}
|
||||
|
|
|
@ -250,11 +250,12 @@
|
|||
}
|
||||
|
||||
.issue-boards-search {
|
||||
width: 290px;
|
||||
width: 395px;
|
||||
|
||||
.form-control {
|
||||
display: inline-block;
|
||||
width: 210px;
|
||||
margin-right: 10px
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -354,3 +355,52 @@
|
|||
padding-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.add-issues-modal {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
background-color: rgba($black, .3);
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.add-issues-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 90vw;
|
||||
height: 85vh;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
padding: 25px 15px 0;
|
||||
background-color: $white-light;
|
||||
border-radius: $border-radius-default;
|
||||
box-shadow: 0 2px 12px rgba($black, .5);
|
||||
}
|
||||
|
||||
.add-issues-header {
|
||||
> h2 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.top-area {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.add-issues-list {
|
||||
flex: 1;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
.add-issues-footer {
|
||||
margin-top: auto;
|
||||
margin-left: -15px;
|
||||
margin-right: -15px;
|
||||
padding-left: 15px;
|
||||
padding-right: 15px;
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
class Projects::BoardsController < Projects::ApplicationController
|
||||
include IssuableCollections
|
||||
|
||||
before_action :authorize_read_board!, only: [:index, :show]
|
||||
# before_action :authorize_read_board!, only: [:index, :show, :backlog]
|
||||
|
||||
def index
|
||||
@boards = ::Boards::ListService.new(project, current_user).execute
|
||||
|
@ -25,6 +25,27 @@ class Projects::BoardsController < Projects::ApplicationController
|
|||
end
|
||||
end
|
||||
|
||||
def backlog
|
||||
board = project.boards.find(params[:id])
|
||||
|
||||
@issues = issues_collection
|
||||
@issues = @issues.where.not(
|
||||
LabelLink.where("label_links.target_type = 'Issue' AND label_links.target_id = issues.id")
|
||||
.where(label_id: board.lists.movable.pluck(:label_id)).limit(1).arel.exists
|
||||
)
|
||||
@issues = @issues.page(params[:page])
|
||||
|
||||
render json: @issues.as_json(
|
||||
labels: true,
|
||||
only: [:iid, :title, :confidential, :due_date],
|
||||
include: {
|
||||
assignee: { only: [:id, :name, :username], methods: [:avatar_url] },
|
||||
milestone: { only: [:id, :title] }
|
||||
},
|
||||
user: current_user
|
||||
)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def authorize_read_board!
|
||||
|
|
|
@ -26,3 +26,4 @@
|
|||
":issue-link-base" => "issueLinkBase",
|
||||
":key" => "_uid" }
|
||||
= render "projects/boards/components/sidebar"
|
||||
%board-add-issues-modal
|
||||
|
|
|
@ -4,25 +4,6 @@
|
|||
"@mousedown" => "mouseDown",
|
||||
"@mousemove" => "mouseMove",
|
||||
"@mouseup" => "showIssue($event)" }
|
||||
%h4.card-title
|
||||
= icon("eye-slash", class: "confidential-icon", "v-if" => "issue.confidential")
|
||||
%a{ ":href" => 'issueLinkBase + "/" + issue.id',
|
||||
":title" => "issue.title" }
|
||||
{{ issue.title }}
|
||||
.card-footer
|
||||
%span.card-number{ "v-if" => "issue.id" }
|
||||
= precede '#' do
|
||||
{{ issue.id }}
|
||||
%a.has-tooltip{ ":href" => "\"#{root_path}\" + issue.assignee.username",
|
||||
":title" => '"Assigned to " + issue.assignee.name',
|
||||
"v-if" => "issue.assignee",
|
||||
data: { container: 'body' } }
|
||||
%img.avatar.avatar-inline.s20{ ":src" => "issue.assignee.avatar", width: 20, height: 20, alt: "Avatar" }
|
||||
%button.label.color-label.has-tooltip{ "v-for" => "label in issue.labels",
|
||||
type: "button",
|
||||
"v-if" => "(!list.label || label.id !== list.label.id)",
|
||||
"@click" => "filterByLabel(label, $event)",
|
||||
":style" => "{ backgroundColor: label.color, color: label.textColor }",
|
||||
":title" => "label.description",
|
||||
data: { container: 'body' } }
|
||||
{{ label.title }}
|
||||
%issue-card-inner{ ":list" => "list",
|
||||
":issue" => "issue",
|
||||
":issue-link-base" => "issueLinkBase" }
|
||||
|
|
|
@ -38,6 +38,8 @@
|
|||
#js-boards-search.issue-boards-search
|
||||
%input.pull-left.form-control{ type: "search", placeholder: "Filter by name...", "v-model" => "filters.search", "debounce" => "250" }
|
||||
- if can?(current_user, :admin_list, @project)
|
||||
%button.btn.btn-create.btn-inverted.js-show-add-issues{ type: "button" }
|
||||
Add issues
|
||||
.dropdown.pull-right
|
||||
%button.btn.btn-create.js-new-board-list{ type: "button", data: { toggle: "dropdown", labels: labels_filter_path, namespace_path: @project.try(:namespace).try(:path), project_path: @project.try(:path) } }
|
||||
Add list
|
||||
|
|
|
@ -266,6 +266,8 @@ constraints(ProjectUrlConstrainer.new) do
|
|||
end
|
||||
|
||||
resources :boards, only: [:index, :show] do
|
||||
get :backlog, on: :member
|
||||
|
||||
scope module: :boards do
|
||||
resources :issues, only: [:update]
|
||||
|
||||
|
|
Loading…
Reference in a new issue