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
This commit is contained in:
commit
9a13f885a9
16 changed files with 380 additions and 24 deletions
|
@ -23,6 +23,7 @@ v 8.13.0 (unreleased)
|
|||
- Fix issue with page scrolling to top when closing or pinning sidebar (lukehowell)
|
||||
- Add Issue Board API support (andrebsguedes)
|
||||
- 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
|
||||
- Add word-wrap to issue title on issue and milestone boards (ClemMakesApps)
|
||||
- Fix todos page mobile viewport layout (ClemMakesApps)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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];
|
||||
|
||||
|
|
|
@ -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 = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
})();
|
|
@ -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,
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -58,4 +58,10 @@ class BoardService {
|
|||
to_list_id
|
||||
});
|
||||
}
|
||||
|
||||
newIssue (id, issue) {
|
||||
return this.issues.save({ id }, {
|
||||
issue
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
16
app/services/boards/issues/create_service.rb
Normal file
16
app/services/boards/issues/create_service.rb
Normal file
|
@ -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
|
|
@ -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" )
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -425,7 +425,7 @@ resources :namespaces, path: '/', constraints: { id: /[a-zA-Z.0-9_\-]+/ }, only:
|
|||
post :generate
|
||||
end
|
||||
|
||||
resources :issues, only: [:index]
|
||||
resources :issues, only: [:index, :create]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -3,6 +3,7 @@ require 'spec_helper'
|
|||
describe Projects::Boards::IssuesController do
|
||||
let(:project) { create(:project_with_board) }
|
||||
let(:user) { create(:user) }
|
||||
let(:guest) { create(:user) }
|
||||
|
||||
let(:planning) { create(:label, project: project, name: 'Planning') }
|
||||
let(:development) { create(:label, project: project, name: 'Development') }
|
||||
|
@ -12,6 +13,7 @@ describe Projects::Boards::IssuesController do
|
|||
|
||||
before do
|
||||
project.team << [user, :master]
|
||||
project.team << [guest, :guest]
|
||||
end
|
||||
|
||||
describe 'GET index' do
|
||||
|
@ -61,6 +63,60 @@ describe Projects::Boards::IssuesController do
|
|||
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
|
||||
let(:issue) { create(:labeled_issue, project: project, labels: [planning]) }
|
||||
|
||||
|
@ -93,13 +149,7 @@ describe Projects::Boards::IssuesController do
|
|||
end
|
||||
|
||||
context 'with unauthorized user' do
|
||||
let(:guest) { create(:user) }
|
||||
|
||||
before do
|
||||
project.team << [guest, :guest]
|
||||
end
|
||||
|
||||
it 'returns a successful 403 response' do
|
||||
it 'returns a forbidden 403 response' do
|
||||
move user: guest, issue: issue, from_list_id: list1.id, to_list_id: list2.id
|
||||
|
||||
expect(response).to have_http_status(403)
|
||||
|
|
80
spec/features/boards/new_issue_spec.rb
Normal file
80
spec/features/boards/new_issue_spec.rb
Normal file
|
@ -0,0 +1,80 @@
|
|||
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
|
33
spec/services/boards/issues/create_service_spec.rb
Normal file
33
spec/services/boards/issues/create_service_spec.rb
Normal file
|
@ -0,0 +1,33 @@
|
|||
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
|
Loading…
Reference in a new issue