diff options
author | Fatih Acet <acetfatih@gmail.com> | 2016-10-06 17:49:59 +0300 |
---|---|---|
committer | Fatih Acet <acetfatih@gmail.com> | 2016-10-06 17:49:59 +0300 |
commit | 9a13f885a9dc7b072d41160a6d3db965c9114b4b (patch) | |
tree | 5becd7566eae9f65806e99035fe3651579e27b44 /app | |
parent | dcfda304aa8c06f427cb5672ceb4e070047436be (diff) | |
parent | 8ad923695e54c7ac6e9c47c37c505a7ac6b34017 (diff) |
Merge branch 'issue-boards-new-issue' into 'master'
Issue boards new issue form
## What does this MR do?
Adds a new issue form into the issue boards lists.
## Screenshots (if relevant)
![Screen_Shot_2016-10-03_at_14.57.30](/uploads/17fe6cd37bd020a2ee1688e0b496c18f/Screen_Shot_2016-10-03_at_14.57.30.png)
![Screen_Shot_2016-10-03_at_14.57.32](/uploads/c3f12bcb9ff9a0e7ce5b0bb06dfb0dd7/Screen_Shot_2016-10-03_at_14.57.32.png)
## What are the relevant issue numbers?
Part of #21219
See merge request !6653
Diffstat (limited to 'app')
11 files changed, 208 insertions, 16 deletions
diff --git a/app/assets/javascripts/boards/components/board.js.es6 b/app/assets/javascripts/boards/components/board.js.es6 index 7e86f001f44..cacb36a897f 100644 --- a/app/assets/javascripts/boards/components/board.js.es6 +++ b/app/assets/javascripts/boards/components/board.js.es6 @@ -21,7 +21,8 @@ }, data () { return { - filters: Store.state.filters + filters: Store.state.filters, + showIssueForm: false }; }, watch: { @@ -33,6 +34,11 @@ deep: true } }, + methods: { + showNewIssueForm() { + this.showIssueForm = !this.showIssueForm; + } + }, ready () { const options = gl.issueBoards.getBoardSortableDefaultOptions({ disabled: this.disabled, diff --git a/app/assets/javascripts/boards/components/board_list.js.es6 b/app/assets/javascripts/boards/components/board_list.js.es6 index 474805c1437..7022a29e818 100644 --- a/app/assets/javascripts/boards/components/board_list.js.es6 +++ b/app/assets/javascripts/boards/components/board_list.js.es6 @@ -1,4 +1,5 @@ //= require ./board_card +//= require ./board_new_issue (() => { const Store = gl.issueBoards.BoardsStore; @@ -8,14 +9,16 @@ gl.issueBoards.BoardList = Vue.extend({ components: { - 'board-card': gl.issueBoards.BoardCard + 'board-card': gl.issueBoards.BoardCard, + 'board-new-issue': gl.issueBoards.BoardNewIssue }, props: { disabled: Boolean, list: Object, issues: Array, loading: Boolean, - issueLinkBase: String + issueLinkBase: String, + showIssueForm: Boolean }, data () { return { @@ -73,7 +76,7 @@ group: 'issues', sort: false, disabled: this.disabled, - filter: '.board-list-count', + filter: '.board-list-count, .is-disabled', onStart: (e) => { const card = this.$refs.issue[e.oldIndex]; diff --git a/app/assets/javascripts/boards/components/board_new_issue.js.es6 b/app/assets/javascripts/boards/components/board_new_issue.js.es6 new file mode 100644 index 00000000000..a4fad422eca --- /dev/null +++ b/app/assets/javascripts/boards/components/board_new_issue.js.es6 @@ -0,0 +1,58 @@ +(() => { + window.gl = window.gl || {}; + + gl.issueBoards.BoardNewIssue = Vue.extend({ + props: { + list: Object, + showIssueForm: Boolean + }, + data() { + return { + title: '', + error: false + }; + }, + watch: { + showIssueForm () { + this.$els.input.focus(); + } + }, + methods: { + submit(e) { + e.preventDefault(); + if (this.title.trim() === '') return; + + this.error = false; + + const labels = this.list.label ? [this.list.label] : []; + const issue = new ListIssue({ + title: this.title, + labels + }); + + this.list.newIssue(issue) + .then((data) => { + // Need this because our jQuery very kindly disables buttons on ALL form submissions + $(this.$els.submitButton).enable(); + }) + .catch(() => { + // Need this because our jQuery very kindly disables buttons on ALL form submissions + $(this.$els.submitButton).enable(); + + // Remove the issue + this.list.removeIssue(issue); + + // Show error message + this.error = true; + this.showIssueForm = true; + }); + + this.cancel(); + }, + cancel() { + this.showIssueForm = false; + this.title = ''; + } + } + }); +})(); diff --git a/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6 b/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6 index 44addb3ea98..f629d45c587 100644 --- a/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6 +++ b/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6 @@ -21,7 +21,7 @@ fallbackClass: 'is-dragging', fallbackOnBody: true, ghostClass: 'is-ghost', - filter: '.has-tooltip', + filter: '.has-tooltip, .btn', delay: gl.issueBoards.touchEnabled ? 100 : 0, scrollSensitivity: gl.issueBoards.touchEnabled ? 60 : 100, scrollSpeed: 20, diff --git a/app/assets/javascripts/boards/models/list.js.es6 b/app/assets/javascripts/boards/models/list.js.es6 index 91fd620fdb3..5d0a561cdba 100644 --- a/app/assets/javascripts/boards/models/list.js.es6 +++ b/app/assets/javascripts/boards/models/list.js.es6 @@ -87,6 +87,17 @@ class List { }); } + newIssue (issue) { + this.addIssue(issue); + this.issuesSize++; + + return gl.boardService.newIssue(this.id, issue) + .then((resp) => { + const data = resp.json(); + issue.id = data.iid; + }); + } + createIssues (data) { data.forEach((issueObj) => { this.addIssue(new ListIssue(issueObj)); diff --git a/app/assets/javascripts/boards/services/board_service.js.es6 b/app/assets/javascripts/boards/services/board_service.js.es6 index 9b80fb2e99f..2b825c3949f 100644 --- a/app/assets/javascripts/boards/services/board_service.js.es6 +++ b/app/assets/javascripts/boards/services/board_service.js.es6 @@ -58,4 +58,10 @@ class BoardService { to_list_id }); } + + newIssue (id, issue) { + return this.issues.save({ id }, { + issue + }); + } }; diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss index ecc5b24e360..6e81c12aa55 100644 --- a/app/assets/stylesheets/pages/boards.scss +++ b/app/assets/stylesheets/pages/boards.scss @@ -162,6 +162,10 @@ lex list-style: none; overflow-y: scroll; overflow-x: hidden; + + &.is-smaller { + height: calc(100% - 185px); + } } .board-list-loading { @@ -233,3 +237,31 @@ lex margin-right: 5px; } } + +.board-new-issue-form { + margin: 5px; +} + +.board-issue-count-holder { + margin-top: -3px; + + .btn { + line-height: 12px; + border-top-left-radius: 0; + border-bottom-left-radius: 0; + } +} + +.board-issue-count { + padding-right: 10px; + padding-left: 10px; + line-height: 21px; + border-radius: $border-radius-base; + border: 1px solid $border-color; + + &.has-btn { + border-top-right-radius: 0; + border-bottom-right-radius: 0; + border-width: 1px 0 1px 1px; + } +} diff --git a/app/controllers/projects/boards/issues_controller.rb b/app/controllers/projects/boards/issues_controller.rb index 4aa7982eab4..095af6c35eb 100644 --- a/app/controllers/projects/boards/issues_controller.rb +++ b/app/controllers/projects/boards/issues_controller.rb @@ -2,6 +2,7 @@ module Projects module Boards class IssuesController < Boards::ApplicationController before_action :authorize_read_issue!, only: [:index] + before_action :authorize_create_issue!, only: [:create] before_action :authorize_update_issue!, only: [:update] def index @@ -9,16 +10,23 @@ module Projects issues = issues.page(params[:page]) render json: { - issues: issues.as_json( - only: [:iid, :title, :confidential], - include: { - assignee: { only: [:id, :name, :username], methods: [:avatar_url] }, - labels: { only: [:id, :title, :description, :color, :priority], methods: [:text_color] } - }), + issues: serialize_as_json(issues), size: issues.total_count } end + def create + list = project.board.lists.find(params[:list_id]) + service = ::Boards::Issues::CreateService.new(project, current_user, issue_params) + issue = service.execute(list) + + if issue.valid? + render json: serialize_as_json(issue) + else + render json: issue.errors, status: :unprocessable_entity + end + end + def update service = ::Boards::Issues::MoveService.new(project, current_user, move_params) @@ -43,6 +51,10 @@ module Projects return render_403 unless can?(current_user, :read_issue, project) end + def authorize_create_issue! + return render_403 unless can?(current_user, :admin_issue, project) + end + def authorize_update_issue! return render_403 unless can?(current_user, :update_issue, issue) end @@ -54,6 +66,19 @@ module Projects def move_params params.permit(:id, :from_list_id, :to_list_id) end + + def issue_params + params.require(:issue).permit(:title).merge(request: request) + end + + def serialize_as_json(resource) + resource.as_json( + only: [:iid, :title, :confidential], + include: { + assignee: { only: [:id, :name, :username], methods: [:avatar_url] }, + labels: { only: [:id, :title, :description, :color, :priority], methods: [:text_color] } + }) + end end end end diff --git a/app/services/boards/issues/create_service.rb b/app/services/boards/issues/create_service.rb new file mode 100644 index 00000000000..3701afd441f --- /dev/null +++ b/app/services/boards/issues/create_service.rb @@ -0,0 +1,16 @@ +module Boards + module Issues + class CreateService < Boards::BaseService + def execute(list) + params.merge!(label_ids: [list.label_id]) + create_issue + end + + private + + def create_issue + ::Issues::CreateService.new(project, current_user, params).execute + end + end + end +end diff --git a/app/views/projects/boards/components/_board.html.haml b/app/views/projects/boards/components/_board.html.haml index 73066150fb3..ba1502c97b6 100644 --- a/app/views/projects/boards/components/_board.html.haml +++ b/app/views/projects/boards/components/_board.html.haml @@ -12,8 +12,17 @@ %header.board-header{ ":class" => "{ 'has-border': list.label }", ":style" => "{ borderTopColor: (list.label ? list.label.color : null) }" } %h3.board-title.js-board-handle{ ":class" => "{ 'user-can-drag': (!disabled && !list.preset) }" } {{ list.title }} - %span.pull-right{ "v-if" => "list.type !== 'blank'" } - {{ list.issuesSize }} + .board-issue-count-holder.pull-right.clearfix{ "v-if" => "list.type !== 'blank'" } + %span.board-issue-count.pull-left{ ":class" => "{ 'has-btn': list.type !== 'done' }" } + {{ list.issuesSize }} + - if can?(current_user, :admin_issue, @project) + %button.btn.btn-small.btn-default.pull-right.has-tooltip{ type: "button", + "@click" => "showNewIssueForm", + "v-if" => "list.type !== 'done'", + "aria-label" => "Add an issue", + "title" => "Add an issue", + data: { placement: "top", container: "body" } } + = icon("plus") - if can?(current_user, :admin_list, @project) %board-delete{ "inline-template" => true, ":list" => "list", @@ -26,12 +35,38 @@ ":issues" => "list.issues", ":loading" => "list.loading", ":disabled" => "disabled", + ":show-issue-form.sync" => "showIssueForm", ":issue-link-base" => "issueLinkBase" } .board-list-loading.text-center{ "v-if" => "loading" } = icon("spinner spin") + - if can? current_user, :create_issue, @project + %board-new-issue{ "inline-template" => true, + ":list" => "list", + ":show-issue-form.sync" => "showIssueForm", + "v-show" => "list.type !== 'done' && showIssueForm" } + .card.board-new-issue-form + %form{ "@submit" => "submit($event)" } + .flash-container{ "v-if" => "error" } + .flash-alert + An error occured. Please try again. + %label.label-light{ ":for" => "list.id + '-title'" } + Title + %input.form-control{ type: "text", + "v-model" => "title", + "v-el:input" => true, + ":id" => "list.id + '-title'" } + .clearfix.prepend-top-10 + %button.btn.btn-success.pull-left{ type: "submit", + ":disabled" => "title === ''", + "v-el:submit-button" => true } + Submit issue + %button.btn.btn-default.pull-right{ type: "button", + "@click" => "cancel" } + Cancel %ul.board-list{ "v-el:list" => true, "v-show" => "!loading", - ":data-board" => "list.id" } + ":data-board" => "list.id", + ":class" => "{ 'is-smaller': showIssueForm }" } = render "projects/boards/components/card" %li.board-list-count.text-center{ "v-if" => "showCount" } = icon("spinner spin", "v-show" => "list.loadingMore" ) diff --git a/app/views/projects/boards/components/_card.html.haml b/app/views/projects/boards/components/_card.html.haml index e8b60b54d80..f15c87c8185 100644 --- a/app/views/projects/boards/components/_card.html.haml +++ b/app/views/projects/boards/components/_card.html.haml @@ -7,7 +7,7 @@ ":issue-link-base" => "issueLinkBase", ":disabled" => "disabled", "track-by" => "id" } - %li.card{ ":class" => "{ 'user-can-drag': !disabled }", + %li.card{ ":class" => "{ 'user-can-drag': !disabled && issue.id, 'is-disabled': disabled || !issue.id }", ":index" => "index" } %h4.card-title = icon("eye-slash", class: "confidential-icon", "v-if" => "issue.confidential") @@ -15,7 +15,7 @@ ":title" => "issue.title" } {{ issue.title }} .card-footer - %span.card-number + %span.card-number{ "v-if" => "issue.id" } = precede '#' do {{ issue.id }} %button.label.color-label.has-tooltip{ "v-for" => "label in issue.labels", |