Commit ebe29877 authored by Vitaly Slobodin's avatar Vitaly Slobodin

Merge branch '294043-add-an-epics-board-index-page' into 'master'

[RUN-AS-IF-FOSS] Epic Boards index page

See merge request gitlab-org/gitlab!53100
parents 1e8e7efc 47dfc12f
...@@ -34,14 +34,24 @@ module BoardsActions ...@@ -34,14 +34,24 @@ module BoardsActions
def boards def boards
strong_memoize(:boards) do strong_memoize(:boards) do
Boards::ListService.new(parent, current_user).execute existing_boards = boards_finder.execute
if existing_boards.any?
existing_boards
else
# if no board exists, create one
[board_create_service.execute.payload]
end
end end
end end
def board def board
strong_memoize(:board) do strong_memoize(:board) do
boards.find(params[:id]) board_finder.execute.first
end
end end
def board_type
board_klass.to_type
end end
def serializer def serializer
......
...@@ -65,6 +65,7 @@ module MultipleBoardsActions ...@@ -65,6 +65,7 @@ module MultipleBoardsActions
private private
def redirect_to_recent_board def redirect_to_recent_board
return unless board_type == Board.to_type
return if request.format.json? || !parent.multiple_issue_boards_available? || !latest_visited_board return if request.format.json? || !parent.multiple_issue_boards_available? || !latest_visited_board
redirect_to board_path(latest_visited_board.board) redirect_to board_path(latest_visited_board.board)
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
class Groups::BoardsController < Groups::ApplicationController class Groups::BoardsController < Groups::ApplicationController
include BoardsActions include BoardsActions
include RecordUserLastActivity include RecordUserLastActivity
include Gitlab::Utils::StrongMemoize
before_action :authorize_read_board!, only: [:index, :show] before_action :authorize_read_board!, only: [:index, :show]
before_action :assign_endpoint_vars before_action :assign_endpoint_vars
...@@ -14,6 +15,28 @@ class Groups::BoardsController < Groups::ApplicationController ...@@ -14,6 +15,28 @@ class Groups::BoardsController < Groups::ApplicationController
private private
def board_klass
Board
end
def boards_finder
strong_memoize :boards_finder do
Boards::ListService.new(parent, current_user)
end
end
def board_finder
strong_memoize :board_finder do
Boards::ListService.new(parent, current_user, board_id: params[:id])
end
end
def board_create_service
strong_memoize :board_create_service do
Boards::CreateService.new(parent, current_user)
end
end
def assign_endpoint_vars def assign_endpoint_vars
@boards_endpoint = group_boards_path(group) @boards_endpoint = group_boards_path(group)
@namespace_path = group.to_param @namespace_path = group.to_param
......
...@@ -15,6 +15,28 @@ class Projects::BoardsController < Projects::ApplicationController ...@@ -15,6 +15,28 @@ class Projects::BoardsController < Projects::ApplicationController
private private
def board_klass
Board
end
def boards_finder
strong_memoize :boards_finder do
Boards::ListService.new(parent, current_user)
end
end
def board_finder
strong_memoize :board_finder do
Boards::ListService.new(parent, current_user, board_id: params[:id])
end
end
def board_create_service
strong_memoize :board_create_service do
Boards::CreateService.new(parent, current_user)
end
end
def assign_endpoint_vars def assign_endpoint_vars
@boards_endpoint = project_boards_path(project) @boards_endpoint = project_boards_path(project)
@bulk_issues_path = bulk_update_project_issues_path(project) @bulk_issues_path = bulk_update_project_issues_path(project)
......
...@@ -13,7 +13,7 @@ module Resolvers ...@@ -13,7 +13,7 @@ module Resolvers
def resolve(id: nil) def resolve(id: nil)
return unless parent return unless parent
::Boards::ListService.new(parent, context[:current_user], board_id: extract_board_id(id)).execute(create_default_board: false).first ::Boards::ListService.new(parent, context[:current_user], board_id: extract_board_id(id)).execute.first
rescue ActiveRecord::RecordNotFound rescue ActiveRecord::RecordNotFound
nil nil
end end
......
...@@ -16,7 +16,7 @@ module Resolvers ...@@ -16,7 +16,7 @@ module Resolvers
return Board.none unless parent return Board.none unless parent
::Boards::ListService.new(parent, context[:current_user], board_id: extract_board_id(id)).execute(create_default_board: false) ::Boards::ListService.new(parent, context[:current_user], board_id: extract_board_id(id)).execute
rescue ActiveRecord::RecordNotFound rescue ActiveRecord::RecordNotFound
Board.none Board.none
end end
......
...@@ -21,7 +21,8 @@ module BoardsHelper ...@@ -21,7 +21,8 @@ module BoardsHelper
group_id: @group&.id, group_id: @group&.id,
labels_filter_base_path: build_issue_link_base, labels_filter_base_path: build_issue_link_base,
labels_fetch_path: labels_fetch_path, labels_fetch_path: labels_fetch_path,
labels_manage_path: labels_manage_path labels_manage_path: labels_manage_path,
board_type: board.to_type
} }
end end
......
...@@ -44,6 +44,14 @@ class Board < ApplicationRecord ...@@ -44,6 +44,14 @@ class Board < ApplicationRecord
def scoped? def scoped?
false false
end end
def self.to_type
name.demodulize
end
def to_type
self.class.to_type
end
end end
Board.prepend_if_ee('EE::Board') Board.prepend_if_ee('EE::Board')
...@@ -2,9 +2,7 @@ ...@@ -2,9 +2,7 @@
module Boards module Boards
class ListService < Boards::BaseService class ListService < Boards::BaseService
def execute(create_default_board: true) def execute
create_board! if create_default_board && parent.boards.empty?
find_boards find_boards
end end
...@@ -18,10 +16,6 @@ module Boards ...@@ -18,10 +16,6 @@ module Boards
parent.boards.first_board parent.boards.first_board
end end
def create_board!
Boards::CreateService.new(parent, current_user).execute
end
def find_boards def find_boards
found = found =
if parent.multiple_issue_boards_available? if parent.multiple_issue_boards_available?
......
...@@ -5,6 +5,7 @@ module Boards ...@@ -5,6 +5,7 @@ module Boards
class CreateService < Boards::BaseService class CreateService < Boards::BaseService
def execute(board) def execute(board)
return unless current_user && Gitlab::Database.read_write? return unless current_user && Gitlab::Database.read_write?
return unless board.is_a?(Board) # other board types do not support board visits yet
if parent.is_a?(Group) if parent.is_a?(Group)
BoardGroupRecentVisit.visited!(current_user, board) BoardGroupRecentVisit.visited!(current_user, board)
......
...@@ -6,7 +6,10 @@ ...@@ -6,7 +6,10 @@
- @no_breadcrumb_container = true - @no_breadcrumb_container = true
- @no_container = true - @no_container = true
- @content_class = "issue-boards-content js-focus-mode-board" - @content_class = "issue-boards-content js-focus-mode-board"
- breadcrumb_title _("Issue Boards") - if board.to_type == "EpicBoard"
- breadcrumb_title _("Epic Boards")
- else
- breadcrumb_title _("Issue Boards")
- page_title("#{board.name}", _("Boards")) - page_title("#{board.name}", _("Boards"))
- add_page_specific_style 'page_bundles/boards' - add_page_specific_style 'page_bundles/boards'
......
// This is a true violation of @gitlab/no-runtime-template-compiler, as it
// relies on app/views/shared/boards/_show.html.haml for its
// template.
/* eslint-disable @gitlab/no-runtime-template-compiler */
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { mapActions } from 'vuex';
import BoardSidebar from 'ee_component/boards/components/board_sidebar';
import toggleLabels from 'ee_component/boards/toggle_labels';
import BoardContent from '~/boards/components/board_content.vue';
import createDefaultClient from '~/lib/graphql';
import store from '~/boards/stores';
import '~/boards/filters/due_date_filters';
import BoardAddIssuesModal from '~/boards/components/modal/index.vue';
import { NavigationType, parseBoolean } from '~/lib/utils/common_utils';
import mountMultipleBoardsSwitcher from '~/boards/mount_multiple_boards_switcher';
Vue.use(VueApollo);
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(),
});
export default () => {
const $boardApp = document.getElementById('board-app');
// check for browser back and trigger a hard reload to circumvent browser caching.
window.addEventListener('pageshow', (event) => {
const isNavTypeBackForward =
window.performance && window.performance.navigation.type === NavigationType.TYPE_BACK_FORWARD;
if (event.persisted || isNavTypeBackForward) {
window.location.reload();
}
});
// eslint-disable-next-line no-new
new Vue({
el: $boardApp,
components: {
BoardContent,
BoardSidebar,
BoardAddIssuesModal,
BoardSettingsSidebar: () => import('~/boards/components/board_settings_sidebar.vue'),
},
provide: {
boardId: $boardApp.dataset.boardId,
groupId: parseInt($boardApp.dataset.groupId, 10),
rootPath: $boardApp.dataset.rootPath,
currentUserId: gon.current_user_id || null,
canUpdate: $boardApp.dataset.canUpdate,
labelsFetchPath: $boardApp.dataset.labelsFetchPath,
labelsManagePath: $boardApp.dataset.labelsManagePath,
labelsFilterBasePath: $boardApp.dataset.labelsFilterBasePath,
timeTrackingLimitToHours: parseBoolean($boardApp.dataset.timeTrackingLimitToHours),
weightFeatureAvailable: parseBoolean($boardApp.dataset.weightFeatureAvailable),
boardWeight: $boardApp.dataset.boardWeight
? parseInt($boardApp.dataset.boardWeight, 10)
: null,
scopedLabelsAvailable: parseBoolean($boardApp.dataset.scopedLabels),
},
store,
apolloProvider,
data() {
return {
state: {},
loading: 0,
boardsEndpoint: $boardApp.dataset.boardsEndpoint,
recentBoardsEndpoint: $boardApp.dataset.recentBoardsEndpoint,
listsEndpoint: $boardApp.dataset.listsEndpoint,
disabled: parseBoolean($boardApp.dataset.disabled),
bulkUpdatePath: $boardApp.dataset.bulkUpdatePath,
parent: $boardApp.dataset.parent,
detailIssueVisible: false,
};
},
created() {
this.setInitialBoardData({
boardId: $boardApp.dataset.boardId,
fullPath: $boardApp.dataset.fullPath,
boardType: this.parent,
disabled: this.disabled,
boardConfig: {
milestoneId: parseInt($boardApp.dataset.boardMilestoneId, 10),
milestoneTitle: $boardApp.dataset.boardMilestoneTitle || '',
iterationId: parseInt($boardApp.dataset.boardIterationId, 10),
iterationTitle: $boardApp.dataset.boardIterationTitle || '',
assigneeId: $boardApp.dataset.boardAssigneeId,
assigneeUsername: $boardApp.dataset.boardAssigneeUsername,
labels: $boardApp.dataset.labels ? JSON.parse($boardApp.dataset.labels) : [],
labelIds: $boardApp.dataset.labelIds ? JSON.parse($boardApp.dataset.labelIds) : [],
weight: $boardApp.dataset.boardWeight
? parseInt($boardApp.dataset.boardWeight, 10)
: null,
},
});
},
methods: {
...mapActions(['setInitialBoardData']),
getNodes(data) {
return data[this.parent]?.board?.lists.nodes;
},
},
});
toggleLabels();
mountMultipleBoardsSwitcher({
fullPath: $boardApp.dataset.fullPath,
rootPath: $boardApp.dataset.boardsEndpoint,
});
};
import initEpicBoards from 'ee/epic_boards';
import UsersSelect from '~/users_select';
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
new UsersSelect(); // eslint-disable-line no-new
new ShortcutsNavigation(); // eslint-disable-line no-new
initEpicBoards();
# frozen_string_literal: true # frozen_string_literal: true
class Groups::EpicBoardsController < Groups::BoardsController class Groups::EpicBoardsController < Groups::ApplicationController
include BoardsActions include BoardsActions
include RecordUserLastActivity
include Gitlab::Utils::StrongMemoize
extend ::Gitlab::Utils::Override
before_action :authorize_read_board!, only: [:index] before_action :authorize_read_board!, only: [:index]
before_action :assign_endpoint_vars
feature_category :boards
private
def board_klass
::Boards::EpicBoard
end
def boards_finder
strong_memoize :boards_finder do
::Boards::EpicBoardsFinder.new(parent)
end
end
def board_finder
strong_memoize :board_finder do
::Boards::EpicBoardsFinder.new(parent, id: params[:id])
end
end
def board_create_service
strong_memoize :board_create_service do
::Boards::EpicBoards::CreateService.new(parent, current_user)
end
end
override :respond_with
def respond_with(resource)
# no JSON for epic boards
respond_to do |format|
format.html
end
end
def assign_endpoint_vars
@boards_endpoint = group_epic_boards_path(group)
@namespace_path = group.to_param
@labels_endpoint = group_labels_path(group)
end
def authorize_read_board! def authorize_read_board!
access_denied! unless Feature.enabled?(:epic_boards, group) && can?(current_user, :read_epic_board, group) access_denied! unless Feature.enabled?(:epic_boards, group) && can?(current_user, :read_epic_board, group)
......
...@@ -14,5 +14,61 @@ module Boards ...@@ -14,5 +14,61 @@ module Boards
def lists def lists
epic_lists epic_lists
end end
def self.to_type
name.demodulize
end
def to_type
self.class.to_type
end
def resource_parent
group
end
def group_board?
true
end
def scoped?
false
end
def milestone_id
nil
end
def milestone
nil
end
def iteration_id
nil
end
def iteration
nil
end
def assignee_id
nil
end
def assignee
nil
end
def label_ids
[]
end
def labels
[]
end
def weight
nil
end
end end
end end
= render "shared/boards/show", board: @boards.first
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Groups::EpicBoardsController do
let(:group) { create(:group) }
let(:user) { create(:user) }
before do
group.add_maintainer(user)
sign_in(user)
end
describe 'GET index' do
it 'creates a new board when group does not have one' do
expect { list_boards }.to change(group.epic_boards, :count).by(1)
end
context 'with unauthorized user' do
let(:other_user) { create(:user) }
before do
sign_in(other_user)
end
it 'returns a not found 404 response' do
list_boards
expect(response).to have_gitlab_http_status(:not_found)
end
end
context 'json request' do
it 'is not supported' do
list_boards(format: :json)
expect(response).to have_gitlab_http_status(:not_found)
end
end
it_behaves_like 'pushes wip limits to frontend' do
let(:params) { { group_id: group } }
let(:parent) { group }
end
def list_boards(format: :html)
get :index, params: { group_id: group }, format: format
end
end
end
...@@ -11523,6 +11523,9 @@ msgstr "" ...@@ -11523,6 +11523,9 @@ msgstr ""
msgid "Epic" msgid "Epic"
msgstr "" msgstr ""
msgid "Epic Boards"
msgstr ""
msgid "Epic cannot be found." msgid "Epic cannot be found."
msgstr "" msgstr ""
......
# frozen_string_literal: true # frozen_string_literal: true
RSpec.shared_examples 'boards list service' do RSpec.shared_examples 'boards list service' do
context 'when parent does not have a board' do
it 'creates a new parent board' do
expect { service.execute }.to change(parent.boards, :count).by(1)
end
it 'delegates the parent board creation to Boards::CreateService' do
expect_any_instance_of(Boards::CreateService).to receive(:execute).once
service.execute
end
context 'when create_default_board is false' do
it 'does not create a new parent board' do
expect { service.execute(create_default_board: false) }.not_to change(parent.boards, :count)
end
end
end
context 'when parent has a board' do
before do
create(:board, resource_parent: parent)
end
it 'does not create a new board' do it 'does not create a new board' do
expect { service.execute }.not_to change(parent.boards, :count) expect { service.execute }.not_to change(parent.boards, :count)
end end
end
it 'returns parent boards' do it 'returns parent boards' do
board = create(:board, resource_parent: parent) board = create(:board, resource_parent: parent)
......
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