[#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:
Alexander Brandon Coles
2026-04-17 11:48:51 +01:00
parent 28e7546420
commit afcdcdcc28
58 changed files with 1119 additions and 802 deletions
+3 -3
View File
@@ -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:)
@@ -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
@@ -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
@@ -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
@@ -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"
@@ -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 %>
@@ -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") }
+5 -4
View File
@@ -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"
+49 -49
View File
@@ -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
@@ -30,7 +30,7 @@
require "rails_helper"
RSpec.describe InboxController do
RSpec.describe Backlogs::InboxController do
current_user { user }
let(:user) { create(:admin) }
@@ -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
@@ -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) }
@@ -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 }
@@ -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
@@ -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
@@ -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
@@ -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"
@@ -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
@@ -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",
@@ -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
+11 -2
View File
@@ -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
@@ -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