diff --git a/app/assets/javascripts/boards/boards_bundle.js.es6 b/app/assets/javascripts/boards/boards_bundle.js.es6
index f9766471780..d87a1de7eee 100644
--- a/app/assets/javascripts/boards/boards_bundle.js.es6
+++ b/app/assets/javascripts/boards/boards_bundle.js.es6
@@ -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;
+ });
});
diff --git a/app/assets/javascripts/boards/components/board_card.js.es6 b/app/assets/javascripts/boards/components/board_card.js.es6
index 5fc50280811..ab4226ded1d 100644
--- a/app/assets/javascripts/boards/components/board_card.js.es6
+++ b/app/assets/javascripts/boards/components/board_card.js.es6
@@ -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(``);
- } 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;
},
diff --git a/app/assets/javascripts/boards/components/issue_card_inner.js.es6 b/app/assets/javascripts/boards/components/issue_card_inner.js.es6
new file mode 100644
index 00000000000..6a7e9419503
--- /dev/null
+++ b/app/assets/javascripts/boards/components/issue_card_inner.js.es6
@@ -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(``);
+ } 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: `
+
+ `,
+ });
+})();
diff --git a/app/assets/javascripts/boards/components/modal/dismiss.js.es6 b/app/assets/javascripts/boards/components/modal/dismiss.js.es6
new file mode 100644
index 00000000000..b5027f004c6
--- /dev/null
+++ b/app/assets/javascripts/boards/components/modal/dismiss.js.es6
@@ -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: `
+
+ `,
+ });
+})();
diff --git a/app/assets/javascripts/boards/components/modal/footer.js.es6 b/app/assets/javascripts/boards/components/modal/footer.js.es6
new file mode 100644
index 00000000000..9cb48448a87
--- /dev/null
+++ b/app/assets/javascripts/boards/components/modal/footer.js.es6
@@ -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: `
+
+ `,
+ });
+})();
diff --git a/app/assets/javascripts/boards/components/modal/header.js.es6 b/app/assets/javascripts/boards/components/modal/header.js.es6
new file mode 100644
index 00000000000..74681fa4bcf
--- /dev/null
+++ b/app/assets/javascripts/boards/components/modal/header.js.es6
@@ -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: `
+
+ `,
+ });
+})();
diff --git a/app/assets/javascripts/boards/components/modal/index.es6 b/app/assets/javascripts/boards/components/modal/index.es6
new file mode 100644
index 00000000000..bedd7c9735a
--- /dev/null
+++ b/app/assets/javascripts/boards/components/modal/index.es6
@@ -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: `
+
+ `,
+ });
+})();
diff --git a/app/assets/javascripts/boards/components/modal/list.js.es6 b/app/assets/javascripts/boards/components/modal/list.js.es6
new file mode 100644
index 00000000000..4e060895c5d
--- /dev/null
+++ b/app/assets/javascripts/boards/components/modal/list.js.es6
@@ -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: `
+
+ `,
+ });
+})();
diff --git a/app/assets/javascripts/boards/components/modal/search.js.es6 b/app/assets/javascripts/boards/components/modal/search.js.es6
new file mode 100644
index 00000000000..714c9240d4d
--- /dev/null
+++ b/app/assets/javascripts/boards/components/modal/search.js.es6
@@ -0,0 +1,14 @@
+/* global Vue */
+(() => {
+ window.gl = window.gl || {};
+ window.gl.issueBoards = window.gl.issueBoards || {};
+
+ gl.issueBoards.ModalSearch = Vue.extend({
+ template: `
+
+ `,
+ });
+})();
diff --git a/app/assets/javascripts/boards/components/modal/tabs.js.es6 b/app/assets/javascripts/boards/components/modal/tabs.js.es6
new file mode 100644
index 00000000000..bfdfd7e2bf5
--- /dev/null
+++ b/app/assets/javascripts/boards/components/modal/tabs.js.es6
@@ -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: `
+
+ `,
+ });
+})();
diff --git a/app/assets/javascripts/boards/services/board_service.js.es6 b/app/assets/javascripts/boards/services/board_service.js.es6
index ea55158306b..4625b13d5f3 100644
--- a/app/assets/javascripts/boards/services/board_service.js.es6
+++ b/app/assets/javascripts/boards/services/board_service.js.es6
@@ -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;
diff --git a/app/assets/javascripts/boards/stores/boards_store.js.es6 b/app/assets/javascripts/boards/stores/boards_store.js.es6
index cdf1b09c0a4..57de80e448c 100644
--- a/app/assets/javascripts/boards/stores/boards_store.js.es6
+++ b/app/assets/javascripts/boards/stores/boards_store.js.es6
@@ -12,6 +12,11 @@
detail: {
issue: {}
},
+ modal: {
+ issues: [],
+ showAddIssuesModal: false,
+ activeTab: 'all',
+ },
moving: {
issue: {},
list: {}
diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss
index f2d60bff2b5..5390bad1b33 100644
--- a/app/assets/stylesheets/pages/boards.scss
+++ b/app/assets/stylesheets/pages/boards.scss
@@ -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;
+}
diff --git a/app/controllers/projects/boards_controller.rb b/app/controllers/projects/boards_controller.rb
index 808affa4f98..7a2e2324323 100644
--- a/app/controllers/projects/boards_controller.rb
+++ b/app/controllers/projects/boards_controller.rb
@@ -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!
diff --git a/app/views/projects/boards/_show.html.haml b/app/views/projects/boards/_show.html.haml
index 356bd50f7f3..798eace7a82 100644
--- a/app/views/projects/boards/_show.html.haml
+++ b/app/views/projects/boards/_show.html.haml
@@ -26,3 +26,4 @@
":issue-link-base" => "issueLinkBase",
":key" => "_uid" }
= render "projects/boards/components/sidebar"
+ %board-add-issues-modal
diff --git a/app/views/projects/boards/components/_card.html.haml b/app/views/projects/boards/components/_card.html.haml
index e4c2aff46ec..51e5d739537 100644
--- a/app/views/projects/boards/components/_card.html.haml
+++ b/app/views/projects/boards/components/_card.html.haml
@@ -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" }
diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml
index b42eaabb111..b94bdf14d5e 100644
--- a/app/views/shared/issuable/_filter.html.haml
+++ b/app/views/shared/issuable/_filter.html.haml
@@ -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
diff --git a/config/routes/project.rb b/config/routes/project.rb
index efe2fbc521d..5c33bb4a6ca 100644
--- a/config/routes/project.rb
+++ b/config/routes/project.rb
@@ -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]