Commit 179cf7c7 authored by Sean McGivern's avatar Sean McGivern

Merge branch 'feature/multiple-issue-boards' into 'master'

Add ability to have multiple issue boards for a single project.

https://gitlab.com/gitlab-org/gitlab-ee/issues/929

https://gitlab.com/gitlab-org/gitlab-ee/issues/1084

See merge request !782
parents 22da02ab 8a69afda
......@@ -5,6 +5,7 @@ Please view this file on the master branch, on stable branches it's out of date.
- Add user activity table and service to query for active users
- Fix 500 error updating mirror URLs for projects
- Fix validations related to mirroring settings form. !773
- Add multiple issue boards. !782
- Fix Git access panel for Wikis when Kerberos authentication is enabled (Borja Aparicio)
- Decrease maximum time that GitLab waits for a mirror to finish !791 (Borja Aparicio)
- User groups (that can be assigned as approvers)
......
......@@ -6,6 +6,7 @@
//= require_tree ./services
//= require_tree ./mixins
//= require ./components/board
//= require ./components/boards_selector
//= require ./components/new_list_dropdown
//= require ./vue_resource_interceptor
......@@ -22,7 +23,8 @@ $(() => {
gl.IssueBoardsApp = new Vue({
el: $boardApp,
components: {
'board': gl.issueBoards.Board
'board': gl.issueBoards.Board,
'boards-selector': gl.issueBoards.BoardsSelector
},
data: {
state: Store.state,
......
(() => {
window.gl = window.gl || {};
window.gl.issueBoards = window.gl.issueBoards || {};
gl.issueBoards.BoardSelectorForm = Vue.extend({
props: {
type: String,
currentBoard: Object,
currentPage: String,
reload: Boolean
},
data () {
return {
board: {
id: false,
name: ''
}
};
},
ready () {
if (this.currentBoard && Object.keys(this.currentBoard).length) {
this.board = Vue.util.extend({}, this.currentBoard);
}
},
computed: {
buttonText () {
if (this.type === 'new') {
return 'Create';
} else {
return 'Save';
}
}
},
methods: {
submit () {
gl.boardService.createBoard(this.board)
.then(() => {
if (this.currentBoard) {
this.currentBoard.name = this.board.name;
}
// Enable the button thanks to our jQuery disabling it
$(this.$els.submitBtn).enable();
// Reset the selectors current page
this.currentPage = '';
this.reload = true;
});
}
}
});
})();
//= require ./board_new_form
(() => {
window.gl = window.gl || {};
window.gl.issueBoards = window.gl.issueBoards || {};
gl.issueBoards.BoardsSelector = Vue.extend({
components: {
'board-selector-form': gl.issueBoards.BoardSelectorForm
},
props: {
currentBoard: Object,
endpoint: String
},
data () {
return {
open: false,
loading: true,
boards: [],
currentPage: '',
reload: false
};
},
watch: {
reload () {
if (this.reload) {
this.boards = [];
this.loading = true;
this.reload = false;
this.loadBoards(false);
}
}
},
computed: {
showDelete () {
return this.boards.length > 1;
},
title () {
if (this.currentPage === 'edit') {
return 'Edit board';
} else if (this.currentPage === 'new') {
return 'Create new board';
} else if (this.currentPage === 'delete') {
return 'Delete board';
} else {
return 'Go to a board';
}
}
},
methods: {
showPage (page) {
this.currentPage = page;
},
toggleDropdown () {
this.open = !this.open;
},
loadBoards (toggleDropdown = true) {
if (toggleDropdown) {
this.toggleDropdown();
}
if (this.open && !this.boards.length) {
gl.boardService.allBoards().then((resp) => {
this.loading = false;
this.boards = resp.json();
});
}
}
}
});
})();
......@@ -2,6 +2,7 @@ class BoardService {
constructor (root, boardId) {
Vue.http.options.root = root;
this.boards = Vue.resource(`${root}{/id}.json`);
this.lists = Vue.resource(`${root}/${boardId}/lists{/id}`, {}, {
generate: {
method: 'POST',
......@@ -17,6 +18,18 @@ class BoardService {
});
}
allBoards () {
return this.boards.get();
}
createBoard (board) {
if (board.id) {
return this.boards.update({ id: board.id }, board);
} else {
return this.boards.save({}, board);
}
}
all () {
return this.lists.get();
}
......
......@@ -151,6 +151,10 @@
white-space: nowrap;
overflow: hidden;
&.text-danger {
color: $gl-danger;
}
&:hover,
&:focus,
&.is-focused {
......
lex
[v-cloak] {
display: none;
}
......@@ -28,7 +27,7 @@ lex
.dropdown-content {
max-height: 150px;
}
}
}
.issue-board-dropdown-content {
......@@ -56,7 +55,6 @@ lex
.boards-list {
height: calc(100vh - 152px);
width: 100%;
padding-top: 25px;
padding-bottom: 25px;
padding-right: ($gl-padding / 2);
padding-left: ($gl-padding / 2);
......@@ -64,9 +62,9 @@ lex
white-space: nowrap;
@media (min-width: $screen-sm-min) {
height: 475px; // Needed for PhantomJS
height: calc(100vh - 220px);
min-height: 475px;
height: 409px; // Needed for PhantomJS
height: calc(100vh - 290px);
min-height: 409px;
}
}
......@@ -265,3 +263,38 @@ lex
border-width: 1px 0 1px 1px;
}
}
.boards-title-holder {
padding: 25px 13px $gl-padding;
.dropdown-menu {
margin-top: -15px;
margin-left: 8px;
}
}
.boards-switcher {
padding: 0;
border: 0;
outline: 0;
background: none;
font-size: 19px;
font-weight: 600;
> .fa {
position: relative;
top: 2px;
margin-left: 5px;
}
}
.board-selector-page-two {
padding-left: 10px;
padding-right: 10px;
white-space: normal;
}
.board-delete-btns {
padding-top: 12px;
border-top: 1px solid $border-color;
}
......@@ -2,6 +2,8 @@ class Projects::BoardsController < Projects::ApplicationController
include IssuableCollections
before_action :authorize_read_board!, only: [:index, :show]
before_action :authorize_admin_board!, only: [:create, :update, :destroy]
before_action :find_board, only: [:show, :update, :destroy]
def index
@boards = ::Boards::ListService.new(project, current_user).execute
......@@ -15,8 +17,6 @@ class Projects::BoardsController < Projects::ApplicationController
end
def show
@board = project.boards.find(params[:id])
respond_to do |format|
format.html
format.json do
......@@ -25,13 +25,64 @@ class Projects::BoardsController < Projects::ApplicationController
end
end
def create
board = ::Boards::CreateService.new(project, current_user, board_params).execute
respond_to do |format|
format.json do
if board.valid?
render json: serialize_as_json(board)
else
render json: board.errors, status: :unprocessable_entity
end
end
end
end
def update
service = ::Boards::UpdateService.new(project, current_user, board_params)
service.execute(@board)
respond_to do |format|
format.json do
if @board.valid?
render json: serialize_as_json(@board)
else
render json: @board.errors, status: :unprocessable_entity
end
end
end
end
def destroy
service = ::Boards::DestroyService.new(project, current_user)
service.execute(@board)
respond_to do |format|
format.html { redirect_to namespace_project_boards_path(@project.namespace, @project) }
end
end
private
def authorize_admin_board!
return render_404 unless can?(current_user, :admin_board, project)
end
def authorize_read_board!
return access_denied! unless can?(current_user, :read_board, project)
return render_404 unless can?(current_user, :read_board, project)
end
def board_params
params.require(:board).permit(:name)
end
def find_board
@board = project.boards.find(params[:id])
end
def serialize_as_json(resource)
resource.as_json(only: [:id])
resource.as_json(only: [:id, :name])
end
end
......@@ -3,7 +3,7 @@ class Board < ActiveRecord::Base
has_many :lists, -> { order(:list_type, :position) }, dependent: :delete_all
validates :project, presence: true
validates :name, :project, presence: true
def backlog_list
lists.merge(List.backlog).take
......
......@@ -17,9 +17,6 @@ class Project < ActiveRecord::Base
extend Gitlab::ConfigHelper
class BoardLimitExceeded < StandardError; end
NUMBER_OF_PERMITTED_BOARDS = 1
UNKNOWN_IMPORT_URL = 'http://unknown.git'
cache_markdown_field :description, pipeline: :description
......@@ -64,7 +61,7 @@ class Project < ActiveRecord::Base
has_one :push_rule, dependent: :destroy
has_one :last_event, -> {order 'events.created_at DESC'}, class_name: 'Event', foreign_key: 'project_id'
has_many :boards, before_add: :validate_board_limit, dependent: :destroy
has_many :boards, dependent: :destroy
# Project services
has_many :services
......@@ -1613,8 +1610,4 @@ class Project < ActiveRecord::Base
shared_projects.any?
end
def validate_board_limit(board)
raise BoardLimitExceeded, 'Number of permitted boards exceeded' if boards.size >= NUMBER_OF_PERMITTED_BOARDS
end
end
......@@ -91,6 +91,7 @@ class ProjectPolicy < BasePolicy
can! :update_container_image
can! :create_environment
can! :create_deployment
can! :admin_board
end
def master_access!
......
module Boards
class CreateService < BaseService
def execute
if project.boards.empty?
create_board!
else
project.boards.first
end
end
board = project.boards.create(params)
private
def create_board!
board = project.boards.create
board.lists.create(list_type: :backlog)
board.lists.create(list_type: :done)
if board.persisted?
board.lists.create(list_type: :backlog)
board.lists.create(list_type: :done)
end
board
end
......
module Boards
class DestroyService < BaseService
def execute(board)
return false if project.boards.size == 1
board.destroy
end
end
end
module Boards
class UpdateService < BaseService
def execute(board)
board.update(name: params[:name])
end
end
end
%boards-selector{ "inline-template" => true,
":current-board" => board.to_json }
.boards-title-holder.dropdown
%button.boards-switcher{ "@click" => "loadBoards",
data: { toggle: "dropdown" } }
{{ currentBoard.name }}
= icon("caret-down")
.dropdown-menu{ ":class" => "{ 'is-loading': loading }" }
.dropdown-title
%button.dropdown-title-button.dropdown-menu-back{ type: "button",
aria: { label: "Go back" },
"@click.stop.prevent" => "showPage('')",
"v-if" => "currentPage !== ''" }
= icon("arrow-left")
{{ title }}
%button.dropdown-title-button.dropdown-menu-close{ type: "button",
aria: { label: "Close" } }
= icon("times", class: "dropdown-menu-close-icon")
.dropdown-content{ "v-if" => "currentPage === ''" }
%ul{ "v-if" => "!loading" }
%li{ "v-for" => "board in boards" }
%a{ ":href" => "'#{namespace_project_boards_path(@project.namespace, @project)}/' + board.id" }
{{ board.name }}
.dropdown-loading{ "v-if" => "loading" }
= icon("spin spinner")
%board-selector-form{ "inline-template" => true,
"v-if" => "currentPage === 'edit'",
"type" => "edit",
":current-board.sync" => "currentBoard",
":current-page.sync" => "currentPage",
":reload.sync" => "reload" }
= render "projects/boards/components/form"
%board-selector-form{ "inline-template" => true,
"v-if" => "currentPage === 'new'",
"type" => "new",
":current-page.sync" => "currentPage",
":reload.sync" => "reload" }
= render "projects/boards/components/form"
.dropdown-content.board-selector-page-two{ "v-if" => "currentPage === 'delete'" }
%p
Are you sure you want to delete this board?
.board-delete-btns.clearfix
= link_to "",
class: "btn btn-danger pull-left",
method: :delete,
":href" => "'#{namespace_project_boards_path(@project.namespace, @project)}/' + currentBoard.id" do
Delete
%button.btn.btn-default.pull-right{ type: "button",
"@click.stop.prevent" => "currentPage = ''" }
Cancel
.dropdown-footer{ "v-if" => "currentPage === ''" }
%ul.dropdown-footer-list
%li
%a{ "href": "#", "@click.stop.prevent" => "showPage('new')" }
Create new board
%li
%a{ "href": "#", "@click.stop.prevent" => "showPage('edit')" }
Edit board name
%li{ "v-if" => "showDelete" }
%a.text-danger{ "href": "#", "@click.stop.prevent" => "showPage('delete')" }
Delete board
.dropdown-content.board-selector-page-two
%form{ "@submit.prevent" => "submit" }
%label.label-light{ for: "board-new-name" }
Board name
%input.form-control{ type: "text",
id: "board-new-name",
"v-model" => "board.name" }
.clearfix.prepend-top-10
%button.btn.btn-primary.pull-left{ type: "submit",
":disabled" => "board.name === ''",
"v-el:submit-btn" => true }
{{ buttonText }}
%button.btn.btn-default.pull-right{ type: "button",
"@click.stop.prevent" => "currentPage = ''" }
Cancel
......@@ -10,7 +10,9 @@
= render 'shared/issuable/filter', type: :boards
.boards-list#board-app{ "v-cloak" => true, data: board_data }
#board-app{ "v-cloak" => true, data: board_data }
= render "title", board: @boards.first
.boards-app-loading.text-center{ "v-if" => "loading" }
= icon("spinner spin")
= render "projects/boards/components/board"
.boards-list
= render "projects/boards/components/board"
......@@ -10,7 +10,9 @@
= render 'shared/issuable/filter', type: :boards
.boards-list#board-app{ "v-cloak" => true, data: board_data }
#board-app{ "v-cloak" => true, data: board_data }
= render "title", board: @board
.boards-app-loading.text-center{ "v-if" => "loading" }
= icon("spinner spin")
= render "projects/boards/components/board"
.boards-list
= render "projects/boards/components/board"
......@@ -459,7 +459,7 @@ resources :namespaces, path: '/', constraints: { id: /[a-zA-Z.0-9_\-]+/ }, only:
end
end
resources :boards, only: [:index, :show] do
resources :boards, only: [:index, :show, :create, :update, :destroy] do
scope module: :boards do
resources :issues, only: [:update]
......
class AddNameToBoards < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
add_column_with_default :boards, :name, :string, default: 'Development'
end
def down
remove_column :boards, :name
end
end
......@@ -167,6 +167,7 @@ ActiveRecord::Schema.define(version: 20161007133303) do
t.integer "project_id", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.string "name", default: "Development", null: false
end
add_index "boards", ["project_id"], name: "index_boards_on_project_id", using: :btree
......
......@@ -22,13 +22,7 @@ module API
segment ':id/boards/:board_id' do
helpers do
def project_board
board = user_project.boards.first
if params[:board_id] == board.id
board
else
not_found!('Board')
end
user_project.boards.find(params[:board_id])
end
def board_lists
......@@ -92,12 +86,12 @@ module API
requires :position, type: Integer, desc: 'The position of the list'
end
put '/lists/:list_id' do
list = project_board.lists.movable.find(params[:list_id])
list = board_lists.find(params[:list_id])
authorize!(:admin_list, user_project)
service = ::Boards::Lists::MoveService.new(user_project, current_user,
{ position: params[:position] })
{ position: params[:position].to_i })
if service.execute(list)
present list, with: Entities::List
......
......@@ -106,6 +106,22 @@ describe Projects::Boards::IssuesController do
expect(response).to have_http_status(404)
end
end
context 'with invalid board id' do
it 'returns a not found 404 response' do
create_issue user: user, board: 999, list: list1, title: 'New issue'
expect(response).to have_http_status(404)
end
end
context 'with invalid list id' do
it 'returns a not found 404 response' do
create_issue user: user, board: board, list: 999, title: 'New issue'
expect(response).to have_http_status(404)
end
end
end
context 'with unauthorized user' do
......
......@@ -21,6 +21,20 @@ describe Projects::BoardsController do
expect(response).to render_template :index
expect(response.content_type).to eq 'text/html'
end
context 'with unauthorized user' do
before do
allow(Ability).to receive(:allowed?).with(user, :read_project, project).and_return(true)
allow(Ability).to receive(:allowed?).with(user, :read_board, project).and_return(false)
end
it 'returns a not found 404 response' do
list_boards
expect(response).to have_http_status(404)
expect(response.content_type).to eq 'text/html'
end
end
end
context 'when format is JSON' do
......@@ -34,18 +48,19 @@ describe Projects::BoardsController do
expect(response).to match_response_schema('boards')
expect(parsed_response.length).to eq 2
end
end
context 'with unauthorized user' do
before do
allow(Ability).to receive(:allowed?).with(user, :read_project, project).and_return(true)
allow(Ability).to receive(:allowed?).with(user, :read_board, project).and_return(false)
end
context 'with unauthorized user' do
before do
allow(Ability).to receive(:allowed?).with(user, :read_project, project).and_return(true)
allow(Ability).to receive(:allowed?).with(user, :read_board, project).and_return(false)
end
it 'returns a not found 404 response' do
list_boards
it 'returns a not found 404 response' do
list_boards format: :json
expect(response).to have_http_status(404)
expect(response).to have_http_status(404)
expect(response.content_type).to eq 'application/json'
end
end
end
......@@ -66,6 +81,20 @@ describe Projects::BoardsController do
expect(response).to render_template :show
expect(response.content_type).to eq 'text/html'
end
context 'with unauthorized user' do
before do
allow(Ability).to receive(:allowed?).with(user, :read_project, project).and_return(true)
allow(Ability).to receive(:allowed?).with(user, :read_board, project).and_return(false)
end
it 'returns a not found 404 response' do
read_board board: board
expect(response).to have_http_status(404)
expect(response.content_type).to eq 'text/html'
end
end
end
context 'when format is JSON' do
......@@ -74,18 +103,19 @@ describe Projects::BoardsController do
expect(response).to match_response_schema('board')
end
end
context 'with unauthorized user' do
before do
allow(Ability).to receive(:allowed?).with(user, :read_project, project).and_return(true)
allow(Ability).to receive(:allowed?).with(user, :read_board, project).and_return(false)
end
context 'with unauthorized user' do
before do
allow(Ability).to receive(:allowed?).with(user, :read_project, project).and_return(true)
allow(Ability).to receive(:allowed?).with(user, :read_board, project).and_return(false)
end
it 'returns a not found 404 response' do
read_board board: board
it 'returns a not found 404 response' do
read_board board: board, format: :json
expect(response).to have_http_status(404)
expect(response).to have_http_status(404)
expect(response.content_type).to eq 'application/json'
end
end
end
......@@ -106,4 +136,150 @@ describe Projects::BoardsController do
format: format
end
end
describe 'POST create' do
context 'with valid params' do
it 'returns a successful 200 response' do
create_board name: 'Backend'
expect(response).to have_http_status(200)
end
it 'returns the created board' do
create_board name: 'Backend'
expect(response).to match_response_schema('board')
end
end
context 'with invalid params' do
it 'returns an unprocessable entity 422 response' do
create_board name: nil
expect(response).to have_http_status(422)
end
end
context 'with unauthorized user' do
before do
allow(Ability).to receive(:allowed?).with(user, :read_project, project).and_return(true)
allow(Ability).to receive(:allowed?).with(user, :admin_board, project).and_return(false)
end
it 'returns a not found 404 response' do
create_board name: 'Backend'
expect(response.content_type).to eq 'application/json'
expect(response).to have_http_status(404)
end
end
def create_board(name:)
post :create, namespace_id: project.namespace.to_param,
project_id: project.to_param,
board: { name: name },
format: :json
end
end
describe 'PATCH update' do
let(:board) { create(:board, project: project, name: 'Backend') }
context 'with valid params' do
it 'returns a successful 200 response' do
update_board board: board, name: 'Frontend'
expect(response).to have_http_status(200)
end
it 'returns the updated board' do
update_board board: board, name: 'Frontend'
expect(response).to match_response_schema('board')
end
end
context 'with invalid params' do
it 'returns an unprocessable entity 422 response' do
update_board board: board, name: nil
expect(response).to have_http_status(422)
end
end
context 'with invalid board id' do
it 'returns a not found 404 response' do
update_board board: 999, name: 'Frontend'
expect(response).to have_http_status(404)
end
end
context 'with unauthorized user' do
before do
allow(Ability).to receive(:allowed?).with(user, :read_project, project).and_return(true)
allow(Ability).to receive(:allowed?).with(user, :admin_board, project).and_return(false)
end
it 'returns a not found 404 response' do
update_board board: board, name: 'Backend'
expect(response.content_type).to eq 'application/json'
expect(response).to have_http_status(404)
end
end
def update_board(board:, name:)
patch :update, namespace_id: project.namespace.to_param,
project_id: project.to_param,
id: board.to_param,
board: { name: name },
format: :json
end
end
describe 'DELETE destroy' do
let!(:boards) { create_pair(:board, project: project) }
let(:board) { project.boards.first }
context 'with valid board id' do
it 'redirects to the issue boards page' do
remove_board board: board
expect(response).to redirect_to(namespace_project_boards_path(project.namespace, project))
end
it 'removes board from project' do
expect { remove_board board: board }.to change(project.boards, :size).by(-1)
end
end
context 'with invalid board id' do
it 'returns a not found 404 response' do
remove_board board: 999
expect(response).to have_http_status(404)
end
end
context 'with unauthorized user' do
before do
allow(Ability).to receive(:allowed?).with(user, :read_project, project).and_return(true)
allow(Ability).to receive(:allowed?).with(user, :admin_board, project).and_return(false)
end
it 'returns a not found 404 response' do
remove_board board: board
expect(response).to have_http_status(404)
end
end
def remove_board(board:)
delete :destroy, namespace_id: project.namespace.to_param,
project_id: project.to_param,
id: board.to_param,
format: :html
end
end
end
FactoryGirl.define do
factory :board do
sequence(:name) { |n| "board#{n}" }
project factory: :empty_project
after(:create) do |board|
......
......@@ -5,7 +5,7 @@ describe 'Issue Boards', feature: true, js: true do
include WaitForVueResource
let(:project) { create(:empty_project, :public) }
let(:board) { create(:board, project: project) }
let!(:board) { create(:board, project: project) }
let(:user) { create(:user) }
let!(:user2) { create(:user) }
......@@ -18,7 +18,7 @@ describe 'Issue Boards', feature: true, js: true do
context 'no lists' do
before do
visit namespace_project_board_path(project.namespace, project, board)
visit namespace_project_boards_path(project.namespace, project)
wait_for_vue_resource
expect(page).to have_selector('.board', count: 3)
end
......@@ -76,7 +76,7 @@ describe 'Issue Boards', feature: true, js: true do
let!(:issue9) { create(:labeled_issue, project: project, labels: [testing, bug, accepting]) }
before do
visit namespace_project_board_path(project.namespace, project, board)
visit namespace_project_boards_path(project.namespace, project)
wait_for_vue_resource
......@@ -170,7 +170,7 @@ describe 'Issue Boards', feature: true, js: true do
create(:issue, project: project)
end
visit namespace_project_board_path(project.namespace, project, board)
visit namespace_project_boards_path(project.namespace, project)
wait_for_vue_resource
page.within(find('.board', match: :first)) do
......@@ -604,7 +604,7 @@ describe 'Issue Boards', feature: true, js: true do
context 'keyboard shortcuts' do
before do
visit namespace_project_board_path(project.namespace, project, board)
visit namespace_project_boards_path(project.namespace, project)
wait_for_vue_resource
end
......@@ -617,7 +617,7 @@ describe 'Issue Boards', feature: true, js: true do
context 'signed out user' do
before do
logout
visit namespace_project_board_path(project.namespace, project, board)
visit namespace_project_boards_path(project.namespace, project)
wait_for_vue_resource
end
......@@ -633,7 +633,7 @@ describe 'Issue Boards', feature: true, js: true do
project.team << [user_guest, :guest]
logout
login_as(user_guest)
visit namespace_project_board_path(project.namespace, project, board)
visit namespace_project_boards_path(project.namespace, project)
wait_for_vue_resource
end
......
require 'rails_helper'
describe 'Multiple Issue Boards', feature: true, js: true do
include WaitForAjax
include WaitForVueResource
let(:user) { create(:user) }
let(:project) { create(:empty_project, :public) }
let!(:planning) { create(:label, project: project, name: 'Planning') }
let!(:board) { create(:board, project: project) }
let!(:board2) { create(:board, project: project) }
before do
project.team << [user, :master]
login_as(user)
visit namespace_project_boards_path(project.namespace, project)
wait_for_vue_resource
end
it 'shows current board name' do
page.within('.boards-switcher') do
expect(page).to have_content(board.name)
end
end
it 'shows a list of boards' do
click_button board.name
page.within('.boards-title-holder .dropdown-menu') do
expect(page).to have_content(board.name)
expect(page).to have_content(board2.name)
end
end
it 'switches current board' do
click_button board.name
page.within('.boards-title-holder .dropdown-menu') do
click_link board2.name
end
wait_for_vue_resource
page.within('.boards-switcher') do
expect(page).to have_content(board2.name)
end
end
it 'creates new board' do
click_button board.name
page.within('.boards-title-holder .dropdown-menu') do
click_link 'Edit board name'
fill_in 'board-new-name', with: 'Testing'
click_button 'Save'
end
wait_for_vue_resource
page.within('.boards-title-holder .dropdown-menu') do
expect(page).to have_content('Testing')
end
end
it 'edits board name' do
click_button board.name
page.within('.boards-title-holder .dropdown-menu') do
click_link 'Edit board name'
fill_in 'board-new-name', with: 'Testing'
click_button 'Save'
end
wait_for_vue_resource
page.within('.boards-title-holder .dropdown-menu') do
expect(page).to have_content('Testing')
end
end
it 'deletes board' do
click_button board.name
wait_for_vue_resource
page.within('.boards-title-holder .dropdown-menu') do
click_link 'Delete board'
page.within('.dropdown-title') do
expect(page).to have_content('Delete board')
end
click_link 'Delete'
end
click_button board2.name
page.within('.boards-title-holder .dropdown-menu') do
expect(page).not_to have_content(board.name)
expect(page).to have_content(board2.name)
end
end
it 'adds a list to the none default board' do
click_button board.name
page.within('.boards-title-holder .dropdown-menu') do
click_link board2.name
end
wait_for_vue_resource
page.within('.boards-switcher') do
expect(page).to have_content(board2.name)
end
click_button 'Create new list'
wait_for_ajax
page.within '.dropdown-menu-issues-board-new' do
click_link planning.title
end
wait_for_vue_resource
expect(page).to have_selector('.board', count: 3)
click_button board2.name
page.within('.boards-title-holder .dropdown-menu') do
click_link board.name
end
wait_for_vue_resource
expect(page).to have_selector('.board', count: 2)
end
end
{
"type": "object",
"required" : [
"id"
"id",
"name"
],
"properties" : {
"id": { "type": "integer" },
......
......@@ -7,6 +7,7 @@ describe Board do
end
describe 'validations' do
it { is_expected.to validate_presence_of(:name) }
it { is_expected.to validate_presence_of(:project) }
end
end
......@@ -97,15 +97,6 @@ describe Project, models: true do
end
end
end
describe '#boards' do
it 'raises an error when attempting to add more than one board to the project' do
subject.boards.build
expect { subject.boards.build }.to raise_error(Project::BoardLimitExceeded, 'Number of permitted boards exceeded')
expect(subject.boards.size).to eq 1
end
end
end
describe 'modules' do
......
......@@ -51,7 +51,7 @@ describe API::API, api: true do
end
context "when authenticated" do
it "returns the project issue board" do
it "returns the project issue boards" do
get api(base_url, user)
expect(response).to have_http_status(200)
......
......@@ -4,14 +4,14 @@ describe Boards::CreateService, services: true do
describe '#execute' do
let(:project) { create(:empty_project) }
subject(:service) { described_class.new(project, double) }
context 'with valid params' do
subject(:service) { described_class.new(project, double, name: 'Backend') }
context 'when project does not have a board' do
it 'creates a new board' do
expect { service.execute }.to change(Board, :count).by(1)
it 'creates a new project board' do
expect { service.execute }.to change(project.boards, :count).by(1)
end
it 'creates default lists' do
it "creates board's default lists" do
board = service.execute
expect(board.lists.size).to eq 2
......@@ -20,14 +20,34 @@ describe Boards::CreateService, services: true do
end
end
context 'when project has a board' do
before do
create(:board, project: project)
end
context 'with invalid params' do
subject(:service) { described_class.new(project, double, name: nil) }
it 'does not create a new board' do
it 'does not create a new project board' do
expect { service.execute }.not_to change(project.boards, :count)
end
it "does not create board's default lists" do
board = service.execute
expect(board.lists.size).to eq 0
end
end
context 'without params' do
subject(:service) { described_class.new(project, double) }
it 'creates a new project board' do
expect { service.execute }.to change(project.boards, :count).by(1)
end
it "creates board's default lists" do
board = service.execute
expect(board.lists.size).to eq 2
expect(board.lists.first).to be_backlog
expect(board.lists.last).to be_done
end
end
end
end
require 'spec_helper'
describe Boards::DestroyService, services: true do
describe '#execute' do
let(:project) { create(:empty_project) }
let!(:board) { create(:board, project: project) }
subject(:service) { described_class.new(project, double) }
context 'when project have more than one board' do
it 'removes board from project' do
create(:board, project: project)
expect { service.execute(board) }.to change(project.boards, :count).by(-1)
end
end
context 'when project have one board' do
it 'does not remove board from project' do
expect { service.execute(board) }.not_to change(project.boards, :count)
end
end
end
end
......@@ -13,10 +13,10 @@ describe Boards::Issues::ListService, services: true do
let(:p2) { create(:label, title: 'P2', project: project, priority: 2) }
let(:p3) { create(:label, title: 'P3', project: project, priority: 3) }
let!(:backlog) { create(:backlog_list, board: board) }
let!(:backlog) { board.backlog_list }
let!(:list1) { create(:list, board: board, label: development, position: 0) }
let!(:list2) { create(:list, board: board, label: testing, position: 1) }
let!(:done) { create(:done_list, board: board) }
let!(:done) { board.done_list }
let!(:opened_issue1) { create(:labeled_issue, project: project, labels: [bug]) }
let!(:opened_issue2) { create(:labeled_issue, project: project, labels: [p2]) }
......
......@@ -10,10 +10,10 @@ describe Boards::Issues::MoveService, services: true do
let(:development) { create(:label, project: project, name: 'Development') }
let(:testing) { create(:label, project: project, name: 'Testing') }
let!(:backlog) { create(:backlog_list, board: board1) }
let!(:backlog) { board1.backlog_list }
let!(:list1) { create(:list, board: board1, label: development, position: 0) }
let!(:list2) { create(:list, board: board1, label: testing, position: 1) }
let!(:done) { create(:done_list, board: board1) }
let!(:done) { board1.done_list }
before do
project.team << [user, :developer]
......
......@@ -29,9 +29,10 @@ describe Boards::ListService, services: true do
end
it 'returns project boards' do
board = create(:board, project: project)
board1 = create(:board, project: project)
board2 = create(:board, project: project)
expect(service.execute).to match_array [board]
expect(service.execute).to match_array [board1, board2]
end
end
end
......@@ -6,12 +6,12 @@ describe Boards::Lists::MoveService, services: true do
let(:board) { create(:board, project: project) }
let(:user) { create(:user) }
let!(:backlog) { create(:backlog_list, board: board) }
let!(:backlog) { board.backlog_list }
let!(:planning) { create(:list, board: board, position: 0) }
let!(:development) { create(:list, board: board, position: 1) }
let!(:review) { create(:list, board: board, position: 2) }
let!(:staging) { create(:list, board: board, position: 3) }
let!(:done) { create(:done_list, board: board) }
let!(:done) { board.done_list }
context 'when list type is set to label' do
it 'keeps position of lists when new position is nil' do
......
require 'spec_helper'
describe Boards::UpdateService, services: true do
describe '#execute' do
let(:project) { create(:empty_project) }
let!(:board) { create(:board, project: project, name: 'Backend') }
it "updates board's name" do
service = described_class.new(project, double, name: 'Engineering')
service.execute(board)
expect(board).to have_attributes(name: 'Engineering')
end
it 'returns true with valid params' do
service = described_class.new(project, double, name: 'Engineering')
expect(service.execute(board)).to eq true
end
it 'returns false with invalid params' do
service = described_class.new(project, double, name: nil)
expect(service.execute(board)).to eq false
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