diff --git a/app/components/open_project/common/work_package_card_box_component.rb b/app/components/open_project/common/work_package_card_box_component.rb index 244443d25b2..c81608da045 100644 --- a/app/components/open_project/common/work_package_card_box_component.rb +++ b/app/components/open_project/common/work_package_card_box_component.rb @@ -86,20 +86,15 @@ module OpenProject # # # # @param work_package [WorkPackage] the work package rendered in the row. # # @param component_klass [Class] row bridge class used instead of the - # # default `Item`. It must accept the arguments documented on + # # box's configured `item_component_klass`. It must accept the + # # arguments documented on # # `#build_item`, expose `#row_args` with valid # # `Primer::Beta::BorderBox#with_row` keyword arguments, and expose # # `#card` returning a renderable object. - # # @param item_menu_src [String, NilClass] optional menu source for the - # # item's `WorkPackageCardComponent`. - # # @param item_metric [Object, NilClass] optional metric content for the - # # item's `WorkPackageCardComponent`. # # @param system_arguments [Hash] forwarded to the item class. # def with_work_package_item( # work_package:, - # component_klass: Item, - # item_menu_src: nil, - # item_metric: nil, + # component_klass: item_component_klass, # **system_arguments, # &block # ) @@ -141,8 +136,7 @@ module OpenProject :project, :container, :drag_and_drop, - :item_menu_src, - :item_metric, + :item_component_klass, :params, :current_user @@ -156,15 +150,10 @@ module OpenProject # exceeds the derived threshold. # @param drag_and_drop [Hash, NilClass] optional generic drag-and-drop # target data. Requires `:target_id` and `:allowed_drag_type` when set. - # @param item_menu_src [Proc, String, NilClass] optional menu source for - # automatically built items. Procs receive the work package. When set, - # callers are responsible for including any URL params they want in the - # returned source. - # @param item_metric [Proc, NilClass] optional metric content for - # automatically built items. Procs receive the work package and return - # renderable content. + # @param item_component_klass [Class] item class used for automatically + # built work package items. # @param params [Hash] optional URL params passed to work package items - # when deriving row and menu URLs. + # when deriving row arguments. # @param current_user [User] passed through to each item for permission # checks; defaults to `User.current`. # @param system_arguments [Hash] forwarded to the underlying @@ -174,8 +163,7 @@ module OpenProject container:, work_packages: [], drag_and_drop: nil, - item_menu_src: nil, - item_metric: nil, + item_component_klass: Item, params: {}, current_user: User.current, **system_arguments @@ -186,8 +174,7 @@ module OpenProject @project = project @container = container @drag_and_drop = drag_and_drop - @item_menu_src = item_menu_src - @item_metric = item_metric + @item_component_klass = item_component_klass @params = params @current_user = current_user @automatic_items = false @@ -203,8 +190,6 @@ module OpenProject # Content must be loaded before mode validation and automatic item builds # so slot calls have already populated `items`. content - validate_item_menu_src! - validate_item_metric! validate_item_mode! build_automatic_items if build_automatic_items? validate_empty_state! @@ -220,20 +205,13 @@ module OpenProject # items outside this box, such as in a separately-loaded page. # # @param work_package [WorkPackage] the work package rendered in the row. - # @param component_klass [Class] item class used instead of the default - # `Item`. It must accept `work_package:`, `project:`, `container:`, - # `params:`, optional `item_menu_src:`, `current_user:`, and - # `**system_arguments`. - # @param item_menu_src [String, NilClass] optional item menu source - # override. When set, callers are responsible for including any URL - # params they want in the source. - # @param item_metric [Object, NilClass] optional item metric content. + # @param component_klass [Class] item class used instead of the configured + # default item class. It must accept `work_package:`, `project:`, + # `container:`, `params:`, `current_user:`, and `**system_arguments`. # @param system_arguments [Hash] forwarded to the item class. def build_item( work_package:, - component_klass: Item, - item_menu_src: item_menu_src_for(work_package), - item_metric: item_metric_for(work_package), + component_klass: item_component_klass, **system_arguments ) component_klass.new( @@ -241,8 +219,6 @@ module OpenProject project:, container:, params:, - item_menu_src:, - item_metric:, current_user:, **system_arguments ) @@ -270,35 +246,6 @@ module OpenProject @automatic_items end - def item_menu_src_for(work_package) - return unless item_menu_src - - if item_menu_src.is_a?(Proc) - item_menu_src.call(work_package) - else - item_menu_src - end - end - - def item_metric_for(work_package) - return unless item_metric - - metric = item_metric.call(work_package) - metric.respond_to?(:render_in) ? render(metric) : metric - end - - def validate_item_menu_src! - return if item_menu_src.nil? || item_menu_src.is_a?(Proc) || item_menu_src.is_a?(String) - - raise ArgumentError, "item_menu_src must be a Proc, String, or nil" - end - - def validate_item_metric! - return if item_metric.nil? || item_metric.is_a?(Proc) - - raise ArgumentError, "item_metric must be a Proc or nil" - end - def validate_item_mode! return unless empty_items.any? diff --git a/app/components/open_project/common/work_package_card_box_component/item.rb b/app/components/open_project/common/work_package_card_box_component/item.rb index b2ba3c82e7a..92b95e6a093 100644 --- a/app/components/open_project/common/work_package_card_box_component/item.rb +++ b/app/components/open_project/common/work_package_card_box_component/item.rb @@ -42,8 +42,6 @@ module OpenProject attr_reader :work_package, :project, :container, - :item_menu_src, - :item_metric, :params, :current_user @@ -54,21 +52,15 @@ module OpenProject project:, container:, params: {}, - item_menu_src: nil, - item_metric: nil, current_user: User.current, **system_arguments ) super() - validate_item_menu_src!(item_menu_src) - @work_package = work_package @project = project @container = container @params = params - @item_menu_src = item_menu_src - @item_metric = item_metric @current_user = current_user @system_arguments = system_arguments end @@ -86,9 +78,7 @@ module OpenProject end def card - @card ||= WorkPackageCardComponent.new(work_package:, menu_src: item_menu_src).tap do |card| - card.with_metric_content(item_metric) if item_metric - end + @card ||= WorkPackageCardComponent.new(work_package:) end def render? = false @@ -97,43 +87,6 @@ module OpenProject private - def draggable? - current_user.allowed_in_project?(:manage_sprint_items, project) - end - - def split_url - url_helpers.project_backlogs_backlog_details_path(project, work_package, params) - end - - def full_url - url_helpers.work_package_path(work_package) - end - - # Sprint is the only positive match; bucket and inbox both fall through - # to inbox routes. - def uses_inbox_routes? - !container.is_a?(Sprint) - end - - def drop_url - if uses_inbox_routes? - url_helpers.move_project_backlogs_inbox_path(project, work_package, params) - else - url_helpers.move_project_backlogs_work_package_path( - project, - container, - work_package, - params - ) - end - end - - def validate_item_menu_src!(source) - return if source.nil? || source.is_a?(String) - - raise ArgumentError, "item_menu_src must be a String or nil" - end - def row_classes class_names( "Box-row--hover-blue", @@ -143,27 +96,20 @@ module OpenProject ) end - # `story` data attrs match the live Stimulus controller and Dragula - # drag-type; renaming requires coordinated JS changes (separate PR). def row_data - base = { - story: true, - controller: "backlogs--story", - backlogs__story_id_value: work_package.id, - backlogs__story_display_id_value: work_package.display_id, - backlogs__story_split_url_value: split_url, - backlogs__story_full_url_value: full_url, - backlogs__story_selected_class: "Box-row--blue", + data = { test_selector: "work-package-#{work_package.id}" } - return base unless draggable? + draggable? ? data.merge(draggable_data) : data + end - base.merge( - draggable_id: work_package.id, - draggable_type: "story", - drop_url: - ) + def draggable? + false + end + + def draggable_data + {} end end end diff --git a/modules/backlogs/app/components/backlogs/bucket_component.html.erb b/modules/backlogs/app/components/backlogs/bucket_component.html.erb index c8c0f26870d..f0ba1b44404 100644 --- a/modules/backlogs/app/components/backlogs/bucket_component.html.erb +++ b/modules/backlogs/app/components/backlogs/bucket_component.html.erb @@ -37,14 +37,7 @@ See COPYRIGHT and LICENSE files for more details. target_id: "backlog_bucket:#{backlog_bucket.id}", allowed_drag_type: "story" }, - item_menu_src: ->(work_package) do - menu_project_backlogs_inbox_path( - project, - work_package, - all_backlogs_params - ) - end, - item_metric: ->(work_package) { Backlogs::StoryPointsComponent.new(work_package:) }, + item_component_klass: Backlogs::WorkPackageCardBoxItemComponent, params: all_backlogs_params, current_user:, data: { test_selector: "backlog-bucket-#{backlog_bucket.id}" } diff --git a/modules/backlogs/app/components/backlogs/inbox_component.html.erb b/modules/backlogs/app/components/backlogs/inbox_component.html.erb index 733fce566ad..4a9d2740490 100644 --- a/modules/backlogs/app/components/backlogs/inbox_component.html.erb +++ b/modules/backlogs/app/components/backlogs/inbox_component.html.erb @@ -37,14 +37,7 @@ See COPYRIGHT and LICENSE files for more details. target_id: "inbox", allowed_drag_type: "story" }, - item_menu_src: ->(work_package) do - menu_project_backlogs_inbox_path( - project, - work_package, - all_backlogs_params - ) - end, - item_metric: ->(work_package) { Backlogs::StoryPointsComponent.new(work_package:) }, + item_component_klass: Backlogs::WorkPackageCardBoxItemComponent, params: all_backlogs_params, current_user:, data: { test_selector: "backlog-inbox" } diff --git a/modules/backlogs/app/components/backlogs/sprint_component.html.erb b/modules/backlogs/app/components/backlogs/sprint_component.html.erb index 7ea388165d3..d211001bf80 100644 --- a/modules/backlogs/app/components/backlogs/sprint_component.html.erb +++ b/modules/backlogs/app/components/backlogs/sprint_component.html.erb @@ -37,15 +37,7 @@ See COPYRIGHT and LICENSE files for more details. target_id: "sprint:#{sprint.id}", allowed_drag_type: "story" }, - item_menu_src: ->(work_package) do - menu_project_backlogs_work_package_path( - project, - sprint, - work_package, - all_backlogs_params - ) - end, - item_metric: ->(work_package) { Backlogs::StoryPointsComponent.new(work_package:) }, + item_component_klass: Backlogs::WorkPackageCardBoxItemComponent, params: all_backlogs_params, current_user:, data: { test_selector: "sprint-#{sprint.id}" } diff --git a/modules/backlogs/app/components/backlogs/work_package_card_box_item_component.rb b/modules/backlogs/app/components/backlogs/work_package_card_box_item_component.rb new file mode 100644 index 00000000000..2ebda056052 --- /dev/null +++ b/modules/backlogs/app/components/backlogs/work_package_card_box_item_component.rb @@ -0,0 +1,105 @@ +# 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 WorkPackageCardBoxItemComponent < OpenProject::Common::WorkPackageCardBoxComponent::Item + def card + @card ||= WorkPackageCardComponent.new(work_package:, menu_src:) + end + + private + + def draggable? + current_user.allowed_in_project?(:manage_sprint_items, project) + end + + def split_url + url_helpers.project_backlogs_backlog_details_path(project, work_package, params) + end + + def full_url + url_helpers.work_package_path(work_package) + end + + # Sprint is the only positive match; bucket and inbox both fall through to + # inbox routes. + def uses_inbox_routes? + !container.is_a?(Sprint) + end + + def drop_url + if uses_inbox_routes? + url_helpers.move_project_backlogs_inbox_path(project, work_package, params) + else + url_helpers.move_project_backlogs_work_package_path( + project, + container, + work_package, + params + ) + end + end + + def menu_src + if uses_inbox_routes? + url_helpers.menu_project_backlogs_inbox_path(project, work_package, params) + else + url_helpers.menu_project_backlogs_work_package_path( + project, + container, + work_package, + params + ) + end + end + + # `story` data attrs match the live Stimulus controller and Dragula + # drag-type; renaming requires coordinated JS changes (separate PR). + def row_data + super.merge( + story: true, + controller: "backlogs--story", + backlogs__story_id_value: work_package.id, + backlogs__story_display_id_value: work_package.display_id, + backlogs__story_split_url_value: split_url, + backlogs__story_full_url_value: full_url, + backlogs__story_selected_class: "Box-row--blue" + ) + end + + def draggable_data + { + draggable_id: work_package.id, + draggable_type: "story", + drop_url: + } + end + end +end diff --git a/modules/backlogs/app/components/backlogs/work_package_card_component.rb b/modules/backlogs/app/components/backlogs/work_package_card_component.rb new file mode 100644 index 00000000000..76590b0d2f0 --- /dev/null +++ b/modules/backlogs/app/components/backlogs/work_package_card_component.rb @@ -0,0 +1,50 @@ +# 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 WorkPackageCardComponent < ApplicationComponent + attr_reader :work_package, :menu_src + + def initialize(work_package:, menu_src: nil) + super() + + @work_package = work_package + @menu_src = menu_src + end + + def call + render(OpenProject::Common::WorkPackageCardComponent.new(work_package:, menu_src:)) do |card| + card.with_metric do + render(Backlogs::StoryPointsComponent.new(work_package:)) + end + end + end + end +end diff --git a/modules/backlogs/spec/components/backlogs/work_package_card_box_item_component_spec.rb b/modules/backlogs/spec/components/backlogs/work_package_card_box_item_component_spec.rb new file mode 100644 index 00000000000..9f113ec2390 --- /dev/null +++ b/modules/backlogs/spec/components/backlogs/work_package_card_box_item_component_spec.rb @@ -0,0 +1,194 @@ +# 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::WorkPackageCardBoxItemComponent, type: :component do + include Rails.application.routes.url_helpers + + shared_let(:type_feature) { create(:type_feature) } + shared_let(:default_status) { create(:default_status) } + shared_let(:default_priority) { create(:default_priority) } + shared_let(:user) { create(:admin) } + current_user { user } + + shared_let(:project) { create(:project, types: [type_feature]) } + shared_let(:sprint) do + create(:sprint, project:, name: "Sprint 1", + start_date: Date.yesterday, finish_date: Date.tomorrow) + end + shared_let(:backlog_bucket) { create(:backlog_bucket, project:, name: "Bucket A") } + + let(:container) { sprint } + let(:params) { {} } + let(:work_package) do + create(:work_package, + project:, + type: type_feature, + status: default_status, + priority: default_priority, + subject: "Card subject", + story_points: 5, + position: 1, + sprint:) + end + let(:item) do + described_class.new(work_package:, project:, container:, params:, current_user: user) + end + + describe "#row_args" do + it "marks the row as clickable and controlled by the Backlogs story controller" do + expect(item.row_args[:classes]).to include( + "Box-row--hover-blue", + "Box-row--focus-gray", + "Box-row--clickable" + ) + expect(item.row_args[:data]).to include( + story: true, + controller: "backlogs--story", + backlogs__story_id_value: work_package.id, + backlogs__story_display_id_value: work_package.display_id, + backlogs__story_full_url_value: work_package_path(work_package), + backlogs__story_selected_class: "Box-row--blue", + test_selector: "work-package-#{work_package.id}" + ) + end + + it "marks the row as draggable for users allowed to manage sprint items" do + expect(item.row_args[:classes]).to include("Box-row--draggable") + expect(item.row_args[:data]).to include( + draggable_id: work_package.id, + draggable_type: "story" + ) + end + + context "when the user cannot manage sprint items" do + let(:role) { create(:project_role, permissions: %i[view_sprints view_work_packages]) } + let(:limited_user) { create(:user, member_with_roles: { project => role }) } + let(:item) do + described_class.new(work_package:, project:, container:, params:, current_user: limited_user) + end + + it "does not mark the row as draggable" do + expect(item.row_args[:classes]).not_to include("Box-row--draggable") + expect(item.row_args[:data]).not_to include(:draggable_id) + expect(item.row_args[:data]).not_to include(:drop_url) + end + end + end + + describe "URL derivation by container" do + context "with a sprint container" do + it "uses sprint routes" do + expect(item.row_args.dig(:data, :backlogs__story_split_url_value)) + .to end_with(project_backlogs_backlog_details_path(project, work_package)) + expect(item.row_args.dig(:data, :drop_url)) + .to end_with(move_project_backlogs_work_package_path(project, sprint, work_package)) + end + end + + context "with a backlog bucket container" do + let(:container) { backlog_bucket } + + it "uses inbox routes" do + expect(item.row_args.dig(:data, :drop_url)) + .to end_with(move_project_backlogs_inbox_path(project, work_package)) + end + end + + context "with an inbox container id" do + let(:container) { "inbox_project_#{project.id}" } + + it "uses inbox routes" do + expect(item.row_args.dig(:data, :drop_url)) + .to end_with(move_project_backlogs_inbox_path(project, work_package)) + end + end + + context "with params" do + let(:params) { { all: 1 } } + + it "passes params into row URLs" do + expect(item.row_args.dig(:data, :backlogs__story_split_url_value)).to match(/all=1/) + expect(item.row_args.dig(:data, :drop_url)).to match(/all=1/) + end + end + end + + describe "#card" do + subject(:rendered_card) { render_inline(item.card) } + + it "builds a Backlogs card with story points" do + expect(rendered_card).to have_text("5 points", normalize_ws: true) + end + + context "with a sprint container" do + it "uses the sprint menu source" do + expect(rendered_card).to have_element( + "include-fragment", + src: menu_project_backlogs_work_package_path(project, sprint, work_package) + ) + end + end + + context "with an inbox container id" do + let(:container) { "inbox_project_#{project.id}" } + + it "uses the inbox menu source" do + expect(rendered_card).to have_element( + "include-fragment", + src: menu_project_backlogs_inbox_path(project, work_package) + ) + end + end + + context "with a backlog bucket container" do + let(:container) { backlog_bucket } + + it "uses the inbox menu source" do + expect(rendered_card).to have_element( + "include-fragment", + src: menu_project_backlogs_inbox_path(project, work_package) + ) + end + end + + context "with params" do + let(:params) { { all: 1 } } + + it "passes params into the menu source" do + expect(rendered_card).to have_element( + "include-fragment", + src: menu_project_backlogs_work_package_path(project, sprint, work_package, all: 1) + ) + end + end + end +end diff --git a/modules/backlogs/spec/components/backlogs/work_package_card_component_spec.rb b/modules/backlogs/spec/components/backlogs/work_package_card_component_spec.rb new file mode 100644 index 00000000000..6612181934d --- /dev/null +++ b/modules/backlogs/spec/components/backlogs/work_package_card_component_spec.rb @@ -0,0 +1,63 @@ +# 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::WorkPackageCardComponent, type: :component do + shared_let(:type_feature) { create(:type_feature) } + shared_let(:project) { create(:project, types: [type_feature]) } + + let(:menu_src) { "/backlogs/work_packages/#{work_package.id}/menu" } + let(:work_package) do + create(:work_package, + project:, + type: type_feature, + story_points: 5, + subject: "Backlogs card") + end + + subject(:rendered_component) do + render_inline(described_class.new(work_package:, menu_src:)) + end + + it "renders the common work package card" do + expect(rendered_component).to have_text("Backlogs card") + expect(rendered_component).to have_text("##{work_package.id}") + end + + it "renders story points as the card metric" do + expect(rendered_component).to have_text("5 points", normalize_ws: true) + end + + it "passes the menu source to the common card" do + expect(rendered_component).to have_element "include-fragment", + src: menu_src + end +end diff --git a/spec/components/open_project/common/work_package_card_box_component/item_spec.rb b/spec/components/open_project/common/work_package_card_box_component/item_spec.rb index 6488a501b35..45ee04a9856 100644 --- a/spec/components/open_project/common/work_package_card_box_component/item_spec.rb +++ b/spec/components/open_project/common/work_package_card_box_component/item_spec.rb @@ -31,8 +31,6 @@ require "rails_helper" RSpec.describe OpenProject::Common::WorkPackageCardBoxComponent::Item, type: :component do - include Rails.application.routes.url_helpers - shared_let(:type_feature) { create(:type_feature) } shared_let(:default_status) { create(:default_status) } shared_let(:default_priority) { create(:default_priority) } @@ -40,13 +38,8 @@ RSpec.describe OpenProject::Common::WorkPackageCardBoxComponent::Item, type: :co current_user { user } shared_let(:project) { create(:project, types: [type_feature]) } - shared_let(:sprint) do - create(:sprint, project:, name: "Sprint 1", - start_date: Date.yesterday, finish_date: Date.tomorrow) - end - shared_let(:backlog_bucket) { create(:backlog_bucket, project:, name: "Bucket A") } - let(:container) { sprint } + let(:container) { project } let(:params) { {} } let(:work_package) do create(:work_package, @@ -56,12 +49,31 @@ RSpec.describe OpenProject::Common::WorkPackageCardBoxComponent::Item, type: :co priority: default_priority, subject: "Card subject", story_points: 5, - position: 1, - sprint:) + position: 1) end let(:item) do described_class.new(work_package:, project:, container:, params:, current_user: user) end + let(:draggable_item_class) do + stub_const( + "DraggableWorkPackageCardBoxItem", + Class.new(described_class) do + private + + def draggable? + true + end + + def draggable_data + { + draggable_id: work_package.id, + draggable_type: "work_package", + drop_url: "/drop" + } + end + end + ) + end describe "#row_args" do it "can be passed to a BorderBox row" do @@ -109,55 +121,25 @@ RSpec.describe OpenProject::Common::WorkPackageCardBoxComponent::Item, type: :co ) end - it "marks the row as draggable for users allowed to manage sprint items" do - expect(item.row_args[:classes]).to include("Box-row--draggable") - expect(item.row_args[:data]).to include( - draggable_id: work_package.id, - draggable_type: "story" + it "does not include Backlogs row wiring" do + expect(item.row_args[:classes]).not_to include("Box-row--draggable") + expect(item.row_args[:data]).not_to include( + :controller, + :draggable_id, + :drop_url, + :backlogs__story_split_url_value ) end - context "when the user cannot manage sprint items" do - let(:role) { create(:project_role, permissions: %i[view_sprints view_work_packages]) } - let(:limited_user) { create(:user, member_with_roles: { project => role }) } - let(:item) do - described_class.new(work_package:, project:, container:, params:, current_user: limited_user) - end + it "supports generic draggable row data from subclasses" do + item = draggable_item_class.new(work_package:, project:, container:, params:, current_user: user) - it "does not mark the row as draggable" do - expect(item.row_args[:classes]).not_to include("Box-row--draggable") - expect(item.row_args[:data]).not_to include(:draggable_id) - expect(item.row_args[:data]).not_to include(:drop_url) - end - end - end - - describe "URL derivation by container" do - context "with a sprint container" do - it "uses sprint routes" do - expect(item.row_args.dig(:data, :backlogs__story_split_url_value)) - .to end_with(project_backlogs_backlog_details_path(project, work_package)) - expect(item.row_args.dig(:data, :drop_url)) - .to end_with(move_project_backlogs_work_package_path(project, sprint, work_package)) - end - end - - context "with a backlog bucket container" do - let(:container) { backlog_bucket } - - it "uses inbox routes" do - expect(item.row_args.dig(:data, :drop_url)) - .to end_with(move_project_backlogs_inbox_path(project, work_package)) - end - end - - context "with an inbox container id" do - let(:container) { "inbox_project_#{project.id}" } - - it "uses inbox routes" do - expect(item.row_args.dig(:data, :drop_url)) - .to end_with(move_project_backlogs_inbox_path(project, work_package)) - end + expect(item.row_args[:classes]).to include("Box-row--draggable") + expect(item.row_args[:data]).to include( + draggable_id: work_package.id, + draggable_type: "work_package", + drop_url: "/drop" + ) end end @@ -168,15 +150,6 @@ RSpec.describe OpenProject::Common::WorkPackageCardBoxComponent::Item, type: :co expect(rendered_card).to have_no_element "include-fragment" end - context "with params" do - let(:params) { { all: 1 } } - - it "passes params into row URLs" do - expect(item.row_args.dig(:data, :backlogs__story_split_url_value)).to match(/all=1/) - expect(item.row_args.dig(:data, :drop_url)).to match(/all=1/) - end - end - it "returns the same card instance across calls" do expect(item.card).to equal(item.card) end @@ -186,38 +159,5 @@ RSpec.describe OpenProject::Common::WorkPackageCardBoxComponent::Item, type: :co expect(rendered_card).to have_text("Forwarded metric") end - - context "with a provided menu source" do - let(:item) do - described_class.new( - work_package:, - project:, - container:, - params:, - item_menu_src: "/provided-menu", - current_user: user - ) - end - - it "uses the provided source" do - expect(rendered_card).to have_element "include-fragment", - src: "/provided-menu" - end - end - - context "with an invalid menu source" do - it "raises ArgumentError" do - expect do - described_class.new( - work_package:, - project:, - container:, - params:, - item_menu_src: :provided_menu, - current_user: user - ) - end.to raise_error(ArgumentError, /item_menu_src/) - end - end end end diff --git a/spec/components/open_project/common/work_package_card_box_component_spec.rb b/spec/components/open_project/common/work_package_card_box_component_spec.rb index 2720acb07da..85e99b2ebae 100644 --- a/spec/components/open_project/common/work_package_card_box_component_spec.rb +++ b/spec/components/open_project/common/work_package_card_box_component_spec.rb @@ -46,14 +46,54 @@ RSpec.describe OpenProject::Common::WorkPackageCardBoxComponent, type: :componen let(:container) { sprint } let(:drag_and_drop) { nil } - let(:item_menu_src) { nil } - let(:item_metric) { nil } + let(:item_component_klass) { described_class::Item } let(:params) { {} } let(:work_packages) { [] } let(:system_arguments) { {} } let(:header_arguments) { nil } let(:footer_content) { nil } + let(:custom_item_component_class) do + stub_const( + "CustomWorkPackageCardBoxItem", + Class.new(ApplicationComponent) do + def initialize( + work_package:, + project:, + container:, + params:, + current_user: User.current, + **system_arguments + ) + super() + + @work_package = work_package + @params = params + @context = [project, container, current_user] + @system_arguments = system_arguments + end + + def row_args + data = @system_arguments.fetch(:data, {}).merge( + params: @params.to_query, + context_size: @context.size + ) + + @system_arguments.merge( + id: "custom_work_package_#{@work_package.id}", + data: + ) + end + + def card + CustomWorkPackageCardBoxItemCard.new(subject: @work_package.subject) + end + + def render? = false + end + ) + end + subject(:rendered_component) do render_component(work_packages:, container:, drag_and_drop:, system_arguments:) end @@ -64,8 +104,7 @@ RSpec.describe OpenProject::Common::WorkPackageCardBoxComponent, type: :componen project:, container:, drag_and_drop:, - item_menu_src:, - item_metric:, + item_component_klass:, params:, current_user: user, **system_arguments @@ -79,6 +118,23 @@ RSpec.describe OpenProject::Common::WorkPackageCardBoxComponent, type: :componen end end + before do + stub_const( + "CustomWorkPackageCardBoxItemCard", + Class.new(ApplicationComponent) do + def initialize(subject:) + super() + + @subject = subject + end + + def call + tag.span("custom #{@subject}") + end + end + ) + end + describe "Box shell" do it_behaves_like "rendering Box", row_count: 1, header: false, footer: false it_behaves_like "rendering Blank Slate", heading: "Sprint 1 is empty" @@ -290,39 +346,25 @@ RSpec.describe OpenProject::Common::WorkPackageCardBoxComponent, type: :componen ) end - context "with an item_menu_src proc" do - let(:item_menu_src) { ->(work_package) { "/custom/#{work_package.id}/menu" } } + it "does not include Backlogs row wiring by default" do + expect(rendered_component).to have_css(".Box-row", count: 2) + expect(rendered_component).to have_no_css(".Box-row[data-controller='backlogs--story']") + expect(rendered_component).to have_no_css(".Box-row[data-drop-url]") + expect(rendered_component).to have_no_css(".Box-row[data-backlogs--story-split-url-value]") + end - it "uses the derived menu source for automatically built items" do - expect(rendered_component).to have_element( - "include-fragment", - src: "/custom/#{work_packages.first.id}/menu" + context "with an item_component_klass" do + let(:item_component_klass) { custom_item_component_class } + + it "uses the configured item class for automatically built items" do + expect(rendered_component).to have_css( + ".Box-row#custom_work_package_#{work_packages.first.id}", + text: "custom WP A" + ) + expect(rendered_component).to have_css( + ".Box-row#custom_work_package_#{work_packages.second.id}", + text: "custom WP B" ) - end - end - - context "with an invalid item_menu_src" do - let(:item_menu_src) { :custom_menu } - - it "raises ArgumentError" do - expect { rendered_component }.to raise_error(ArgumentError, /item_menu_src/) - end - end - - context "with an item_metric proc" do - let(:item_metric) { ->(work_package) { "metric #{work_package.id}" } } - - it "uses the derived metric for automatically built items" do - expect(rendered_component).to have_text("metric #{work_packages.first.id}") - expect(rendered_component).to have_text("metric #{work_packages.second.id}") - end - end - - context "with an invalid item_metric" do - let(:item_metric) { "static metric" } - - it "raises ArgumentError" do - expect { rendered_component }.to raise_error(ArgumentError, /item_metric/) end end end @@ -338,69 +380,6 @@ RSpec.describe OpenProject::Common::WorkPackageCardBoxComponent, type: :componen end let(:params) { { all: 1 } } let(:slot_work_package) { work_packages.first } - let(:custom_item_component_class) do - stub_const( - "CustomWorkPackageCardBoxItem", - Class.new(ApplicationComponent) do - def initialize( - work_package:, - project:, - container:, - params:, - item_menu_src: nil, - item_metric: nil, - current_user: User.current, - **system_arguments - ) - super() - - @work_package = work_package - @item_menu_src = item_menu_src - @item_metric = item_metric - @params = params - @context = [project, container, current_user] - @system_arguments = system_arguments - end - - def row_args - data = @system_arguments.fetch(:data, {}).merge( - params: @params.to_query, - context_size: @context.size - ) - data[:item_menu_src] = @item_menu_src if @item_menu_src - data[:item_metric] = @item_metric if @item_metric - - @system_arguments.merge( - id: "custom_work_package_#{@work_package.id}", - data: - ) - end - - def card - CustomWorkPackageCardBoxItemCard.new(subject: @work_package.subject) - end - - def render? = false - end - ) - end - - before do - stub_const( - "CustomWorkPackageCardBoxItemCard", - Class.new(ApplicationComponent) do - def initialize(subject:) - super() - - @subject = subject - end - - def call - tag.span("custom #{@subject}") - end - end - ) - end def render_with_manual_item render_inline( @@ -436,17 +415,6 @@ RSpec.describe OpenProject::Common::WorkPackageCardBoxComponent, type: :componen expect(rendered).to have_no_text("WP B") end - it "uses the provided menu source for manual work package items" do - rendered = render_inline( - described_class.new(project:, container:, params:, current_user: user) - ) do |box| - box.with_empty_state(title: "empty", description: "drag here") - box.with_work_package_item(work_package: slot_work_package, item_menu_src: "/manual-menu") - end - - expect(rendered).to have_element("include-fragment", src: "/manual-menu") - end - it "uses caller-provided metric content for manual work package items" do rendered = render_inline( described_class.new(project:, container:, params:, current_user: user)