Commit ddd47d9e authored by Douwe Maan's avatar Douwe Maan

Merge branch 'bvl-license-check-issue-board-milestone' into 'master'

Check license for milestones on issue boards

Closes #2568

See merge request !2315
parents 907f91f4 4e5e21df
class Board < ActiveRecord::Base class Board < ActiveRecord::Base
prepend EE::Board
belongs_to :project belongs_to :project
belongs_to :milestone
has_many :lists, -> { order(:list_type, :position) }, dependent: :delete_all has_many :lists, -> { order(:list_type, :position) }, dependent: :delete_all
...@@ -13,26 +14,4 @@ class Board < ActiveRecord::Base ...@@ -13,26 +14,4 @@ class Board < ActiveRecord::Base
def closed_list def closed_list
lists.merge(List.closed).take lists.merge(List.closed).take
end end
def milestone
if milestone_id == Milestone::Upcoming.id
Milestone::Upcoming
else
super
end
end
def as_json(options = {})
milestone_attrs = options.fetch(:include, {})
.extract!(:milestone)
.dig(:milestone, :only)
super(options).tap do |json|
if milestone.present? && milestone_attrs.present?
json[:milestone] = milestone_attrs.each_with_object({}) do |attr, json|
json[attr] = milestone.public_send(attr)
end
end
end
end
end end
module EE
module Board
extend ActiveSupport::Concern
prepended do
belongs_to :milestone
end
def milestone
return nil unless project.feature_available?(:issue_board_milestone)
if milestone_id == ::Milestone::Upcoming.id
::Milestone::Upcoming
else
super
end
end
def as_json(options = {})
milestone_attrs = options.fetch(:include, {})
.extract!(:milestone)
.dig(:milestone, :only)
super(options).tap do |json|
if milestone.present? && milestone_attrs.present?
json[:milestone] = milestone_attrs.each_with_object({}) do |attr, json|
json[attr] = milestone.public_send(attr)
end
end
end
end
end
end
...@@ -12,6 +12,7 @@ class License < ActiveRecord::Base ...@@ -12,6 +12,7 @@ class License < ActiveRecord::Base
GEO_FEATURE = 'GitLab_Geo'.freeze GEO_FEATURE = 'GitLab_Geo'.freeze
ISSUABLE_DEFAULT_TEMPLATES_FEATURE = 'GitLab_IssuableDefaultTemplates'.freeze ISSUABLE_DEFAULT_TEMPLATES_FEATURE = 'GitLab_IssuableDefaultTemplates'.freeze
ISSUE_BOARDS_FOCUS_MODE_FEATURE = 'IssueBoardsFocusMode'.freeze ISSUE_BOARDS_FOCUS_MODE_FEATURE = 'IssueBoardsFocusMode'.freeze
ISSUE_BOARD_MILESTONE_FEATURE = 'IssueBoardMilestone'.freeze
ISSUE_WEIGHTS_FEATURE = 'GitLab_IssueWeights'.freeze ISSUE_WEIGHTS_FEATURE = 'GitLab_IssueWeights'.freeze
MERGE_REQUEST_APPROVERS_FEATURE = 'GitLab_MergeRequestApprovers'.freeze MERGE_REQUEST_APPROVERS_FEATURE = 'GitLab_MergeRequestApprovers'.freeze
MERGE_REQUEST_REBASE_FEATURE = 'GitLab_MergeRequestRebase'.freeze MERGE_REQUEST_REBASE_FEATURE = 'GitLab_MergeRequestRebase'.freeze
...@@ -38,6 +39,7 @@ class License < ActiveRecord::Base ...@@ -38,6 +39,7 @@ class License < ActiveRecord::Base
file_lock: FILE_LOCK_FEATURE, file_lock: FILE_LOCK_FEATURE,
issuable_default_templates: ISSUABLE_DEFAULT_TEMPLATES_FEATURE, issuable_default_templates: ISSUABLE_DEFAULT_TEMPLATES_FEATURE,
issue_board_focus_mode: ISSUE_BOARDS_FOCUS_MODE_FEATURE, issue_board_focus_mode: ISSUE_BOARDS_FOCUS_MODE_FEATURE,
issue_board_milestone: ISSUE_BOARD_MILESTONE_FEATURE,
issue_weights: ISSUE_WEIGHTS_FEATURE, issue_weights: ISSUE_WEIGHTS_FEATURE,
merge_request_approvers: MERGE_REQUEST_APPROVERS_FEATURE, merge_request_approvers: MERGE_REQUEST_APPROVERS_FEATURE,
merge_request_rebase: MERGE_REQUEST_REBASE_FEATURE, merge_request_rebase: MERGE_REQUEST_REBASE_FEATURE,
...@@ -58,6 +60,7 @@ class License < ActiveRecord::Base ...@@ -58,6 +60,7 @@ class License < ActiveRecord::Base
{ FAST_FORWARD_MERGE_FEATURE => 1 }, { FAST_FORWARD_MERGE_FEATURE => 1 },
{ ISSUABLE_DEFAULT_TEMPLATES_FEATURE => 1 }, { ISSUABLE_DEFAULT_TEMPLATES_FEATURE => 1 },
{ ISSUE_BOARDS_FOCUS_MODE_FEATURE => 1 }, { ISSUE_BOARDS_FOCUS_MODE_FEATURE => 1 },
{ ISSUE_BOARD_MILESTONE_FEATURE => 1 },
{ ISSUE_WEIGHTS_FEATURE => 1 }, { ISSUE_WEIGHTS_FEATURE => 1 },
{ MERGE_REQUEST_APPROVERS_FEATURE => 1 }, { MERGE_REQUEST_APPROVERS_FEATURE => 1 },
{ MERGE_REQUEST_REBASE_FEATURE => 1 }, { MERGE_REQUEST_REBASE_FEATURE => 1 },
...@@ -98,6 +101,7 @@ class License < ActiveRecord::Base ...@@ -98,6 +101,7 @@ class License < ActiveRecord::Base
{ GEO_FEATURE => 1 }, { GEO_FEATURE => 1 },
{ ISSUABLE_DEFAULT_TEMPLATES_FEATURE => 1 }, { ISSUABLE_DEFAULT_TEMPLATES_FEATURE => 1 },
{ ISSUE_BOARDS_FOCUS_MODE_FEATURE => 1 }, { ISSUE_BOARDS_FOCUS_MODE_FEATURE => 1 },
{ ISSUE_BOARD_MILESTONE_FEATURE => 1 },
{ ISSUE_WEIGHTS_FEATURE => 1 }, { ISSUE_WEIGHTS_FEATURE => 1 },
{ MERGE_REQUEST_APPROVERS_FEATURE => 1 }, { MERGE_REQUEST_APPROVERS_FEATURE => 1 },
{ MERGE_REQUEST_REBASE_FEATURE => 1 }, { MERGE_REQUEST_REBASE_FEATURE => 1 },
......
module Boards module Boards
class UpdateService < BaseService class UpdateService < BaseService
def execute(board) def execute(board)
board.update(name: params[:name], milestone_id: params[:milestone_id]) params.delete(:milestone_id) unless project.feature_available?(:issue_board_milestone)
board.update(params)
end end
end end
end end
...@@ -49,9 +49,10 @@ ...@@ -49,9 +49,10 @@
%li %li
%a{ "href" => "#", "@click.stop.prevent" => "showPage('edit')" } %a{ "href" => "#", "@click.stop.prevent" => "showPage('edit')" }
Edit board name Edit board name
%li - if @project.feature_available?(:issue_board_milestone, current_user)
%a{ "href" => "#", "@click.stop.prevent" => "showPage('milestone')" } %li
Edit board milestone %a{ "href" => "#", "@click.stop.prevent" => "showPage('milestone')" }
Edit board milestone
%li{ "v-if" => "showDelete" } %li{ "v-if" => "showDelete" }
%a.text-danger{ "href" => "#", "@click.stop.prevent" => "showPage('delete')" } %a.text-danger{ "href" => "#", "@click.stop.prevent" => "showPage('delete')" }
Delete board Delete board
...@@ -10,32 +10,33 @@ ...@@ -10,32 +10,33 @@
%input.form-control{ type: "text", %input.form-control{ type: "text",
id: "board-new-name", id: "board-new-name",
"v-model" => "board.name" } "v-model" => "board.name" }
.dropdown.board-inner-milestone-dropdown{ ":class" => "{ open: milestoneDropdownOpen }", - if @project.feature_available?(:issue_board_milestone, current_user)
"v-if" => "currentPage === 'new'" } .dropdown.board-inner-milestone-dropdown{ ":class" => "{ open: milestoneDropdownOpen }",
%label.label-light{ for: "board-milestone" } "v-if" => "currentPage === 'new'" }
Board milestone %label.label-light{ for: "board-milestone" }
%button.dropdown-menu-toggle.wide{ type: "button", Board milestone
"@click.stop.prevent" => "loadMilestones($event)" } %button.dropdown-menu-toggle.wide{ type: "button",
{{ milestoneToggleText }} "@click.stop.prevent" => "loadMilestones($event)" }
= icon("chevron-down") {{ milestoneToggleText }}
.dropdown-menu.dropdown-menu-selectable{ "v-if" => "milestoneDropdownOpen", = icon("chevron-down")
ref: "milestoneDropdown" } .dropdown-menu.dropdown-menu-selectable{ "v-if" => "milestoneDropdownOpen",
.dropdown-content ref: "milestoneDropdown" }
%ul .dropdown-content
%li{ "v-for" => "milestone in extraMilestones" } %ul
%a{ href: "#", %li{ "v-for" => "milestone in extraMilestones" }
":class" => "{ 'is-active': milestone.id === board.milestone_id }", %a{ href: "#",
"@click.stop.prevent" => "selectMilestone(milestone)" } ":class" => "{ 'is-active': milestone.id === board.milestone_id }",
{{ milestone.title }} "@click.stop.prevent" => "selectMilestone(milestone)" }
%li.divider {{ milestone.title }}
%li{ "v-for" => "milestone in milestones" } %li.divider
%a{ href: "#", %li{ "v-for" => "milestone in milestones" }
":class" => "{ 'is-active': milestone.id === board.milestone_id }", %a{ href: "#",
"@click.stop.prevent" => "selectMilestone(milestone)" } ":class" => "{ 'is-active': milestone.id === board.milestone_id }",
{{ milestone.title }} "@click.stop.prevent" => "selectMilestone(milestone)" }
= dropdown_loading {{ milestone.title }}
%span = dropdown_loading
Only show issues scheduled for the selected milestone %span
Only show issues scheduled for the selected milestone
%board-milestone-select{ "v-if" => "currentPage == 'milestone'", %board-milestone-select{ "v-if" => "currentPage == 'milestone'",
":milestone-path" => "milestonePath", ":milestone-path" => "milestonePath",
":select-milestone" => "selectMilestone", ":select-milestone" => "selectMilestone",
......
---
title: Add license checks for issue boards with milestones
merge_request: 2315
author:
...@@ -619,7 +619,8 @@ module API ...@@ -619,7 +619,8 @@ module API
expose :id expose :id
expose :name expose :name
expose :project, using: Entities::BasicProjectDetails expose :project, using: Entities::BasicProjectDetails
expose :milestone expose :milestone,
if: -> (board, _) { board.project.feature_available?(:issue_board_milestone) }
expose :lists, using: Entities::List do |board| expose :lists, using: Entities::List do |board|
board.lists.destroyable board.lists.destroyable
end end
......
...@@ -10,156 +10,193 @@ describe 'Board with milestone', :feature, :js do ...@@ -10,156 +10,193 @@ describe 'Board with milestone', :feature, :js do
before do before do
project.team << [user, :master] project.team << [user, :master]
gitlab_sign_in(user) login_as(user)
end end
context 'new board' do context 'with the feature enabled' do
before do before do
visit namespace_project_boards_path(project.namespace, project) stub_licensed_features(issue_board_milestone: true)
end end
it 'creates board with milestone' do context 'new board' do
create_board_with_milestone before do
visit namespace_project_boards_path(project.namespace, project)
end
it 'creates board with milestone' do
create_board_with_milestone
expect(find('.tokens-container')).to have_content(milestone.title) expect(find('.tokens-container')).to have_content(milestone.title)
wait_for_requests wait_for_requests
find('.card', match: :first) find('.card', match: :first)
expect(all('.board').last).to have_selector('.card', count: 1) expect(all('.board').last).to have_selector('.card', count: 1)
end
end end
end
context 'update board' do context 'update board' do
let!(:milestone_two) { create(:milestone, project: project) } let!(:milestone_two) { create(:milestone, project: project) }
let!(:board) { create(:board, project: project, milestone: milestone) } let!(:board) { create(:board, project: project, milestone: milestone) }
before do before do
visit namespace_project_boards_path(project.namespace, project) visit namespace_project_boards_path(project.namespace, project)
end end
it 'defaults milestone filter' do it 'defaults milestone filter' do
page.within '#js-multiple-boards-switcher' do page.within '#js-multiple-boards-switcher' do
find('.dropdown-menu-toggle').click find('.dropdown-menu-toggle').click
wait_for_requests wait_for_requests
click_link board.name click_link board.name
end end
expect(find('.tokens-container')).to have_content(milestone.title) expect(find('.tokens-container')).to have_content(milestone.title)
find('.card', match: :first) find('.card', match: :first)
expect(all('.board').last).to have_selector('.card', count: 1) expect(all('.board').last).to have_selector('.card', count: 1)
end end
it 'sets board to any milestone' do it 'sets board to any milestone' do
update_board_milestone('Any Milestone') update_board_milestone('Any Milestone')
expect(page).not_to have_css('.js-visual-token') expect(page).not_to have_css('.js-visual-token')
expect(find('.tokens-container')).not_to have_content(milestone.title) expect(find('.tokens-container')).not_to have_content(milestone.title)
find('.card', match: :first) find('.card', match: :first)
expect(page).to have_selector('.board', count: 3) expect(page).to have_selector('.board', count: 3)
expect(all('.board').last).to have_selector('.card', count: 2) expect(all('.board').last).to have_selector('.card', count: 2)
end end
it 'sets board to upcoming milestone' do it 'sets board to upcoming milestone' do
update_board_milestone('Upcoming') update_board_milestone('Upcoming')
expect(find('.tokens-container')).not_to have_content(milestone.title) expect(find('.tokens-container')).not_to have_content(milestone.title)
find('.board', match: :first) find('.board', match: :first)
expect(all('.board')[1]).to have_selector('.card', count: 0) expect(all('.board')[1]).to have_selector('.card', count: 0)
end end
it 'does not allow milestone in filter to be editted' do it 'does not allow milestone in filter to be editted' do
find('.filtered-search').native.send_keys(:backspace) find('.filtered-search').native.send_keys(:backspace)
page.within('.tokens-container') do page.within('.tokens-container') do
expect(page).to have_selector('.value') expect(page).to have_selector('.value')
end
end end
end
it 'does not render milestone in hint dropdown' do it 'does not render milestone in hint dropdown' do
find('.filtered-search').click find('.filtered-search').click
page.within('#js-dropdown-hint') do page.within('#js-dropdown-hint') do
expect(page).not_to have_button('Milestone') expect(page).not_to have_button('Milestone')
end
end end
end end
end
context 'removing issue from board' do context 'removing issue from board' do
let(:label) { create(:label, project: project) } let(:label) { create(:label, project: project) }
let!(:issue) { create(:labeled_issue, project: project, labels: [label], milestone: milestone) } let!(:issue) { create(:labeled_issue, project: project, labels: [label], milestone: milestone) }
let!(:board) { create(:board, project: project, milestone: milestone) } let!(:board) { create(:board, project: project, milestone: milestone) }
let!(:list) { create(:list, board: board, label: label, position: 0) } let!(:list) { create(:list, board: board, label: label, position: 0) }
before do before do
visit namespace_project_boards_path(project.namespace, project) visit namespace_project_boards_path(project.namespace, project)
end end
it 'removes issues milestone when removing from the board' do it 'removes issues milestone when removing from the board' do
wait_for_requests wait_for_requests
first('.card .card-number').click first('.card .card-number').click
click_button('Remove from board') click_button('Remove from board')
wait_for_requests wait_for_requests
expect(issue.reload.milestone).to be_nil expect(issue.reload.milestone).to be_nil
end
end end
end
context 'new issues' do context 'new issues' do
let(:label) { create(:label, project: project) } let(:label) { create(:label, project: project) }
let!(:list1) { create(:list, board: board, label: label, position: 0) } let!(:list1) { create(:list, board: board, label: label, position: 0) }
let!(:board) { create(:board, project: project, milestone: milestone) } let!(:board) { create(:board, project: project, milestone: milestone) }
let!(:issue) { create(:issue, project: project) } let!(:issue) { create(:issue, project: project) }
before do before do
visit namespace_project_boards_path(project.namespace, project) visit namespace_project_boards_path(project.namespace, project)
end end
it 'creates new issue with boards milestone' do it 'creates new issue with boards milestone' do
wait_for_requests wait_for_requests
page.within(first('.board')) do page.within(first('.board')) do
find('.btn-default').click find('.btn-default').click
find('.form-control').set('testing new issue with milestone') find('.form-control').set('testing new issue with milestone')
click_button('Submit issue') click_button('Submit issue')
wait_for_requests wait_for_requests
click_link('testing new issue with milestone') click_link('testing new issue with milestone')
end
expect(page).to have_content(milestone.title)
end end
expect(page).to have_content(milestone.title) it 'updates issue with milestone from add issues modal' do
wait_for_requests
click_button 'Add issues'
page.within('.add-issues-modal') do
card = find('.card', :first)
expect(page).to have_selector('.card', count: 1)
card.click
click_button 'Add 1 issue'
end
click_link(issue.title)
expect(page).to have_content(milestone.title)
end
end end
end
it 'updates issue with milestone from add issues modal' do context 'with the feature disabled' do
wait_for_requests before do
stub_licensed_features(issue_board_milestone: false)
visit namespace_project_boards_path(project.namespace, project)
end
click_button 'Add issues' it "doesn't show the input when creating a board" do
page.within '#js-multiple-boards-switcher' do
find('.dropdown-menu-toggle').click
page.within('.add-issues-modal') do click_link 'Create new board'
card = find('.card', :first)
expect(page).to have_selector('.card', count: 1)
card.click # To make sure the form is shown
expect(page).to have_selector('#board-new-name')
click_button 'Add 1 issue' expect(page).not_to have_button('Milestone')
end end
end
click_link(issue.title) it "doesn't show the option to edit the milestone" do
page.within '#js-multiple-boards-switcher' do
find('.dropdown-menu-toggle').click
# To make sure the dropdown is open
expect(page).to have_link('Edit board name')
expect(page).to have_content(milestone.title) expect(page).not_to have_link('Edit board milestone')
end
end end
end end
......
...@@ -12,27 +12,4 @@ describe Board do ...@@ -12,27 +12,4 @@ describe Board do
it { is_expected.to validate_presence_of(:name) } it { is_expected.to validate_presence_of(:name) }
it { is_expected.to validate_presence_of(:project) } it { is_expected.to validate_presence_of(:project) }
end end
describe 'milestone' do
subject { described_class.new }
it 'returns Milestone::Upcoming for upcoming milestone id' do
subject.milestone_id = Milestone::Upcoming.id
expect(subject.milestone).to eq Milestone::Upcoming
end
it 'returns milestone for valid milestone id' do
milestone = create(:milestone)
subject.milestone_id = milestone.id
expect(subject.milestone).to eq milestone
end
it 'returns nil for invalid milestone id' do
subject.milestone_id = -1
expect(subject.milestone).to be_nil
end
end
end end
require 'spec_helper'
describe Board do
describe 'milestone' do
subject(:board) { build(:board) }
context 'when the feature is available' do
before do
stub_licensed_features(issue_board_milestone: true)
end
it 'returns Milestone::Upcoming for upcoming milestone id' do
board.milestone_id = Milestone::Upcoming.id
expect(board.milestone).to eq Milestone::Upcoming
end
it 'returns milestone for valid milestone id' do
milestone = create(:milestone)
board.milestone_id = milestone.id
expect(board.milestone).to eq milestone
end
it 'returns nil for invalid milestone id' do
board.milestone_id = -1
expect(board.milestone).to be_nil
end
end
it 'returns nil when the feature is not available' do
stub_licensed_features(issue_board_milestone: false)
milestone = create(:milestone)
board.milestone_id = milestone.id
expect(board.milestone).to be_nil
end
end
end
...@@ -57,6 +57,24 @@ describe API::Boards do ...@@ -57,6 +57,24 @@ describe API::Boards do
expect(response).to match_response_schema('public_api/v4/boards') expect(response).to match_response_schema('public_api/v4/boards')
end end
end end
context 'with the issue_board_milestone-feature available' do
it 'returns the milestone when the `issue_board_milestone`-feature is enabled' do
stub_licensed_features(issue_board_milestone: true)
get api(base_url, user)
expect(json_response.first["milestone"]).not_to be_nil
end
it 'hides the milestone when the `issue_board_milestone`-feature is disabled' do
stub_licensed_features(issue_board_milestone: false)
get api(base_url, user)
expect(json_response.first["milestone"]).to be_nil
end
end
end end
describe "GET /projects/:id/boards/:board_id/lists" do describe "GET /projects/:id/boards/:board_id/lists" do
......
...@@ -24,5 +24,25 @@ describe Boards::UpdateService, services: true do ...@@ -24,5 +24,25 @@ describe Boards::UpdateService, services: true do
expect(service.execute(board)).to eq false expect(service.execute(board)).to eq false
end end
it 'udpates the milestone with issue board milestones enabled' do
stub_licensed_features(issue_board_milestone: true)
milestone = create(:milestone, project: project)
service = described_class.new(project, double, milestone_id: milestone.id)
service.execute(board)
expect(board.reload.milestone).to eq(milestone)
end
it 'udpates the milestone with the issue board milestones feature enabled' do
stub_licensed_features(issue_board_milestone: false)
milestone = create(:milestone, project: project)
service = described_class.new(project, double, milestone_id: milestone.id)
service.execute(board)
expect(board.reload.milestone).to be_nil
end
end 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