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: ` +
+

+ + + {{ issue.title }} + +

+ +
+ `, + }); +})(); 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: ` +
+

+ Add issues to board + +

+ + +
+ `, + }); +})(); 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]