[#73968] Add card menu slot

Expose a top-level menu slot on the common work package card so card
and card-box callers can configure action menus in the same style. Keep
menu_src as a compatibility shortcut for existing deferred Backlogs
menus while allowing inline non-deferred menu items through the slot.

https://community.openproject.org/wp/73968
This commit is contained in:
Alexander Brandon Coles
2026-05-01 21:41:24 +02:00
parent b6d78999d6
commit 6c1a72dda1
6 changed files with 93 additions and 10 deletions
@@ -39,13 +39,17 @@ See COPYRIGHT and LICENSE files for more details.
<% end %>
<% grid.with_area(:menu) do %>
<%= render(
OpenProject::Common::WorkPackageCardComponent::Menu.new(
work_package:,
src: menu_src,
button_aria_label: t(".menu.label_actions")
)
) %>
<% if menu? %>
<%= menu %>
<% else %>
<%= render(
OpenProject::Common::WorkPackageCardComponent::Menu.new(
work_package:,
src: menu_src,
button_aria_label: t(".menu.label_actions")
)
) %>
<% end %>
<% end %>
<% grid.with_area(:subject) do %>
@@ -35,11 +35,20 @@ module OpenProject
include OpPrimer::ComponentHelpers
renders_one :metric, Primer::Content
renders_one :menu, ->(src: nil, button_aria_label: nil, **system_arguments) {
Menu.new(
work_package:,
src:,
button_aria_label:,
**system_arguments
)
}
attr_reader :work_package, :menu_src
# @param work_package [WorkPackage] the work package this card represents.
# @param menu_src [String, NilClass] optional lazy menu source.
# @param menu_src [String, NilClass] optional lazy menu source. Prefer the
# `with_menu(src:)` slot for new call sites.
def initialize(work_package:, menu_src: nil)
super()
@@ -54,6 +54,19 @@ module OpenProject
end
end
def with_menu
work_package = WorkPackage.first
return preview_message("No work packages in the database.") unless work_package
render OpenProject::Common::WorkPackageCardComponent.new(
work_package:
) do |card|
card.with_menu do |menu|
menu.with_item(label: "Open", href: "/work_packages/#{work_package.id}")
end
end
end
private
def preview_message(text)
@@ -32,6 +32,8 @@ module Backlogs
class WorkPackageCardComponent < ApplicationComponent
attr_reader :work_package, :menu_src
delegate :with_menu, to: :card
def initialize(work_package:, menu_src: nil)
super()
@@ -40,11 +42,21 @@ module Backlogs
end
def call
render(OpenProject::Common::WorkPackageCardComponent.new(work_package:, menu_src:)) do |card|
card.with_metric do
render(card) do |common_card|
common_card.with_metric do
render(Backlogs::StoryPointsComponent.new(work_package:))
end
end
end
private
def card
@card ||= OpenProject::Common::WorkPackageCardComponent.new(work_package:, menu_src:)
end
def before_render
content
end
end
end
@@ -60,4 +60,20 @@ RSpec.describe Backlogs::WorkPackageCardComponent, type: :component do
expect(rendered_component).to have_element "include-fragment",
src: menu_src
end
it "supports inline menu items through the menu slot" do
rendered = render_inline(described_class.new(work_package:, menu_src:)) do |card|
card.with_menu(button_aria_label: "Backlogs card actions") do |menu|
menu.with_item(label: "Open", href: "/work_packages/#{work_package.id}")
end
end
expect(rendered).to have_link "Open", href: "/work_packages/#{work_package.id}"
expect(rendered).to have_button(
"work_package_#{work_package.id}_menu-button",
accessible_name: "Backlogs card actions"
)
expect(rendered).to have_no_element "include-fragment"
expect(rendered).to have_text("5 points", normalize_ws: true)
end
end
@@ -96,5 +96,34 @@ RSpec.describe OpenProject::Common::WorkPackageCardComponent, type: :component d
it "uses the provided menu src" do
expect(rendered_component).to have_element "include-fragment", src: menu_src
end
it "supports inline menu items through the menu slot" do
rendered = render_inline(component) do |card|
card.with_menu(button_aria_label: "Card actions") do |menu|
menu.with_item(label: "Open", href: "/work_packages/#{work_package.id}")
end
end
expect(rendered).to have_link "Open", href: "/work_packages/#{work_package.id}"
expect(rendered).to have_button(menu_button_id, accessible_name: "Card actions")
expect(rendered).to have_no_element "include-fragment"
end
it "supports deferred menu loading through the menu slot" do
rendered = render_inline(described_class.new(work_package:)) do |card|
card.with_menu(src: menu_src)
end
expect(rendered).to have_element "include-fragment", src: menu_src
end
it "uses the menu slot before the initializer menu source" do
rendered = render_inline(component) do |card|
card.with_menu(src: "/slot-menu")
end
expect(rendered).to have_element "include-fragment", src: "/slot-menu"
expect(rendered).to have_no_element "include-fragment", src: menu_src
end
end
end