mirror of
https://github.com/opf/openproject.git
synced 2026-06-14 03:30:14 +00:00
[#72661] Namespace Backlogs Rails routes
Move the Rails-facing Backlogs page and sprint endpoints under the Backlogs namespace and standardize their route helpers. This renames the legacy `rb_*` page controllers, moves the canonical backlog details route under `/backlogs/backlog`, and updates the touched sprint endpoints to use more conventional controller/action naming. It leaves the deeper `OpenProject::Backlogs` library internals (patches, API endpoints, hooks) untouched so this stays a Rails-edge rename. https://community.openproject.org/wp/72661
This commit is contained in:
@@ -48,8 +48,8 @@ module DemoData
|
||||
#
|
||||
# For instance:
|
||||
# - Turns `##sprint:sprint_backlog` into
|
||||
# `/projects/demo-project/sprints/23/taskboard` given there is a sprint
|
||||
# referenced with :sprint_backlog and its ID here is 23.
|
||||
# `/projects/demo-project/backlogs/sprints/23/taskboard` given there is a
|
||||
# sprint referenced with :sprint_backlog and its ID here is 23.
|
||||
#
|
||||
# Alternatively `##sprint.id:sprint_backlog` is translated into just the
|
||||
# id.
|
||||
@@ -123,7 +123,7 @@ module DemoData
|
||||
end
|
||||
|
||||
def sprint_link(sprint)
|
||||
url_helpers.backlogs_project_sprint_taskboard_path(
|
||||
url_helpers.project_backlogs_sprint_taskboard_path(
|
||||
sprint_id: sprint.id,
|
||||
project_id: sprint.project.identifier
|
||||
)
|
||||
|
||||
@@ -39,7 +39,7 @@ See COPYRIGHT and LICENSE files for more details.
|
||||
|
||||
d.with_body do
|
||||
primer_form_with(
|
||||
url: finish_project_sprint_path(project, sprint),
|
||||
url: finish_project_backlogs_sprint_path(project, sprint),
|
||||
method: :post,
|
||||
id: FORM_ID,
|
||||
data: { controller: "show-when-value-selected" }
|
||||
|
||||
@@ -99,7 +99,7 @@ See COPYRIGHT and LICENSE files for more details.
|
||||
block: true,
|
||||
align_content: :start,
|
||||
underline: true,
|
||||
href: backlog_backlogs_project_backlogs_path(project, all: 1)
|
||||
href: project_backlogs_backlog_path(project, all: 1)
|
||||
)
|
||||
) { t(".show_more", count: middle_count) }
|
||||
%>
|
||||
|
||||
@@ -52,7 +52,7 @@ See COPYRIGHT and LICENSE files for more details.
|
||||
<%= render(
|
||||
Primer::Alpha::ActionMenu.new(
|
||||
menu_id: dom_target(work_package, :menu),
|
||||
src: menu_project_inbox_path(project, work_package),
|
||||
src: menu_project_backlogs_inbox_path(project, work_package),
|
||||
anchor_align: :end,
|
||||
classes: "hide-when-print"
|
||||
)
|
||||
|
||||
@@ -64,11 +64,11 @@ module Backlogs
|
||||
data: {
|
||||
draggable_id: work_package.id,
|
||||
draggable_type: "story",
|
||||
drop_url: move_project_inbox_path(project, work_package),
|
||||
drop_url: move_project_backlogs_inbox_path(project, work_package),
|
||||
story: true,
|
||||
controller: "backlogs--story",
|
||||
backlogs__story_id_value: work_package.id,
|
||||
backlogs__story_split_url_value: details_backlogs_project_backlogs_path(project, work_package),
|
||||
backlogs__story_split_url_value: project_backlogs_backlog_details_path(project, work_package),
|
||||
backlogs__story_full_url_value: work_package_path(work_package),
|
||||
backlogs__story_selected_class: "Box-row--blue",
|
||||
test_selector: card_test_selector
|
||||
|
||||
@@ -33,7 +33,7 @@ See COPYRIGHT and LICENSE files for more details.
|
||||
id: dom_target(work_package, :menu, :open_details),
|
||||
tag: :a,
|
||||
label: t(:"js.button_open_details"),
|
||||
href: details_backlogs_project_backlogs_path(project, work_package),
|
||||
href: project_backlogs_backlog_details_path(project, work_package),
|
||||
content_arguments: { data: { turbo_frame: "content-bodyRight", turbo_action: "advance" } }
|
||||
) do |item|
|
||||
item.with_leading_visual_icon(icon: :"op-view-split")
|
||||
@@ -87,7 +87,7 @@ See COPYRIGHT and LICENSE files for more details.
|
||||
move_menu.with_item(
|
||||
id: dom_target(work_package, :menu, :move_to_sprint),
|
||||
label: t(".label_move_to_sprint"),
|
||||
href: move_to_sprint_dialog_project_inbox_path(project, work_package),
|
||||
href: move_to_sprint_dialog_project_backlogs_inbox_path(project, work_package),
|
||||
content_arguments: { data: { controller: "async-dialog" } }
|
||||
) do |item|
|
||||
item.with_leading_visual_icon(icon: :zap)
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
#++
|
||||
|
||||
module Backlogs
|
||||
# Renders Primer::Alpha::ActionMenu::List for the deferred menu (InboxController#menu).
|
||||
# Renders Primer::Alpha::ActionMenu::List for the deferred menu (Backlogs::InboxController#menu).
|
||||
# +menu_id+ must match the row ActionMenu in InboxItemComponent.
|
||||
class InboxMenuComponent < ApplicationComponent
|
||||
include OpPrimer::ComponentHelpers
|
||||
@@ -85,7 +85,7 @@ module Backlogs
|
||||
id: dom_target(work_package, :menu, direction),
|
||||
label:,
|
||||
tag: :button,
|
||||
href: reorder_project_inbox_path(project, work_package),
|
||||
href: reorder_project_backlogs_inbox_path(project, work_package),
|
||||
form_arguments: { method: :post, inputs: [{ name: "direction", value: direction }] }
|
||||
) do |item|
|
||||
item.with_leading_visual_icon(icon:)
|
||||
|
||||
@@ -33,7 +33,7 @@ See COPYRIGHT and LICENSE files for more details.
|
||||
|
||||
d.with_body do
|
||||
primer_form_with(
|
||||
url: move_project_inbox_path(project, work_package),
|
||||
url: move_project_backlogs_inbox_path(project, work_package),
|
||||
method: :put,
|
||||
id: FORM_ID,
|
||||
data: { turbo_stream: true }
|
||||
|
||||
@@ -51,9 +51,9 @@ module Backlogs
|
||||
|
||||
def form_url
|
||||
if @sprint.new_record?
|
||||
project_sprints_path(@sprint.project_id)
|
||||
project_backlogs_sprints_path(@sprint.project_id)
|
||||
else
|
||||
update_agile_sprint_project_sprint_path(@sprint.project_id, @sprint.id)
|
||||
project_backlogs_sprint_path(@sprint.project_id, @sprint.id)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -61,7 +61,7 @@ module Backlogs
|
||||
{
|
||||
controller: "refresh-on-form-changes",
|
||||
"refresh-on-form-changes-target": "form",
|
||||
"refresh-on-form-changes-turbo-stream-url-value": refresh_form_project_sprints_path(@sprint.project_id)
|
||||
"refresh-on-form-changes-turbo-stream-url-value": refresh_form_project_backlogs_sprints_path(@sprint.project_id)
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -91,7 +91,7 @@ module Backlogs
|
||||
story: true,
|
||||
controller: "backlogs--story",
|
||||
backlogs__story_id_value: story.id,
|
||||
backlogs__story_split_url_value: details_backlogs_project_backlogs_path(project, story),
|
||||
backlogs__story_split_url_value: project_backlogs_backlog_details_path(project, story),
|
||||
backlogs__story_full_url_value: work_package_path(story),
|
||||
backlogs__story_selected_class: "Box-row--blue",
|
||||
test_selector: card_test_selector(story)
|
||||
@@ -104,7 +104,7 @@ module Backlogs
|
||||
{
|
||||
draggable_id: story.id,
|
||||
draggable_type: "story",
|
||||
drop_url: move_project_sprint_story_path(project, sprint, story)
|
||||
drop_url: move_project_backlogs_story_path(project, sprint_id: sprint.id, id: story.id)
|
||||
}
|
||||
end
|
||||
|
||||
|
||||
@@ -85,7 +85,7 @@ module Backlogs
|
||||
if disable_start_sprint_action?
|
||||
args.merge(tag: :button, inactive: true, aria: { disabled: true })
|
||||
else
|
||||
args.merge(tag: :a, href: start_project_sprint_path(project, sprint), data: { turbo_method: :post })
|
||||
args.merge(tag: :a, href: start_project_backlogs_sprint_path(project, sprint), data: { turbo_method: :post })
|
||||
end
|
||||
end
|
||||
|
||||
@@ -94,7 +94,7 @@ module Backlogs
|
||||
id: dom_target(sprint, :finish_button),
|
||||
scheme: :invisible,
|
||||
tag: :a,
|
||||
href: finish_project_sprint_path(project, sprint),
|
||||
href: finish_project_backlogs_sprint_path(project, sprint),
|
||||
data: { turbo_method: :post }
|
||||
}
|
||||
end
|
||||
|
||||
@@ -41,7 +41,7 @@ See COPYRIGHT and LICENSE files for more details.
|
||||
menu.with_item(
|
||||
id: dom_target(sprint, :menu, :edit_sprint),
|
||||
label: t(".action_menu.edit_sprint"),
|
||||
href: edit_dialog_project_sprint_path(project, sprint),
|
||||
href: edit_dialog_project_backlogs_sprint_path(project, sprint),
|
||||
content_arguments: { data: { controller: "async-dialog" } }
|
||||
) do |item|
|
||||
item.with_leading_visual_icon(icon: :pencil)
|
||||
@@ -70,7 +70,7 @@ See COPYRIGHT and LICENSE files for more details.
|
||||
menu.with_item(
|
||||
label: t("backlogs.label_sprint_board"),
|
||||
tag: :a,
|
||||
href: backlogs_project_sprint_taskboard_path(project, sprint)
|
||||
href: project_backlogs_sprint_taskboard_path(project, sprint)
|
||||
) do |item|
|
||||
item.with_leading_visual_icon(icon: :"op-view-cards")
|
||||
end
|
||||
@@ -80,7 +80,7 @@ See COPYRIGHT and LICENSE files for more details.
|
||||
menu.with_item(
|
||||
label: t("backlogs.label_burndown_chart"),
|
||||
tag: :a,
|
||||
href: backlogs_project_sprint_burndown_chart_path(project, sprint),
|
||||
href: project_backlogs_sprint_burndown_chart_path(project, sprint),
|
||||
disabled: !sprint.date_range_set?
|
||||
) do |item|
|
||||
item.with_leading_visual_icon(icon: :graph)
|
||||
|
||||
@@ -46,7 +46,7 @@ module Backlogs
|
||||
|
||||
def breadcrumb_items
|
||||
[{ href: project_overview_path(@project), text: @project.name },
|
||||
{ href: backlogs_project_backlogs_path(@project), text: t(:label_backlogs) },
|
||||
{ href: project_backlogs_backlog_path(@project), text: t(:label_backlogs) },
|
||||
@sprint.name]
|
||||
end
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@ module Backlogs
|
||||
end
|
||||
|
||||
def menu_src
|
||||
menu_project_sprint_story_path(project, sprint, story)
|
||||
menu_project_backlogs_story_path(project, sprint_id: sprint.id, id: story.id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -33,7 +33,7 @@ See COPYRIGHT and LICENSE files for more details.
|
||||
id: dom_target(story, :menu, :open_details),
|
||||
tag: :a,
|
||||
label: t(:"js.button_open_details"),
|
||||
href: details_backlogs_project_backlogs_path(project, story),
|
||||
href: project_backlogs_backlog_details_path(project, story),
|
||||
content_arguments: { data: { turbo_frame: "content-bodyRight", turbo_action: "advance" } }
|
||||
) do |item|
|
||||
item.with_leading_visual_icon(icon: :"op-view-split")
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
#++
|
||||
|
||||
module Backlogs
|
||||
# Renders Primer::Alpha::ActionMenu::List for the deferred menu (RbStoriesController#menu).
|
||||
# Renders Primer::Alpha::ActionMenu::List for the deferred menu (Backlogs::StoriesController#menu).
|
||||
# +menu_id+ must match the row ActionMenu in StoryComponent.
|
||||
class StoryMenuListComponent < ApplicationComponent
|
||||
include OpPrimer::ComponentHelpers
|
||||
@@ -77,7 +77,7 @@ module Backlogs
|
||||
id: dom_target(story, :menu, direction),
|
||||
label:,
|
||||
tag: :button,
|
||||
href: reorder_project_sprint_story_path(project, sprint, story),
|
||||
href: reorder_project_backlogs_story_path(project, sprint_id: sprint.id, id: story.id),
|
||||
form_arguments: { method: :post, inputs: [{ name: "direction", value: direction }] }
|
||||
) do |item|
|
||||
item.with_leading_visual_icon(icon:)
|
||||
|
||||
+36
-38
@@ -28,51 +28,49 @@
|
||||
# See COPYRIGHT and LICENSE files for more details.
|
||||
#++
|
||||
|
||||
class RbMasterBacklogsController < RbApplicationController
|
||||
include WorkPackages::WithSplitView
|
||||
module Backlogs
|
||||
class BacklogController < ::RbApplicationController
|
||||
include WorkPackages::WithSplitView
|
||||
|
||||
current_menu_item [:backlog, :details] do
|
||||
:backlog
|
||||
end
|
||||
|
||||
before_action :load_backlogs, only: %i[index backlog]
|
||||
|
||||
def backlog
|
||||
case turbo_frame_request_id
|
||||
when "backlogs_container"
|
||||
render partial: "backlog_list", layout: false
|
||||
else
|
||||
render :backlog
|
||||
current_menu_item %i[show details] do
|
||||
:backlog
|
||||
end
|
||||
end
|
||||
|
||||
def index
|
||||
redirect_to action: :backlog
|
||||
end
|
||||
before_action :load_backlogs, only: :show
|
||||
|
||||
def details
|
||||
if turbo_frame_request?
|
||||
render "work_packages/split_view", layout: false
|
||||
else
|
||||
load_backlogs
|
||||
render :backlog
|
||||
def show
|
||||
case turbo_frame_request_id
|
||||
when "backlogs_container"
|
||||
render partial: "backlogs/backlog/backlog_list", layout: false
|
||||
else
|
||||
render "backlogs/backlog/show"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def details
|
||||
if turbo_frame_request?
|
||||
render "work_packages/split_view", layout: false
|
||||
else
|
||||
load_backlogs
|
||||
render "backlogs/backlog/show"
|
||||
end
|
||||
end
|
||||
|
||||
def split_view_base_route
|
||||
backlog_backlogs_project_backlogs_path(request.query_parameters)
|
||||
end
|
||||
private
|
||||
|
||||
def load_backlogs
|
||||
@sprints = Agile::Sprint.for_project(@project).not_completed.order_by_date
|
||||
@stories_by_sprint_id = WorkPackage
|
||||
.where(sprint: @sprints, project: @project)
|
||||
.includes(:type, :status)
|
||||
.order_by_position
|
||||
.group_by(&:sprint_id)
|
||||
@active_sprint_ids = @sprints.select(&:active?).map(&:id)
|
||||
@inbox_work_packages = Backlog.inbox_for(project: @project)
|
||||
def split_view_base_route
|
||||
project_backlogs_backlog_path(@project, request.query_parameters)
|
||||
end
|
||||
|
||||
def load_backlogs
|
||||
@sprints = Agile::Sprint.for_project(@project).not_completed.order_by_date
|
||||
@stories_by_sprint_id = WorkPackage
|
||||
.where(sprint: @sprints, project: @project)
|
||||
.includes(:type, :status)
|
||||
.order_by_position
|
||||
.group_by(&:sprint_id)
|
||||
@active_sprint_ids = @sprints.select(&:active?).map(&:id)
|
||||
@inbox_work_packages = Backlog.inbox_for(project: @project)
|
||||
end
|
||||
end
|
||||
end
|
||||
+9
-7
@@ -28,14 +28,16 @@
|
||||
# See COPYRIGHT and LICENSE files for more details.
|
||||
#++
|
||||
|
||||
class RbTaskboardsController < RbApplicationController
|
||||
menu_item :backlogs
|
||||
module Backlogs
|
||||
class BurndownChartsController < ::RbApplicationController
|
||||
helper :burndown_charts
|
||||
|
||||
def show
|
||||
@board = @sprint.task_board_for(@project)
|
||||
def show
|
||||
@burndown = Burndown.new(@sprint, @project) if @sprint.date_range_set?
|
||||
|
||||
return redirect_to(project_work_package_board_path(@project, @board)) if @board
|
||||
|
||||
render_404
|
||||
respond_to do |format|
|
||||
format.html { render "backlogs/burndown_charts/show", layout: true }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,137 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
#-- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) the OpenProject GmbH
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License version 3.
|
||||
#
|
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
# Copyright (C) 2010-2013 the ChiliProject Team
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# See COPYRIGHT and LICENSE files for more details.
|
||||
#++
|
||||
|
||||
module Backlogs
|
||||
class InboxController < ::RbApplicationController
|
||||
include OpTurbo::ComponentStream
|
||||
|
||||
before_action :load_work_package
|
||||
|
||||
# Deferred ActionMenu items (Primer include-fragment).
|
||||
def menu
|
||||
max_position = Backlog.inbox_for(project: @project).maximum(:position) || 0
|
||||
open_sprints_exist = Agile::Sprint.for_project(@project).visible.not_completed.exists?
|
||||
|
||||
render(Backlogs::InboxMenuComponent.new(
|
||||
work_package: @work_package,
|
||||
project: @project,
|
||||
max_position:,
|
||||
open_sprints_exist:,
|
||||
current_user:
|
||||
),
|
||||
layout: false)
|
||||
end
|
||||
|
||||
def move_to_sprint_dialog
|
||||
respond_with_dialog Backlogs::MoveToSprintDialogComponent.new(
|
||||
work_package: @work_package,
|
||||
project: @project
|
||||
)
|
||||
end
|
||||
|
||||
def reorder
|
||||
call = Stories::UpdateService
|
||||
.new(user: current_user, story: @work_package)
|
||||
.call(attributes: { move_to: reorder_param })
|
||||
|
||||
return failure_response(call.message) unless call.success?
|
||||
|
||||
replace_inbox_component_via_turbo_stream
|
||||
respond_with_turbo_streams
|
||||
end
|
||||
|
||||
# Move a work package from the Inbox to a Sprint, or reorder it within the Inbox.
|
||||
def move
|
||||
target_type, sprint_id = move_params[:target_id].split(":", 2)
|
||||
attributes = target_type == "sprint" ? { sprint_id: } : {}
|
||||
|
||||
call = Stories::UpdateService
|
||||
.new(user: current_user, story: @work_package)
|
||||
.call(attributes:, **position_attributes)
|
||||
|
||||
return failure_response(call.message) unless call.success?
|
||||
|
||||
replace_inbox_component_via_turbo_stream
|
||||
replace_sprint_component_via_turbo_stream(sprint_id) if target_type == "sprint"
|
||||
respond_with_turbo_streams
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def load_work_package
|
||||
@work_package = WorkPackage.visible.where(project: @project).find(params[:id])
|
||||
end
|
||||
|
||||
def replace_inbox_component_via_turbo_stream
|
||||
work_packages = Backlog.inbox_for(project: @project)
|
||||
replace_via_turbo_stream(
|
||||
component: Backlogs::InboxComponent.new(
|
||||
work_packages:,
|
||||
project: @project
|
||||
),
|
||||
method: :morph
|
||||
)
|
||||
end
|
||||
|
||||
def replace_sprint_component_via_turbo_stream(sprint_id)
|
||||
sprint = Agile::Sprint.for_project(@project).visible.find(sprint_id)
|
||||
replace_via_turbo_stream(
|
||||
component: Backlogs::SprintComponent.new(sprint: sprint, project: @project),
|
||||
method: :morph
|
||||
)
|
||||
end
|
||||
|
||||
def failure_response(reason)
|
||||
render_error_flash_message_via_turbo_stream(
|
||||
message: I18n.t(:notice_unsuccessful_update_with_reason, reason:)
|
||||
)
|
||||
respond_with_turbo_streams(status: :unprocessable_entity)
|
||||
end
|
||||
|
||||
def move_params
|
||||
params.require(%i[target_id])
|
||||
params.permit(:position, :prev_id, :target_id)
|
||||
end
|
||||
|
||||
def position_attributes
|
||||
if move_params.has_key?(:prev_id)
|
||||
{ prev_id: move_params[:prev_id].to_i }
|
||||
elsif move_params.has_key?(:position)
|
||||
{ position: move_params[:position].to_i }
|
||||
else
|
||||
{}
|
||||
end
|
||||
end
|
||||
|
||||
def reorder_param
|
||||
params.expect(:direction)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,232 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
#-- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) the OpenProject GmbH
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License version 3.
|
||||
#
|
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
# Copyright (C) 2010-2013 the ChiliProject Team
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# See COPYRIGHT and LICENSE files for more details.
|
||||
#++
|
||||
|
||||
module Backlogs
|
||||
class SprintsController < ::RbApplicationController
|
||||
include OpTurbo::ComponentStream
|
||||
|
||||
NEW_SPRINT_ACTIONS = %i[new_dialog
|
||||
edit_dialog
|
||||
create
|
||||
refresh_form].freeze
|
||||
SPRINT_STATE_ACTIONS = %i[start finish].freeze
|
||||
|
||||
skip_before_action :load_sprint_and_project, only: NEW_SPRINT_ACTIONS
|
||||
skip_before_action :authorize, only: SPRINT_STATE_ACTIONS
|
||||
|
||||
before_action :load_project, only: NEW_SPRINT_ACTIONS
|
||||
before_action :authorize_start!, only: :start
|
||||
before_action :authorize_finish!, only: :finish
|
||||
|
||||
def new_dialog
|
||||
call = ::Sprints::SetAttributesService.new(
|
||||
user: current_user,
|
||||
model: Agile::Sprint.new,
|
||||
contract_class: ::EmptyContract
|
||||
).call(attributes: converted_agile_sprint_params)
|
||||
|
||||
respond_with_dialog Backlogs::NewSprintDialogComponent.new(sprint: call.result)
|
||||
end
|
||||
|
||||
def edit_dialog
|
||||
@sprint = Agile::Sprint.for_project(@project).visible.find(params[:sprint_id])
|
||||
|
||||
respond_with_dialog Backlogs::NewSprintDialogComponent.new(sprint: @sprint, state: :edit)
|
||||
end
|
||||
|
||||
def refresh_form
|
||||
id = edit_agile_sprint_params.dig(:sprint, :id)
|
||||
sprint = id.present? ? Agile::Sprint.for_project(@project).visible.find(id) : Agile::Sprint.new
|
||||
|
||||
call = ::Sprints::SetAttributesService.new(
|
||||
user: current_user,
|
||||
model: sprint,
|
||||
contract_class: ::EmptyContract
|
||||
).call(attributes: converted_agile_sprint_params)
|
||||
|
||||
update_via_turbo_stream(component: Backlogs::NewSprintFormComponent.new(sprint: call.result))
|
||||
|
||||
respond_with_turbo_streams
|
||||
end
|
||||
|
||||
def create # rubocop:disable Metrics/AbcSize
|
||||
call = ::Sprints::CreateService
|
||||
.new(user: current_user)
|
||||
.call(attributes: converted_agile_sprint_params)
|
||||
|
||||
if call.success?
|
||||
flash[:notice] = I18n.t(:notice_successful_create)
|
||||
render turbo_stream: turbo_stream.redirect_to(project_backlogs_backlog_path(@project))
|
||||
else
|
||||
update_new_sprint_form_component_via_turbo_stream(sprint: call.result, base_errors: call.errors[:base])
|
||||
respond_with_turbo_streams
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
call = ::Sprints::UpdateService
|
||||
.new(user: current_user, model: @sprint)
|
||||
.call(attributes: agile_sprint_params[:sprint])
|
||||
|
||||
if call.success?
|
||||
render_success_flash_message_via_turbo_stream(message: I18n.t(:notice_successful_update))
|
||||
update_sprint_header_component_via_turbo_stream(sprint: call.result)
|
||||
else
|
||||
update_new_sprint_form_component_via_turbo_stream(sprint: call.result, base_errors: call.errors[:base])
|
||||
end
|
||||
|
||||
respond_with_turbo_streams
|
||||
end
|
||||
|
||||
def start
|
||||
result = start_sprint
|
||||
|
||||
if result.success?
|
||||
@sprint = result.result
|
||||
flash[:notice] = I18n.t(:notice_successful_start)
|
||||
render turbo_stream: turbo_stream.redirect_to(
|
||||
project_work_package_board_path(@project, @sprint.task_board_for(@project))
|
||||
)
|
||||
else
|
||||
respond_with_start_finish_failure(message: start_finish_failure_message(:start, result.message))
|
||||
end
|
||||
end
|
||||
|
||||
def finish
|
||||
result = finish_sprint
|
||||
|
||||
if result.success?
|
||||
flash[:notice] = I18n.t(:notice_successful_finish)
|
||||
render turbo_stream: turbo_stream.redirect_to(project_backlogs_backlog_path(@project))
|
||||
elsif result.includes_error?(:base, :unfinished_work_packages)
|
||||
show_finish_sprint_dialog
|
||||
else
|
||||
respond_with_start_finish_failure(message: start_finish_failure_message(:finish, result.message))
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def update_sprint_header_component_via_turbo_stream(sprint:)
|
||||
update_via_turbo_stream(
|
||||
component: Backlogs::SprintHeaderComponent.new(sprint:, project: @project),
|
||||
method: :morph
|
||||
)
|
||||
end
|
||||
|
||||
def update_new_sprint_form_component_via_turbo_stream(sprint:, base_errors: nil)
|
||||
update_via_turbo_stream(
|
||||
component: Backlogs::NewSprintFormComponent.new(
|
||||
sprint:,
|
||||
base_errors:
|
||||
),
|
||||
status: :bad_request
|
||||
)
|
||||
end
|
||||
|
||||
def show_finish_sprint_dialog
|
||||
respond_with_dialog(
|
||||
Backlogs::FinishSprintDialogComponent.new(
|
||||
sprint: @sprint,
|
||||
project: @project,
|
||||
available_sprints: Agile::Sprint.native_to_sprint_source(@project).in_planning.where.not(id: @sprint.id).order_by_date
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
def load_sprint_and_project
|
||||
load_project
|
||||
|
||||
sprint_id = params[:sprint_id]
|
||||
@sprint = Agile::Sprint.for_project(@project).visible.find(sprint_id) if sprint_id
|
||||
end
|
||||
|
||||
def agile_sprint_params
|
||||
params.permit(sprint: %i[name start_date finish_date])
|
||||
end
|
||||
|
||||
def edit_agile_sprint_params
|
||||
params.permit(sprint: %i[id name start_date finish_date])
|
||||
end
|
||||
|
||||
def converted_agile_sprint_params
|
||||
converted_sprint_params = agile_sprint_params[:sprint].to_h
|
||||
converted_sprint_params[:project] = @project
|
||||
|
||||
converted_sprint_params
|
||||
end
|
||||
|
||||
def start_sprint
|
||||
::Sprints::StartService
|
||||
.new(user: current_user, model: @sprint)
|
||||
.call(send_notifications: false)
|
||||
end
|
||||
|
||||
def finish_sprint
|
||||
::Sprints::FinishService
|
||||
.new(user: current_user, model: @sprint)
|
||||
.call(
|
||||
unfinished_action: params[:unfinished_action],
|
||||
move_to_sprint_id: params[:move_to_sprint_id],
|
||||
send_notifications: false
|
||||
)
|
||||
end
|
||||
|
||||
def respond_with_start_finish_failure(message:)
|
||||
render_error_flash_message_via_turbo_stream(message:)
|
||||
|
||||
respond_with_turbo_streams(status: :unprocessable_entity) do |format|
|
||||
fallback_responses_for(format, alert: message)
|
||||
end
|
||||
end
|
||||
|
||||
def fallback_responses_for(format, **)
|
||||
format.html { redirect_back_or_to(project_backlogs_backlog_path(@project), **) }
|
||||
end
|
||||
|
||||
def start_finish_failure_message(action, reason)
|
||||
if reason.present?
|
||||
I18n.t(:"notice_unsuccessful_#{action}_with_reason", reason:)
|
||||
else
|
||||
I18n.t(:"notice_unsuccessful_#{action}")
|
||||
end
|
||||
end
|
||||
|
||||
def authorize_start!
|
||||
deny_access unless current_user.allowed_in_project?(:view_sprints, @project) &&
|
||||
::Sprints::StartContract.can_start?(user: current_user, sprint: @sprint, project: @project)
|
||||
end
|
||||
|
||||
def authorize_finish!
|
||||
deny_access unless current_user.allowed_in_project?(:view_sprints, @project) &&
|
||||
::Sprints::StartContract.can_start_or_complete?(user: current_user, sprint: @sprint)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,185 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
#-- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) the OpenProject GmbH
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License version 3.
|
||||
#
|
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
# Copyright (C) 2010-2013 the ChiliProject Team
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# See COPYRIGHT and LICENSE files for more details.
|
||||
#++
|
||||
|
||||
module Backlogs
|
||||
class StoriesController < ::RbApplicationController
|
||||
include OpTurbo::ComponentStream
|
||||
|
||||
before_action :load_story
|
||||
|
||||
# Deferred ActionMenu items (Primer include-fragment).
|
||||
def menu
|
||||
max_position = @allowed_stories.maximum(:position) || 0
|
||||
|
||||
render(Backlogs::StoryMenuListComponent.new(
|
||||
story: @story,
|
||||
sprint: @sprint,
|
||||
project: @project,
|
||||
max_position:,
|
||||
current_user:
|
||||
),
|
||||
layout: false)
|
||||
end
|
||||
|
||||
# Move a story from an Agile::Sprint to another Agile::Sprint, or the Inbox.
|
||||
def move
|
||||
# The update service reloads the story internally (via #move_after),
|
||||
# so we memoize the previous sprint_id before the call.
|
||||
sprint_id_was = @story.sprint_id
|
||||
|
||||
move_attributes = infer_attributes_from_target
|
||||
unless move_story(move_attributes).success?
|
||||
return respond_with_turbo_streams(status: :unprocessable_entity)
|
||||
end
|
||||
|
||||
if target_inbox?(move_attributes)
|
||||
moved_to_inbox
|
||||
elsif target_sprint?(move_attributes) && @story.sprint_id != sprint_id_was
|
||||
moved_to_sprint
|
||||
end
|
||||
|
||||
respond_with_turbo_streams
|
||||
end
|
||||
|
||||
def reorder
|
||||
call = Stories::UpdateService
|
||||
.new(user: current_user, story: @story)
|
||||
.call(attributes: { move_to: reorder_param })
|
||||
|
||||
unless call.success?
|
||||
render_error_flash_message_via_turbo_stream(
|
||||
message: I18n.t(:notice_unsuccessful_update_with_reason, reason: call.message)
|
||||
)
|
||||
return respond_with_turbo_streams(status: :unprocessable_entity)
|
||||
end
|
||||
|
||||
replace_sprint_component_via_turbo_stream(sprint: @sprint)
|
||||
|
||||
respond_with_turbo_streams
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def move_story(move_attributes)
|
||||
call = update_story_with_target_and_position(attributes: move_attributes)
|
||||
|
||||
if call.success?
|
||||
# Update source component so that the moved story disappears
|
||||
replace_sprint_component_via_turbo_stream(sprint: @sprint)
|
||||
else
|
||||
render_error_flash_message_via_turbo_stream(
|
||||
message: I18n.t(:notice_unsuccessful_update_with_reason, reason: call.message)
|
||||
)
|
||||
end
|
||||
|
||||
call
|
||||
end
|
||||
|
||||
def update_story_with_target_and_position(attributes:)
|
||||
Stories::UpdateService
|
||||
.new(user: current_user, story: @story)
|
||||
.call(attributes:, **position_attributes)
|
||||
end
|
||||
|
||||
def moved_to_inbox
|
||||
render_success_flash_message_via_turbo_stream(
|
||||
message: I18n.t(:notice_successful_move, from: @sprint.name, to: I18n.t(:label_inbox))
|
||||
)
|
||||
work_packages = Backlog.inbox_for(project: @project)
|
||||
replace_via_turbo_stream(
|
||||
component: Backlogs::InboxComponent.new(work_packages:, project: @project),
|
||||
method: :morph
|
||||
)
|
||||
end
|
||||
|
||||
def moved_to_sprint
|
||||
moved_to(new_sprint: @story.sprint)
|
||||
end
|
||||
|
||||
def moved_to(new_sprint:)
|
||||
render_success_flash_message_via_turbo_stream(
|
||||
message: I18n.t(:notice_successful_move, from: @sprint.name, to: new_sprint.name)
|
||||
)
|
||||
|
||||
# Update the target component so that the moved story shows up
|
||||
replace_sprint_component_via_turbo_stream(sprint: new_sprint)
|
||||
end
|
||||
|
||||
def infer_attributes_from_target
|
||||
target_type, target_id = move_params[:target_id].split(":")
|
||||
|
||||
case target_type
|
||||
when "sprint"
|
||||
{ sprint_id: target_id }
|
||||
when "inbox"
|
||||
{ sprint_id: nil }
|
||||
else
|
||||
raise ArgumentError, "target_type must include one of: sprint, inbox."
|
||||
end
|
||||
end
|
||||
|
||||
def target_sprint?(move_attributes)
|
||||
move_attributes[:sprint_id].present?
|
||||
end
|
||||
|
||||
def target_inbox?(move_attributes)
|
||||
move_attributes.key?(:sprint_id) && move_attributes[:sprint_id].nil?
|
||||
end
|
||||
|
||||
def replace_sprint_component_via_turbo_stream(sprint:)
|
||||
replace_via_turbo_stream(component: Backlogs::SprintComponent.new(sprint: sprint, project: @project),
|
||||
method: :morph)
|
||||
end
|
||||
|
||||
def load_story
|
||||
@allowed_stories = WorkPackage.visible.where(sprint: @sprint, project: @project)
|
||||
@story = @allowed_stories.find(params[:id])
|
||||
end
|
||||
|
||||
def move_params
|
||||
params.require(%i[target_id])
|
||||
params.permit(:position, :prev_id, :target_id)
|
||||
end
|
||||
|
||||
def position_attributes
|
||||
if move_params.has_key?(:prev_id)
|
||||
{ prev_id: move_params[:prev_id].to_i }
|
||||
elsif move_params.has_key?(:position)
|
||||
{ position: move_params[:position].to_i }
|
||||
else
|
||||
{}
|
||||
end
|
||||
end
|
||||
|
||||
def reorder_param
|
||||
params.expect(:direction)
|
||||
end
|
||||
end
|
||||
end
|
||||
+8
-6
@@ -28,14 +28,16 @@
|
||||
# See COPYRIGHT and LICENSE files for more details.
|
||||
#++
|
||||
|
||||
class RbBurndownChartsController < RbApplicationController
|
||||
helper :burndown_charts
|
||||
module Backlogs
|
||||
class TaskboardController < ::RbApplicationController
|
||||
menu_item :backlogs
|
||||
|
||||
def show
|
||||
@burndown = Burndown.new(@sprint, @project) if @sprint.date_range_set?
|
||||
def show
|
||||
@board = @sprint.task_board_for(@project)
|
||||
|
||||
respond_to do |format|
|
||||
format.html { render layout: true }
|
||||
return redirect_to(project_work_package_board_path(@project, @board)) if @board
|
||||
|
||||
render_404
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,135 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
#-- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) the OpenProject GmbH
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License version 3.
|
||||
#
|
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
# Copyright (C) 2010-2013 the ChiliProject Team
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# See COPYRIGHT and LICENSE files for more details.
|
||||
#++
|
||||
|
||||
class InboxController < RbApplicationController
|
||||
include OpTurbo::ComponentStream
|
||||
|
||||
before_action :load_work_package
|
||||
|
||||
# Deferred ActionMenu items (Primer include-fragment).
|
||||
def menu
|
||||
max_position = Backlog.inbox_for(project: @project).maximum(:position) || 0
|
||||
open_sprints_exist = Agile::Sprint.for_project(@project).visible.not_completed.exists?
|
||||
|
||||
render(Backlogs::InboxMenuComponent.new(
|
||||
work_package: @work_package,
|
||||
project: @project,
|
||||
max_position:,
|
||||
open_sprints_exist:,
|
||||
current_user:
|
||||
),
|
||||
layout: false)
|
||||
end
|
||||
|
||||
def move_to_sprint_dialog
|
||||
respond_with_dialog Backlogs::MoveToSprintDialogComponent.new(
|
||||
work_package: @work_package,
|
||||
project: @project
|
||||
)
|
||||
end
|
||||
|
||||
def reorder
|
||||
call = Stories::UpdateService
|
||||
.new(user: current_user, story: @work_package)
|
||||
.call(attributes: { move_to: reorder_param })
|
||||
|
||||
return failure_response(call.message) unless call.success?
|
||||
|
||||
replace_inbox_component_via_turbo_stream
|
||||
respond_with_turbo_streams
|
||||
end
|
||||
|
||||
# Move a work package from the Inbox to a Sprint, or reorder it within the Inbox.
|
||||
def move
|
||||
target_type, sprint_id = move_params[:target_id].split(":", 2)
|
||||
attributes = target_type == "sprint" ? { sprint_id: } : {}
|
||||
|
||||
call = Stories::UpdateService
|
||||
.new(user: current_user, story: @work_package)
|
||||
.call(attributes:, **position_attributes)
|
||||
|
||||
return failure_response(call.message) unless call.success?
|
||||
|
||||
replace_inbox_component_via_turbo_stream
|
||||
replace_sprint_component_via_turbo_stream(sprint_id) if target_type == "sprint"
|
||||
respond_with_turbo_streams
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def load_work_package
|
||||
@work_package = WorkPackage.visible.where(project: @project).find(params[:id])
|
||||
end
|
||||
|
||||
def replace_inbox_component_via_turbo_stream
|
||||
work_packages = Backlog.inbox_for(project: @project)
|
||||
replace_via_turbo_stream(
|
||||
component: Backlogs::InboxComponent.new(
|
||||
work_packages:,
|
||||
project: @project
|
||||
),
|
||||
method: :morph
|
||||
)
|
||||
end
|
||||
|
||||
def replace_sprint_component_via_turbo_stream(sprint_id)
|
||||
sprint = Agile::Sprint.for_project(@project).visible.find(sprint_id)
|
||||
replace_via_turbo_stream(
|
||||
component: Backlogs::SprintComponent.new(sprint: sprint, project: @project),
|
||||
method: :morph
|
||||
)
|
||||
end
|
||||
|
||||
def failure_response(reason)
|
||||
render_error_flash_message_via_turbo_stream(
|
||||
message: I18n.t(:notice_unsuccessful_update_with_reason, reason:)
|
||||
)
|
||||
respond_with_turbo_streams(status: :unprocessable_entity)
|
||||
end
|
||||
|
||||
def move_params
|
||||
params.require(%i[target_id])
|
||||
params.permit(:position, :prev_id, :target_id)
|
||||
end
|
||||
|
||||
def position_attributes
|
||||
if move_params.has_key?(:prev_id)
|
||||
{ prev_id: move_params[:prev_id].to_i }
|
||||
elsif move_params.has_key?(:position)
|
||||
{ position: move_params[:position].to_i }
|
||||
else
|
||||
{}
|
||||
end
|
||||
end
|
||||
|
||||
def reorder_param
|
||||
params.expect(:direction)
|
||||
end
|
||||
end
|
||||
@@ -1,235 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
#-- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) the OpenProject GmbH
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License version 3.
|
||||
#
|
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
# Copyright (C) 2010-2013 the ChiliProject Team
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# See COPYRIGHT and LICENSE files for more details.
|
||||
#++
|
||||
|
||||
class RbSprintsController < RbApplicationController
|
||||
include OpTurbo::ComponentStream
|
||||
|
||||
NEW_SPRINT_ACTIONS = %i[new_dialog
|
||||
edit_dialog
|
||||
create
|
||||
refresh_form
|
||||
update_agile_sprint].freeze
|
||||
SPRINT_STATE_ACTIONS = %i[start finish].freeze
|
||||
|
||||
skip_before_action :load_sprint_and_project, only: NEW_SPRINT_ACTIONS
|
||||
skip_before_action :authorize, only: SPRINT_STATE_ACTIONS
|
||||
|
||||
before_action :load_project, only: NEW_SPRINT_ACTIONS
|
||||
before_action :authorize_start!, only: :start
|
||||
before_action :authorize_finish!, only: :finish
|
||||
|
||||
def new_dialog
|
||||
call = Sprints::SetAttributesService.new(
|
||||
user: current_user,
|
||||
model: Agile::Sprint.new,
|
||||
contract_class: EmptyContract
|
||||
).call(attributes: converted_agile_sprint_params)
|
||||
|
||||
respond_with_dialog Backlogs::NewSprintDialogComponent.new(sprint: call.result)
|
||||
end
|
||||
|
||||
def edit_dialog
|
||||
@sprint = Agile::Sprint.for_project(@project).visible.find(params[:id])
|
||||
|
||||
respond_with_dialog Backlogs::NewSprintDialogComponent.new(sprint: @sprint, state: :edit)
|
||||
end
|
||||
|
||||
def refresh_form
|
||||
id = edit_agile_sprint_params.dig(:sprint, :id)
|
||||
sprint = id.present? ? Agile::Sprint.for_project(@project).visible.find(id) : Agile::Sprint.new
|
||||
|
||||
call = Sprints::SetAttributesService.new(
|
||||
user: current_user,
|
||||
model: sprint,
|
||||
contract_class: EmptyContract
|
||||
).call(attributes: converted_agile_sprint_params)
|
||||
|
||||
update_via_turbo_stream(component: Backlogs::NewSprintFormComponent.new(sprint: call.result))
|
||||
|
||||
respond_with_turbo_streams
|
||||
end
|
||||
|
||||
def create # rubocop:disable Metrics/AbcSize
|
||||
call = Sprints::CreateService
|
||||
.new(user: current_user)
|
||||
.call(attributes: converted_agile_sprint_params)
|
||||
|
||||
if call.success?
|
||||
flash[:notice] = I18n.t(:notice_successful_create)
|
||||
render turbo_stream: turbo_stream.redirect_to(backlog_backlogs_project_backlogs_path(@project))
|
||||
else
|
||||
update_new_sprint_form_component_via_turbo_stream(sprint: call.result, base_errors: call.errors[:base])
|
||||
respond_with_turbo_streams
|
||||
end
|
||||
end
|
||||
|
||||
# Called like this due to `update` being taken by legacy sprints.
|
||||
def update_agile_sprint # rubocop:disable Metrics/AbcSize
|
||||
@sprint = Agile::Sprint.for_project(@project).visible.find(params[:id])
|
||||
|
||||
call = Sprints::UpdateService
|
||||
.new(user: current_user, model: @sprint)
|
||||
.call(attributes: agile_sprint_params[:sprint])
|
||||
|
||||
if call.success?
|
||||
render_success_flash_message_via_turbo_stream(message: I18n.t(:notice_successful_update))
|
||||
update_sprint_header_component_via_turbo_stream(sprint: call.result)
|
||||
else
|
||||
update_new_sprint_form_component_via_turbo_stream(sprint: call.result, base_errors: call.errors[:base])
|
||||
end
|
||||
|
||||
respond_with_turbo_streams
|
||||
end
|
||||
|
||||
def start
|
||||
result = start_sprint
|
||||
|
||||
if result.success?
|
||||
@sprint = result.result
|
||||
flash[:notice] = I18n.t(:notice_successful_start)
|
||||
render turbo_stream: turbo_stream.redirect_to(
|
||||
project_work_package_board_path(@project, @sprint.task_board_for(@project))
|
||||
)
|
||||
else
|
||||
respond_with_start_finish_failure(message: start_finish_failure_message(:start, result.message))
|
||||
end
|
||||
end
|
||||
|
||||
def finish
|
||||
result = finish_sprint
|
||||
|
||||
if result.success?
|
||||
flash[:notice] = I18n.t(:notice_successful_finish)
|
||||
render turbo_stream: turbo_stream.redirect_to(backlog_backlogs_project_backlogs_path(@project))
|
||||
elsif result.includes_error?(:base, :unfinished_work_packages)
|
||||
show_finish_sprint_dialog
|
||||
else
|
||||
respond_with_start_finish_failure(message: start_finish_failure_message(:finish, result.message))
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def update_sprint_header_component_via_turbo_stream(sprint:)
|
||||
update_via_turbo_stream(
|
||||
component: Backlogs::SprintHeaderComponent.new(sprint:, project: @project),
|
||||
method: :morph
|
||||
)
|
||||
end
|
||||
|
||||
def update_new_sprint_form_component_via_turbo_stream(sprint:, base_errors: nil)
|
||||
update_via_turbo_stream(
|
||||
component: Backlogs::NewSprintFormComponent.new(
|
||||
sprint:,
|
||||
base_errors:
|
||||
),
|
||||
status: :bad_request
|
||||
)
|
||||
end
|
||||
|
||||
def show_finish_sprint_dialog
|
||||
respond_with_dialog(
|
||||
Backlogs::FinishSprintDialogComponent.new(
|
||||
sprint: @sprint,
|
||||
project: @project,
|
||||
available_sprints: Agile::Sprint.native_to_sprint_source(@project).in_planning.where.not(id: @sprint.id).order_by_date
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
# Overrides load_sprint_and_project to load the sprint from :id instead of :sprint_id
|
||||
def load_sprint_and_project
|
||||
load_project
|
||||
|
||||
@sprint = Agile::Sprint.for_project(@project).visible.find(params[:id])
|
||||
end
|
||||
|
||||
def agile_sprint_params
|
||||
params.permit(sprint: %i[name start_date finish_date])
|
||||
end
|
||||
|
||||
def edit_agile_sprint_params
|
||||
params.permit(sprint: %i[id name start_date finish_date])
|
||||
end
|
||||
|
||||
def converted_agile_sprint_params
|
||||
# Do some preprocessing to make the params easier to use
|
||||
converted_sprint_params = agile_sprint_params[:sprint].to_h
|
||||
converted_sprint_params[:project] = @project
|
||||
|
||||
converted_sprint_params
|
||||
end
|
||||
|
||||
def start_sprint
|
||||
Sprints::StartService
|
||||
.new(user: current_user, model: @sprint)
|
||||
.call(send_notifications: false)
|
||||
end
|
||||
|
||||
def finish_sprint
|
||||
Sprints::FinishService
|
||||
.new(user: current_user, model: @sprint)
|
||||
.call(
|
||||
unfinished_action: params[:unfinished_action],
|
||||
move_to_sprint_id: params[:move_to_sprint_id],
|
||||
send_notifications: false
|
||||
)
|
||||
end
|
||||
|
||||
def respond_with_start_finish_failure(message:)
|
||||
render_error_flash_message_via_turbo_stream(message:)
|
||||
|
||||
respond_with_turbo_streams(status: :unprocessable_entity) do |format|
|
||||
fallback_responses_for(format, alert: message)
|
||||
end
|
||||
end
|
||||
|
||||
def fallback_responses_for(format, **)
|
||||
format.html { redirect_back_or_to(backlogs_project_backlogs_path(@project), **) }
|
||||
end
|
||||
|
||||
def start_finish_failure_message(action, reason)
|
||||
if reason.present?
|
||||
I18n.t(:"notice_unsuccessful_#{action}_with_reason", reason:)
|
||||
else
|
||||
I18n.t(:"notice_unsuccessful_#{action}")
|
||||
end
|
||||
end
|
||||
|
||||
def authorize_start!
|
||||
deny_access unless current_user.allowed_in_project?(:view_sprints, @project) &&
|
||||
Sprints::StartContract.can_start?(user: current_user, sprint: @sprint, project: @project)
|
||||
end
|
||||
|
||||
def authorize_finish!
|
||||
deny_access unless current_user.allowed_in_project?(:view_sprints, @project) &&
|
||||
Sprints::StartContract.can_start_or_complete?(user: current_user, sprint: @sprint)
|
||||
end
|
||||
end
|
||||
@@ -1,183 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
#-- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) the OpenProject GmbH
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License version 3.
|
||||
#
|
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
# Copyright (C) 2010-2013 the ChiliProject Team
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# See COPYRIGHT and LICENSE files for more details.
|
||||
#++
|
||||
|
||||
class RbStoriesController < RbApplicationController
|
||||
include OpTurbo::ComponentStream
|
||||
|
||||
before_action :load_story
|
||||
|
||||
# Deferred ActionMenu items (Primer include-fragment).
|
||||
def menu
|
||||
max_position = @allowed_stories.maximum(:position) || 0
|
||||
|
||||
render(Backlogs::StoryMenuListComponent.new(
|
||||
story: @story,
|
||||
sprint: @sprint,
|
||||
project: @project,
|
||||
max_position:,
|
||||
current_user:
|
||||
),
|
||||
layout: false)
|
||||
end
|
||||
|
||||
# Move a story from an Agile::Sprint to another Agile::Sprint, or the Inbox.
|
||||
def move
|
||||
# The update service reloads the story internally (via #move_after),
|
||||
# so we memoize the previous sprint_id before the call.
|
||||
sprint_id_was = @story.sprint_id
|
||||
|
||||
move_attributes = infer_attributes_from_target
|
||||
unless move_story(move_attributes).success?
|
||||
return respond_with_turbo_streams(status: :unprocessable_entity)
|
||||
end
|
||||
|
||||
if target_inbox?(move_attributes)
|
||||
moved_to_inbox
|
||||
elsif target_sprint?(move_attributes) && @story.sprint_id != sprint_id_was
|
||||
moved_to_sprint
|
||||
end
|
||||
|
||||
respond_with_turbo_streams
|
||||
end
|
||||
|
||||
def reorder
|
||||
call = Stories::UpdateService
|
||||
.new(user: current_user, story: @story)
|
||||
.call(attributes: { move_to: reorder_param })
|
||||
|
||||
unless call.success?
|
||||
render_error_flash_message_via_turbo_stream(
|
||||
message: I18n.t(:notice_unsuccessful_update_with_reason, reason: call.message)
|
||||
)
|
||||
return respond_with_turbo_streams(status: :unprocessable_entity)
|
||||
end
|
||||
|
||||
replace_sprint_component_via_turbo_stream(sprint: @sprint)
|
||||
|
||||
respond_with_turbo_streams
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def move_story(move_attributes)
|
||||
call = update_story_with_target_and_position(attributes: move_attributes)
|
||||
|
||||
if call.success?
|
||||
# Update source component so that the moved story disappears
|
||||
replace_sprint_component_via_turbo_stream(sprint: @sprint)
|
||||
else
|
||||
render_error_flash_message_via_turbo_stream(
|
||||
message: I18n.t(:notice_unsuccessful_update_with_reason, reason: call.message)
|
||||
)
|
||||
end
|
||||
|
||||
call
|
||||
end
|
||||
|
||||
def update_story_with_target_and_position(attributes:)
|
||||
Stories::UpdateService
|
||||
.new(user: current_user, story: @story)
|
||||
.call(attributes:, **position_attributes)
|
||||
end
|
||||
|
||||
def moved_to_inbox
|
||||
render_success_flash_message_via_turbo_stream(
|
||||
message: I18n.t(:notice_successful_move, from: @sprint.name, to: I18n.t(:label_inbox))
|
||||
)
|
||||
work_packages = Backlog.inbox_for(project: @project)
|
||||
replace_via_turbo_stream(
|
||||
component: Backlogs::InboxComponent.new(work_packages:, project: @project),
|
||||
method: :morph
|
||||
)
|
||||
end
|
||||
|
||||
def moved_to_sprint
|
||||
moved_to(new_sprint: @story.sprint)
|
||||
end
|
||||
|
||||
def moved_to(new_sprint:)
|
||||
render_success_flash_message_via_turbo_stream(
|
||||
message: I18n.t(:notice_successful_move, from: @sprint.name, to: new_sprint.name)
|
||||
)
|
||||
|
||||
# Update the target component so that the moved story shows up
|
||||
replace_sprint_component_via_turbo_stream(sprint: new_sprint)
|
||||
end
|
||||
|
||||
def infer_attributes_from_target
|
||||
target_type, target_id = move_params[:target_id].split(":")
|
||||
|
||||
case target_type
|
||||
when "sprint"
|
||||
{ sprint_id: target_id }
|
||||
when "inbox"
|
||||
{ sprint_id: nil }
|
||||
else
|
||||
raise ArgumentError, "target_type must include one of: sprint, inbox."
|
||||
end
|
||||
end
|
||||
|
||||
def target_sprint?(move_attributes)
|
||||
move_attributes[:sprint_id].present?
|
||||
end
|
||||
|
||||
def target_inbox?(move_attributes)
|
||||
move_attributes.key?(:sprint_id) && move_attributes[:sprint_id].nil?
|
||||
end
|
||||
|
||||
def replace_sprint_component_via_turbo_stream(sprint:)
|
||||
replace_via_turbo_stream(component: Backlogs::SprintComponent.new(sprint: sprint, project: @project),
|
||||
method: :morph)
|
||||
end
|
||||
|
||||
def load_story
|
||||
@allowed_stories = WorkPackage.visible.where(sprint: @sprint, project: @project)
|
||||
@story = @allowed_stories.find(params[:id])
|
||||
end
|
||||
|
||||
def move_params
|
||||
params.require(%i[target_id])
|
||||
params.permit(:position, :prev_id, :target_id)
|
||||
end
|
||||
|
||||
def position_attributes
|
||||
if move_params.has_key?(:prev_id)
|
||||
{ prev_id: move_params[:prev_id].to_i }
|
||||
elsif move_params.has_key?(:position)
|
||||
{ position: move_params[:position].to_i }
|
||||
else
|
||||
{}
|
||||
end
|
||||
end
|
||||
|
||||
def reorder_param
|
||||
params.expect(:direction)
|
||||
end
|
||||
end
|
||||
+2
-2
@@ -49,7 +49,7 @@ See COPYRIGHT and LICENSE files for more details.
|
||||
render Primer::Beta::Button.new(
|
||||
tag: :a,
|
||||
label: Agile::Sprint.human_model_name,
|
||||
href: new_dialog_project_sprints_path(@project),
|
||||
href: new_dialog_project_backlogs_sprints_path(@project),
|
||||
data: {
|
||||
controller: "async-dialog",
|
||||
test_selector: "op-sprints--new-sprint-button"
|
||||
@@ -69,7 +69,7 @@ See COPYRIGHT and LICENSE files for more details.
|
||||
leading_icon: :plus,
|
||||
label: Agile::Sprint.human_model_name,
|
||||
tag: :a,
|
||||
href: new_dialog_project_sprints_path(@project),
|
||||
href: new_dialog_project_backlogs_sprints_path(@project),
|
||||
data: {
|
||||
controller: "async-dialog",
|
||||
test_selector: "op-sprints--new-sprint-button"
|
||||
+3
-3
@@ -30,7 +30,7 @@ See COPYRIGHT and LICENSE files for more details.
|
||||
<% html_title t(:label_backlogs) %>
|
||||
|
||||
<% content_controller "backlogs",
|
||||
"backlogs-list-url-value": backlogs_project_backlogs_path(@project) %>
|
||||
"backlogs-list-url-value": project_backlogs_backlog_path(@project) %>
|
||||
|
||||
<% content_for :content_header do %>
|
||||
<%=
|
||||
@@ -38,7 +38,7 @@ See COPYRIGHT and LICENSE files for more details.
|
||||
header.with_title { t(:label_backlog_and_sprints) }
|
||||
header.with_breadcrumbs(
|
||||
[{ href: project_overview_path(@project), text: @project.name },
|
||||
{ href: backlog_backlogs_project_backlogs_path(@project), text: t(:label_backlogs) },
|
||||
{ href: project_backlogs_backlog_path(@project), text: t(:label_backlogs) },
|
||||
t(:label_backlog_and_sprints)]
|
||||
)
|
||||
end
|
||||
@@ -48,7 +48,7 @@ See COPYRIGHT and LICENSE files for more details.
|
||||
<% content_for :content_body do %>
|
||||
<%= turbo_frame_tag :backlogs_container,
|
||||
refresh: :morph,
|
||||
src: backlog_backlogs_project_backlogs_path(@project, all: show_all_backlog),
|
||||
src: project_backlogs_backlog_path(@project, all: show_all_backlog),
|
||||
class: "op-backlogs-page" %>
|
||||
<% end %>
|
||||
|
||||
+1
-1
@@ -37,7 +37,7 @@ See COPYRIGHT and LICENSE files for more details.
|
||||
render(Backlogs::SprintPageHeaderComponent.new(sprint: @sprint, project: @project)) do |header|
|
||||
header.with_action_button(
|
||||
tag: :a,
|
||||
href: backlogs_project_sprint_taskboard_path(@project, @sprint),
|
||||
href: project_backlogs_sprint_taskboard_path(@project, @sprint),
|
||||
mobile_icon: :"op-view-cards",
|
||||
mobile_label: t("backlogs.label_sprint_board"),
|
||||
aria: { label: t("backlogs.label_sprint_board") }
|
||||
@@ -201,6 +201,11 @@ en:
|
||||
copy_work_package_id: "Copy work package ID"
|
||||
move_menu: "Move"
|
||||
|
||||
burndown_charts:
|
||||
show:
|
||||
blankslate_title: "No burndown data available"
|
||||
blankslate_description: "Set start and end date for the sprint to generate a burndown chart."
|
||||
|
||||
burndown:
|
||||
story_points: "Story points"
|
||||
story_points_ideal: "Story points (ideal)"
|
||||
@@ -253,9 +258,5 @@ en:
|
||||
caption: "Sprints created in this project will be available to all subprojects of the current project."
|
||||
info: "Sharing a sprint will share the name, status and the start and finish dates in all projects. These cannot be modified in projects that receive and use these sprints."
|
||||
sprint_sharing: Share sprints
|
||||
rb_burndown_charts:
|
||||
show:
|
||||
blankslate_title: "No burndown data available"
|
||||
blankslate_description: "Set start and end date for the sprint to generate a burndown chart."
|
||||
|
||||
remaining_hours: "remaining work"
|
||||
|
||||
@@ -29,69 +29,69 @@
|
||||
#++
|
||||
|
||||
Rails.application.routes.draw do
|
||||
rails_relative_url_root = OpenProject::Configuration["rails_relative_url_root"] || ""
|
||||
|
||||
scope "admin" do
|
||||
resource :backlogs, only: :show, controller: :backlogs_settings, as: "admin_backlogs_settings"
|
||||
end
|
||||
|
||||
# Routes for the new Agile::Sprint
|
||||
# Scoped under projects for permissions:
|
||||
resources :projects, only: [] do
|
||||
resources :sprints, controller: :rb_sprints, only: %i[create] do
|
||||
collection do
|
||||
get :new_dialog
|
||||
get :refresh_form
|
||||
end
|
||||
|
||||
member do
|
||||
post :start
|
||||
post :finish
|
||||
get :edit_dialog
|
||||
put :update_agile_sprint
|
||||
end
|
||||
|
||||
resources :stories, controller: :rb_stories, only: [] do
|
||||
member do
|
||||
get :menu
|
||||
put :move
|
||||
post :reorder
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
resources :inbox, only: [] do
|
||||
member do
|
||||
get :menu
|
||||
put :move
|
||||
post :reorder
|
||||
get :move_to_sprint_dialog
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
scope "projects/:project_id", as: "project", module: "projects" do
|
||||
namespace "settings" do
|
||||
resource :backlog_sharing, only: %i[show update]
|
||||
end
|
||||
end
|
||||
|
||||
# Legacy routes
|
||||
scope "", as: "backlogs" do
|
||||
scope "projects/:project_id", as: "project" do
|
||||
resources :backlogs, controller: :rb_master_backlogs, only: :index do
|
||||
collection do
|
||||
get "details/:work_package_id(/:tab)",
|
||||
action: :details,
|
||||
as: :details,
|
||||
work_package_split_view: true,
|
||||
defaults: { tab: :overview }
|
||||
resources :projects, only: [] do
|
||||
get "backlogs",
|
||||
to: redirect { |params, request|
|
||||
query = request.query_string.presence
|
||||
path = "#{rails_relative_url_root}/projects/#{params[:project_id]}/backlogs/backlog"
|
||||
|
||||
get :backlog
|
||||
query ? "#{path}?#{query}" : path
|
||||
},
|
||||
as: :backlogs
|
||||
|
||||
namespace :backlogs do
|
||||
resource :backlog, controller: :backlog, only: :show
|
||||
get "backlog/details/:work_package_id(/:tab)",
|
||||
to: "backlog#details",
|
||||
as: :backlog_details,
|
||||
work_package_split_view: true,
|
||||
defaults: { tab: :overview }
|
||||
|
||||
resources :sprints, param: :sprint_id, only: %i[create update] do
|
||||
collection do
|
||||
get :new_dialog
|
||||
get :refresh_form
|
||||
end
|
||||
|
||||
member do
|
||||
post :start
|
||||
post :finish
|
||||
get :edit_dialog
|
||||
end
|
||||
end
|
||||
|
||||
resources :sprints, controller: :rb_sprints, only: [] do
|
||||
resource :taskboard, controller: :rb_taskboards, only: :show
|
||||
resource :burndown_chart, controller: :rb_burndown_charts, only: :show
|
||||
scope "sprints/:sprint_id" do
|
||||
resources :stories, only: [] do
|
||||
member do
|
||||
get :menu
|
||||
put :move
|
||||
post :reorder
|
||||
end
|
||||
end
|
||||
|
||||
get "taskboard", to: "taskboard#show", as: :sprint_taskboard
|
||||
get "burndown_chart", to: "burndown_charts#show", as: :sprint_burndown_chart
|
||||
end
|
||||
|
||||
resources :inbox, only: [] do
|
||||
member do
|
||||
get :menu
|
||||
put :move
|
||||
post :reorder
|
||||
get :move_to_sprint_dialog
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -53,12 +53,11 @@ module OpenProject::Backlogs
|
||||
settings:) do
|
||||
project_module :backlogs, dependencies: :work_package_tracking do
|
||||
permission :view_sprints,
|
||||
{ rb_master_backlogs: %i[index backlog details],
|
||||
rb_sprints: %i[index show],
|
||||
rb_stories: %i[index show menu],
|
||||
inbox: :menu,
|
||||
rb_burndown_charts: :show,
|
||||
rb_taskboards: :show },
|
||||
{ "backlogs/backlog": %i[show details],
|
||||
"backlogs/stories": %i[index show menu],
|
||||
"backlogs/inbox": :menu,
|
||||
"backlogs/burndown_charts": :show,
|
||||
"backlogs/taskboard": :show },
|
||||
permissible_on: :project,
|
||||
dependencies: %i[view_work_packages show_board_views]
|
||||
|
||||
@@ -70,20 +69,20 @@ module OpenProject::Backlogs
|
||||
require: :member
|
||||
|
||||
permission :create_sprints,
|
||||
{ rb_sprints: %i[new_dialog refresh_form create edit_dialog update_agile_sprint] },
|
||||
{ "backlogs/sprints": %i[new_dialog refresh_form create edit_dialog update] },
|
||||
permissible_on: :project,
|
||||
require: :member,
|
||||
dependencies: :view_sprints
|
||||
|
||||
permission :start_complete_sprint,
|
||||
{ rb_sprints: %i[start finish] },
|
||||
{ "backlogs/sprints": %i[start finish] },
|
||||
permissible_on: :project,
|
||||
require: :member,
|
||||
dependencies: %i[view_sprints manage_board_views manage_sprint_items]
|
||||
|
||||
permission :manage_sprint_items,
|
||||
{ rb_stories: %i[move reorder],
|
||||
inbox: %i[move reorder move_to_sprint_dialog] },
|
||||
{ "backlogs/stories": %i[move reorder],
|
||||
"backlogs/inbox": %i[move reorder move_to_sprint_dialog] },
|
||||
permissible_on: :project,
|
||||
require: :member,
|
||||
dependencies: :view_sprints
|
||||
@@ -97,7 +96,7 @@ module OpenProject::Backlogs
|
||||
|
||||
menu :project_menu,
|
||||
:backlogs,
|
||||
{ controller: "/rb_master_backlogs", action: :backlog },
|
||||
{ controller: "/backlogs/backlog", action: :show },
|
||||
if: Proc.new { |project| project.module_enabled?(:backlogs) },
|
||||
caption: :project_module_backlogs,
|
||||
after: :work_packages,
|
||||
@@ -105,7 +104,7 @@ module OpenProject::Backlogs
|
||||
|
||||
menu :project_menu,
|
||||
:backlog,
|
||||
{ controller: "/rb_master_backlogs", action: :backlog },
|
||||
{ controller: "/backlogs/backlog", action: :show },
|
||||
if: Proc.new { |project| project.module_enabled?(:backlogs) },
|
||||
caption: :label_backlog_and_sprints,
|
||||
parent: :backlogs
|
||||
|
||||
@@ -87,7 +87,7 @@ RSpec.describe Backlogs::InboxItemComponent, type: :component do
|
||||
|
||||
it "sets the split-view and full-view URLs for the story controller" do
|
||||
expect(row["data-backlogs--story-split-url-value"])
|
||||
.to end_with(details_backlogs_project_backlogs_path(project, work_package))
|
||||
.to end_with(project_backlogs_backlog_details_path(project, work_package))
|
||||
expect(row["data-backlogs--story-full-url-value"])
|
||||
.to end_with(work_package_path(work_package))
|
||||
end
|
||||
|
||||
@@ -33,7 +33,7 @@ require "rails_helper"
|
||||
RSpec.describe Backlogs::MoveToSprintDialogComponent, type: :component do
|
||||
let(:project) { create(:project) }
|
||||
let(:work_package) { create(:work_package, project:) }
|
||||
let(:move_path) { Rails.application.routes.url_helpers.move_project_inbox_path(project, work_package) }
|
||||
let(:move_path) { Rails.application.routes.url_helpers.move_project_backlogs_inbox_path(project, work_package) }
|
||||
|
||||
def render_component
|
||||
render_inline(described_class.new(work_package:, project:))
|
||||
|
||||
@@ -118,7 +118,8 @@ RSpec.describe Backlogs::SprintComponent, type: :component do
|
||||
story_row = page.find(".Box-row[id='work_package_#{story1.id}']")
|
||||
expect(story_row["data-draggable-id"]).to eq(story1.id.to_s)
|
||||
expect(story_row["data-draggable-type"]).to eq("story")
|
||||
expect(story_row["data-drop-url"]).to end_with(move_project_sprint_story_path(project, sprint, story1))
|
||||
expected_path = move_project_backlogs_story_path(project, sprint_id: sprint.id, id: story1.id)
|
||||
expect(story_row["data-drop-url"]).to end_with(expected_path)
|
||||
end
|
||||
|
||||
it "renders story rows with proper classes" do
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
#-- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) the OpenProject GmbH
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License version 3.
|
||||
#
|
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
# Copyright (C) 2010-2013 the ChiliProject Team
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# See COPYRIGHT and LICENSE files for more details.
|
||||
#++
|
||||
|
||||
require "rails_helper"
|
||||
|
||||
RSpec.describe Backlogs::BacklogController do
|
||||
shared_let(:type_feature) { create(:type_feature) }
|
||||
shared_let(:type_task) { create(:type_task) }
|
||||
shared_let(:user) { create(:admin) }
|
||||
shared_let(:project) { create(:project) }
|
||||
shared_let(:status) { create(:status, name: "status 1", is_default: true) }
|
||||
shared_let(:sprint) { create(:agile_sprint, project:) }
|
||||
|
||||
current_user { user }
|
||||
|
||||
describe "GET #show" do
|
||||
it "loads the backlog page and preserves the backlog menu item", :aggregate_failures do
|
||||
get :show, params: { project_id: project.id }, format: :html
|
||||
|
||||
expect(response).to be_successful
|
||||
expect(response).to render_template("backlogs/backlog/show")
|
||||
expect(assigns(:project)).to eq(project)
|
||||
expect(assigns(:sprints)).to be_present
|
||||
expect(controller.controller_path).to eq("backlogs/backlog")
|
||||
expect(controller.action_name).to eq("show")
|
||||
expect(controller.current_menu_item).to eq(:backlog)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,54 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
#-- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) the OpenProject GmbH
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License version 3.
|
||||
#
|
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
# Copyright (C) 2010-2013 the ChiliProject Team
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# See COPYRIGHT and LICENSE files for more details.
|
||||
#++
|
||||
|
||||
require "rails_helper"
|
||||
|
||||
RSpec.describe Backlogs::BurndownChartsController do
|
||||
shared_let(:type_feature) { create(:type_feature) }
|
||||
shared_let(:type_task) { create(:type_task) }
|
||||
shared_let(:user) { create(:admin) }
|
||||
shared_let(:project) { create(:project) }
|
||||
shared_let(:status) { create(:status, name: "status 1", is_default: true) }
|
||||
shared_let(:sprint) { create(:agile_sprint, project:) }
|
||||
|
||||
current_user { user }
|
||||
|
||||
describe "GET #show" do
|
||||
it "renders under the namespaced controller runtime", :aggregate_failures do
|
||||
get :show, params: { project_id: project.id, sprint_id: sprint.id }, format: :html
|
||||
|
||||
expect(response).to be_successful
|
||||
expect(response).to render_template("backlogs/burndown_charts/show")
|
||||
expect(controller.controller_path).to eq("backlogs/burndown_charts")
|
||||
expect(assigns(:project)).to eq(project)
|
||||
expect(assigns(:sprint)).to eq(sprint)
|
||||
end
|
||||
end
|
||||
end
|
||||
+1
-1
@@ -30,7 +30,7 @@
|
||||
|
||||
require "rails_helper"
|
||||
|
||||
RSpec.describe InboxController do
|
||||
RSpec.describe Backlogs::InboxController do
|
||||
current_user { user }
|
||||
|
||||
let(:user) { create(:admin) }
|
||||
+24
-22
@@ -30,7 +30,7 @@
|
||||
|
||||
require "rails_helper"
|
||||
|
||||
RSpec.describe RbSprintsController do
|
||||
RSpec.describe Backlogs::SprintsController do
|
||||
describe "new actions" do
|
||||
shared_let(:type_feature) { create(:type_feature) }
|
||||
shared_let(:type_task) { create(:type_task) }
|
||||
@@ -70,7 +70,7 @@ RSpec.describe RbSprintsController do
|
||||
let!(:sprint) { create(:agile_sprint, project:) }
|
||||
|
||||
it "responds with success", :aggregate_failures do
|
||||
get :edit_dialog, params: { project_id: project.id, id: sprint.id }, format: :turbo_stream
|
||||
get :edit_dialog, params: { project_id: project.id, sprint_id: sprint.id }, format: :turbo_stream
|
||||
|
||||
expect(response).to be_successful
|
||||
expect(response).to have_http_status :ok
|
||||
@@ -83,7 +83,7 @@ RSpec.describe RbSprintsController do
|
||||
let(:permissions) { all_permissions - [:create_sprints] }
|
||||
|
||||
it "responds with forbidden", :aggregate_failures do
|
||||
get :edit_dialog, params: { project_id: project.id, id: sprint.id }, format: :turbo_stream
|
||||
get :edit_dialog, params: { project_id: project.id, sprint_id: sprint.id }, format: :turbo_stream
|
||||
|
||||
expect(response).not_to be_successful
|
||||
expect(response).to have_http_status :forbidden
|
||||
@@ -106,7 +106,7 @@ RSpec.describe RbSprintsController do
|
||||
expect(response).to have_http_status :ok
|
||||
expect(response.body).to include("turbo-stream")
|
||||
expect(response.body).to include("action=\"redirect_to\"")
|
||||
expect(response.body).to include(backlogs_project_backlogs_path(project))
|
||||
expect(response.body).to include(project_backlogs_backlog_path(project))
|
||||
expect(project.reload.sprints.last.name).to eq("My Sprint")
|
||||
expect(flash[:notice]).to eq(I18n.t(:notice_successful_create))
|
||||
end
|
||||
@@ -123,19 +123,19 @@ RSpec.describe RbSprintsController do
|
||||
end
|
||||
end
|
||||
|
||||
describe "PUT #update_agile_sprint" do
|
||||
describe "PUT #update" do
|
||||
let!(:sprint) { create(:agile_sprint, name: "Original sprint name", project:) }
|
||||
|
||||
let(:params) do
|
||||
{
|
||||
id: sprint.id,
|
||||
project_id: project.id,
|
||||
sprint_id: sprint.id,
|
||||
sprint: { name: "Changed sprint name" }
|
||||
}
|
||||
end
|
||||
|
||||
it "responds with success", :aggregate_failures do
|
||||
put :update_agile_sprint, format: :turbo_stream, params: params
|
||||
it "responds with success via the namespaced update action", :aggregate_failures do
|
||||
put :update, format: :turbo_stream, params: params
|
||||
|
||||
expect(response).to be_successful
|
||||
expect(response).to have_http_status :ok
|
||||
@@ -144,13 +144,15 @@ RSpec.describe RbSprintsController do
|
||||
assert_select %(turbo-stream[action="update"][target="backlogs-sprint-header-component-#{sprint.id}"][method="morph"])
|
||||
expect(response.body).to include("Successful update.")
|
||||
expect(sprint.reload.name).to eq("Changed sprint name")
|
||||
expect(controller.controller_path).to eq("backlogs/sprints")
|
||||
expect(controller.action_name).to eq("update")
|
||||
end
|
||||
|
||||
context "without the 'create_sprints' permission" do
|
||||
let(:permissions) { all_permissions - [:create_sprints] }
|
||||
|
||||
it "responds with forbidden", :aggregate_failures do
|
||||
put :update_agile_sprint, format: :turbo_stream, params: params
|
||||
put :update, format: :turbo_stream, params: params
|
||||
|
||||
expect(response).not_to be_successful
|
||||
expect(response).to have_http_status :forbidden
|
||||
@@ -162,7 +164,7 @@ RSpec.describe RbSprintsController do
|
||||
let!(:sprint) { create(:agile_sprint, project:) }
|
||||
let(:service_result) { ServiceResult.success(result: sprint.tap { it.status = "active" }) }
|
||||
let(:service) { instance_double(Sprints::StartService, call: service_result) }
|
||||
let(:request_params) { { project_id: project.id, id: sprint.id } }
|
||||
let(:request_params) { { project_id: project.id, sprint_id: sprint.id } }
|
||||
|
||||
before do
|
||||
allow(Sprints::StartService)
|
||||
@@ -261,7 +263,7 @@ RSpec.describe RbSprintsController do
|
||||
it "redirects back to the backlog and leaves the sprint in planning", :aggregate_failures do
|
||||
post :start, params: request_params
|
||||
|
||||
expect(response).to redirect_to(backlogs_project_backlogs_path(project))
|
||||
expect(response).to redirect_to(project_backlogs_backlog_path(project))
|
||||
expect(flash[:alert]).to eq(
|
||||
I18n.t(:notice_unsuccessful_start_with_reason, reason: "something went wrong")
|
||||
)
|
||||
@@ -275,7 +277,7 @@ RSpec.describe RbSprintsController do
|
||||
it "redirects back with the default start failure message", :aggregate_failures do
|
||||
post :start, params: request_params
|
||||
|
||||
expect(response).to redirect_to(backlogs_project_backlogs_path(project))
|
||||
expect(response).to redirect_to(project_backlogs_backlog_path(project))
|
||||
expect(flash[:alert]).to eq(I18n.t(:notice_unsuccessful_start))
|
||||
expect(service).to have_received(:call)
|
||||
end
|
||||
@@ -293,7 +295,7 @@ RSpec.describe RbSprintsController do
|
||||
it "redirects back to the backlog and leaves the sprint in planning", :aggregate_failures do
|
||||
post :start, params: request_params
|
||||
|
||||
expect(response).to redirect_to(backlogs_project_backlogs_path(project))
|
||||
expect(response).to redirect_to(project_backlogs_backlog_path(project))
|
||||
expect(flash[:alert]).to eq(I18n.t(:notice_unsuccessful_start))
|
||||
expect(service).to have_received(:call)
|
||||
end
|
||||
@@ -317,7 +319,7 @@ RSpec.describe RbSprintsController do
|
||||
it "redirects back with the default start failure message", :aggregate_failures do
|
||||
post :start, params: request_params
|
||||
|
||||
expect(response).to redirect_to(backlogs_project_backlogs_path(project))
|
||||
expect(response).to redirect_to(project_backlogs_backlog_path(project))
|
||||
expect(flash[:alert]).to eq(I18n.t(:notice_unsuccessful_start))
|
||||
expect(service).to have_received(:call)
|
||||
end
|
||||
@@ -326,7 +328,7 @@ RSpec.describe RbSprintsController do
|
||||
|
||||
describe "POST #finish" do
|
||||
let!(:sprint) { create(:agile_sprint, project:, status: "active") }
|
||||
let(:request_params) { { project_id: project.id, id: sprint.id } }
|
||||
let(:request_params) { { project_id: project.id, sprint_id: sprint.id } }
|
||||
let(:service_result) do
|
||||
ServiceResult.success(
|
||||
result: sprint.tap { |finished_sprint| finished_sprint.status = "completed" }
|
||||
@@ -359,7 +361,7 @@ RSpec.describe RbSprintsController do
|
||||
|
||||
expect(response).to be_successful
|
||||
expect(response.body).to include("action=\"redirect_to\"")
|
||||
expect(response.body).to include(backlogs_project_backlogs_path(project))
|
||||
expect(response.body).to include(project_backlogs_backlog_path(project))
|
||||
expect(flash[:notice]).to eq(I18n.t(:notice_successful_finish))
|
||||
expect(service).to have_received(:call)
|
||||
end
|
||||
@@ -382,7 +384,7 @@ RSpec.describe RbSprintsController do
|
||||
|
||||
expect(response).to be_successful
|
||||
expect(response.body).to include("action=\"redirect_to\"")
|
||||
expect(response.body).to include(backlogs_project_backlogs_path(project))
|
||||
expect(response.body).to include(project_backlogs_backlog_path(project))
|
||||
expect(flash[:notice]).to eq(I18n.t(:notice_successful_finish))
|
||||
expect(service).to have_received(:call)
|
||||
end
|
||||
@@ -393,7 +395,7 @@ RSpec.describe RbSprintsController do
|
||||
it "redirects back to the backlog", :aggregate_failures do
|
||||
post :finish, params: request_params
|
||||
|
||||
expect(response).to redirect_to(backlogs_project_backlogs_path(project))
|
||||
expect(response).to redirect_to(project_backlogs_backlog_path(project))
|
||||
expect(flash[:alert]).to eq(
|
||||
I18n.t(:notice_unsuccessful_finish_with_reason, reason: "something went wrong")
|
||||
)
|
||||
@@ -407,7 +409,7 @@ RSpec.describe RbSprintsController do
|
||||
it "redirects back with the default finish failure message", :aggregate_failures do
|
||||
post :finish, params: request_params
|
||||
|
||||
expect(response).to redirect_to(backlogs_project_backlogs_path(project))
|
||||
expect(response).to redirect_to(project_backlogs_backlog_path(project))
|
||||
expect(flash[:alert]).to eq(I18n.t(:notice_unsuccessful_finish))
|
||||
expect(service).to have_received(:call)
|
||||
end
|
||||
@@ -431,14 +433,14 @@ RSpec.describe RbSprintsController do
|
||||
it "redirects back with the default finish failure message", :aggregate_failures do
|
||||
post :finish, params: request_params
|
||||
|
||||
expect(response).to redirect_to(backlogs_project_backlogs_path(project))
|
||||
expect(response).to redirect_to(project_backlogs_backlog_path(project))
|
||||
expect(flash[:alert]).to eq(I18n.t(:notice_unsuccessful_finish))
|
||||
expect(service).to have_received(:call)
|
||||
end
|
||||
end
|
||||
|
||||
context "when moving to the top of the backlog" do
|
||||
let(:request_params) { { project_id: project.id, id: sprint.id, unfinished_action: "move_to_top_of_backlog" } }
|
||||
let(:request_params) { { project_id: project.id, sprint_id: sprint.id, unfinished_action: "move_to_top_of_backlog" } }
|
||||
|
||||
it "passes unfinished_action to the service and redirects via turbo stream", :aggregate_failures do
|
||||
post :finish, format: :turbo_stream, params: request_params
|
||||
@@ -451,7 +453,7 @@ RSpec.describe RbSprintsController do
|
||||
end
|
||||
|
||||
context "when moving to the bottom of the backlog" do
|
||||
let(:request_params) { { project_id: project.id, id: sprint.id, unfinished_action: "move_to_bottom_of_backlog" } }
|
||||
let(:request_params) { { project_id: project.id, sprint_id: sprint.id, unfinished_action: "move_to_bottom_of_backlog" } }
|
||||
|
||||
it "passes unfinished_action to the service and redirects via turbo stream", :aggregate_failures do
|
||||
post :finish, format: :turbo_stream, params: request_params
|
||||
+1
-1
@@ -30,7 +30,7 @@
|
||||
|
||||
require "rails_helper"
|
||||
|
||||
RSpec.describe RbStoriesController do
|
||||
RSpec.describe Backlogs::StoriesController do
|
||||
shared_let(:type_feature) { create(:type_feature) }
|
||||
shared_let(:type_task) { create(:type_task) }
|
||||
|
||||
+2
-1
@@ -30,7 +30,7 @@
|
||||
|
||||
require "spec_helper"
|
||||
|
||||
RSpec.describe RbTaskboardsController do
|
||||
RSpec.describe Backlogs::TaskboardController do
|
||||
shared_let(:type_feature) { create(:type_feature) }
|
||||
shared_let(:type_task) { create(:type_task) }
|
||||
let(:user) { create(:user) }
|
||||
@@ -66,6 +66,7 @@ RSpec.describe RbTaskboardsController do
|
||||
it "uses the board for the current project" do
|
||||
expect(response).to redirect_to(project_work_package_board_path(project, board))
|
||||
expect(response).not_to redirect_to(project_work_package_board_path(other_project, other_board))
|
||||
expect(controller.controller_path).to eq("backlogs/taskboard")
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -52,7 +52,7 @@ RSpec.describe "Create", :js do
|
||||
|
||||
within ".PageHeader-breadcrumbs" do
|
||||
expect(page).to have_link(href: project_path(project), text: project.name)
|
||||
expect(page).to have_link(href: backlog_backlogs_project_backlogs_path(project), text: "Backlogs")
|
||||
expect(page).to have_link(href: project_backlogs_backlog_path(project), text: "Backlogs")
|
||||
expect(page).to have_text("Backlog and sprints")
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
#-- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) the OpenProject GmbH
|
||||
@@ -27,6 +29,7 @@
|
||||
#++
|
||||
|
||||
require "spec_helper"
|
||||
require_relative "../support/pages/backlog"
|
||||
|
||||
RSpec.describe "Empty backlogs project",
|
||||
:js do
|
||||
@@ -34,10 +37,11 @@ RSpec.describe "Empty backlogs project",
|
||||
shared_let(:task) { create(:type_task) }
|
||||
shared_let(:project) { create(:project, types: [story, task], enabled_module_names: %w(backlogs)) }
|
||||
shared_let(:status) { create(:status, is_default: true) }
|
||||
let(:planning_page) { Pages::Backlog.new(project) }
|
||||
|
||||
before do
|
||||
login_as current_user
|
||||
visit backlogs_project_backlogs_path(project)
|
||||
planning_page.visit!
|
||||
end
|
||||
|
||||
context "as admin" do
|
||||
|
||||
@@ -41,4 +41,17 @@ RSpec.describe OpenProject::Backlogs::Engine do
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
describe "project menu" do
|
||||
it "points the backlog entries at the canonical backlog route" do
|
||||
project = create(:project)
|
||||
menu_items = Redmine::MenuManager.items(:project_menu, project).root.children
|
||||
|
||||
backlogs = menu_items.detect { |item| item.name == :backlogs }
|
||||
backlog = backlogs.children.detect { |item| item.name == :backlog }
|
||||
|
||||
expect(backlogs.url(project)).to eq(controller: "/backlogs/backlog", action: :show)
|
||||
expect(backlog.url(project)).to eq(controller: "/backlogs/backlog", action: :show)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -38,8 +38,18 @@ RSpec.describe OpenProject::AccessControl, "Backlogs module permissions" do # ru
|
||||
expect(subject.dependencies).to contain_exactly(:view_work_packages, :show_board_views)
|
||||
end
|
||||
|
||||
it "includes the namespaced backlog page and sprint controller actions" do
|
||||
expect(subject.controller_actions).to include(
|
||||
"backlogs/backlog/show",
|
||||
"backlogs/backlog/details",
|
||||
"backlogs/burndown_charts/show",
|
||||
"backlogs/taskboard/show"
|
||||
)
|
||||
expect(subject.controller_actions).not_to include("backlogs/backlog/index")
|
||||
end
|
||||
|
||||
it "includes deferred backlog story and inbox menu fragments" do
|
||||
expect(subject.controller_actions).to include("rb_stories/menu", "inbox/menu")
|
||||
expect(subject.controller_actions).to include("backlogs/stories/menu", "backlogs/inbox/menu")
|
||||
end
|
||||
end
|
||||
|
||||
@@ -49,6 +59,16 @@ RSpec.describe OpenProject::AccessControl, "Backlogs module permissions" do # ru
|
||||
it "depends on view_sprints" do
|
||||
expect(subject.dependencies).to contain_exactly(:view_sprints)
|
||||
end
|
||||
|
||||
it "uses the namespaced sprint controller actions" do
|
||||
expect(subject.controller_actions).to include(
|
||||
"backlogs/sprints/new_dialog",
|
||||
"backlogs/sprints/refresh_form",
|
||||
"backlogs/sprints/create",
|
||||
"backlogs/sprints/edit_dialog",
|
||||
"backlogs/sprints/update"
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
describe "manage_sprint_items" do
|
||||
@@ -67,7 +87,7 @@ RSpec.describe OpenProject::AccessControl, "Backlogs module permissions" do # ru
|
||||
end
|
||||
|
||||
it "covers both start and finish sprint actions" do
|
||||
expect(subject.controller_actions).to include("rb_sprints/start", "rb_sprints/finish")
|
||||
expect(subject.controller_actions).to include("backlogs/sprints/start", "backlogs/sprints/finish")
|
||||
end
|
||||
|
||||
it { is_expected.to be_visible }
|
||||
|
||||
+6
-6
@@ -30,7 +30,7 @@
|
||||
|
||||
require "spec_helper"
|
||||
|
||||
RSpec.describe "RbMasterBacklogs", :skip_csrf, type: :rails_request do
|
||||
RSpec.describe "Backlogs::Backlog", :skip_csrf, type: :rails_request do
|
||||
include Turbo::TestAssertions
|
||||
|
||||
shared_let(:type_feature) { create(:type_feature) }
|
||||
@@ -64,7 +64,7 @@ RSpec.describe "RbMasterBacklogs", :skip_csrf, type: :rails_request do
|
||||
get "/projects/#{project.identifier}/backlogs/backlog"
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(response).to render_template(:backlog)
|
||||
expect(response).to render_template("backlogs/backlog/show")
|
||||
expect(response).to have_turbo_frame "backlogs_container",
|
||||
src: "/projects/#{project.identifier}/backlogs/backlog?all=false"
|
||||
expect(response).to have_turbo_frame "content-bodyRight"
|
||||
@@ -83,7 +83,7 @@ RSpec.describe "RbMasterBacklogs", :skip_csrf, type: :rails_request do
|
||||
get "/projects/#{project.identifier}/backlogs/backlog", headers: { "Turbo-Frame" => "backlogs_container" }
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(response).to render_template("rb_master_backlogs/_backlog_list")
|
||||
expect(response).to render_template("backlogs/backlog/_backlog_list")
|
||||
|
||||
expect(response).to have_turbo_frame "backlogs_container"
|
||||
expect(response).to have_no_turbo_frame "content-bodyRight"
|
||||
@@ -111,10 +111,10 @@ RSpec.describe "RbMasterBacklogs", :skip_csrf, type: :rails_request do
|
||||
|
||||
describe "GET #details" do
|
||||
it "is successful" do
|
||||
get "/projects/#{project.identifier}/backlogs/details/#{story.id}"
|
||||
get "/projects/#{project.identifier}/backlogs/backlog/details/#{story.id}"
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(response).to render_template(:backlog)
|
||||
expect(response).to render_template("backlogs/backlog/show")
|
||||
|
||||
expect(response).to have_turbo_frame "backlogs_container",
|
||||
src: "/projects/#{project.identifier}/backlogs/backlog?all=false"
|
||||
@@ -123,7 +123,7 @@ RSpec.describe "RbMasterBacklogs", :skip_csrf, type: :rails_request do
|
||||
|
||||
context "with a Turbo Frame request" do
|
||||
it "renders the split view" do
|
||||
get "/projects/#{project.identifier}/backlogs/details/#{story.id}",
|
||||
get "/projects/#{project.identifier}/backlogs/backlog/details/#{story.id}",
|
||||
headers: { "Turbo-Frame" => "content-bodyRight" }
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
@@ -0,0 +1,51 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
#-- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) the OpenProject GmbH
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License version 3.
|
||||
#
|
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
# Copyright (C) 2010-2013 the ChiliProject Team
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# See COPYRIGHT and LICENSE files for more details.
|
||||
#++
|
||||
|
||||
require "spec_helper"
|
||||
|
||||
RSpec.describe "Backlogs::BurndownCharts", :skip_csrf, type: :rails_request do
|
||||
shared_let(:type_feature) { create(:type_feature) }
|
||||
shared_let(:type_task) { create(:type_task) }
|
||||
shared_let(:user) { create(:admin) }
|
||||
shared_let(:project) { create(:project) }
|
||||
shared_let(:status) { create(:status, name: "status 1", is_default: true) }
|
||||
shared_let(:sprint) { create(:agile_sprint, project:) }
|
||||
|
||||
current_user { user }
|
||||
|
||||
describe "GET #show" do
|
||||
it "renders the namespaced burndown chart template" do
|
||||
get "/projects/#{project.identifier}/backlogs/sprints/#{sprint.id}/burndown_chart"
|
||||
|
||||
expect(response).to be_successful
|
||||
expect(response).to render_template("backlogs/burndown_charts/show")
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,53 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
#-- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) the OpenProject GmbH
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License version 3.
|
||||
#
|
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
# Copyright (C) 2010-2013 the ChiliProject Team
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# See COPYRIGHT and LICENSE files for more details.
|
||||
#++
|
||||
|
||||
require "spec_helper"
|
||||
|
||||
RSpec.describe "Backlogs::Sprints", :skip_csrf, type: :rails_request do
|
||||
shared_let(:type_feature) { create(:type_feature) }
|
||||
shared_let(:type_task) { create(:type_task) }
|
||||
shared_let(:user) { create(:admin) }
|
||||
shared_let(:project) { create(:project) }
|
||||
shared_let(:status) { create(:status, name: "status 1", is_default: true) }
|
||||
shared_let(:sprint) { create(:agile_sprint, name: "Original sprint name", project:) }
|
||||
|
||||
current_user { user }
|
||||
|
||||
describe "PUT #update" do
|
||||
it "loads the sprint from sprint_id and updates it", :aggregate_failures do
|
||||
put "/projects/#{project.identifier}/backlogs/sprints/#{sprint.id}",
|
||||
headers: { "ACCEPT" => "text/vnd.turbo-stream.html" },
|
||||
params: { sprint: { name: "Changed sprint name" } }
|
||||
|
||||
expect(response).to be_successful
|
||||
expect(sprint.reload.name).to eq("Changed sprint name")
|
||||
end
|
||||
end
|
||||
end
|
||||
+22
-11
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
#-- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) the OpenProject GmbH
|
||||
@@ -28,24 +30,18 @@
|
||||
|
||||
require "spec_helper"
|
||||
|
||||
RSpec.describe RbMasterBacklogsController do
|
||||
RSpec.describe Backlogs::BacklogController do
|
||||
describe "routing" do
|
||||
it {
|
||||
expect(get("/projects/project_42/backlogs")).to route_to(controller: "rb_master_backlogs",
|
||||
action: "index",
|
||||
project_id: "project_42")
|
||||
}
|
||||
|
||||
it {
|
||||
route = "/projects/project_42/backlogs/backlog"
|
||||
expect(get(route)).to route_to(controller: "rb_master_backlogs",
|
||||
action: "backlog",
|
||||
expect(get(route)).to route_to(controller: "backlogs/backlog",
|
||||
action: "show",
|
||||
project_id: "project_42")
|
||||
}
|
||||
|
||||
it {
|
||||
expect(get("/projects/project_42/backlogs/details/33")).to route_to(
|
||||
controller: "rb_master_backlogs",
|
||||
expect(get("/projects/project_42/backlogs/backlog/details/33")).to route_to(
|
||||
controller: "backlogs/backlog",
|
||||
action: "details",
|
||||
project_id: "project_42",
|
||||
work_package_id: "33",
|
||||
@@ -54,4 +50,19 @@ RSpec.describe RbMasterBacklogsController do
|
||||
)
|
||||
}
|
||||
end
|
||||
|
||||
describe "named routing" do
|
||||
it {
|
||||
expect(project_backlogs_path("project_42")).to eq("/projects/project_42/backlogs")
|
||||
}
|
||||
|
||||
it {
|
||||
expect(project_backlogs_backlog_path("project_42")).to eq("/projects/project_42/backlogs/backlog")
|
||||
}
|
||||
|
||||
it {
|
||||
expect(project_backlogs_backlog_details_path("project_42", "33"))
|
||||
.to eq("/projects/project_42/backlogs/backlog/details/33")
|
||||
}
|
||||
end
|
||||
end
|
||||
+16
-5
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
#-- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) the OpenProject GmbH
|
||||
@@ -28,13 +30,22 @@
|
||||
|
||||
require "spec_helper"
|
||||
|
||||
RSpec.describe RbBurndownChartsController do
|
||||
RSpec.describe Backlogs::BurndownChartsController do
|
||||
describe "routing" do
|
||||
it {
|
||||
expect(get("/projects/project_42/sprints/21/burndown_chart")).to route_to(controller: "rb_burndown_charts",
|
||||
action: "show",
|
||||
project_id: "project_42",
|
||||
sprint_id: "21")
|
||||
expect(get("/projects/project_42/backlogs/sprints/21/burndown_chart")).to route_to(
|
||||
controller: "backlogs/burndown_charts",
|
||||
action: "show",
|
||||
project_id: "project_42",
|
||||
sprint_id: "21"
|
||||
)
|
||||
}
|
||||
end
|
||||
|
||||
describe "named routing" do
|
||||
it {
|
||||
expect(project_backlogs_sprint_burndown_chart_path("project_42", "21"))
|
||||
.to eq("/projects/project_42/backlogs/sprints/21/burndown_chart")
|
||||
}
|
||||
end
|
||||
end
|
||||
+7
-7
@@ -30,11 +30,11 @@
|
||||
|
||||
require "spec_helper"
|
||||
|
||||
RSpec.describe InboxController do
|
||||
RSpec.describe Backlogs::InboxController do
|
||||
describe "routing" do
|
||||
it {
|
||||
expect(put("/projects/project_42/inbox/85/move")).to route_to(
|
||||
controller: "inbox",
|
||||
expect(put("/projects/project_42/backlogs/inbox/85/move")).to route_to(
|
||||
controller: "backlogs/inbox",
|
||||
action: "move",
|
||||
project_id: "project_42",
|
||||
id: "85"
|
||||
@@ -42,8 +42,8 @@ RSpec.describe InboxController do
|
||||
}
|
||||
|
||||
it {
|
||||
expect(post("/projects/project_42/inbox/85/reorder")).to route_to(
|
||||
controller: "inbox",
|
||||
expect(post("/projects/project_42/backlogs/inbox/85/reorder")).to route_to(
|
||||
controller: "backlogs/inbox",
|
||||
action: "reorder",
|
||||
project_id: "project_42",
|
||||
id: "85"
|
||||
@@ -51,8 +51,8 @@ RSpec.describe InboxController do
|
||||
}
|
||||
|
||||
it {
|
||||
expect(get("/projects/project_42/inbox/85/menu")).to route_to(
|
||||
controller: "inbox",
|
||||
expect(get("/projects/project_42/backlogs/inbox/85/menu")).to route_to(
|
||||
controller: "backlogs/inbox",
|
||||
action: "menu",
|
||||
project_id: "project_42",
|
||||
id: "85"
|
||||
+37
-20
@@ -30,66 +30,83 @@
|
||||
|
||||
require "spec_helper"
|
||||
|
||||
RSpec.describe RbSprintsController do
|
||||
RSpec.describe Backlogs::SprintsController do
|
||||
describe "routing" do
|
||||
it {
|
||||
expect(get("/projects/project_42/sprints/new_dialog")).to route_to(
|
||||
controller: "rb_sprints",
|
||||
expect(get("/projects/project_42/backlogs/sprints/new_dialog")).to route_to(
|
||||
controller: "backlogs/sprints",
|
||||
action: "new_dialog",
|
||||
project_id: "project_42"
|
||||
)
|
||||
}
|
||||
|
||||
it {
|
||||
expect(get("/projects/project_42/sprints/refresh_form")).to route_to(
|
||||
controller: "rb_sprints",
|
||||
expect(get("/projects/project_42/backlogs/sprints/refresh_form")).to route_to(
|
||||
controller: "backlogs/sprints",
|
||||
action: "refresh_form",
|
||||
project_id: "project_42"
|
||||
)
|
||||
}
|
||||
|
||||
it {
|
||||
expect(post("/projects/project_42/sprints")).to route_to(
|
||||
controller: "rb_sprints",
|
||||
expect(post("/projects/project_42/backlogs/sprints")).to route_to(
|
||||
controller: "backlogs/sprints",
|
||||
action: "create",
|
||||
project_id: "project_42"
|
||||
)
|
||||
}
|
||||
|
||||
it {
|
||||
expect(get("/projects/project_42/sprints/21/edit_dialog")).to route_to(
|
||||
controller: "rb_sprints",
|
||||
expect(get("/projects/project_42/backlogs/sprints/21/edit_dialog")).to route_to(
|
||||
controller: "backlogs/sprints",
|
||||
action: "edit_dialog",
|
||||
project_id: "project_42",
|
||||
id: "21"
|
||||
sprint_id: "21"
|
||||
)
|
||||
}
|
||||
|
||||
it {
|
||||
expect(put("/projects/project_42/sprints/21/update_agile_sprint")).to route_to(
|
||||
controller: "rb_sprints",
|
||||
action: "update_agile_sprint",
|
||||
expect(put("/projects/project_42/backlogs/sprints/21")).to route_to(
|
||||
controller: "backlogs/sprints",
|
||||
action: "update",
|
||||
project_id: "project_42",
|
||||
id: "21"
|
||||
sprint_id: "21"
|
||||
)
|
||||
}
|
||||
|
||||
it {
|
||||
expect(post("/projects/project_42/sprints/21/start")).to route_to(
|
||||
controller: "rb_sprints",
|
||||
expect(post("/projects/project_42/backlogs/sprints/21/start")).to route_to(
|
||||
controller: "backlogs/sprints",
|
||||
action: "start",
|
||||
project_id: "project_42",
|
||||
id: "21"
|
||||
sprint_id: "21"
|
||||
)
|
||||
}
|
||||
|
||||
it {
|
||||
expect(post("/projects/project_42/sprints/21/finish")).to route_to(
|
||||
controller: "rb_sprints",
|
||||
expect(post("/projects/project_42/backlogs/sprints/21/finish")).to route_to(
|
||||
controller: "backlogs/sprints",
|
||||
action: "finish",
|
||||
project_id: "project_42",
|
||||
id: "21"
|
||||
sprint_id: "21"
|
||||
)
|
||||
}
|
||||
end
|
||||
|
||||
describe "named routing" do
|
||||
it {
|
||||
expect(new_dialog_project_backlogs_sprints_path("project_42"))
|
||||
.to eq("/projects/project_42/backlogs/sprints/new_dialog")
|
||||
}
|
||||
|
||||
it {
|
||||
expect(project_backlogs_sprints_path("project_42"))
|
||||
.to eq("/projects/project_42/backlogs/sprints")
|
||||
}
|
||||
|
||||
it {
|
||||
expect(project_backlogs_sprint_path("project_42", "21"))
|
||||
.to eq("/projects/project_42/backlogs/sprints/21")
|
||||
}
|
||||
end
|
||||
end
|
||||
+7
-7
@@ -30,11 +30,11 @@
|
||||
|
||||
require "spec_helper"
|
||||
|
||||
RSpec.describe RbStoriesController do
|
||||
RSpec.describe Backlogs::StoriesController do
|
||||
describe "routing" do
|
||||
it {
|
||||
expect(put("/projects/project_42/sprints/21/stories/85/move")).to route_to(
|
||||
controller: "rb_stories",
|
||||
expect(put("/projects/project_42/backlogs/sprints/21/stories/85/move")).to route_to(
|
||||
controller: "backlogs/stories",
|
||||
action: "move",
|
||||
project_id: "project_42",
|
||||
sprint_id: "21",
|
||||
@@ -43,8 +43,8 @@ RSpec.describe RbStoriesController do
|
||||
}
|
||||
|
||||
it {
|
||||
expect(post("/projects/project_42/sprints/21/stories/85/reorder")).to route_to(
|
||||
controller: "rb_stories",
|
||||
expect(post("/projects/project_42/backlogs/sprints/21/stories/85/reorder")).to route_to(
|
||||
controller: "backlogs/stories",
|
||||
action: "reorder",
|
||||
project_id: "project_42",
|
||||
sprint_id: "21",
|
||||
@@ -53,8 +53,8 @@ RSpec.describe RbStoriesController do
|
||||
}
|
||||
|
||||
it {
|
||||
expect(get("/projects/project_42/sprints/21/stories/85/menu")).to route_to(
|
||||
controller: "rb_stories",
|
||||
expect(get("/projects/project_42/backlogs/sprints/21/stories/85/menu")).to route_to(
|
||||
controller: "backlogs/stories",
|
||||
action: "menu",
|
||||
project_id: "project_42",
|
||||
sprint_id: "21",
|
||||
+16
-5
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
#-- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) the OpenProject GmbH
|
||||
@@ -28,13 +30,22 @@
|
||||
|
||||
require "spec_helper"
|
||||
|
||||
RSpec.describe RbTaskboardsController do
|
||||
RSpec.describe Backlogs::TaskboardController do
|
||||
describe "routing" do
|
||||
it {
|
||||
expect(get("/projects/project_42/sprints/21/taskboard")).to route_to(controller: "rb_taskboards",
|
||||
action: "show",
|
||||
project_id: "project_42",
|
||||
sprint_id: "21")
|
||||
expect(get("/projects/project_42/backlogs/sprints/21/taskboard")).to route_to(
|
||||
controller: "backlogs/taskboard",
|
||||
action: "show",
|
||||
project_id: "project_42",
|
||||
sprint_id: "21"
|
||||
)
|
||||
}
|
||||
end
|
||||
|
||||
describe "named routing" do
|
||||
it {
|
||||
expect(project_backlogs_sprint_taskboard_path("project_42", "21"))
|
||||
.to eq("/projects/project_42/backlogs/sprints/21/taskboard")
|
||||
}
|
||||
end
|
||||
end
|
||||
@@ -334,8 +334,17 @@ module Pages
|
||||
click_on "Cancel"
|
||||
end
|
||||
|
||||
def visit!
|
||||
super
|
||||
|
||||
expect(page).to have_css("turbo-frame#backlogs_container", wait: 10)
|
||||
expect(page).to have_css("#owner_backlogs_container", wait: 10)
|
||||
expect(page).to have_css("#sprint_backlogs_container", wait: 10)
|
||||
wait_for_network_idle
|
||||
end
|
||||
|
||||
def path
|
||||
backlog_backlogs_project_backlogs_path(project)
|
||||
project_backlogs_backlog_path(project)
|
||||
end
|
||||
|
||||
def within_story_menu(story, &)
|
||||
@@ -356,7 +365,7 @@ module Pages
|
||||
details_view.expect_tab :overview
|
||||
details_view.expect_subject
|
||||
|
||||
expect(page).to have_current_path details_backlogs_project_backlogs_path(story.project, story)
|
||||
expect(page).to have_current_path project_backlogs_backlog_details_path(story.project, story)
|
||||
wait_for_network_idle
|
||||
|
||||
details_view
|
||||
|
||||
+2
-2
@@ -30,7 +30,7 @@
|
||||
|
||||
require "spec_helper"
|
||||
|
||||
RSpec.describe "rb_burndown_charts/show" do
|
||||
RSpec.describe "backlogs/burndown_charts/show" do
|
||||
let(:user1) { create(:user) }
|
||||
let(:user2) { create(:user) }
|
||||
let(:role_allowed) do
|
||||
@@ -115,7 +115,7 @@ RSpec.describe "rb_burndown_charts/show" do
|
||||
render
|
||||
|
||||
expect(view).to render_template(partial: "_burndown", count: 0)
|
||||
expect(rendered).to include(I18n.t("rb_burndown_charts.show.blankslate_title"))
|
||||
expect(rendered).to include(I18n.t("backlogs.burndown_charts.show.blankslate_title"))
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -87,7 +87,7 @@ RSpec.describe DemoData::ProjectSeeder do
|
||||
project_seeder.seed!
|
||||
created_version = Version.find_by!(name: "First sprint")
|
||||
expect(created_version.wiki_page.text)
|
||||
.to eq("Please see the [Task board](/projects/some-project/sprints/#{created_version.id}/taskboard).")
|
||||
.to eq("Please see the [Task board](/projects/some-project/backlogs/sprints/#{created_version.id}/taskboard).")
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -357,7 +357,7 @@ RSpec.describe DemoData::WorkPackageSeeder do
|
||||
|
||||
it "creates link to the sprint with the right id" do
|
||||
expect(WorkPackage.last.description)
|
||||
.to eq("The [sprint](/projects/#{project.identifier}/sprints/#{sprint.id}/taskboard) of id #{sprint.id}.")
|
||||
.to eq("The [sprint](/projects/#{project.identifier}/backlogs/sprints/#{sprint.id}/taskboard) of id #{sprint.id}.")
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
Reference in New Issue
Block a user