From 6c1a72dda1fbd6595ad6bfa811c528ee733ecce9 Mon Sep 17 00:00:00 2001 From: Alexander Brandon Coles Date: Fri, 1 May 2026 21:41:24 +0200 Subject: [PATCH] [#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 --- .../work_package_card_component.html.erb | 18 +++++++----- .../common/work_package_card_component.rb | 11 ++++++- .../work_package_card_component_preview.rb | 13 +++++++++ .../backlogs/work_package_card_component.rb | 16 ++++++++-- .../work_package_card_component_spec.rb | 16 ++++++++++ .../work_package_card_component_spec.rb | 29 +++++++++++++++++++ 6 files changed, 93 insertions(+), 10 deletions(-) diff --git a/app/components/open_project/common/work_package_card_component.html.erb b/app/components/open_project/common/work_package_card_component.html.erb index cb2bcf0b5ba..38d68255cdc 100644 --- a/app/components/open_project/common/work_package_card_component.html.erb +++ b/app/components/open_project/common/work_package_card_component.html.erb @@ -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 %> diff --git a/app/components/open_project/common/work_package_card_component.rb b/app/components/open_project/common/work_package_card_component.rb index bcbea5c8e56..12bb0fb8430 100644 --- a/app/components/open_project/common/work_package_card_component.rb +++ b/app/components/open_project/common/work_package_card_component.rb @@ -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() diff --git a/lookbook/previews/open_project/common/work_package_card_component_preview.rb b/lookbook/previews/open_project/common/work_package_card_component_preview.rb index 023d64a9667..cb15d220239 100644 --- a/lookbook/previews/open_project/common/work_package_card_component_preview.rb +++ b/lookbook/previews/open_project/common/work_package_card_component_preview.rb @@ -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) diff --git a/modules/backlogs/app/components/backlogs/work_package_card_component.rb b/modules/backlogs/app/components/backlogs/work_package_card_component.rb index 76590b0d2f0..9f431f7b980 100644 --- a/modules/backlogs/app/components/backlogs/work_package_card_component.rb +++ b/modules/backlogs/app/components/backlogs/work_package_card_component.rb @@ -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 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 index 6612181934d..793f1afe317 100644 --- a/modules/backlogs/spec/components/backlogs/work_package_card_component_spec.rb +++ b/modules/backlogs/spec/components/backlogs/work_package_card_component_spec.rb @@ -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 diff --git a/spec/components/open_project/common/work_package_card_component_spec.rb b/spec/components/open_project/common/work_package_card_component_spec.rb index d9034a3ea84..66778329f50 100644 --- a/spec/components/open_project/common/work_package_card_component_spec.rb +++ b/spec/components/open_project/common/work_package_card_component_spec.rb @@ -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