mirror of
https://github.com/opf/openproject.git
synced 2026-06-14 03:30:14 +00:00
[#73968] Extract Backlogs card box items
Move Backlogs-specific item/card concerns behind Backlogs components while keeping the common card box API focused on generic item rendering. The common item keeps generic draggability hooks, while Backlogs owns story points, menu URLs, and story controller row data. https://community.openproject.org/wp/73968
This commit is contained in:
@@ -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?
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}" }
|
||||
|
||||
@@ -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" }
|
||||
|
||||
@@ -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}" }
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
+194
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user