From afcdcdcc28c0c4f50227c86c565c222609e52ddd Mon Sep 17 00:00:00 2001 From: Alexander Brandon Coles Date: Fri, 17 Apr 2026 11:48:51 +0100 Subject: [PATCH] [#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 --- app/seeders/demo_data/references.rb | 6 +- .../finish_sprint_dialog_component.html.erb | 2 +- .../backlogs/inbox_component.html.erb | 2 +- .../backlogs/inbox_item_component.html.erb | 2 +- .../backlogs/inbox_item_component.rb | 4 +- .../backlogs/inbox_menu_component.html.erb | 4 +- .../backlogs/inbox_menu_component.rb | 4 +- .../move_to_sprint_dialog_component.html.erb | 2 +- .../backlogs/new_sprint_form_component.rb | 6 +- .../components/backlogs/sprint_component.rb | 4 +- .../backlogs/sprint_header_component.rb | 4 +- .../backlogs/sprint_menu_component.html.erb | 6 +- .../backlogs/sprint_page_header_component.rb | 2 +- .../components/backlogs/story_component.rb | 2 +- .../story_menu_list_component.html.erb | 2 +- .../backlogs/story_menu_list_component.rb | 4 +- .../backlog_controller.rb} | 74 +++--- .../burndown_charts_controller.rb} | 16 +- .../controllers/backlogs/inbox_controller.rb | 137 ++++++++++ .../backlogs/sprints_controller.rb | 232 +++++++++++++++++ .../backlogs/stories_controller.rb | 185 ++++++++++++++ .../taskboard_controller.rb} | 14 +- .../app/controllers/inbox_controller.rb | 135 ---------- .../app/controllers/rb_sprints_controller.rb | 235 ------------------ .../app/controllers/rb_stories_controller.rb | 183 -------------- .../backlog}/_backlog_list.html.erb | 4 +- .../backlog/show.html.erb} | 6 +- .../burndown_charts}/_burndown.html.erb | 0 .../burndown_charts}/show.html.erb | 2 +- modules/backlogs/config/locales/en.yml | 9 +- modules/backlogs/config/routes.rb | 98 ++++---- .../lib/open_project/backlogs/engine.rb | 23 +- .../backlogs/inbox_item_component_spec.rb | 2 +- .../move_to_sprint_dialog_component_spec.rb | 2 +- .../backlogs/sprint_component_spec.rb | 3 +- .../backlogs/backlog_controller_spec.rb | 56 +++++ .../burndown_charts_controller_spec.rb | 54 ++++ .../{ => backlogs}/inbox_controller_spec.rb | 2 +- .../sprints_controller_spec.rb} | 46 ++-- .../stories_controller_spec.rb} | 2 +- .../taskboard_controller_spec.rb} | 3 +- .../spec/features/backlogs/create_spec.rb | 2 +- .../spec/features/empty_backlogs_spec.rb | 6 +- .../lib/open_project/backlogs/engine_spec.rb | 13 + .../open_project/backlogs/permissions_spec.rb | 24 +- .../backlog_spec.rb} | 12 +- .../requests/backlogs/burndown_charts_spec.rb | 51 ++++ .../spec/requests/backlogs/sprints_spec.rb | 53 ++++ .../backlog_routing_spec.rb} | 33 ++- .../burndown_charts_routing_spec.rb} | 21 +- .../{ => backlogs}/inbox_routing_spec.rb | 14 +- .../sprints_routing_spec.rb} | 57 +++-- .../stories_routing_spec.rb} | 14 +- .../taskboard_routing_spec.rb} | 21 +- .../backlogs/spec/support/pages/backlog.rb | 13 +- .../burndown_charts}/show_spec.rb | 4 +- spec/seeders/demo_data/project_seeder_spec.rb | 2 +- .../demo_data/work_package_seeder_spec.rb | 2 +- 58 files changed, 1119 insertions(+), 802 deletions(-) rename modules/backlogs/app/controllers/{rb_master_backlogs_controller.rb => backlogs/backlog_controller.rb} (50%) rename modules/backlogs/app/controllers/{rb_taskboards_controller.rb => backlogs/burndown_charts_controller.rb} (79%) create mode 100644 modules/backlogs/app/controllers/backlogs/inbox_controller.rb create mode 100644 modules/backlogs/app/controllers/backlogs/sprints_controller.rb create mode 100644 modules/backlogs/app/controllers/backlogs/stories_controller.rb rename modules/backlogs/app/controllers/{rb_burndown_charts_controller.rb => backlogs/taskboard_controller.rb} (82%) delete mode 100644 modules/backlogs/app/controllers/inbox_controller.rb delete mode 100644 modules/backlogs/app/controllers/rb_sprints_controller.rb delete mode 100644 modules/backlogs/app/controllers/rb_stories_controller.rb rename modules/backlogs/app/views/{rb_master_backlogs => backlogs/backlog}/_backlog_list.html.erb (97%) rename modules/backlogs/app/views/{rb_master_backlogs/backlog.html.erb => backlogs/backlog/show.html.erb} (87%) rename modules/backlogs/app/views/{rb_burndown_charts => backlogs/burndown_charts}/_burndown.html.erb (100%) rename modules/backlogs/app/views/{rb_burndown_charts => backlogs/burndown_charts}/show.html.erb (97%) create mode 100644 modules/backlogs/spec/controllers/backlogs/backlog_controller_spec.rb create mode 100644 modules/backlogs/spec/controllers/backlogs/burndown_charts_controller_spec.rb rename modules/backlogs/spec/controllers/{ => backlogs}/inbox_controller_spec.rb (99%) rename modules/backlogs/spec/controllers/{rb_sprints_controller_spec.rb => backlogs/sprints_controller_spec.rb} (90%) rename modules/backlogs/spec/controllers/{rb_stories_controller_spec.rb => backlogs/stories_controller_spec.rb} (99%) rename modules/backlogs/spec/controllers/{rb_taskboards_controller_spec.rb => backlogs/taskboard_controller_spec.rb} (97%) rename modules/backlogs/spec/requests/{rb_master_backlogs_spec.rb => backlogs/backlog_spec.rb} (91%) create mode 100644 modules/backlogs/spec/requests/backlogs/burndown_charts_spec.rb create mode 100644 modules/backlogs/spec/requests/backlogs/sprints_spec.rb rename modules/backlogs/spec/routing/{rb_master_backlogs_routing_spec.rb => backlogs/backlog_routing_spec.rb} (68%) rename modules/backlogs/spec/routing/{rb_burndown_charts_routing_spec.rb => backlogs/burndown_charts_routing_spec.rb} (71%) rename modules/backlogs/spec/routing/{ => backlogs}/inbox_routing_spec.rb (80%) rename modules/backlogs/spec/routing/{rb_sprints_routing_spec.rb => backlogs/sprints_routing_spec.rb} (56%) rename modules/backlogs/spec/routing/{rb_stories_routing_spec.rb => backlogs/stories_routing_spec.rb} (79%) rename modules/backlogs/spec/routing/{rb_taskboards_routing_spec.rb => backlogs/taskboard_routing_spec.rb} (72%) rename modules/backlogs/spec/views/{rb_burndown_charts => backlogs/burndown_charts}/show_spec.rb (96%) diff --git a/app/seeders/demo_data/references.rb b/app/seeders/demo_data/references.rb index 543e10f821c..9a71c86d505 100644 --- a/app/seeders/demo_data/references.rb +++ b/app/seeders/demo_data/references.rb @@ -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 ) diff --git a/modules/backlogs/app/components/backlogs/finish_sprint_dialog_component.html.erb b/modules/backlogs/app/components/backlogs/finish_sprint_dialog_component.html.erb index b8a179fcc14..38e38818077 100644 --- a/modules/backlogs/app/components/backlogs/finish_sprint_dialog_component.html.erb +++ b/modules/backlogs/app/components/backlogs/finish_sprint_dialog_component.html.erb @@ -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" } diff --git a/modules/backlogs/app/components/backlogs/inbox_component.html.erb b/modules/backlogs/app/components/backlogs/inbox_component.html.erb index 9165cd7de7f..46ceb0f45a5 100644 --- a/modules/backlogs/app/components/backlogs/inbox_component.html.erb +++ b/modules/backlogs/app/components/backlogs/inbox_component.html.erb @@ -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) } %> diff --git a/modules/backlogs/app/components/backlogs/inbox_item_component.html.erb b/modules/backlogs/app/components/backlogs/inbox_item_component.html.erb index b56c2466e64..d03e639e608 100644 --- a/modules/backlogs/app/components/backlogs/inbox_item_component.html.erb +++ b/modules/backlogs/app/components/backlogs/inbox_item_component.html.erb @@ -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" ) diff --git a/modules/backlogs/app/components/backlogs/inbox_item_component.rb b/modules/backlogs/app/components/backlogs/inbox_item_component.rb index bad17f8b371..4099606ed01 100644 --- a/modules/backlogs/app/components/backlogs/inbox_item_component.rb +++ b/modules/backlogs/app/components/backlogs/inbox_item_component.rb @@ -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 diff --git a/modules/backlogs/app/components/backlogs/inbox_menu_component.html.erb b/modules/backlogs/app/components/backlogs/inbox_menu_component.html.erb index 94fd84e98f9..8b1ddd19aa9 100644 --- a/modules/backlogs/app/components/backlogs/inbox_menu_component.html.erb +++ b/modules/backlogs/app/components/backlogs/inbox_menu_component.html.erb @@ -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) diff --git a/modules/backlogs/app/components/backlogs/inbox_menu_component.rb b/modules/backlogs/app/components/backlogs/inbox_menu_component.rb index 02d6ffa4195..b316a64efea 100644 --- a/modules/backlogs/app/components/backlogs/inbox_menu_component.rb +++ b/modules/backlogs/app/components/backlogs/inbox_menu_component.rb @@ -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:) diff --git a/modules/backlogs/app/components/backlogs/move_to_sprint_dialog_component.html.erb b/modules/backlogs/app/components/backlogs/move_to_sprint_dialog_component.html.erb index f847411559a..1357f0d57e6 100644 --- a/modules/backlogs/app/components/backlogs/move_to_sprint_dialog_component.html.erb +++ b/modules/backlogs/app/components/backlogs/move_to_sprint_dialog_component.html.erb @@ -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 } diff --git a/modules/backlogs/app/components/backlogs/new_sprint_form_component.rb b/modules/backlogs/app/components/backlogs/new_sprint_form_component.rb index 1b8532e6add..cc4001dfc5d 100644 --- a/modules/backlogs/app/components/backlogs/new_sprint_form_component.rb +++ b/modules/backlogs/app/components/backlogs/new_sprint_form_component.rb @@ -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 diff --git a/modules/backlogs/app/components/backlogs/sprint_component.rb b/modules/backlogs/app/components/backlogs/sprint_component.rb index 3e97c8f7037..506b83c7a32 100644 --- a/modules/backlogs/app/components/backlogs/sprint_component.rb +++ b/modules/backlogs/app/components/backlogs/sprint_component.rb @@ -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 diff --git a/modules/backlogs/app/components/backlogs/sprint_header_component.rb b/modules/backlogs/app/components/backlogs/sprint_header_component.rb index c2e124c8f2f..4c15c3a9560 100644 --- a/modules/backlogs/app/components/backlogs/sprint_header_component.rb +++ b/modules/backlogs/app/components/backlogs/sprint_header_component.rb @@ -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 diff --git a/modules/backlogs/app/components/backlogs/sprint_menu_component.html.erb b/modules/backlogs/app/components/backlogs/sprint_menu_component.html.erb index 9e552ff5668..826e918f223 100644 --- a/modules/backlogs/app/components/backlogs/sprint_menu_component.html.erb +++ b/modules/backlogs/app/components/backlogs/sprint_menu_component.html.erb @@ -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) diff --git a/modules/backlogs/app/components/backlogs/sprint_page_header_component.rb b/modules/backlogs/app/components/backlogs/sprint_page_header_component.rb index 6b752e30b97..8a170c409d5 100644 --- a/modules/backlogs/app/components/backlogs/sprint_page_header_component.rb +++ b/modules/backlogs/app/components/backlogs/sprint_page_header_component.rb @@ -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 diff --git a/modules/backlogs/app/components/backlogs/story_component.rb b/modules/backlogs/app/components/backlogs/story_component.rb index c3e6092a7c0..a8c52c840ea 100644 --- a/modules/backlogs/app/components/backlogs/story_component.rb +++ b/modules/backlogs/app/components/backlogs/story_component.rb @@ -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 diff --git a/modules/backlogs/app/components/backlogs/story_menu_list_component.html.erb b/modules/backlogs/app/components/backlogs/story_menu_list_component.html.erb index 7d8d01fe645..c07509cce5f 100644 --- a/modules/backlogs/app/components/backlogs/story_menu_list_component.html.erb +++ b/modules/backlogs/app/components/backlogs/story_menu_list_component.html.erb @@ -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") diff --git a/modules/backlogs/app/components/backlogs/story_menu_list_component.rb b/modules/backlogs/app/components/backlogs/story_menu_list_component.rb index 9e79dbea2a6..0f1fac3561e 100644 --- a/modules/backlogs/app/components/backlogs/story_menu_list_component.rb +++ b/modules/backlogs/app/components/backlogs/story_menu_list_component.rb @@ -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:) diff --git a/modules/backlogs/app/controllers/rb_master_backlogs_controller.rb b/modules/backlogs/app/controllers/backlogs/backlog_controller.rb similarity index 50% rename from modules/backlogs/app/controllers/rb_master_backlogs_controller.rb rename to modules/backlogs/app/controllers/backlogs/backlog_controller.rb index be4d2940f71..a0aa90f04c3 100644 --- a/modules/backlogs/app/controllers/rb_master_backlogs_controller.rb +++ b/modules/backlogs/app/controllers/backlogs/backlog_controller.rb @@ -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 diff --git a/modules/backlogs/app/controllers/rb_taskboards_controller.rb b/modules/backlogs/app/controllers/backlogs/burndown_charts_controller.rb similarity index 79% rename from modules/backlogs/app/controllers/rb_taskboards_controller.rb rename to modules/backlogs/app/controllers/backlogs/burndown_charts_controller.rb index cc4ed55519c..71dd61e886b 100644 --- a/modules/backlogs/app/controllers/rb_taskboards_controller.rb +++ b/modules/backlogs/app/controllers/backlogs/burndown_charts_controller.rb @@ -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 diff --git a/modules/backlogs/app/controllers/backlogs/inbox_controller.rb b/modules/backlogs/app/controllers/backlogs/inbox_controller.rb new file mode 100644 index 00000000000..6fe3e354910 --- /dev/null +++ b/modules/backlogs/app/controllers/backlogs/inbox_controller.rb @@ -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 diff --git a/modules/backlogs/app/controllers/backlogs/sprints_controller.rb b/modules/backlogs/app/controllers/backlogs/sprints_controller.rb new file mode 100644 index 00000000000..04673985346 --- /dev/null +++ b/modules/backlogs/app/controllers/backlogs/sprints_controller.rb @@ -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 diff --git a/modules/backlogs/app/controllers/backlogs/stories_controller.rb b/modules/backlogs/app/controllers/backlogs/stories_controller.rb new file mode 100644 index 00000000000..cf04c489b79 --- /dev/null +++ b/modules/backlogs/app/controllers/backlogs/stories_controller.rb @@ -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 diff --git a/modules/backlogs/app/controllers/rb_burndown_charts_controller.rb b/modules/backlogs/app/controllers/backlogs/taskboard_controller.rb similarity index 82% rename from modules/backlogs/app/controllers/rb_burndown_charts_controller.rb rename to modules/backlogs/app/controllers/backlogs/taskboard_controller.rb index c43b2fe40ea..7be0ccb1742 100644 --- a/modules/backlogs/app/controllers/rb_burndown_charts_controller.rb +++ b/modules/backlogs/app/controllers/backlogs/taskboard_controller.rb @@ -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 diff --git a/modules/backlogs/app/controllers/inbox_controller.rb b/modules/backlogs/app/controllers/inbox_controller.rb deleted file mode 100644 index 3334fba19b7..00000000000 --- a/modules/backlogs/app/controllers/inbox_controller.rb +++ /dev/null @@ -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 diff --git a/modules/backlogs/app/controllers/rb_sprints_controller.rb b/modules/backlogs/app/controllers/rb_sprints_controller.rb deleted file mode 100644 index c060a96bf78..00000000000 --- a/modules/backlogs/app/controllers/rb_sprints_controller.rb +++ /dev/null @@ -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 diff --git a/modules/backlogs/app/controllers/rb_stories_controller.rb b/modules/backlogs/app/controllers/rb_stories_controller.rb deleted file mode 100644 index 92e9ab99c7d..00000000000 --- a/modules/backlogs/app/controllers/rb_stories_controller.rb +++ /dev/null @@ -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 diff --git a/modules/backlogs/app/views/rb_master_backlogs/_backlog_list.html.erb b/modules/backlogs/app/views/backlogs/backlog/_backlog_list.html.erb similarity index 97% rename from modules/backlogs/app/views/rb_master_backlogs/_backlog_list.html.erb rename to modules/backlogs/app/views/backlogs/backlog/_backlog_list.html.erb index 598080216ae..cdefb411789 100644 --- a/modules/backlogs/app/views/rb_master_backlogs/_backlog_list.html.erb +++ b/modules/backlogs/app/views/backlogs/backlog/_backlog_list.html.erb @@ -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" diff --git a/modules/backlogs/app/views/rb_master_backlogs/backlog.html.erb b/modules/backlogs/app/views/backlogs/backlog/show.html.erb similarity index 87% rename from modules/backlogs/app/views/rb_master_backlogs/backlog.html.erb rename to modules/backlogs/app/views/backlogs/backlog/show.html.erb index 8c472eb29e2..1cefd15bc23 100644 --- a/modules/backlogs/app/views/rb_master_backlogs/backlog.html.erb +++ b/modules/backlogs/app/views/backlogs/backlog/show.html.erb @@ -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 %> diff --git a/modules/backlogs/app/views/rb_burndown_charts/_burndown.html.erb b/modules/backlogs/app/views/backlogs/burndown_charts/_burndown.html.erb similarity index 100% rename from modules/backlogs/app/views/rb_burndown_charts/_burndown.html.erb rename to modules/backlogs/app/views/backlogs/burndown_charts/_burndown.html.erb diff --git a/modules/backlogs/app/views/rb_burndown_charts/show.html.erb b/modules/backlogs/app/views/backlogs/burndown_charts/show.html.erb similarity index 97% rename from modules/backlogs/app/views/rb_burndown_charts/show.html.erb rename to modules/backlogs/app/views/backlogs/burndown_charts/show.html.erb index f3c123b33e1..a442508c099 100644 --- a/modules/backlogs/app/views/rb_burndown_charts/show.html.erb +++ b/modules/backlogs/app/views/backlogs/burndown_charts/show.html.erb @@ -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") } diff --git a/modules/backlogs/config/locales/en.yml b/modules/backlogs/config/locales/en.yml index 7edcc21c263..9a77e932a53 100644 --- a/modules/backlogs/config/locales/en.yml +++ b/modules/backlogs/config/locales/en.yml @@ -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" diff --git a/modules/backlogs/config/routes.rb b/modules/backlogs/config/routes.rb index 59f315a1936..623f6e3655f 100644 --- a/modules/backlogs/config/routes.rb +++ b/modules/backlogs/config/routes.rb @@ -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 diff --git a/modules/backlogs/lib/open_project/backlogs/engine.rb b/modules/backlogs/lib/open_project/backlogs/engine.rb index 07dbc5b6113..11ae07d7448 100644 --- a/modules/backlogs/lib/open_project/backlogs/engine.rb +++ b/modules/backlogs/lib/open_project/backlogs/engine.rb @@ -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 diff --git a/modules/backlogs/spec/components/backlogs/inbox_item_component_spec.rb b/modules/backlogs/spec/components/backlogs/inbox_item_component_spec.rb index 1c1a7dbd3f9..bd33723248b 100644 --- a/modules/backlogs/spec/components/backlogs/inbox_item_component_spec.rb +++ b/modules/backlogs/spec/components/backlogs/inbox_item_component_spec.rb @@ -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 diff --git a/modules/backlogs/spec/components/backlogs/move_to_sprint_dialog_component_spec.rb b/modules/backlogs/spec/components/backlogs/move_to_sprint_dialog_component_spec.rb index e6d61975619..ece199704f8 100644 --- a/modules/backlogs/spec/components/backlogs/move_to_sprint_dialog_component_spec.rb +++ b/modules/backlogs/spec/components/backlogs/move_to_sprint_dialog_component_spec.rb @@ -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:)) diff --git a/modules/backlogs/spec/components/backlogs/sprint_component_spec.rb b/modules/backlogs/spec/components/backlogs/sprint_component_spec.rb index 237989f40f1..4e3ce114967 100644 --- a/modules/backlogs/spec/components/backlogs/sprint_component_spec.rb +++ b/modules/backlogs/spec/components/backlogs/sprint_component_spec.rb @@ -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 diff --git a/modules/backlogs/spec/controllers/backlogs/backlog_controller_spec.rb b/modules/backlogs/spec/controllers/backlogs/backlog_controller_spec.rb new file mode 100644 index 00000000000..03e6608d719 --- /dev/null +++ b/modules/backlogs/spec/controllers/backlogs/backlog_controller_spec.rb @@ -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 diff --git a/modules/backlogs/spec/controllers/backlogs/burndown_charts_controller_spec.rb b/modules/backlogs/spec/controllers/backlogs/burndown_charts_controller_spec.rb new file mode 100644 index 00000000000..48bf59a8d12 --- /dev/null +++ b/modules/backlogs/spec/controllers/backlogs/burndown_charts_controller_spec.rb @@ -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 diff --git a/modules/backlogs/spec/controllers/inbox_controller_spec.rb b/modules/backlogs/spec/controllers/backlogs/inbox_controller_spec.rb similarity index 99% rename from modules/backlogs/spec/controllers/inbox_controller_spec.rb rename to modules/backlogs/spec/controllers/backlogs/inbox_controller_spec.rb index 26b4ef0ba91..ce663d4a598 100644 --- a/modules/backlogs/spec/controllers/inbox_controller_spec.rb +++ b/modules/backlogs/spec/controllers/backlogs/inbox_controller_spec.rb @@ -30,7 +30,7 @@ require "rails_helper" -RSpec.describe InboxController do +RSpec.describe Backlogs::InboxController do current_user { user } let(:user) { create(:admin) } diff --git a/modules/backlogs/spec/controllers/rb_sprints_controller_spec.rb b/modules/backlogs/spec/controllers/backlogs/sprints_controller_spec.rb similarity index 90% rename from modules/backlogs/spec/controllers/rb_sprints_controller_spec.rb rename to modules/backlogs/spec/controllers/backlogs/sprints_controller_spec.rb index cf37c280e6b..6467bc77b6c 100644 --- a/modules/backlogs/spec/controllers/rb_sprints_controller_spec.rb +++ b/modules/backlogs/spec/controllers/backlogs/sprints_controller_spec.rb @@ -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 diff --git a/modules/backlogs/spec/controllers/rb_stories_controller_spec.rb b/modules/backlogs/spec/controllers/backlogs/stories_controller_spec.rb similarity index 99% rename from modules/backlogs/spec/controllers/rb_stories_controller_spec.rb rename to modules/backlogs/spec/controllers/backlogs/stories_controller_spec.rb index befc0704c9b..4d452f175d8 100644 --- a/modules/backlogs/spec/controllers/rb_stories_controller_spec.rb +++ b/modules/backlogs/spec/controllers/backlogs/stories_controller_spec.rb @@ -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) } diff --git a/modules/backlogs/spec/controllers/rb_taskboards_controller_spec.rb b/modules/backlogs/spec/controllers/backlogs/taskboard_controller_spec.rb similarity index 97% rename from modules/backlogs/spec/controllers/rb_taskboards_controller_spec.rb rename to modules/backlogs/spec/controllers/backlogs/taskboard_controller_spec.rb index 5de66667681..5a06903faf7 100644 --- a/modules/backlogs/spec/controllers/rb_taskboards_controller_spec.rb +++ b/modules/backlogs/spec/controllers/backlogs/taskboard_controller_spec.rb @@ -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 diff --git a/modules/backlogs/spec/features/backlogs/create_spec.rb b/modules/backlogs/spec/features/backlogs/create_spec.rb index 789e235717e..4b1481d4385 100644 --- a/modules/backlogs/spec/features/backlogs/create_spec.rb +++ b/modules/backlogs/spec/features/backlogs/create_spec.rb @@ -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 diff --git a/modules/backlogs/spec/features/empty_backlogs_spec.rb b/modules/backlogs/spec/features/empty_backlogs_spec.rb index 3a3a290ffa0..2e5fddcf5db 100644 --- a/modules/backlogs/spec/features/empty_backlogs_spec.rb +++ b/modules/backlogs/spec/features/empty_backlogs_spec.rb @@ -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 diff --git a/modules/backlogs/spec/lib/open_project/backlogs/engine_spec.rb b/modules/backlogs/spec/lib/open_project/backlogs/engine_spec.rb index c35b020a474..cae9c0850d1 100644 --- a/modules/backlogs/spec/lib/open_project/backlogs/engine_spec.rb +++ b/modules/backlogs/spec/lib/open_project/backlogs/engine_spec.rb @@ -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 diff --git a/modules/backlogs/spec/lib/open_project/backlogs/permissions_spec.rb b/modules/backlogs/spec/lib/open_project/backlogs/permissions_spec.rb index 39c9546a71b..2880e31ed56 100644 --- a/modules/backlogs/spec/lib/open_project/backlogs/permissions_spec.rb +++ b/modules/backlogs/spec/lib/open_project/backlogs/permissions_spec.rb @@ -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 } diff --git a/modules/backlogs/spec/requests/rb_master_backlogs_spec.rb b/modules/backlogs/spec/requests/backlogs/backlog_spec.rb similarity index 91% rename from modules/backlogs/spec/requests/rb_master_backlogs_spec.rb rename to modules/backlogs/spec/requests/backlogs/backlog_spec.rb index 116851e41fe..e734ec6ce15 100644 --- a/modules/backlogs/spec/requests/rb_master_backlogs_spec.rb +++ b/modules/backlogs/spec/requests/backlogs/backlog_spec.rb @@ -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) diff --git a/modules/backlogs/spec/requests/backlogs/burndown_charts_spec.rb b/modules/backlogs/spec/requests/backlogs/burndown_charts_spec.rb new file mode 100644 index 00000000000..c211eee7e23 --- /dev/null +++ b/modules/backlogs/spec/requests/backlogs/burndown_charts_spec.rb @@ -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 diff --git a/modules/backlogs/spec/requests/backlogs/sprints_spec.rb b/modules/backlogs/spec/requests/backlogs/sprints_spec.rb new file mode 100644 index 00000000000..2a4c8a58a78 --- /dev/null +++ b/modules/backlogs/spec/requests/backlogs/sprints_spec.rb @@ -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 diff --git a/modules/backlogs/spec/routing/rb_master_backlogs_routing_spec.rb b/modules/backlogs/spec/routing/backlogs/backlog_routing_spec.rb similarity index 68% rename from modules/backlogs/spec/routing/rb_master_backlogs_routing_spec.rb rename to modules/backlogs/spec/routing/backlogs/backlog_routing_spec.rb index e47e641e80a..74b3ad879f0 100644 --- a/modules/backlogs/spec/routing/rb_master_backlogs_routing_spec.rb +++ b/modules/backlogs/spec/routing/backlogs/backlog_routing_spec.rb @@ -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 diff --git a/modules/backlogs/spec/routing/rb_burndown_charts_routing_spec.rb b/modules/backlogs/spec/routing/backlogs/burndown_charts_routing_spec.rb similarity index 71% rename from modules/backlogs/spec/routing/rb_burndown_charts_routing_spec.rb rename to modules/backlogs/spec/routing/backlogs/burndown_charts_routing_spec.rb index 2f1d9a1e652..30d7375aa0a 100644 --- a/modules/backlogs/spec/routing/rb_burndown_charts_routing_spec.rb +++ b/modules/backlogs/spec/routing/backlogs/burndown_charts_routing_spec.rb @@ -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 diff --git a/modules/backlogs/spec/routing/inbox_routing_spec.rb b/modules/backlogs/spec/routing/backlogs/inbox_routing_spec.rb similarity index 80% rename from modules/backlogs/spec/routing/inbox_routing_spec.rb rename to modules/backlogs/spec/routing/backlogs/inbox_routing_spec.rb index 0bdca34a520..1637b0d591c 100644 --- a/modules/backlogs/spec/routing/inbox_routing_spec.rb +++ b/modules/backlogs/spec/routing/backlogs/inbox_routing_spec.rb @@ -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" diff --git a/modules/backlogs/spec/routing/rb_sprints_routing_spec.rb b/modules/backlogs/spec/routing/backlogs/sprints_routing_spec.rb similarity index 56% rename from modules/backlogs/spec/routing/rb_sprints_routing_spec.rb rename to modules/backlogs/spec/routing/backlogs/sprints_routing_spec.rb index e1cc8bd04aa..a13b7d2d337 100644 --- a/modules/backlogs/spec/routing/rb_sprints_routing_spec.rb +++ b/modules/backlogs/spec/routing/backlogs/sprints_routing_spec.rb @@ -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 diff --git a/modules/backlogs/spec/routing/rb_stories_routing_spec.rb b/modules/backlogs/spec/routing/backlogs/stories_routing_spec.rb similarity index 79% rename from modules/backlogs/spec/routing/rb_stories_routing_spec.rb rename to modules/backlogs/spec/routing/backlogs/stories_routing_spec.rb index 3e855da4466..263db6d7bc2 100644 --- a/modules/backlogs/spec/routing/rb_stories_routing_spec.rb +++ b/modules/backlogs/spec/routing/backlogs/stories_routing_spec.rb @@ -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", diff --git a/modules/backlogs/spec/routing/rb_taskboards_routing_spec.rb b/modules/backlogs/spec/routing/backlogs/taskboard_routing_spec.rb similarity index 72% rename from modules/backlogs/spec/routing/rb_taskboards_routing_spec.rb rename to modules/backlogs/spec/routing/backlogs/taskboard_routing_spec.rb index 4aaa96fa937..b0a2c06c065 100644 --- a/modules/backlogs/spec/routing/rb_taskboards_routing_spec.rb +++ b/modules/backlogs/spec/routing/backlogs/taskboard_routing_spec.rb @@ -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 diff --git a/modules/backlogs/spec/support/pages/backlog.rb b/modules/backlogs/spec/support/pages/backlog.rb index 8f60b597cc6..5e638118117 100644 --- a/modules/backlogs/spec/support/pages/backlog.rb +++ b/modules/backlogs/spec/support/pages/backlog.rb @@ -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 diff --git a/modules/backlogs/spec/views/rb_burndown_charts/show_spec.rb b/modules/backlogs/spec/views/backlogs/burndown_charts/show_spec.rb similarity index 96% rename from modules/backlogs/spec/views/rb_burndown_charts/show_spec.rb rename to modules/backlogs/spec/views/backlogs/burndown_charts/show_spec.rb index 71c76e5262f..f641d21c55d 100644 --- a/modules/backlogs/spec/views/rb_burndown_charts/show_spec.rb +++ b/modules/backlogs/spec/views/backlogs/burndown_charts/show_spec.rb @@ -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 diff --git a/spec/seeders/demo_data/project_seeder_spec.rb b/spec/seeders/demo_data/project_seeder_spec.rb index 86628de9bd8..cc978472078 100644 --- a/spec/seeders/demo_data/project_seeder_spec.rb +++ b/spec/seeders/demo_data/project_seeder_spec.rb @@ -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 diff --git a/spec/seeders/demo_data/work_package_seeder_spec.rb b/spec/seeders/demo_data/work_package_seeder_spec.rb index ae1542c9d14..f9856da5297 100644 --- a/spec/seeders/demo_data/work_package_seeder_spec.rb +++ b/spec/seeders/demo_data/work_package_seeder_spec.rb @@ -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