Commit 9a13f885 authored by Fatih Acet's avatar Fatih Acet

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
parents dcfda304 8ad92369
...@@ -23,6 +23,7 @@ v 8.13.0 (unreleased) ...@@ -23,6 +23,7 @@ v 8.13.0 (unreleased)
- Fix issue with page scrolling to top when closing or pinning sidebar (lukehowell) - Fix issue with page scrolling to top when closing or pinning sidebar (lukehowell)
- Add Issue Board API support (andrebsguedes) - Add Issue Board API support (andrebsguedes)
- Allow the Koding integration to be configured through the API - Allow the Koding integration to be configured through the API
- Add new issue button to each list on Issues Board
- Added soft wrap button to repository file/blob editor - Added soft wrap button to repository file/blob editor
- Add word-wrap to issue title on issue and milestone boards (ClemMakesApps) - Add word-wrap to issue title on issue and milestone boards (ClemMakesApps)
- Fix todos page mobile viewport layout (ClemMakesApps) - Fix todos page mobile viewport layout (ClemMakesApps)
......
...@@ -21,7 +21,8 @@ ...@@ -21,7 +21,8 @@
}, },
data () { data () {
return { return {
filters: Store.state.filters filters: Store.state.filters,
showIssueForm: false
}; };
}, },
watch: { watch: {
...@@ -33,6 +34,11 @@ ...@@ -33,6 +34,11 @@
deep: true deep: true
} }
}, },
methods: {
showNewIssueForm() {
this.showIssueForm = !this.showIssueForm;
}
},
ready () { ready () {
const options = gl.issueBoards.getBoardSortableDefaultOptions({ const options = gl.issueBoards.getBoardSortableDefaultOptions({
disabled: this.disabled, disabled: this.disabled,
......
//= require ./board_card //= require ./board_card
//= require ./board_new_issue
(() => { (() => {
const Store = gl.issueBoards.BoardsStore; const Store = gl.issueBoards.BoardsStore;
...@@ -8,14 +9,16 @@ ...@@ -8,14 +9,16 @@
gl.issueBoards.BoardList = Vue.extend({ gl.issueBoards.BoardList = Vue.extend({
components: { components: {
'board-card': gl.issueBoards.BoardCard 'board-card': gl.issueBoards.BoardCard,
'board-new-issue': gl.issueBoards.BoardNewIssue
}, },
props: { props: {
disabled: Boolean, disabled: Boolean,
list: Object, list: Object,
issues: Array, issues: Array,
loading: Boolean, loading: Boolean,
issueLinkBase: String issueLinkBase: String,
showIssueForm: Boolean
}, },
data () { data () {
return { return {
...@@ -73,7 +76,7 @@ ...@@ -73,7 +76,7 @@
group: 'issues', group: 'issues',
sort: false, sort: false,
disabled: this.disabled, disabled: this.disabled,
filter: '.board-list-count', filter: '.board-list-count, .is-disabled',
onStart: (e) => { onStart: (e) => {
const card = this.$refs.issue[e.oldIndex]; const card = this.$refs.issue[e.oldIndex];
......
(() => {
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 = '';
}
}
});
})();
...@@ -21,7 +21,7 @@ ...@@ -21,7 +21,7 @@
fallbackClass: 'is-dragging', fallbackClass: 'is-dragging',
fallbackOnBody: true, fallbackOnBody: true,
ghostClass: 'is-ghost', ghostClass: 'is-ghost',
filter: '.has-tooltip', filter: '.has-tooltip, .btn',
delay: gl.issueBoards.touchEnabled ? 100 : 0, delay: gl.issueBoards.touchEnabled ? 100 : 0,
scrollSensitivity: gl.issueBoards.touchEnabled ? 60 : 100, scrollSensitivity: gl.issueBoards.touchEnabled ? 60 : 100,
scrollSpeed: 20, scrollSpeed: 20,
......
...@@ -87,6 +87,17 @@ class List { ...@@ -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) { createIssues (data) {
data.forEach((issueObj) => { data.forEach((issueObj) => {
this.addIssue(new ListIssue(issueObj)); this.addIssue(new ListIssue(issueObj));
......
...@@ -58,4 +58,10 @@ class BoardService { ...@@ -58,4 +58,10 @@ class BoardService {
to_list_id to_list_id
}); });
} }
newIssue (id, issue) {
return this.issues.save({ id }, {
issue
});
}
}; };
...@@ -162,6 +162,10 @@ lex ...@@ -162,6 +162,10 @@ lex
list-style: none; list-style: none;
overflow-y: scroll; overflow-y: scroll;
overflow-x: hidden; overflow-x: hidden;
&.is-smaller {
height: calc(100% - 185px);
}
} }
.board-list-loading { .board-list-loading {
...@@ -233,3 +237,31 @@ lex ...@@ -233,3 +237,31 @@ lex
margin-right: 5px; 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;
}
}
...@@ -2,6 +2,7 @@ module Projects ...@@ -2,6 +2,7 @@ module Projects
module Boards module Boards
class IssuesController < Boards::ApplicationController class IssuesController < Boards::ApplicationController
before_action :authorize_read_issue!, only: [:index] before_action :authorize_read_issue!, only: [:index]
before_action :authorize_create_issue!, only: [:create]
before_action :authorize_update_issue!, only: [:update] before_action :authorize_update_issue!, only: [:update]
def index def index
...@@ -9,16 +10,23 @@ module Projects ...@@ -9,16 +10,23 @@ module Projects
issues = issues.page(params[:page]) issues = issues.page(params[:page])
render json: { render json: {
issues: issues.as_json( issues: serialize_as_json(issues),
only: [:iid, :title, :confidential],
include: {
assignee: { only: [:id, :name, :username], methods: [:avatar_url] },
labels: { only: [:id, :title, :description, :color, :priority], methods: [:text_color] }
}),
size: issues.total_count size: issues.total_count
} }
end 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 def update
service = ::Boards::Issues::MoveService.new(project, current_user, move_params) service = ::Boards::Issues::MoveService.new(project, current_user, move_params)
...@@ -43,6 +51,10 @@ module Projects ...@@ -43,6 +51,10 @@ module Projects
return render_403 unless can?(current_user, :read_issue, project) return render_403 unless can?(current_user, :read_issue, project)
end end
def authorize_create_issue!
return render_403 unless can?(current_user, :admin_issue, project)
end
def authorize_update_issue! def authorize_update_issue!
return render_403 unless can?(current_user, :update_issue, issue) return render_403 unless can?(current_user, :update_issue, issue)
end end
...@@ -54,6 +66,19 @@ module Projects ...@@ -54,6 +66,19 @@ module Projects
def move_params def move_params
params.permit(:id, :from_list_id, :to_list_id) params.permit(:id, :from_list_id, :to_list_id)
end 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 end
end end
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
...@@ -12,8 +12,17 @@ ...@@ -12,8 +12,17 @@
%header.board-header{ ":class" => "{ 'has-border': list.label }", ":style" => "{ borderTopColor: (list.label ? list.label.color : null) }" } %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) }" } %h3.board-title.js-board-handle{ ":class" => "{ 'user-can-drag': (!disabled && !list.preset) }" }
{{ list.title }} {{ list.title }}
%span.pull-right{ "v-if" => "list.type !== 'blank'" } .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 }} {{ 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) - if can?(current_user, :admin_list, @project)
%board-delete{ "inline-template" => true, %board-delete{ "inline-template" => true,
":list" => "list", ":list" => "list",
...@@ -26,12 +35,38 @@ ...@@ -26,12 +35,38 @@
":issues" => "list.issues", ":issues" => "list.issues",
":loading" => "list.loading", ":loading" => "list.loading",
":disabled" => "disabled", ":disabled" => "disabled",
":show-issue-form.sync" => "showIssueForm",
":issue-link-base" => "issueLinkBase" } ":issue-link-base" => "issueLinkBase" }
.board-list-loading.text-center{ "v-if" => "loading" } .board-list-loading.text-center{ "v-if" => "loading" }
= icon("spinner spin") = 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, %ul.board-list{ "v-el:list" => true,
"v-show" => "!loading", "v-show" => "!loading",
":data-board" => "list.id" } ":data-board" => "list.id",
":class" => "{ 'is-smaller': showIssueForm }" }
= render "projects/boards/components/card" = render "projects/boards/components/card"
%li.board-list-count.text-center{ "v-if" => "showCount" } %li.board-list-count.text-center{ "v-if" => "showCount" }
= icon("spinner spin", "v-show" => "list.loadingMore" ) = icon("spinner spin", "v-show" => "list.loadingMore" )
......
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
":issue-link-base" => "issueLinkBase", ":issue-link-base" => "issueLinkBase",
":disabled" => "disabled", ":disabled" => "disabled",
"track-by" => "id" } "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" } ":index" => "index" }
%h4.card-title %h4.card-title
= icon("eye-slash", class: "confidential-icon", "v-if" => "issue.confidential") = icon("eye-slash", class: "confidential-icon", "v-if" => "issue.confidential")
...@@ -15,7 +15,7 @@ ...@@ -15,7 +15,7 @@
":title" => "issue.title" } ":title" => "issue.title" }
{{ issue.title }} {{ issue.title }}
.card-footer .card-footer
%span.card-number %span.card-number{ "v-if" => "issue.id" }
= precede '#' do = precede '#' do
{{ issue.id }} {{ issue.id }}
%button.label.color-label.has-tooltip{ "v-for" => "label in issue.labels", %button.label.color-label.has-tooltip{ "v-for" => "label in issue.labels",
......
...@@ -425,7 +425,7 @@ resources :namespaces, path: '/', constraints: { id: /[a-zA-Z.0-9_\-]+/ }, only: ...@@ -425,7 +425,7 @@ resources :namespaces, path: '/', constraints: { id: /[a-zA-Z.0-9_\-]+/ }, only:
post :generate post :generate
end end
resources :issues, only: [:index] resources :issues, only: [:index, :create]
end end
end end
end end
......
...@@ -3,6 +3,7 @@ require 'spec_helper' ...@@ -3,6 +3,7 @@ require 'spec_helper'
describe Projects::Boards::IssuesController do describe Projects::Boards::IssuesController do
let(:project) { create(:project_with_board) } let(:project) { create(:project_with_board) }
let(:user) { create(:user) } let(:user) { create(:user) }
let(:guest) { create(:user) }
let(:planning) { create(:label, project: project, name: 'Planning') } let(:planning) { create(:label, project: project, name: 'Planning') }
let(:development) { create(:label, project: project, name: 'Development') } let(:development) { create(:label, project: project, name: 'Development') }
...@@ -12,6 +13,7 @@ describe Projects::Boards::IssuesController do ...@@ -12,6 +13,7 @@ describe Projects::Boards::IssuesController do
before do before do
project.team << [user, :master] project.team << [user, :master]
project.team << [guest, :guest]
end end
describe 'GET index' do describe 'GET index' do
...@@ -61,6 +63,60 @@ describe Projects::Boards::IssuesController do ...@@ -61,6 +63,60 @@ describe Projects::Boards::IssuesController do
end end
end end
describe 'POST create' do
context 'with valid params' do
it 'returns a successful 200 response' do
create_issue user: user, list: list1, title: 'New issue'
expect(response).to have_http_status(200)
end
it 'returns the created issue' do
create_issue user: user, list: list1, title: 'New issue'
expect(response).to match_response_schema('issue')
end
end
context 'with invalid params' do
context 'when title is nil' do
it 'returns an unprocessable entity 422 response' do
create_issue user: user, list: list1, title: nil
expect(response).to have_http_status(422)
end
end
context 'when list does not belongs to project board' do
it 'returns a not found 404 response' do
list = create(:list)
create_issue user: user, list: list, title: 'New issue'
expect(response).to have_http_status(404)
end
end
end
context 'with unauthorized user' do
it 'returns a forbidden 403 response' do
create_issue user: guest, list: list1, title: 'New issue'
expect(response).to have_http_status(403)
end
end
def create_issue(user:, list:, title:)
sign_in(user)
post :create, namespace_id: project.namespace.to_param,
project_id: project.to_param,
list_id: list.to_param,
issue: { title: title },
format: :json
end
end
describe 'PATCH update' do describe 'PATCH update' do
let(:issue) { create(:labeled_issue, project: project, labels: [planning]) } let(:issue) { create(:labeled_issue, project: project, labels: [planning]) }
...@@ -93,13 +149,7 @@ describe Projects::Boards::IssuesController do ...@@ -93,13 +149,7 @@ describe Projects::Boards::IssuesController do
end end
context 'with unauthorized user' do context 'with unauthorized user' do
let(:guest) { create(:user) } it 'returns a forbidden 403 response' do
before do
project.team << [guest, :guest]
end
it 'returns a successful 403 response' do
move user: guest, issue: issue, from_list_id: list1.id, to_list_id: list2.id move user: guest, issue: issue, from_list_id: list1.id, to_list_id: list2.id
expect(response).to have_http_status(403) expect(response).to have_http_status(403)
......
require 'rails_helper'
describe 'Issue Boards new issue', feature: true, js: true do
include WaitForAjax
include WaitForVueResource
let(:project) { create(:project_with_board, :public) }
let(:user) { create(:user) }
context 'authorized user' do
before do
project.team << [user, :master]
login_as(user)
visit namespace_project_board_path(project.namespace, project)
wait_for_vue_resource
expect(page).to have_selector('.board', count: 3)
end
it 'displays new issue button' do
expect(page).to have_selector('.board-issue-count-holder .btn', count: 1)
end
it 'does not display new issue button in done list' do
page.within('.board:nth-child(3)') do
expect(page).not_to have_selector('.board-issue-count-holder .btn')
end
end
it 'shows form when clicking button' do
page.within(first('.board')) do
find('.board-issue-count-holder .btn').click
expect(page).to have_selector('.board-new-issue-form')
end
end
it 'hides form when clicking cancel' do
page.within(first('.board')) do
find('.board-issue-count-holder .btn').click
expect(page).to have_selector('.board-new-issue-form')
click_button 'Cancel'
expect(page).to have_selector('.board-new-issue-form', visible: false)
end
end
it 'creates new issue' do
page.within(first('.board')) do
find('.board-issue-count-holder .btn').click
end
page.within(first('.board-new-issue-form')) do
find('.form-control').set('bug')
click_button 'Submit issue'
end
wait_for_vue_resource
page.within(first('.board .board-issue-count')) do
expect(page).to have_content('1')
end
end
end
context 'unauthorized user' do
before do
visit namespace_project_board_path(project.namespace, project)
wait_for_vue_resource
end
it 'does not display new issue button' do
expect(page).to have_selector('.board-issue-count-holder .btn', count: 0)
end
end
end
require 'spec_helper'
describe Boards::Issues::CreateService, services: true do
describe '#execute' do
let(:project) { create(:project_with_board) }
let(:board) { project.board }
let(:user) { create(:user) }
let(:label) { create(:label, project: project, name: 'in-progress') }
let!(:list) { create(:list, board: board, label: label, position: 0) }
subject(:service) { described_class.new(project, user, title: 'New issue') }
before do
project.team << [user, :developer]
end
it 'delegates the create proceedings to Issues::CreateService' do
expect_any_instance_of(Issues::CreateService).to receive(:execute).once
service.execute(list)
end
it 'creates a new issue' do
expect { service.execute(list) }.to change(project.issues, :count).by(1)
end
it 'adds the label of the list to the issue' do
issue = service.execute(list)
expect(issue.labels).to eq [label]
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment