diff --git a/frontend/src/turbo/action-menu-morph-remount.ts b/frontend/src/turbo/action-menu-morph-remount.ts new file mode 100644 index 000000000000..29266102da88 --- /dev/null +++ b/frontend/src/turbo/action-menu-morph-remount.ts @@ -0,0 +1,60 @@ +/* + * -- 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. + * ++ + */ + +/** + * Primer `` with `src` (via `Primer::Alpha::ActionMenu`) lazy loads + * the menu items via ``. + * It registers `include-fragment-replaced` only in `connectedCallback`, while Turbo + * Idiomorph can leave the same `` host connected while swapping + * in a new ``. Since the `` is not replaced, the fragment + * replaced event is never fired, the component will stay in a loading state after morph. + * + * Replacing each affected `` host forces `connectedCallback` and + * a correct listener. We hook `turbo:morph-element` so this applies to turbo-stream + * morph, frame morph, and full-page morph alike. + */ + +interface TurboMorphElementDetail { + currentElement:HTMLElement; + newElement:HTMLElement; +} + +export function registerActionMenuMorphRemount():void { + document.addEventListener('turbo:morph-element', (event:Event) => { + const { detail } = event as CustomEvent; + const currentElement = detail?.currentElement; + if (!currentElement?.matches('action-menu:has(include-fragment[src])')) { + return; + } + + const clone = currentElement.cloneNode(true); + currentElement.replaceWith(clone); + }); +} diff --git a/frontend/src/turbo/setup.ts b/frontend/src/turbo/setup.ts index c384b98ab41b..2b1f597f3a79 100644 --- a/frontend/src/turbo/setup.ts +++ b/frontend/src/turbo/setup.ts @@ -11,6 +11,7 @@ import { debugLog, whenDebugging } from 'core-app/shared/helpers/debug_output'; import { TURBO_EVENTS } from './constants'; import { StreamActions } from '@hotwired/turbo'; import { addTurboAngularWrapper } from 'core-turbo/turbo-angular-wrapper'; +import { registerActionMenuMorphRemount } from './action-menu-morph-remount'; Turbo.session.drive = true; Turbo.config.drive.progressBarDelay = 100; @@ -37,6 +38,7 @@ whenDebugging(() => { // Register our own actions addTurboEventListeners(); addTurboGlobalListeners(); +registerActionMenuMorphRemount(); registerDialogStreamAction(); registerFlashStreamAction(); registerLiveRegionStreamAction(); diff --git a/modules/backlogs/app/components/backlogs/backlog_component.html.erb b/modules/backlogs/app/components/backlogs/backlog_component.html.erb index dd18db687a75..5ce7f8ffec2c 100644 --- a/modules/backlogs/app/components/backlogs/backlog_component.html.erb +++ b/modules/backlogs/app/components/backlogs/backlog_component.html.erb @@ -56,7 +56,7 @@ See COPYRIGHT and LICENSE files for more details. ), tabindex: 0 ) do %> - <%= render(Backlogs::StoryComponent.new(story:, project:, sprint:, max_position:)) %> + <%= render(Backlogs::StoryComponent.new(story:, project:, sprint:)) %> <% end %> <% end %> <% end %> diff --git a/modules/backlogs/app/components/backlogs/backlog_component.rb b/modules/backlogs/app/components/backlogs/backlog_component.rb index 888de7921d63..5d17e318a82f 100644 --- a/modules/backlogs/app/components/backlogs/backlog_component.rb +++ b/modules/backlogs/app/components/backlogs/backlog_component.rb @@ -65,10 +65,6 @@ def folded? current_user.backlogs_preference(:versions_default_fold_state) == "closed" end - def max_position - stories.filter_map(&:position).max - end - def drop_target_config { generic_drag_and_drop_target: "container", diff --git a/modules/backlogs/app/components/backlogs/inbox_component.html.erb b/modules/backlogs/app/components/backlogs/inbox_component.html.erb index 81a3d13708a6..9165cd7de7f7 100644 --- a/modules/backlogs/app/components/backlogs/inbox_component.html.erb +++ b/modules/backlogs/app/components/backlogs/inbox_component.html.erb @@ -76,9 +76,7 @@ See COPYRIGHT and LICENSE files for more details. <%= render Backlogs::InboxItemComponent.with_collection( paginate? ? first_page : work_packages, container: border_box, - project:, - max_position:, - open_sprints_exist: + project: ) %> <% if paginate? %> @@ -110,9 +108,7 @@ See COPYRIGHT and LICENSE files for more details. <%= render Backlogs::InboxItemComponent.with_collection( last_page, container: border_box, - project:, - max_position:, - open_sprints_exist: + project: ) %> <% end %> <% end %> diff --git a/modules/backlogs/app/components/backlogs/inbox_component.rb b/modules/backlogs/app/components/backlogs/inbox_component.rb index bce41a65225f..6749070823dd 100644 --- a/modules/backlogs/app/components/backlogs/inbox_component.rb +++ b/modules/backlogs/app/components/backlogs/inbox_component.rb @@ -38,16 +38,15 @@ class InboxComponent < ApplicationComponent FIRST_PAGE_SIZE = 50 LAST_PAGE_SIZE = 10 - attr_reader :work_packages, :project, :current_user, :show_all, :open_sprints_exist + attr_reader :work_packages, :project, :current_user, :show_all - def initialize(work_packages:, project:, open_sprints_exist:, show_all: false, current_user: User.current, **system_arguments) + def initialize(work_packages:, project:, show_all: false, current_user: User.current, **system_arguments) super() @work_packages = work_packages @project = project @show_all = show_all @current_user = current_user - @open_sprints_exist = open_sprints_exist @system_arguments = system_arguments @system_arguments[:id] = inbox_dom_id @@ -100,9 +99,5 @@ def drop_target_config target_allowed_drag_type: "story" } end - - def max_position - work_packages.maximum(:position) - end end end 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 984d03e2ecdb..b56c2466e644 100644 --- a/modules/backlogs/app/components/backlogs/inbox_item_component.html.erb +++ b/modules/backlogs/app/components/backlogs/inbox_item_component.html.erb @@ -49,12 +49,21 @@ See COPYRIGHT and LICENSE files for more details. <% end %> <% end %> <% grid.with_area(:menu) do %> - <%= render Backlogs::InboxMenuComponent.new( - work_package:, - project:, - max_position:, - open_sprints_exist: - ) %> + <%= render( + Primer::Alpha::ActionMenu.new( + menu_id: dom_target(work_package, :menu), + src: menu_project_inbox_path(project, work_package), + anchor_align: :end, + classes: "hide-when-print" + ) + ) do |menu| %> + <% menu.with_show_button( + scheme: :invisible, + icon: :"kebab-horizontal", + "aria-label": t(".label_actions"), + tooltip_direction: :se + ) %> + <% end %> <% end %> <% grid.with_area(:subject) do %> <%= render(Primer::Beta::Text.new(font_weight: :semibold)) { work_package.subject } %> diff --git a/modules/backlogs/app/components/backlogs/inbox_item_component.rb b/modules/backlogs/app/components/backlogs/inbox_item_component.rb index 554fbf45cd58..bad17f8b371d 100644 --- a/modules/backlogs/app/components/backlogs/inbox_item_component.rb +++ b/modules/backlogs/app/components/backlogs/inbox_item_component.rb @@ -32,17 +32,14 @@ module Backlogs class InboxItemComponent < ApplicationComponent include OpPrimer::ComponentHelpers - attr_reader :work_package, :project, :container, :max_position, :current_user, :open_sprints_exist + attr_reader :work_package, :project, :container, :current_user - def initialize(inbox_item:, project:, container:, max_position:, open_sprints_exist:, - current_user: User.current) + def initialize(inbox_item:, project:, container:, current_user: User.current) super() @work_package = inbox_item @project = project @container = container - @max_position = max_position - @open_sprints_exist = open_sprints_exist @current_user = current_user end 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 0d6614f107b1..94fd84e98f91 100644 --- a/modules/backlogs/app/components/backlogs/inbox_menu_component.html.erb +++ b/modules/backlogs/app/components/backlogs/inbox_menu_component.html.erb @@ -28,15 +28,8 @@ See COPYRIGHT and LICENSE files for more details. ++# %> <%= - render(Primer::Alpha::ActionMenu.new(**@system_arguments)) do |menu| - menu.with_show_button( - scheme: :invisible, - icon: :"kebab-horizontal", - "aria-label": t(".label_actions"), - tooltip_direction: :se - ) - - menu.with_item( + render(Primer::Alpha::ActionMenu::List.new(menu_id:)) do |list| + list.with_item( id: dom_target(work_package, :menu, :open_details), tag: :a, label: t(:"js.button_open_details"), @@ -46,7 +39,7 @@ See COPYRIGHT and LICENSE files for more details. item.with_leading_visual_icon(icon: :"op-view-split") end - menu.with_item( + list.with_item( id: dom_target(work_package, :menu, :open_fullscreen), tag: :a, label: t(:"js.button_open_fullscreen"), @@ -56,7 +49,7 @@ See COPYRIGHT and LICENSE files for more details. item.with_leading_visual_icon(icon: :"screen-full") end - menu.with_item( + list.with_item( id: dom_target(work_package, :menu, :copy_url_to_clipboard), tag: :"clipboard-copy", label: t(".action_menu.copy_url_to_clipboard"), @@ -65,7 +58,7 @@ See COPYRIGHT and LICENSE files for more details. item.with_leading_visual_icon(icon: :copy) end - menu.with_item( + list.with_item( id: dom_target(work_package, :menu, :copy_work_package_id), tag: :"clipboard-copy", label: t(".action_menu.copy_work_package_id"), @@ -75,9 +68,14 @@ See COPYRIGHT and LICENSE files for more details. end if show_move_submenu? - menu.with_divider - - menu.with_sub_menu_item(label: t(".action_menu.move_menu")) do |move_menu| + list.with_divider + + list.with_item( + component_klass: Primer::Alpha::ActionMenu::SubMenuItem, + label: t(".action_menu.move_menu"), + select_variant: :none, + form_arguments: {} + ) do |move_menu| move_menu.with_leading_visual_icon(icon: :"op-arrow-in") with_item_group(move_menu) do diff --git a/modules/backlogs/app/components/backlogs/inbox_menu_component.rb b/modules/backlogs/app/components/backlogs/inbox_menu_component.rb index 28331cfe4aa6..02d6ffa4195a 100644 --- a/modules/backlogs/app/components/backlogs/inbox_menu_component.rb +++ b/modules/backlogs/app/components/backlogs/inbox_menu_component.rb @@ -29,27 +29,25 @@ #++ module Backlogs + # Renders Primer::Alpha::ActionMenu::List for the deferred menu (InboxController#menu). + # +menu_id+ must match the row ActionMenu in InboxItemComponent. class InboxMenuComponent < ApplicationComponent include OpPrimer::ComponentHelpers attr_reader :work_package, :project, :max_position, :current_user, :open_sprints_exist - def initialize(work_package:, project:, max_position:, open_sprints_exist:, current_user: User.current, **system_arguments) + def initialize(work_package:, project:, max_position:, open_sprints_exist:, current_user: User.current) super() @work_package = work_package @project = project - @max_position = max_position @current_user = current_user + @max_position = max_position @open_sprints_exist = open_sprints_exist + end - @system_arguments = system_arguments - @system_arguments[:menu_id] = dom_target(work_package, :menu) - @system_arguments[:anchor_align] = :end - @system_arguments[:classes] = class_names( - @system_arguments[:classes], - "hide-when-print" - ) + def menu_id + dom_target(work_package, :menu) end private @@ -60,7 +58,7 @@ def show_move_submenu? def show_move_items? allowed_to_manage_sprint_items? && - !(first_item? && last_item?) + !(first_item? && last_item?) end def show_move_to_sprint? diff --git a/modules/backlogs/app/components/backlogs/sprint_component.html.erb b/modules/backlogs/app/components/backlogs/sprint_component.html.erb index c6c1bbd5c06c..675a2ce4ccf3 100644 --- a/modules/backlogs/app/components/backlogs/sprint_component.html.erb +++ b/modules/backlogs/app/components/backlogs/sprint_component.html.erb @@ -49,7 +49,7 @@ See COPYRIGHT and LICENSE files for more details. data: story_data_attribute(story), tabindex: 0 ) do %> - <%= render(Backlogs::StoryComponent.new(story:, sprint:, project:, max_position:)) %> + <%= render(Backlogs::StoryComponent.new(story:, sprint:, project:)) %> <% end %> <% end %> <% end %> diff --git a/modules/backlogs/app/components/backlogs/sprint_component.rb b/modules/backlogs/app/components/backlogs/sprint_component.rb index 1cc7e06696b1..ad819f90219f 100644 --- a/modules/backlogs/app/components/backlogs/sprint_component.rb +++ b/modules/backlogs/app/components/backlogs/sprint_component.rb @@ -67,10 +67,6 @@ def folded? current_user.backlogs_preference(:versions_default_fold_state) == "closed" end - def max_position - stories.filter_map(&:position).max - end - def drop_target_config { generic_drag_and_drop_target: "container", diff --git a/modules/backlogs/app/components/backlogs/story_component.html.erb b/modules/backlogs/app/components/backlogs/story_component.html.erb index 37dd1cc7bc00..9465c851ea9f 100644 --- a/modules/backlogs/app/components/backlogs/story_component.html.erb +++ b/modules/backlogs/app/components/backlogs/story_component.html.erb @@ -51,7 +51,21 @@ See COPYRIGHT and LICENSE files for more details. <% end %> <% grid.with_area(:menu) do %> - <%= render(Backlogs::StoryMenuComponent.new(story:, sprint:, project:, max_position:)) %> + <%= render( + Primer::Alpha::ActionMenu.new( + menu_id: dom_target(story, :menu), + src: menu_backlogs_project_sprint_story_path(project, sprint, story), + anchor_align: :end, + classes: "hide-when-print" + ) + ) do |menu| %> + <% menu.with_show_button( + scheme: :invisible, + icon: :"kebab-horizontal", + "aria-label": t(".label_actions"), + tooltip_direction: :se + ) %> + <% end %> <% end %> <% grid.with_area(:subject) do %> diff --git a/modules/backlogs/app/components/backlogs/story_component.rb b/modules/backlogs/app/components/backlogs/story_component.rb index 50f2841b6d12..3e23fb94a323 100644 --- a/modules/backlogs/app/components/backlogs/story_component.rb +++ b/modules/backlogs/app/components/backlogs/story_component.rb @@ -32,15 +32,14 @@ module Backlogs class StoryComponent < ApplicationComponent include OpPrimer::ComponentHelpers - attr_reader :story, :sprint, :project, :max_position, :current_user + attr_reader :story, :sprint, :project, :current_user - def initialize(story:, sprint:, project:, max_position:, current_user: User.current) + def initialize(story:, sprint:, project:, current_user: User.current) super() @story = story @sprint = sprint @project = project - @max_position = max_position @current_user = current_user end diff --git a/modules/backlogs/app/components/backlogs/story_menu_component.html.erb b/modules/backlogs/app/components/backlogs/story_menu_list_component.html.erb similarity index 85% rename from modules/backlogs/app/components/backlogs/story_menu_component.html.erb rename to modules/backlogs/app/components/backlogs/story_menu_list_component.html.erb index 3b9f1d4b560a..7d8d01fe645c 100644 --- a/modules/backlogs/app/components/backlogs/story_menu_component.html.erb +++ b/modules/backlogs/app/components/backlogs/story_menu_list_component.html.erb @@ -28,15 +28,8 @@ See COPYRIGHT and LICENSE files for more details. ++# %> <%= - render(Primer::Alpha::ActionMenu.new(**@system_arguments)) do |menu| - menu.with_show_button( - scheme: :invisible, - icon: :"kebab-horizontal", - "aria-label": t(".label_actions"), - tooltip_direction: :se - ) - - menu.with_item( + render(Primer::Alpha::ActionMenu::List.new(menu_id:)) do |list| + list.with_item( id: dom_target(story, :menu, :open_details), tag: :a, label: t(:"js.button_open_details"), @@ -46,7 +39,7 @@ See COPYRIGHT and LICENSE files for more details. item.with_leading_visual_icon(icon: :"op-view-split") end - menu.with_item( + list.with_item( id: dom_target(story, :menu, :open_fullscreen), tag: :a, label: t(:"js.button_open_fullscreen"), @@ -56,7 +49,7 @@ See COPYRIGHT and LICENSE files for more details. item.with_leading_visual_icon(icon: :"screen-full") end - menu.with_item( + list.with_item( id: dom_target(story, :menu, :copy_url_to_clipboard), tag: :"clipboard-copy", label: t(".action_menu.copy_url_to_clipboard"), @@ -65,7 +58,7 @@ See COPYRIGHT and LICENSE files for more details. item.with_leading_visual_icon(icon: :copy) end - menu.with_item( + list.with_item( id: dom_target(story, :menu, :copy_work_package_id), tag: :"clipboard-copy", label: t(".action_menu.copy_work_package_id"), @@ -75,9 +68,13 @@ See COPYRIGHT and LICENSE files for more details. end if show_move_items? - menu.with_divider - - menu.with_sub_menu_item(label: t(".action_menu.move_menu")) do |move_menu| + list.with_divider + list.with_item( + component_klass: Primer::Alpha::ActionMenu::SubMenuItem, + label: t(".action_menu.move_menu"), + select_variant: :none, + form_arguments: {} + ) do |move_menu| move_menu.with_leading_visual_icon(icon: :"op-arrow-in") with_item_group(move_menu) do diff --git a/modules/backlogs/app/components/backlogs/story_menu_component.rb b/modules/backlogs/app/components/backlogs/story_menu_list_component.rb similarity index 87% rename from modules/backlogs/app/components/backlogs/story_menu_component.rb rename to modules/backlogs/app/components/backlogs/story_menu_list_component.rb index 4f486f81a158..c4bf7b66cc83 100644 --- a/modules/backlogs/app/components/backlogs/story_menu_component.rb +++ b/modules/backlogs/app/components/backlogs/story_menu_list_component.rb @@ -29,12 +29,14 @@ #++ module Backlogs - class StoryMenuComponent < ApplicationComponent + # Renders Primer::Alpha::ActionMenu::List for the deferred menu (RbStoriesController#menu). + # +menu_id+ must match the row ActionMenu in StoryComponent. + class StoryMenuListComponent < ApplicationComponent include OpPrimer::ComponentHelpers attr_reader :story, :sprint, :project, :max_position, :current_user - def initialize(story:, sprint:, project:, max_position:, current_user: User.current, **system_arguments) + def initialize(story:, sprint:, project:, max_position:, current_user: User.current) super() @story = story @@ -42,21 +44,17 @@ def initialize(story:, sprint:, project:, max_position:, current_user: User.curr @project = project @max_position = max_position @current_user = current_user + end - @system_arguments = system_arguments - @system_arguments[:menu_id] = dom_target(story, :menu) - @system_arguments[:anchor_align] = :end - @system_arguments[:classes] = class_names( - @system_arguments[:classes], - "hide-when-print" - ) + def menu_id + dom_target(story, :menu) end private def show_move_items? allowed_to_manage_sprint_items? && - !(first_item? && last_item?) + !(first_item? && last_item?) end def allowed_to_manage_sprint_items? diff --git a/modules/backlogs/app/controllers/inbox_controller.rb b/modules/backlogs/app/controllers/inbox_controller.rb index 653009c6a728..8b44cfe30f40 100644 --- a/modules/backlogs/app/controllers/inbox_controller.rb +++ b/modules/backlogs/app/controllers/inbox_controller.rb @@ -34,6 +34,21 @@ class InboxController < RbApplicationController before_action :not_authorized_on_feature_flag_inactive 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, @@ -71,7 +86,7 @@ def move private def load_work_package - @work_package = WorkPackage.visible.find(params[:id]) + @work_package = WorkPackage.visible.where(project: @project).find(params[:id]) end def replace_inbox_component_via_turbo_stream @@ -79,8 +94,7 @@ def replace_inbox_component_via_turbo_stream replace_via_turbo_stream( component: Backlogs::InboxComponent.new( work_packages:, - project: @project, - open_sprints_exist: true + project: @project ), method: :morph ) diff --git a/modules/backlogs/app/controllers/rb_stories_controller.rb b/modules/backlogs/app/controllers/rb_stories_controller.rb index 488d598c5f29..4696e7035153 100644 --- a/modules/backlogs/app/controllers/rb_stories_controller.rb +++ b/modules/backlogs/app/controllers/rb_stories_controller.rb @@ -33,6 +33,20 @@ class RbStoriesController < RbApplicationController 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 a Sprint to another Sprint or an Agile::Sprint. def move_legacy # The update service reloads the story internally (via #move_after), @@ -128,9 +142,8 @@ def moved_to_inbox message: I18n.t(:notice_successful_move, from: @sprint.name, to: I18n.t(:label_inbox)) ) work_packages = Backlog.inbox_for(project: @project) - open_sprints_exist = Agile::Sprint.for_project(@project).not_completed.exists? replace_via_turbo_stream( - component: Backlogs::InboxComponent.new(work_packages:, project: @project, open_sprints_exist:), + component: Backlogs::InboxComponent.new(work_packages:, project: @project), method: :morph ) end @@ -202,11 +215,13 @@ def replace_sprint_component_via_turbo_stream(sprint:) end def load_story - @story = if OpenProject::FeatureDecisions.scrum_projects_active? - WorkPackage.visible.find(params[:id]) - else - Story.visible.find(params[:id]) - end + @allowed_stories = + if OpenProject::FeatureDecisions.scrum_projects_active? + WorkPackage.visible.where(sprint: @sprint, project: @project) + else + Story.visible.where(Story.condition(@project, @sprint)) + end + @story = @allowed_stories.find(params[:id]) end def move_params diff --git a/modules/backlogs/app/views/rb_master_backlogs/_backlog_list.html.erb b/modules/backlogs/app/views/rb_master_backlogs/_backlog_list.html.erb index a31c53d9ff00..598080216ae0 100644 --- a/modules/backlogs/app/views/rb_master_backlogs/_backlog_list.html.erb +++ b/modules/backlogs/app/views/rb_master_backlogs/_backlog_list.html.erb @@ -35,8 +35,7 @@ See COPYRIGHT and LICENSE files for more details. <%= render Backlogs::InboxComponent.new( work_packages: @inbox_work_packages, project: @project, - show_all: show_all_backlog, - open_sprints_exist: @sprints.any? + show_all: show_all_backlog ) %>
[type_feature.id], "task_type" => type_task.id }) end + describe "load_story" do + subject do + get :menu, + params: { project_id: project.id, sprint_id: requested_sprint.id, id: load_story_id }, + format: :html + end + + context "when scrum_projects flag is inactive", with_flag: { scrum_projects: false } do + let(:load_story_id) { story.id } + let(:requested_sprint) { version_sprint } + + context "when the story is in the requested sprint" do + it "assigns the visible story", :aggregate_failures do + subject + expect(response).to be_successful + expect(response).to have_http_status :ok + expect(assigns(:story)).to eq(story) + end + end + + context "when the story is not in the requested sprint" do + let(:requested_sprint) { create(:sprint, name: "Sprint load_story other", project:) } + + it { is_expected.to have_http_status :not_found } + end + end + + context "when scrum_projects flag is active", with_flag: { scrum_projects: true } do + let(:agile_sprint) { create(:agile_sprint, name: "Agile Sprint load_story", project:) } + let(:work_package_in_sprint) { create(:work_package, status:, sprint: agile_sprint, project:) } + let(:load_story_id) { work_package_in_sprint.id } + + context "when the work package is in the requested sprint" do + let(:requested_sprint) { agile_sprint } + + it "assigns the visible work package", :aggregate_failures do + subject + expect(response).to be_successful + expect(response).to have_http_status :ok + expect(assigns(:story)).to eq(work_package_in_sprint) + end + end + + context "when the work package is not in the requested sprint" do + let(:requested_sprint) { create(:agile_sprint, name: "Other Sprint load_story", project:) } + + it { is_expected.to have_http_status :not_found } + end + end + end + describe "PUT #move_legacy" do context "with a user lacking project permission" do let(:user) { create(:user) } @@ -334,4 +385,25 @@ end end end + + describe "GET #menu" do + subject do + get :menu, params: { project_id: project.id, sprint_id: version_sprint.id, id: story.id }, format: :html + end + + it "returns deferred action menu list HTML", :aggregate_failures do + subject + expect(response).to have_http_status :ok + expect(response.body).to include(I18n.t(:"js.button_open_details")) + end + + context "with a user lacking project permission" do + let(:user) { create(:user) } + + it "responds with 404" do + subject + expect(response).to have_http_status :not_found + end + 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 4e4a5724a2db..7aaa9c0605f0 100644 --- a/modules/backlogs/spec/lib/open_project/backlogs/permissions_spec.rb +++ b/modules/backlogs/spec/lib/open_project/backlogs/permissions_spec.rb @@ -37,6 +37,10 @@ it "depends on view_work_packages and show_board_views" do expect(subject.dependencies).to contain_exactly(:view_work_packages, :show_board_views) end + + it "includes deferred backlog story and inbox menu fragments" do + expect(subject.controller_actions).to include("rb_stories/menu", "inbox/menu") + end end describe "create_sprints" do diff --git a/modules/backlogs/spec/routing/inbox_routing_spec.rb b/modules/backlogs/spec/routing/inbox_routing_spec.rb index 21a81f2ddb38..cfa1e7288daf 100644 --- a/modules/backlogs/spec/routing/inbox_routing_spec.rb +++ b/modules/backlogs/spec/routing/inbox_routing_spec.rb @@ -49,5 +49,14 @@ id: "85" ) } + + it { + expect(get("/projects/project_42/inbox/85/menu")).to route_to( + controller: "inbox", + action: "menu", + project_id: "project_42", + id: "85" + ) + } end end diff --git a/modules/backlogs/spec/routing/rb_stories_routing_spec.rb b/modules/backlogs/spec/routing/rb_stories_routing_spec.rb index 910795cf440b..4bf6b239918d 100644 --- a/modules/backlogs/spec/routing/rb_stories_routing_spec.rb +++ b/modules/backlogs/spec/routing/rb_stories_routing_spec.rb @@ -67,5 +67,15 @@ id: "85" ) } + + it { + expect(get("/projects/project_42/sprints/21/stories/85/menu")).to route_to( + controller: "rb_stories", + action: "menu", + project_id: "project_42", + sprint_id: "21", + id: "85" + ) + } end end